1use std::io::Write;
15
16use crate::cli::wprintln;
17use crate::innodb::compliance::{encryption_audit, scan_residue, verify_deleted, Pattern};
18use crate::IdbError;
19
20pub struct ComplyOptions {
22 pub file: String,
24 pub verify_deleted: bool,
26 pub scan_residue: bool,
28 pub encryption_audit: bool,
30 pub table: Option<String>,
32 pub where_clause: Option<String>,
34 pub pattern: Option<String>,
36 pub thorough: bool,
38 pub max_hits: usize,
40 pub page_size: Option<u32>,
42 pub keyring: Option<String>,
44 pub mmap: bool,
46 pub json: bool,
48 pub csv: bool,
50 pub verbose: bool,
52}
53
54pub fn execute(opts: &ComplyOptions, writer: &mut dyn Write) -> Result<(), IdbError> {
56 let mode_count =
58 opts.verify_deleted as u8 + opts.scan_residue as u8 + opts.encryption_audit as u8;
59 if mode_count == 0 {
60 return Err(IdbError::Argument(
61 "select a mode: --verify-deleted, --scan-residue, or --encryption-audit".to_string(),
62 ));
63 }
64 if mode_count > 1 {
65 return Err(IdbError::Argument(
66 "modes are mutually exclusive: pick one of --verify-deleted, --scan-residue, --encryption-audit"
67 .to_string(),
68 ));
69 }
70
71 let mut ts = crate::cli::open_tablespace(&opts.file, opts.page_size, opts.mmap)?;
72 if let Some(ref keyring_path) = opts.keyring {
73 crate::cli::setup_decryption(&mut ts, keyring_path)?;
74 }
75
76 if opts.verify_deleted {
77 run_verify(opts, &mut ts, writer)
78 } else if opts.scan_residue {
79 run_scan(opts, &mut ts, writer)
80 } else {
81 run_encryption_audit(opts, &mut ts, writer)
82 }
83}
84
85fn run_verify(
90 opts: &ComplyOptions,
91 ts: &mut crate::innodb::tablespace::Tablespace,
92 writer: &mut dyn Write,
93) -> Result<(), IdbError> {
94 let where_clause = opts.where_clause.as_deref().ok_or_else(|| {
95 IdbError::Argument("--verify-deleted requires --where <column>=<value>".to_string())
96 })?;
97 let (col, val) = where_clause.split_once('=').ok_or_else(|| {
98 IdbError::Argument("--where must be of the form <column>=<value>".to_string())
99 })?;
100 let col = col.trim();
101 if col.is_empty() {
102 return Err(IdbError::Argument(
103 "--where column name must not be empty".to_string(),
104 ));
105 }
106 if val.is_empty() {
107 return Err(IdbError::Argument(
108 "--where value must not be empty (an empty value would match every NULL row)"
109 .to_string(),
110 ));
111 }
112
113 if let Some(ref filter) = opts.table {
115 match crate::innodb::export::extract_table_name(ts) {
116 Some(name) if name.eq_ignore_ascii_case(filter) => {}
117 Some(name) => {
118 return Err(IdbError::Argument(format!(
119 "Table name '{}' does not match filter '{}'",
120 name, filter
121 )))
122 }
123 None => {
124 return Err(IdbError::Argument(
125 "Cannot filter by table name: SDI metadata not available".to_string(),
126 ))
127 }
128 }
129 }
130
131 let report = verify_deleted(ts, col, val, opts.thorough)?;
132
133 if opts.json {
134 let json =
135 serde_json::to_string_pretty(&report).map_err(|e| IdbError::Parse(e.to_string()))?;
136 wprintln!(writer, "{}", json)?;
137 return Ok(());
138 }
139
140 if opts.csv {
141 wprintln!(writer, "region,page,offset,delete_marked,trx_id")?;
142 for s in &report.residue_sites {
143 wprintln!(
144 writer,
145 "{},{},{},{},{}",
146 s.region,
147 s.page_number,
148 s.offset,
149 if s.delete_marked { "Y" } else { "N" },
150 s.trx_id.map_or(String::new(), |t| t.to_string()),
151 )?;
152 }
153 return Ok(());
154 }
155
156 let table = report.table_name.as_deref().unwrap_or("(unknown table)");
158 wprintln!(
159 writer,
160 "Deletion verification: {}.{} = '{}'",
161 table,
162 report.column,
163 report.target_value
164 )?;
165 wprintln!(
166 writer,
167 "Scanned: {} ({} records examined){}",
168 report.regions_scanned.join(", "),
169 report.records_examined,
170 if report.thorough {
171 ""
172 } else {
173 " - logical structures only; pass --thorough to also sweep slack space"
174 }
175 )?;
176
177 if report.fully_purged {
178 wprintln!(
179 writer,
180 "\nRESULT: fully purged - no residue found in the scanned regions of this file."
181 )?;
182 } else {
183 wprintln!(
184 writer,
185 "\nRESULT: NOT purged - {} residue site(s) found:",
186 report.residue_sites.len()
187 )?;
188 for s in &report.residue_sites {
189 let trx = s
190 .trx_id
191 .map_or(String::new(), |t| format!(", trx_id={}", t));
192 wprintln!(
193 writer,
194 " [{}] page {} offset {}{}{}",
195 s.region,
196 s.page_number,
197 s.offset,
198 if s.delete_marked {
199 ", delete-marked"
200 } else {
201 ""
202 },
203 trx
204 )?;
205 }
206 }
207
208 Ok(())
209}
210
211fn run_scan(
216 opts: &ComplyOptions,
217 ts: &mut crate::innodb::tablespace::Tablespace,
218 writer: &mut dyn Write,
219) -> Result<(), IdbError> {
220 let pattern_str = opts.pattern.as_deref().ok_or_else(|| {
221 IdbError::Argument("--scan-residue requires --pattern <text|hex:...>".to_string())
222 })?;
223 let pattern = Pattern::parse(pattern_str)?;
224
225 let matches = scan_residue(ts, &pattern, opts.max_hits)?;
226
227 if opts.json {
228 let json =
229 serde_json::to_string_pretty(&matches).map_err(|e| IdbError::Parse(e.to_string()))?;
230 wprintln!(writer, "{}", json)?;
231 return Ok(());
232 }
233
234 if opts.csv {
235 wprintln!(writer, "page,page_type,offset,region,context_hex")?;
236 for m in &matches {
237 wprintln!(
238 writer,
239 "{},{},{},{},{}",
240 m.page_number,
241 m.page_type,
242 m.offset,
243 m.region.name(),
244 m.context_hex,
245 )?;
246 }
247 return Ok(());
248 }
249
250 if matches.is_empty() {
251 wprintln!(
252 writer,
253 "No residue found for pattern in the scanned pages of this file."
254 )?;
255 return Ok(());
256 }
257
258 wprintln!(
259 writer,
260 "{} match(es){}:",
261 matches.len(),
262 if matches.len() >= opts.max_hits {
263 format!(" (capped at {})", opts.max_hits)
264 } else {
265 String::new()
266 }
267 )?;
268 wprintln!(
269 writer,
270 "{:<8} {:<16} {:<8} {:<12} {}",
271 "PAGE",
272 "PAGE_TYPE",
273 "OFFSET",
274 "REGION",
275 "CONTEXT (hex)"
276 )?;
277 for m in &matches {
278 wprintln!(
279 writer,
280 "{:<8} {:<16} {:<8} {:<12} {}",
281 m.page_number,
282 m.page_type,
283 m.offset,
284 m.region.name(),
285 m.context_hex,
286 )?;
287 }
288
289 Ok(())
290}
291
292fn run_encryption_audit(
297 opts: &ComplyOptions,
298 ts: &mut crate::innodb::tablespace::Tablespace,
299 writer: &mut dyn Write,
300) -> Result<(), IdbError> {
301 let report = encryption_audit(ts)?;
302
303 if opts.json {
304 let json =
305 serde_json::to_string_pretty(&report).map_err(|e| IdbError::Parse(e.to_string()))?;
306 wprintln!(writer, "{}", json)?;
307 return Ok(());
308 }
309
310 if opts.csv {
311 wprintln!(
312 writer,
313 "tablespace_encrypted,algorithm,key_available,encrypted_pages,total_pages"
314 )?;
315 wprintln!(
316 writer,
317 "{},{},{},{},{}",
318 if report.tablespace_encrypted {
319 "Y"
320 } else {
321 "N"
322 },
323 report.algorithm.as_deref().unwrap_or(""),
324 if report.key_available { "Y" } else { "N" },
325 report.encrypted_page_count,
326 report.total_pages,
327 )?;
328 return Ok(());
329 }
330
331 wprintln!(
332 writer,
333 "Encryption audit: {}",
334 if report.tablespace_encrypted {
335 "ENCRYPTED tablespace"
336 } else {
337 "plaintext tablespace"
338 }
339 )?;
340 if let Some(ref algo) = report.algorithm {
341 wprintln!(writer, " Algorithm: {}", algo)?;
342 }
343 wprintln!(
344 writer,
345 " Key available: {}",
346 if report.key_available { "yes" } else { "no" }
347 )?;
348 wprintln!(
349 writer,
350 " Encrypted pages: {} / {}",
351 report.encrypted_page_count,
352 report.total_pages
353 )?;
354
355 Ok(())
356}