1use std::io::Write;
8
9use crate::cli::wprintln;
10use crate::innodb::export::csv_escape;
11use crate::innodb::undelete::{
12 field_value_to_json, field_value_to_sql, scan_undeleted, RecoverySource, UndeleteScanResult,
13};
14use crate::IdbError;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum UndeleteFormat {
19 Csv,
20 Json,
21 Sql,
22 Hex,
23}
24
25impl UndeleteFormat {
26 fn from_str(s: &str) -> Result<Self, IdbError> {
27 match s.to_lowercase().as_str() {
28 "csv" => Ok(UndeleteFormat::Csv),
29 "json" => Ok(UndeleteFormat::Json),
30 "sql" => Ok(UndeleteFormat::Sql),
31 "hex" => Ok(UndeleteFormat::Hex),
32 _ => Err(IdbError::Argument(format!(
33 "Unknown format '{}'. Use csv, json, sql, or hex.",
34 s
35 ))),
36 }
37 }
38}
39
40pub struct UndeleteOptions {
42 pub file: String,
44 pub undo_file: Option<String>,
46 pub table: Option<String>,
48 pub min_trx_id: Option<u64>,
50 pub confidence: f64,
52 pub format: String,
54 pub json: bool,
56 pub verbose: bool,
58 pub page: Option<u64>,
60 pub page_size: Option<u32>,
62 pub keyring: Option<String>,
64 pub mmap: bool,
66}
67
68pub fn execute(opts: &UndeleteOptions, writer: &mut dyn Write) -> Result<(), IdbError> {
70 let format = UndeleteFormat::from_str(&opts.format)?;
71
72 let mut ts = crate::cli::open_tablespace(&opts.file, opts.page_size, opts.mmap)?;
73 if let Some(ref keyring_path) = opts.keyring {
74 crate::cli::setup_decryption(&mut ts, keyring_path)?;
75 }
76
77 let mut undo_ts_opt = match &opts.undo_file {
79 Some(path) => Some(crate::cli::open_tablespace(
80 path,
81 opts.page_size,
82 opts.mmap,
83 )?),
84 None => None,
85 };
86
87 let result = scan_undeleted(
88 &mut ts,
89 undo_ts_opt.as_mut(),
90 opts.confidence,
91 opts.min_trx_id,
92 opts.page,
93 )?;
94
95 if let Some(ref filter_table) = opts.table {
97 match result.table_name {
98 Some(ref table_name) => {
99 if !table_name.eq_ignore_ascii_case(filter_table) {
100 return Err(IdbError::Argument(format!(
101 "Table name '{}' does not match filter '{}'",
102 table_name, filter_table
103 )));
104 }
105 }
106 None => {
107 return Err(IdbError::Argument(
108 "Cannot filter by table name: SDI metadata not available (pre-8.0 tablespace)"
109 .to_string(),
110 ));
111 }
112 }
113 }
114
115 if opts.verbose && !opts.json {
116 eprintln!(
117 "Recovered {} records ({} delete-marked, {} free-list, {} undo-log)",
118 result.summary.total,
119 result.summary.delete_marked,
120 result.summary.free_list,
121 result.summary.undo_log,
122 );
123 }
124
125 if opts.json {
126 let json =
128 serde_json::to_string_pretty(&result).map_err(|e| IdbError::Parse(e.to_string()))?;
129 wprintln!(writer, "{}", json)?;
130 } else {
131 match format {
132 UndeleteFormat::Csv => output_csv(writer, &result)?,
133 UndeleteFormat::Json => output_json(writer, &result)?,
134 UndeleteFormat::Sql => output_sql(writer, &result)?,
135 UndeleteFormat::Hex => output_hex(writer, &result)?,
136 }
137 }
138
139 Ok(())
140}
141
142fn output_csv(writer: &mut dyn Write, result: &UndeleteScanResult) -> Result<(), IdbError> {
144 let mut headers = vec![
146 "_source".to_string(),
147 "_confidence".to_string(),
148 "_trx_id".to_string(),
149 "_page".to_string(),
150 ];
151 headers.extend(result.column_names.clone());
152 wprintln!(writer, "{}", headers.join(","))?;
153
154 for rec in &result.records {
155 let source_str = match rec.source {
156 RecoverySource::DeleteMarked => "delete_marked",
157 RecoverySource::FreeList => "free_list",
158 RecoverySource::UndoLog => "undo_log",
159 };
160 let mut values = vec![
161 source_str.to_string(),
162 format!("{:.2}", rec.confidence),
163 rec.trx_id.map_or(String::new(), |t| t.to_string()),
164 rec.page_number.to_string(),
165 ];
166
167 for (_, val) in &rec.columns {
168 values.push(csv_escape(val));
169 }
170
171 wprintln!(writer, "{}", values.join(","))?;
172 }
173
174 Ok(())
175}
176
177fn output_json(writer: &mut dyn Write, result: &UndeleteScanResult) -> Result<(), IdbError> {
179 let mut json_records: Vec<serde_json::Value> = Vec::new();
180
181 for rec in &result.records {
182 let mut obj = serde_json::Map::new();
183 obj.insert(
184 "source".to_string(),
185 serde_json::json!(match rec.source {
186 RecoverySource::DeleteMarked => "delete_marked",
187 RecoverySource::FreeList => "free_list",
188 RecoverySource::UndoLog => "undo_log",
189 }),
190 );
191 obj.insert("confidence".to_string(), serde_json::json!(rec.confidence));
192 if let Some(trx) = rec.trx_id {
193 obj.insert("trx_id".to_string(), serde_json::json!(trx));
194 }
195 obj.insert("page".to_string(), serde_json::json!(rec.page_number));
196
197 let mut cols = serde_json::Map::new();
198 for (name, val) in &rec.columns {
199 cols.insert(name.clone(), field_value_to_json(val));
200 }
201 obj.insert("columns".to_string(), serde_json::Value::Object(cols));
202
203 json_records.push(serde_json::Value::Object(obj));
204 }
205
206 let output =
207 serde_json::to_string_pretty(&json_records).map_err(|e| IdbError::Parse(e.to_string()))?;
208 wprintln!(writer, "{}", output)?;
209
210 Ok(())
211}
212
213fn output_sql(writer: &mut dyn Write, result: &UndeleteScanResult) -> Result<(), IdbError> {
215 let table_name = result.table_name.as_deref().unwrap_or("unknown_table");
216
217 let col_names = if !result.column_names.is_empty() {
218 result.column_names.join(", ")
219 } else if let Some(first_rec) = result.records.first() {
220 first_rec
221 .columns
222 .iter()
223 .map(|(n, _)| n.as_str())
224 .collect::<Vec<_>>()
225 .join(", ")
226 } else {
227 return Ok(());
228 };
229
230 for rec in &result.records {
231 let source_str = match rec.source {
232 RecoverySource::DeleteMarked => "delete_marked",
233 RecoverySource::FreeList => "free_list",
234 RecoverySource::UndoLog => "undo_log",
235 };
236 wprintln!(
237 writer,
238 "-- source: {}, confidence: {:.2}, page: {}",
239 source_str,
240 rec.confidence,
241 rec.page_number
242 )?;
243
244 let values: Vec<String> = rec
245 .columns
246 .iter()
247 .map(|(_, val)| field_value_to_sql(val))
248 .collect();
249
250 wprintln!(
251 writer,
252 "INSERT INTO {} ({}) VALUES ({});",
253 table_name,
254 col_names,
255 values.join(", ")
256 )?;
257 }
258
259 Ok(())
260}
261
262fn output_hex(writer: &mut dyn Write, result: &UndeleteScanResult) -> Result<(), IdbError> {
264 wprintln!(
265 writer,
266 "{:<12} {:<8} {:<8} {:<6} {}",
267 "SOURCE",
268 "CONF",
269 "PAGE",
270 "OFFSET",
271 "DATA (hex)"
272 )?;
273
274 for rec in &result.records {
275 let source_str = match rec.source {
276 RecoverySource::DeleteMarked => "delete_marked",
277 RecoverySource::FreeList => "free_list",
278 RecoverySource::UndoLog => "undo_log",
279 };
280
281 let hex = rec.raw_hex.as_deref().unwrap_or("");
282
283 wprintln!(
284 writer,
285 "{:<12} {:<8.2} {:<8} {:<6} {}",
286 source_str,
287 rec.confidence,
288 rec.page_number,
289 rec.offset,
290 hex
291 )?;
292 }
293
294 Ok(())
295}