Skip to main content

idb/cli/
transplant.rs

1use std::io::Write;
2use std::sync::Arc;
3
4use byteorder::{BigEndian, ByteOrder};
5use colored::Colorize;
6use serde::Serialize;
7
8use crate::cli::wprintln;
9use crate::innodb::checksum::validate_checksum;
10use crate::innodb::constants::FIL_PAGE_SPACE_ID;
11use crate::innodb::write;
12use crate::util::audit::AuditLogger;
13use crate::IdbError;
14
15/// Options for the `inno transplant` subcommand.
16pub struct TransplantOptions {
17    /// Path to the donor tablespace file (source of pages).
18    pub donor: String,
19    /// Path to the target tablespace file (destination for pages).
20    pub target: String,
21    /// Page numbers to transplant from donor to target.
22    pub pages: Vec<u64>,
23    /// Skip creating a backup of the target.
24    pub no_backup: bool,
25    /// Allow transplanting despite space ID mismatch or corrupt donor pages.
26    pub force: bool,
27    /// Preview without modifying the target file.
28    pub dry_run: bool,
29    /// Show per-page details.
30    pub verbose: bool,
31    /// Emit output as JSON.
32    pub json: bool,
33    /// Override the auto-detected page size.
34    pub page_size: Option<u32>,
35    /// Path to MySQL keyring file for decrypting encrypted tablespaces.
36    pub keyring: Option<String>,
37    /// Use memory-mapped I/O for file access.
38    pub mmap: bool,
39    /// Audit logger for recording write operations.
40    pub audit_logger: Option<Arc<AuditLogger>>,
41}
42
43#[derive(Serialize)]
44struct TransplantReport {
45    donor: String,
46    target: String,
47    #[serde(skip_serializing_if = "Option::is_none")]
48    backup_path: Option<String>,
49    dry_run: bool,
50    transplanted: u64,
51    skipped: u64,
52    pages: Vec<PageTransplantInfo>,
53}
54
55#[derive(Serialize)]
56struct PageTransplantInfo {
57    page_number: u64,
58    action: String,
59    #[serde(skip_serializing_if = "Option::is_none")]
60    reason: Option<String>,
61    #[serde(skip_serializing_if = "Option::is_none")]
62    donor_checksum_valid: Option<bool>,
63    #[serde(skip_serializing_if = "Option::is_none")]
64    post_checksum_valid: Option<bool>,
65}
66
67/// Copy specific pages from a donor tablespace into a target.
68pub fn execute(opts: &TransplantOptions, writer: &mut dyn Write) -> Result<(), IdbError> {
69    if opts.pages.is_empty() {
70        return Err(IdbError::Argument(
71            "No pages specified. Use --pages to specify page numbers.".to_string(),
72        ));
73    }
74
75    // Open both tablespaces read-only to validate
76    let mut donor_ts = crate::cli::open_tablespace(&opts.donor, opts.page_size, opts.mmap)?;
77    let mut target_ts = crate::cli::open_tablespace(&opts.target, opts.page_size, opts.mmap)?;
78
79    if let Some(ref keyring_path) = opts.keyring {
80        crate::cli::setup_decryption(&mut donor_ts, keyring_path)?;
81        crate::cli::setup_decryption(&mut target_ts, keyring_path)?;
82    }
83
84    let donor_page_size = donor_ts.page_size();
85    let target_page_size = target_ts.page_size();
86    let donor_count = donor_ts.page_count();
87    let target_count = target_ts.page_count();
88    let donor_vendor = donor_ts.vendor_info().clone();
89    let target_vendor = target_ts.vendor_info().clone();
90
91    // Validate page sizes match
92    if donor_page_size != target_page_size {
93        return Err(IdbError::Argument(format!(
94            "Page size mismatch: donor={}, target={}",
95            donor_page_size, target_page_size
96        )));
97    }
98    let page_size = donor_page_size;
99
100    // Check space ID match
101    let donor_page0 = donor_ts.read_page(0)?;
102    let target_page0 = target_ts.read_page(0)?;
103    let donor_space_id = BigEndian::read_u32(&donor_page0[FIL_PAGE_SPACE_ID..]);
104    let target_space_id = BigEndian::read_u32(&target_page0[FIL_PAGE_SPACE_ID..]);
105
106    if donor_space_id != target_space_id {
107        if !opts.force {
108            return Err(IdbError::Argument(format!(
109                "Space ID mismatch: donor={}, target={}. Use --force to override.",
110                donor_space_id, target_space_id
111            )));
112        }
113        if !opts.json {
114            wprintln!(
115                writer,
116                "{}: Space ID mismatch (donor={}, target={}), proceeding with --force",
117                "Warning".yellow(),
118                donor_space_id,
119                target_space_id
120            )?;
121        }
122    }
123
124    // Create backup unless --no-backup or --dry-run
125    let backup_path = if !opts.no_backup && !opts.dry_run {
126        let path = write::create_backup(&opts.target)?;
127        if !opts.json {
128            wprintln!(writer, "Backup created: {}", path.display())?;
129        }
130        if let Some(ref logger) = opts.audit_logger {
131            let _ = logger.log_backup(&opts.target, &path.display().to_string());
132        }
133        Some(path)
134    } else {
135        None
136    };
137
138    if !opts.json && !opts.dry_run {
139        wprintln!(
140            writer,
141            "Transplanting {} pages from {} to {}...",
142            opts.pages.len(),
143            opts.donor,
144            opts.target
145        )?;
146    } else if !opts.json && opts.dry_run {
147        wprintln!(
148            writer,
149            "Dry run: previewing transplant of {} pages from {} to {}...",
150            opts.pages.len(),
151            opts.donor,
152            opts.target
153        )?;
154    }
155
156    let mut transplanted = 0u64;
157    let mut skipped = 0u64;
158    let mut page_details: Vec<PageTransplantInfo> = Vec::new();
159
160    for &page_num in &opts.pages {
161        // Reject page 0 unless --force
162        if page_num == 0 && !opts.force {
163            if opts.verbose && !opts.json {
164                wprintln!(
165                    writer,
166                    "Page {:>4}: {} (FSP_HDR — use --force to override)",
167                    page_num,
168                    "skipped".yellow()
169                )?;
170            }
171            page_details.push(PageTransplantInfo {
172                page_number: page_num,
173                action: "skipped".to_string(),
174                reason: Some("FSP_HDR page — use --force to override".to_string()),
175                donor_checksum_valid: None,
176                post_checksum_valid: None,
177            });
178            skipped += 1;
179            continue;
180        }
181
182        // Check page number in range
183        if page_num >= donor_count {
184            if opts.verbose && !opts.json {
185                wprintln!(
186                    writer,
187                    "Page {:>4}: {} (out of range in donor, {} pages)",
188                    page_num,
189                    "skipped".yellow(),
190                    donor_count
191                )?;
192            }
193            page_details.push(PageTransplantInfo {
194                page_number: page_num,
195                action: "skipped".to_string(),
196                reason: Some(format!("Out of range in donor ({} pages)", donor_count)),
197                donor_checksum_valid: None,
198                post_checksum_valid: None,
199            });
200            skipped += 1;
201            continue;
202        }
203
204        if page_num >= target_count {
205            if opts.verbose && !opts.json {
206                wprintln!(
207                    writer,
208                    "Page {:>4}: {} (out of range in target, {} pages)",
209                    page_num,
210                    "skipped".yellow(),
211                    target_count
212                )?;
213            }
214            page_details.push(PageTransplantInfo {
215                page_number: page_num,
216                action: "skipped".to_string(),
217                reason: Some(format!("Out of range in target ({} pages)", target_count)),
218                donor_checksum_valid: None,
219                post_checksum_valid: None,
220            });
221            skipped += 1;
222            continue;
223        }
224
225        // Read donor page and validate
226        let donor_page = write::read_page_raw(&opts.donor, page_num, page_size)?;
227        let donor_valid = validate_checksum(&donor_page, page_size, Some(&donor_vendor)).valid;
228
229        if !donor_valid && !opts.force {
230            if opts.verbose && !opts.json {
231                wprintln!(
232                    writer,
233                    "Page {:>4}: {} (donor page has invalid checksum — use --force)",
234                    page_num,
235                    "skipped".yellow()
236                )?;
237            }
238            page_details.push(PageTransplantInfo {
239                page_number: page_num,
240                action: "skipped".to_string(),
241                reason: Some("Donor page has invalid checksum".to_string()),
242                donor_checksum_valid: Some(false),
243                post_checksum_valid: None,
244            });
245            skipped += 1;
246            continue;
247        }
248
249        if !donor_valid && opts.force && !opts.json {
250            wprintln!(
251                writer,
252                "{}: Donor page {} has invalid checksum, transplanting anyway (--force)",
253                "Warning".yellow(),
254                page_num
255            )?;
256        }
257
258        // Write to target
259        if !opts.dry_run {
260            write::write_page(&opts.target, page_num, page_size, &donor_page)?;
261            if let Some(ref logger) = opts.audit_logger {
262                let _ = logger.log_page_write(&opts.target, page_num, "transplant", None, None);
263            }
264
265            // Post-validate
266            let written = write::read_page_raw(&opts.target, page_num, page_size)?;
267            let post_valid = validate_checksum(&written, page_size, Some(&target_vendor)).valid;
268
269            if opts.verbose && !opts.json {
270                let status = if post_valid {
271                    "OK".green().to_string()
272                } else {
273                    "CHECKSUM INVALID".red().to_string()
274                };
275                wprintln!(
276                    writer,
277                    "Page {:>4}: transplanted (post-validate: {})",
278                    page_num,
279                    status
280                )?;
281            }
282
283            page_details.push(PageTransplantInfo {
284                page_number: page_num,
285                action: "transplanted".to_string(),
286                reason: None,
287                donor_checksum_valid: Some(donor_valid),
288                post_checksum_valid: Some(post_valid),
289            });
290        } else {
291            if opts.verbose && !opts.json {
292                wprintln!(
293                    writer,
294                    "Page {:>4}: would transplant (donor checksum: {})",
295                    page_num,
296                    if donor_valid { "OK" } else { "INVALID" }
297                )?;
298            }
299            page_details.push(PageTransplantInfo {
300                page_number: page_num,
301                action: "would_transplant".to_string(),
302                reason: None,
303                donor_checksum_valid: Some(donor_valid),
304                post_checksum_valid: None,
305            });
306        }
307
308        transplanted += 1;
309    }
310
311    if opts.json {
312        let report = TransplantReport {
313            donor: opts.donor.clone(),
314            target: opts.target.clone(),
315            backup_path: backup_path.map(|p| p.display().to_string()),
316            dry_run: opts.dry_run,
317            transplanted,
318            skipped,
319            pages: page_details,
320        };
321        let json = serde_json::to_string_pretty(&report)
322            .map_err(|e| IdbError::Parse(format!("JSON serialization error: {}", e)))?;
323        wprintln!(writer, "{}", json)?;
324    } else {
325        wprintln!(writer)?;
326        wprintln!(writer, "Transplant Summary:")?;
327        if transplanted > 0 {
328            let label = if opts.dry_run {
329                "Would transplant"
330            } else {
331                "Transplanted"
332            };
333            wprintln!(
334                writer,
335                "  {}: {}",
336                label,
337                format!("{}", transplanted).green()
338            )?;
339        } else {
340            wprintln!(writer, "  Transplanted: 0")?;
341        }
342        if skipped > 0 {
343            wprintln!(
344                writer,
345                "  Skipped:      {}",
346                format!("{}", skipped).yellow()
347            )?;
348        } else {
349            wprintln!(writer, "  Skipped:      0")?;
350        }
351    }
352
353    Ok(())
354}