Skip to main content

idb/cli/
corrupt.rs

1use std::fs::{File, OpenOptions};
2use std::io::{Seek, SeekFrom, Write};
3
4use colored::Colorize;
5use rand::Rng;
6use serde::Serialize;
7
8use crate::cli::wprintln;
9use crate::innodb::checksum::{validate_checksum, ChecksumAlgorithm};
10use crate::innodb::constants::{SIZE_FIL_HEAD, SIZE_FIL_TRAILER};
11use crate::innodb::tablespace::Tablespace;
12use crate::util::hex::format_bytes;
13use crate::IdbError;
14
15pub struct CorruptOptions {
16    pub file: String,
17    pub page: Option<u64>,
18    pub bytes: usize,
19    pub header: bool,
20    pub records: bool,
21    pub offset: Option<u64>,
22    pub verify: bool,
23    pub json: bool,
24    pub page_size: Option<u32>,
25}
26
27#[derive(Serialize)]
28struct CorruptResultJson {
29    file: String,
30    offset: u64,
31    page: Option<u64>,
32    bytes_written: usize,
33    data: String,
34    #[serde(skip_serializing_if = "Option::is_none")]
35    verify: Option<VerifyResultJson>,
36}
37
38#[derive(Serialize)]
39struct VerifyResultJson {
40    page: u64,
41    before: ChecksumInfoJson,
42    after: ChecksumInfoJson,
43}
44
45#[derive(Serialize)]
46struct ChecksumInfoJson {
47    valid: bool,
48    algorithm: String,
49    stored_checksum: u32,
50    calculated_checksum: u32,
51}
52
53pub fn execute(opts: &CorruptOptions, writer: &mut dyn Write) -> Result<(), IdbError> {
54    // Absolute offset mode: bypass page calculation entirely
55    if let Some(abs_offset) = opts.offset {
56        return corrupt_at_offset(opts, abs_offset, writer);
57    }
58
59    // Open tablespace to get page size and count
60    let ts = match opts.page_size {
61        Some(ps) => Tablespace::open_with_page_size(&opts.file, ps)?,
62        None => Tablespace::open(&opts.file)?,
63    };
64
65    let page_size = ts.page_size() as usize;
66    let page_count = ts.page_count();
67
68    let mut rng = rand::rng();
69
70    // Choose page
71    let page_num = match opts.page {
72        Some(p) => {
73            if p >= page_count {
74                return Err(IdbError::Argument(format!(
75                    "Page {} out of range (tablespace has {} pages)",
76                    p, page_count
77                )));
78            }
79            p
80        }
81        None => {
82            let p = rng.random_range(0..page_count);
83            if !opts.json {
84                wprintln!(
85                    writer,
86                    "No page specified. Choosing random page {}.",
87                    format!("{}", p).yellow()
88                )?;
89            }
90            p
91        }
92    };
93
94    let byte_start = page_num * page_size as u64;
95
96    // Calculate the offset to corrupt within the page
97    let corrupt_offset = if opts.header {
98        // Corrupt within the FIL header area (first 38 bytes)
99        let header_offset = rng.random_range(0..SIZE_FIL_HEAD as u64);
100        byte_start + header_offset
101    } else if opts.records {
102        // Corrupt within the record data area (after page header, before trailer)
103        let user_data_start = 120u64; // matches Perl USER_DATA_START
104        let max_offset = page_size as u64 - user_data_start - SIZE_FIL_TRAILER as u64;
105        let record_offset = rng.random_range(0..max_offset);
106        byte_start + user_data_start + record_offset
107    } else {
108        // Default: corrupt at page start
109        byte_start
110    };
111
112    // Generate random bytes (full bytes, not nibbles like the Perl version)
113    let random_data: Vec<u8> = (0..opts.bytes).map(|_| rng.random::<u8>()).collect();
114
115    // Read pre-corruption page data for --verify
116    let pre_checksum = if opts.verify {
117        let pre_data = read_page_bytes(&opts.file, page_num, page_size as u32)?;
118        Some(validate_checksum(&pre_data, page_size as u32))
119    } else {
120        None
121    };
122
123    if opts.json {
124        // Write the corruption first, then verify
125        write_corruption(&opts.file, corrupt_offset, &random_data)?;
126        let verify_json = if opts.verify {
127            let post_data = read_page_bytes(&opts.file, page_num, page_size as u32)?;
128            let post_result = validate_checksum(&post_data, page_size as u32);
129            let pre = pre_checksum.unwrap();
130            Some(VerifyResultJson {
131                page: page_num,
132                before: checksum_to_json(&pre),
133                after: checksum_to_json(&post_result),
134            })
135        } else {
136            None
137        };
138        return output_json_with_verify(opts, corrupt_offset, Some(page_num), &random_data, verify_json, writer);
139    }
140
141    wprintln!(
142        writer,
143        "Writing {} bytes of random data to {} at offset {} (page {})...",
144        opts.bytes,
145        opts.file,
146        corrupt_offset,
147        format!("{}", page_num).yellow()
148    )?;
149
150    write_corruption(&opts.file, corrupt_offset, &random_data)?;
151
152    wprintln!(writer, "Data written: {}", format_bytes(&random_data).red())?;
153    wprintln!(writer, "Completed.")?;
154
155    // --verify: show before/after checksum comparison
156    if opts.verify {
157        let post_data = read_page_bytes(&opts.file, page_num, page_size as u32)?;
158        let post_result = validate_checksum(&post_data, page_size as u32);
159        let pre = pre_checksum.unwrap();
160        wprintln!(writer)?;
161        wprintln!(writer, "{}:", "Verification".bold())?;
162        wprintln!(
163            writer,
164            "  Before: {} (algorithm={:?}, stored={}, calculated={})",
165            if pre.valid { "OK".green().to_string() } else { "INVALID".red().to_string() },
166            pre.algorithm, pre.stored_checksum, pre.calculated_checksum
167        )?;
168        wprintln!(
169            writer,
170            "  After:  {} (algorithm={:?}, stored={}, calculated={})",
171            if post_result.valid { "OK".green().to_string() } else { "INVALID".red().to_string() },
172            post_result.algorithm, post_result.stored_checksum, post_result.calculated_checksum
173        )?;
174    }
175
176    Ok(())
177}
178
179fn corrupt_at_offset(opts: &CorruptOptions, abs_offset: u64, writer: &mut dyn Write) -> Result<(), IdbError> {
180    // Validate offset is within file
181    let file_size = File::open(&opts.file)
182        .map_err(|e| IdbError::Io(format!("Cannot open {}: {}", opts.file, e)))?
183        .metadata()
184        .map_err(|e| IdbError::Io(format!("Cannot stat {}: {}", opts.file, e)))?
185        .len();
186
187    if abs_offset >= file_size {
188        return Err(IdbError::Argument(format!(
189            "Offset {} is beyond file size {}",
190            abs_offset, file_size
191        )));
192    }
193
194    let mut rng = rand::rng();
195    let random_data: Vec<u8> = (0..opts.bytes).map(|_| rng.random::<u8>()).collect();
196
197    // Write the corruption
198    write_corruption(&opts.file, abs_offset, &random_data)?;
199
200    if opts.json {
201        return output_json_with_verify(opts, abs_offset, None, &random_data, None, writer);
202    }
203
204    wprintln!(
205        writer,
206        "Writing {} bytes of random data to {} at offset {}...",
207        opts.bytes, opts.file, abs_offset
208    )?;
209
210    wprintln!(writer, "Data written: {}", format_bytes(&random_data).red())?;
211    wprintln!(writer, "Completed.")?;
212
213    if opts.verify {
214        wprintln!(writer, "Note: --verify is not available in absolute offset mode (no page context).")?;
215    }
216
217    Ok(())
218}
219
220fn write_corruption(file_path: &str, offset: u64, data: &[u8]) -> Result<(), IdbError> {
221    let mut file = OpenOptions::new()
222        .write(true)
223        .open(file_path)
224        .map_err(|e| IdbError::Io(format!("Cannot open {} for writing: {}", file_path, e)))?;
225
226    file.seek(SeekFrom::Start(offset))
227        .map_err(|e| IdbError::Io(format!("Cannot seek to offset {}: {}", offset, e)))?;
228
229    file.write_all(data)
230        .map_err(|e| IdbError::Io(format!("Cannot write corruption data: {}", e)))?;
231
232    Ok(())
233}
234
235fn output_json_with_verify(
236    opts: &CorruptOptions,
237    offset: u64,
238    page: Option<u64>,
239    data: &[u8],
240    verify: Option<VerifyResultJson>,
241    writer: &mut dyn Write,
242) -> Result<(), IdbError> {
243    let result = CorruptResultJson {
244        file: opts.file.clone(),
245        offset,
246        page,
247        bytes_written: data.len(),
248        data: format_bytes(data),
249        verify,
250    };
251
252    let json = serde_json::to_string_pretty(&result)
253        .map_err(|e| IdbError::Parse(format!("JSON serialization error: {}", e)))?;
254    wprintln!(writer, "{}", json)?;
255
256    Ok(())
257}
258
259fn read_page_bytes(file_path: &str, page_num: u64, page_size: u32) -> Result<Vec<u8>, IdbError> {
260    use std::io::Read;
261    let offset = page_num * page_size as u64;
262    let mut f = File::open(file_path)
263        .map_err(|e| IdbError::Io(format!("Cannot open {}: {}", file_path, e)))?;
264    f.seek(SeekFrom::Start(offset))
265        .map_err(|e| IdbError::Io(format!("Cannot seek to offset {}: {}", offset, e)))?;
266    let mut buf = vec![0u8; page_size as usize];
267    f.read_exact(&mut buf)
268        .map_err(|e| IdbError::Io(format!("Cannot read page {}: {}", page_num, e)))?;
269    Ok(buf)
270}
271
272fn checksum_to_json(result: &crate::innodb::checksum::ChecksumResult) -> ChecksumInfoJson {
273    let algorithm_name = match result.algorithm {
274        ChecksumAlgorithm::Crc32c => "crc32c",
275        ChecksumAlgorithm::InnoDB => "innodb",
276        ChecksumAlgorithm::None => "none",
277    };
278    ChecksumInfoJson {
279        valid: result.valid,
280        algorithm: algorithm_name.to_string(),
281        stored_checksum: result.stored_checksum,
282        calculated_checksum: result.calculated_checksum,
283    }
284}