use std::io::Write;
use std::sync::Arc;
use byteorder::{BigEndian, ByteOrder};
use colored::Colorize;
use serde::Serialize;
use crate::cli::wprintln;
use crate::innodb::checksum::validate_checksum;
use crate::innodb::constants::FIL_PAGE_SPACE_ID;
use crate::innodb::write;
use crate::util::audit::AuditLogger;
use crate::IdbError;
pub struct TransplantOptions {
pub donor: String,
pub target: String,
pub pages: Vec<u64>,
pub no_backup: bool,
pub force: bool,
pub dry_run: bool,
pub verbose: bool,
pub json: bool,
pub page_size: Option<u32>,
pub keyring: Option<String>,
pub mmap: bool,
pub audit_logger: Option<Arc<AuditLogger>>,
}
#[derive(Serialize)]
struct TransplantReport {
donor: String,
target: String,
#[serde(skip_serializing_if = "Option::is_none")]
backup_path: Option<String>,
dry_run: bool,
transplanted: u64,
skipped: u64,
pages: Vec<PageTransplantInfo>,
}
#[derive(Serialize)]
struct PageTransplantInfo {
page_number: u64,
action: String,
#[serde(skip_serializing_if = "Option::is_none")]
reason: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
donor_checksum_valid: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
post_checksum_valid: Option<bool>,
}
pub fn execute(opts: &TransplantOptions, writer: &mut dyn Write) -> Result<(), IdbError> {
if opts.pages.is_empty() {
return Err(IdbError::Argument(
"No pages specified. Use --pages to specify page numbers.".to_string(),
));
}
let mut donor_ts = crate::cli::open_tablespace(&opts.donor, opts.page_size, opts.mmap)?;
let mut target_ts = crate::cli::open_tablespace(&opts.target, opts.page_size, opts.mmap)?;
if let Some(ref keyring_path) = opts.keyring {
crate::cli::setup_decryption(&mut donor_ts, keyring_path)?;
crate::cli::setup_decryption(&mut target_ts, keyring_path)?;
}
let donor_page_size = donor_ts.page_size();
let target_page_size = target_ts.page_size();
let donor_count = donor_ts.page_count();
let target_count = target_ts.page_count();
let donor_vendor = donor_ts.vendor_info().clone();
let target_vendor = target_ts.vendor_info().clone();
if donor_page_size != target_page_size {
return Err(IdbError::Argument(format!(
"Page size mismatch: donor={}, target={}",
donor_page_size, target_page_size
)));
}
let page_size = donor_page_size;
let donor_page0 = donor_ts.read_page(0)?;
let target_page0 = target_ts.read_page(0)?;
let donor_space_id = BigEndian::read_u32(&donor_page0[FIL_PAGE_SPACE_ID..]);
let target_space_id = BigEndian::read_u32(&target_page0[FIL_PAGE_SPACE_ID..]);
if donor_space_id != target_space_id {
if !opts.force {
return Err(IdbError::Argument(format!(
"Space ID mismatch: donor={}, target={}. Use --force to override.",
donor_space_id, target_space_id
)));
}
if !opts.json {
wprintln!(
writer,
"{}: Space ID mismatch (donor={}, target={}), proceeding with --force",
"Warning".yellow(),
donor_space_id,
target_space_id
)?;
}
}
let backup_path = if !opts.no_backup && !opts.dry_run {
let path = write::create_backup(&opts.target)?;
if !opts.json {
wprintln!(writer, "Backup created: {}", path.display())?;
}
if let Some(ref logger) = opts.audit_logger {
let _ = logger.log_backup(&opts.target, &path.display().to_string());
}
Some(path)
} else {
None
};
if !opts.json && !opts.dry_run {
wprintln!(
writer,
"Transplanting {} pages from {} to {}...",
opts.pages.len(),
opts.donor,
opts.target
)?;
} else if !opts.json && opts.dry_run {
wprintln!(
writer,
"Dry run: previewing transplant of {} pages from {} to {}...",
opts.pages.len(),
opts.donor,
opts.target
)?;
}
let mut transplanted = 0u64;
let mut skipped = 0u64;
let mut page_details: Vec<PageTransplantInfo> = Vec::new();
for &page_num in &opts.pages {
if page_num == 0 && !opts.force {
if opts.verbose && !opts.json {
wprintln!(
writer,
"Page {:>4}: {} (FSP_HDR — use --force to override)",
page_num,
"skipped".yellow()
)?;
}
page_details.push(PageTransplantInfo {
page_number: page_num,
action: "skipped".to_string(),
reason: Some("FSP_HDR page — use --force to override".to_string()),
donor_checksum_valid: None,
post_checksum_valid: None,
});
skipped += 1;
continue;
}
if page_num >= donor_count {
if opts.verbose && !opts.json {
wprintln!(
writer,
"Page {:>4}: {} (out of range in donor, {} pages)",
page_num,
"skipped".yellow(),
donor_count
)?;
}
page_details.push(PageTransplantInfo {
page_number: page_num,
action: "skipped".to_string(),
reason: Some(format!("Out of range in donor ({} pages)", donor_count)),
donor_checksum_valid: None,
post_checksum_valid: None,
});
skipped += 1;
continue;
}
if page_num >= target_count {
if opts.verbose && !opts.json {
wprintln!(
writer,
"Page {:>4}: {} (out of range in target, {} pages)",
page_num,
"skipped".yellow(),
target_count
)?;
}
page_details.push(PageTransplantInfo {
page_number: page_num,
action: "skipped".to_string(),
reason: Some(format!("Out of range in target ({} pages)", target_count)),
donor_checksum_valid: None,
post_checksum_valid: None,
});
skipped += 1;
continue;
}
let donor_page = write::read_page_raw(&opts.donor, page_num, page_size)?;
let donor_valid = validate_checksum(&donor_page, page_size, Some(&donor_vendor)).valid;
if !donor_valid && !opts.force {
if opts.verbose && !opts.json {
wprintln!(
writer,
"Page {:>4}: {} (donor page has invalid checksum — use --force)",
page_num,
"skipped".yellow()
)?;
}
page_details.push(PageTransplantInfo {
page_number: page_num,
action: "skipped".to_string(),
reason: Some("Donor page has invalid checksum".to_string()),
donor_checksum_valid: Some(false),
post_checksum_valid: None,
});
skipped += 1;
continue;
}
if !donor_valid && opts.force && !opts.json {
wprintln!(
writer,
"{}: Donor page {} has invalid checksum, transplanting anyway (--force)",
"Warning".yellow(),
page_num
)?;
}
if !opts.dry_run {
write::write_page(&opts.target, page_num, page_size, &donor_page)?;
if let Some(ref logger) = opts.audit_logger {
let _ = logger.log_page_write(&opts.target, page_num, "transplant", None, None);
}
let written = write::read_page_raw(&opts.target, page_num, page_size)?;
let post_valid = validate_checksum(&written, page_size, Some(&target_vendor)).valid;
if opts.verbose && !opts.json {
let status = if post_valid {
"OK".green().to_string()
} else {
"CHECKSUM INVALID".red().to_string()
};
wprintln!(
writer,
"Page {:>4}: transplanted (post-validate: {})",
page_num,
status
)?;
}
page_details.push(PageTransplantInfo {
page_number: page_num,
action: "transplanted".to_string(),
reason: None,
donor_checksum_valid: Some(donor_valid),
post_checksum_valid: Some(post_valid),
});
} else {
if opts.verbose && !opts.json {
wprintln!(
writer,
"Page {:>4}: would transplant (donor checksum: {})",
page_num,
if donor_valid { "OK" } else { "INVALID" }
)?;
}
page_details.push(PageTransplantInfo {
page_number: page_num,
action: "would_transplant".to_string(),
reason: None,
donor_checksum_valid: Some(donor_valid),
post_checksum_valid: None,
});
}
transplanted += 1;
}
if opts.json {
let report = TransplantReport {
donor: opts.donor.clone(),
target: opts.target.clone(),
backup_path: backup_path.map(|p| p.display().to_string()),
dry_run: opts.dry_run,
transplanted,
skipped,
pages: page_details,
};
let json = serde_json::to_string_pretty(&report)
.map_err(|e| IdbError::Parse(format!("JSON serialization error: {}", e)))?;
wprintln!(writer, "{}", json)?;
} else {
wprintln!(writer)?;
wprintln!(writer, "Transplant Summary:")?;
if transplanted > 0 {
let label = if opts.dry_run {
"Would transplant"
} else {
"Transplanted"
};
wprintln!(
writer,
" {}: {}",
label,
format!("{}", transplanted).green()
)?;
} else {
wprintln!(writer, " Transplanted: 0")?;
}
if skipped > 0 {
wprintln!(
writer,
" Skipped: {}",
format!("{}", skipped).yellow()
)?;
} else {
wprintln!(writer, " Skipped: 0")?;
}
}
Ok(())
}