use std::io::Write;
use crate::cli::wprintln;
use crate::innodb::export::csv_escape;
use crate::innodb::undelete::{
field_value_to_json, field_value_to_sql, scan_undeleted, RecoverySource, UndeleteScanResult,
};
use crate::IdbError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UndeleteFormat {
Csv,
Json,
Sql,
Hex,
}
impl UndeleteFormat {
fn from_str(s: &str) -> Result<Self, IdbError> {
match s.to_lowercase().as_str() {
"csv" => Ok(UndeleteFormat::Csv),
"json" => Ok(UndeleteFormat::Json),
"sql" => Ok(UndeleteFormat::Sql),
"hex" => Ok(UndeleteFormat::Hex),
_ => Err(IdbError::Argument(format!(
"Unknown format '{}'. Use csv, json, sql, or hex.",
s
))),
}
}
}
pub struct UndeleteOptions {
pub file: String,
pub undo_file: Option<String>,
pub table: Option<String>,
pub min_trx_id: Option<u64>,
pub confidence: f64,
pub format: String,
pub json: bool,
pub verbose: bool,
pub page: Option<u64>,
pub page_size: Option<u32>,
pub keyring: Option<String>,
pub mmap: bool,
}
pub fn execute(opts: &UndeleteOptions, writer: &mut dyn Write) -> Result<(), IdbError> {
let format = UndeleteFormat::from_str(&opts.format)?;
let mut ts = crate::cli::open_tablespace(&opts.file, opts.page_size, opts.mmap)?;
if let Some(ref keyring_path) = opts.keyring {
crate::cli::setup_decryption(&mut ts, keyring_path)?;
}
let mut undo_ts_opt = match &opts.undo_file {
Some(path) => Some(crate::cli::open_tablespace(
path,
opts.page_size,
opts.mmap,
)?),
None => None,
};
let result = scan_undeleted(
&mut ts,
undo_ts_opt.as_mut(),
opts.confidence,
opts.min_trx_id,
opts.page,
)?;
if let Some(ref filter_table) = opts.table {
match result.table_name {
Some(ref table_name) => {
if !table_name.eq_ignore_ascii_case(filter_table) {
return Err(IdbError::Argument(format!(
"Table name '{}' does not match filter '{}'",
table_name, filter_table
)));
}
}
None => {
return Err(IdbError::Argument(
"Cannot filter by table name: SDI metadata not available (pre-8.0 tablespace)"
.to_string(),
));
}
}
}
if opts.verbose && !opts.json {
eprintln!(
"Recovered {} records ({} delete-marked, {} free-list, {} undo-log)",
result.summary.total,
result.summary.delete_marked,
result.summary.free_list,
result.summary.undo_log,
);
}
if opts.json {
let json =
serde_json::to_string_pretty(&result).map_err(|e| IdbError::Parse(e.to_string()))?;
wprintln!(writer, "{}", json)?;
} else {
match format {
UndeleteFormat::Csv => output_csv(writer, &result)?,
UndeleteFormat::Json => output_json(writer, &result)?,
UndeleteFormat::Sql => output_sql(writer, &result)?,
UndeleteFormat::Hex => output_hex(writer, &result)?,
}
}
Ok(())
}
fn output_csv(writer: &mut dyn Write, result: &UndeleteScanResult) -> Result<(), IdbError> {
let mut headers = vec![
"_source".to_string(),
"_confidence".to_string(),
"_trx_id".to_string(),
"_page".to_string(),
];
headers.extend(result.column_names.clone());
wprintln!(writer, "{}", headers.join(","))?;
for rec in &result.records {
let source_str = match rec.source {
RecoverySource::DeleteMarked => "delete_marked",
RecoverySource::FreeList => "free_list",
RecoverySource::UndoLog => "undo_log",
};
let mut values = vec![
source_str.to_string(),
format!("{:.2}", rec.confidence),
rec.trx_id.map_or(String::new(), |t| t.to_string()),
rec.page_number.to_string(),
];
for (_, val) in &rec.columns {
values.push(csv_escape(val));
}
wprintln!(writer, "{}", values.join(","))?;
}
Ok(())
}
fn output_json(writer: &mut dyn Write, result: &UndeleteScanResult) -> Result<(), IdbError> {
let mut json_records: Vec<serde_json::Value> = Vec::new();
for rec in &result.records {
let mut obj = serde_json::Map::new();
obj.insert(
"source".to_string(),
serde_json::json!(match rec.source {
RecoverySource::DeleteMarked => "delete_marked",
RecoverySource::FreeList => "free_list",
RecoverySource::UndoLog => "undo_log",
}),
);
obj.insert("confidence".to_string(), serde_json::json!(rec.confidence));
if let Some(trx) = rec.trx_id {
obj.insert("trx_id".to_string(), serde_json::json!(trx));
}
obj.insert("page".to_string(), serde_json::json!(rec.page_number));
let mut cols = serde_json::Map::new();
for (name, val) in &rec.columns {
cols.insert(name.clone(), field_value_to_json(val));
}
obj.insert("columns".to_string(), serde_json::Value::Object(cols));
json_records.push(serde_json::Value::Object(obj));
}
let output =
serde_json::to_string_pretty(&json_records).map_err(|e| IdbError::Parse(e.to_string()))?;
wprintln!(writer, "{}", output)?;
Ok(())
}
fn output_sql(writer: &mut dyn Write, result: &UndeleteScanResult) -> Result<(), IdbError> {
let table_name = result.table_name.as_deref().unwrap_or("unknown_table");
let col_names = if !result.column_names.is_empty() {
result.column_names.join(", ")
} else if let Some(first_rec) = result.records.first() {
first_rec
.columns
.iter()
.map(|(n, _)| n.as_str())
.collect::<Vec<_>>()
.join(", ")
} else {
return Ok(());
};
for rec in &result.records {
let source_str = match rec.source {
RecoverySource::DeleteMarked => "delete_marked",
RecoverySource::FreeList => "free_list",
RecoverySource::UndoLog => "undo_log",
};
wprintln!(
writer,
"-- source: {}, confidence: {:.2}, page: {}",
source_str,
rec.confidence,
rec.page_number
)?;
let values: Vec<String> = rec
.columns
.iter()
.map(|(_, val)| field_value_to_sql(val))
.collect();
wprintln!(
writer,
"INSERT INTO {} ({}) VALUES ({});",
table_name,
col_names,
values.join(", ")
)?;
}
Ok(())
}
fn output_hex(writer: &mut dyn Write, result: &UndeleteScanResult) -> Result<(), IdbError> {
wprintln!(
writer,
"{:<12} {:<8} {:<8} {:<6} {}",
"SOURCE",
"CONF",
"PAGE",
"OFFSET",
"DATA (hex)"
)?;
for rec in &result.records {
let source_str = match rec.source {
RecoverySource::DeleteMarked => "delete_marked",
RecoverySource::FreeList => "free_list",
RecoverySource::UndoLog => "undo_log",
};
let hex = rec.raw_hex.as_deref().unwrap_or("");
wprintln!(
writer,
"{:<12} {:<8.2} {:<8} {:<6} {}",
source_str,
rec.confidence,
rec.page_number,
rec.offset,
hex
)?;
}
Ok(())
}