vault_audit_tools/commands/
kv_compare.rs1use anyhow::{Context, Result};
32use std::collections::HashSet;
33use std::fs::File;
34
35fn format_number(n: usize) -> String {
36 let s = n.to_string();
37 let mut result = String::new();
38 for (i, c) in s.chars().rev().enumerate() {
39 if i > 0 && i % 3 == 0 {
40 result.push(',');
41 }
42 result.push(c);
43 }
44 result.chars().rev().collect()
45}
46
47struct MountData {
49 operations: usize,
50 paths: usize,
51 entities: HashSet<String>,
52}
53
54fn analyze_mount(csvfile: &str) -> Result<Option<MountData>> {
55 let file = match File::open(csvfile) {
56 Ok(f) => f,
57 Err(_) => return Ok(None),
58 };
59
60 let mut reader = csv::Reader::from_reader(file);
61 let mut operations = 0;
62 let mut paths = 0;
63 let mut entities: HashSet<String> = HashSet::new();
64
65 for result in reader.records() {
66 let record = result?;
67
68 if let Some(ops_str) = record.get(2) {
70 if let Ok(ops) = ops_str.parse::<usize>() {
71 operations += ops;
72 }
73 }
74
75 paths += 1;
76
77 if let Some(entity_ids_str) = record.get(3) {
79 for eid in entity_ids_str.split(',') {
80 let trimmed = eid.trim();
81 if !trimmed.is_empty() {
82 entities.insert(trimmed.to_string());
83 }
84 }
85 }
86 }
87
88 if paths == 0 {
89 return Ok(None);
90 }
91
92 Ok(Some(MountData {
93 operations,
94 paths,
95 entities,
96 }))
97}
98
99pub fn run(csv1: &str, csv2: &str) -> Result<()> {
100 let csv_files = vec![csv1.to_string(), csv2.to_string()];
101
102 println!("{}", "=".repeat(95));
103 println!(
104 "{:<20} {:<18} {:<18} {:<20}",
105 "KV Mount", "Operations", "Unique Paths", "Unique Entities"
106 );
107 println!("{}", "=".repeat(95));
108
109 let mut results = Vec::new();
110 let mut total_ops = 0;
111 let mut total_paths = 0;
112 let mut all_entities: HashSet<String> = HashSet::new();
113
114 for csv_file in &csv_files {
115 let mount_name = std::path::Path::new(csv_file)
117 .file_stem()
118 .and_then(|s| s.to_str())
119 .unwrap_or(csv_file);
120
121 match analyze_mount(csv_file).context(format!("Failed to analyze {}", csv_file))? {
122 Some(data) => {
123 println!(
124 "{:<20} {:<18} {:<18} {:<20}",
125 mount_name,
126 format_number(data.operations),
127 format_number(data.paths),
128 format_number(data.entities.len())
129 );
130
131 total_ops += data.operations;
132 total_paths += data.paths;
133 all_entities.extend(data.entities.iter().cloned());
134
135 results.push((mount_name.to_string(), data));
136 }
137 None => {
138 println!("{:<20} {:<18}", mount_name, "(file not found)");
139 }
140 }
141 }
142
143 println!("{}", "=".repeat(95));
144 println!(
145 "{:<20} {:<18} {:<18} {:<20}",
146 "TOTAL",
147 format_number(total_ops),
148 format_number(total_paths),
149 format_number(all_entities.len())
150 );
151 println!("{}", "=".repeat(95));
152
153 if !results.is_empty() {
155 println!("\nPercentage Breakdown by Operations:");
156 println!("{}", "-".repeat(50));
157
158 results.sort_by(|a, b| b.1.operations.cmp(&a.1.operations));
160
161 for (mount, data) in results {
162 let pct = if total_ops > 0 {
163 (data.operations as f64 / total_ops as f64) * 100.0
164 } else {
165 0.0
166 };
167 println!("{:<20} {:>6.2}%", mount, pct);
168 }
169 }
170
171 Ok(())
172}