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