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
15pub struct TransplantOptions {
17 pub donor: String,
19 pub target: String,
21 pub pages: Vec<u64>,
23 pub no_backup: bool,
25 pub force: bool,
27 pub dry_run: bool,
29 pub verbose: bool,
31 pub json: bool,
33 pub page_size: Option<u32>,
35 pub keyring: Option<String>,
37 pub mmap: bool,
39 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
67pub 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 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 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 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 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 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 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 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 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 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}