Skip to main content

idb/innodb/
write.rs

1//! Write utilities for InnoDB page-level operations.
2//!
3//! Provides reusable functions for creating backups, reading/writing individual
4//! pages, constructing valid FSP_HDR pages, detecting checksum algorithms, and
5//! fixing page checksums. These are the building blocks for the `repair`,
6//! `defrag`, `transplant`, and `recover --rebuild` subcommands.
7//!
8//! This module is not available on WASM targets since file I/O is not supported.
9
10use std::fs::{self, File, OpenOptions};
11use std::io::{Read, Seek, SeekFrom, Write};
12use std::path::{Path, PathBuf};
13
14use byteorder::{BigEndian, ByteOrder};
15
16use crate::innodb::checksum::{recalculate_checksum, validate_checksum, ChecksumAlgorithm};
17use crate::innodb::constants::*;
18use crate::innodb::vendor::VendorInfo;
19use crate::IdbError;
20
21/// Create a backup of a file by copying it to `<path>.bak`.
22///
23/// If `<path>.bak` already exists, tries `.bak.1`, `.bak.2`, etc.
24/// Returns the path of the created backup file.
25pub fn create_backup(path: &str) -> Result<PathBuf, IdbError> {
26    let src = Path::new(path);
27    if !src.exists() {
28        return Err(IdbError::Io(format!("File not found: {}", path)));
29    }
30
31    let mut backup_path = PathBuf::from(format!("{}.bak", path));
32    let mut counter = 1u32;
33    while backup_path.exists() {
34        backup_path = PathBuf::from(format!("{}.bak.{}", path, counter));
35        counter += 1;
36        if counter > 999 {
37            return Err(IdbError::Io(format!("Too many backup files for {}", path)));
38        }
39    }
40
41    fs::copy(src, &backup_path).map_err(|e| {
42        IdbError::Io(format!(
43            "Cannot create backup {}: {}",
44            backup_path.display(),
45            e
46        ))
47    })?;
48
49    Ok(backup_path)
50}
51
52/// Read a single page from a tablespace file.
53///
54/// Returns a `Vec<u8>` of exactly `page_size` bytes.
55pub fn read_page_raw(path: &str, page_num: u64, page_size: u32) -> Result<Vec<u8>, IdbError> {
56    let offset = page_num * page_size as u64;
57    let mut f =
58        File::open(path).map_err(|e| IdbError::Io(format!("Cannot open {}: {}", path, e)))?;
59    f.seek(SeekFrom::Start(offset))
60        .map_err(|e| IdbError::Io(format!("Cannot seek to offset {}: {}", offset, e)))?;
61    let mut buf = vec![0u8; page_size as usize];
62    f.read_exact(&mut buf)
63        .map_err(|e| IdbError::Io(format!("Cannot read page {}: {}", page_num, e)))?;
64    Ok(buf)
65}
66
67/// Write a single page to a tablespace file at the correct offset.
68///
69/// The `data` slice must be exactly `page_size` bytes.
70pub fn write_page(path: &str, page_num: u64, page_size: u32, data: &[u8]) -> Result<(), IdbError> {
71    if data.len() != page_size as usize {
72        return Err(IdbError::Argument(format!(
73            "Page data is {} bytes, expected {}",
74            data.len(),
75            page_size
76        )));
77    }
78    let offset = page_num * page_size as u64;
79    let mut f = OpenOptions::new()
80        .write(true)
81        .open(path)
82        .map_err(|e| IdbError::Io(format!("Cannot open {} for writing: {}", path, e)))?;
83    f.seek(SeekFrom::Start(offset))
84        .map_err(|e| IdbError::Io(format!("Cannot seek to offset {}: {}", offset, e)))?;
85    f.write_all(data)
86        .map_err(|e| IdbError::Io(format!("Cannot write page {}: {}", page_num, e)))?;
87    Ok(())
88}
89
90/// Write all pages sequentially to a new file.
91///
92/// Creates (or truncates) the file at `path` and writes each page in order.
93pub fn write_tablespace(path: &str, pages: &[Vec<u8>]) -> Result<(), IdbError> {
94    let mut f =
95        File::create(path).map_err(|e| IdbError::Io(format!("Cannot create {}: {}", path, e)))?;
96    for (i, page) in pages.iter().enumerate() {
97        f.write_all(page)
98            .map_err(|e| IdbError::Io(format!("Cannot write page {}: {}", i, e)))?;
99    }
100    f.flush()
101        .map_err(|e| IdbError::Io(format!("Cannot flush {}: {}", path, e)))?;
102    Ok(())
103}
104
105/// Build a minimal valid FSP_HDR page (page 0) with correct FIL header,
106/// FSP header fields, FIL trailer, and checksum.
107pub fn build_fsp_page(
108    space_id: u32,
109    total_pages: u32,
110    flags: u32,
111    lsn: u64,
112    page_size: u32,
113    algorithm: ChecksumAlgorithm,
114) -> Vec<u8> {
115    let ps = page_size as usize;
116    let mut page = vec![0u8; ps];
117
118    // FIL header
119    BigEndian::write_u32(&mut page[FIL_PAGE_OFFSET..], 0); // page 0
120    BigEndian::write_u32(&mut page[FIL_PAGE_PREV..], FIL_NULL);
121    BigEndian::write_u32(&mut page[FIL_PAGE_NEXT..], FIL_NULL);
122    BigEndian::write_u64(&mut page[FIL_PAGE_LSN..], lsn);
123    BigEndian::write_u16(&mut page[FIL_PAGE_TYPE..], 8); // FSP_HDR
124    BigEndian::write_u64(&mut page[FIL_PAGE_FILE_FLUSH_LSN..], lsn);
125    BigEndian::write_u32(&mut page[FIL_PAGE_SPACE_ID..], space_id);
126
127    // FSP header at FIL_PAGE_DATA (offset 38)
128    let fsp = FIL_PAGE_DATA;
129    BigEndian::write_u32(&mut page[fsp + FSP_SPACE_ID..], space_id);
130    BigEndian::write_u32(&mut page[fsp + FSP_SIZE..], total_pages);
131    BigEndian::write_u32(&mut page[fsp + FSP_FREE_LIMIT..], total_pages);
132    BigEndian::write_u32(&mut page[fsp + FSP_SPACE_FLAGS..], flags);
133
134    // FIL trailer: low 32 bits of LSN
135    let trailer = ps - SIZE_FIL_TRAILER;
136    BigEndian::write_u32(&mut page[trailer + 4..], (lsn & 0xFFFFFFFF) as u32);
137
138    // Calculate and write checksum
139    recalculate_checksum(&mut page, page_size, algorithm);
140
141    page
142}
143
144/// Detect the checksum algorithm in use for a page.
145///
146/// Mirrors the logic of `validate_checksum()` but returns the detected
147/// algorithm instead of a full validation result. Returns `None` for
148/// empty/zero pages and `BUF_NO_CHECKSUM_MAGIC` pages.
149pub fn detect_algorithm(
150    page_data: &[u8],
151    page_size: u32,
152    vendor_info: Option<&VendorInfo>,
153) -> ChecksumAlgorithm {
154    let result = validate_checksum(page_data, page_size, vendor_info);
155    result.algorithm
156}
157
158/// Fix the checksum and trailer LSN consistency for a page.
159///
160/// 1. Ensures the trailer LSN low-32 matches the header LSN low-32.
161/// 2. Recalculates the checksum using the given algorithm.
162///
163/// Returns `(old_checksum, new_checksum)`.
164pub fn fix_page_checksum(
165    page_data: &mut [u8],
166    page_size: u32,
167    algorithm: ChecksumAlgorithm,
168) -> (u32, u32) {
169    let ps = page_size as usize;
170    if page_data.len() < ps {
171        return (0, 0);
172    }
173
174    // Read old checksum
175    let old_checksum = match algorithm {
176        ChecksumAlgorithm::MariaDbFullCrc32 => BigEndian::read_u32(&page_data[ps - 4..]),
177        _ => BigEndian::read_u32(&page_data[FIL_PAGE_SPACE_OR_CHKSUM..]),
178    };
179
180    // Fix trailer LSN consistency: low 32 bits of header LSN → trailer
181    let header_lsn = BigEndian::read_u64(&page_data[FIL_PAGE_LSN..]);
182    let trailer_offset = ps - SIZE_FIL_TRAILER;
183    BigEndian::write_u32(
184        &mut page_data[trailer_offset + 4..],
185        (header_lsn & 0xFFFFFFFF) as u32,
186    );
187
188    // Recalculate checksum
189    recalculate_checksum(page_data, page_size, algorithm);
190
191    // Read new checksum
192    let new_checksum = match algorithm {
193        ChecksumAlgorithm::MariaDbFullCrc32 => BigEndian::read_u32(&page_data[ps - 4..]),
194        _ => BigEndian::read_u32(&page_data[FIL_PAGE_SPACE_OR_CHKSUM..]),
195    };
196
197    (old_checksum, new_checksum)
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203    use crate::innodb::checksum::validate_lsn;
204    use crate::innodb::tablespace::Tablespace;
205    use std::io::Write as IoWrite;
206    use tempfile::NamedTempFile;
207
208    const PS: u32 = 16384;
209
210    fn make_test_page(page_num: u32, space_id: u32, lsn: u64) -> Vec<u8> {
211        let ps = PS as usize;
212        let mut page = vec![0u8; ps];
213        BigEndian::write_u32(&mut page[FIL_PAGE_OFFSET..], page_num);
214        BigEndian::write_u32(&mut page[FIL_PAGE_PREV..], FIL_NULL);
215        BigEndian::write_u32(&mut page[FIL_PAGE_NEXT..], FIL_NULL);
216        BigEndian::write_u64(&mut page[FIL_PAGE_LSN..], lsn);
217        BigEndian::write_u16(&mut page[FIL_PAGE_TYPE..], 17855); // INDEX
218        BigEndian::write_u32(&mut page[FIL_PAGE_SPACE_ID..], space_id);
219        let trailer = ps - SIZE_FIL_TRAILER;
220        BigEndian::write_u32(&mut page[trailer + 4..], (lsn & 0xFFFFFFFF) as u32);
221        recalculate_checksum(&mut page, PS, ChecksumAlgorithm::Crc32c);
222        page
223    }
224
225    fn write_temp_tablespace(pages: &[Vec<u8>]) -> NamedTempFile {
226        let mut tmp = NamedTempFile::new().unwrap();
227        for page in pages {
228            tmp.write_all(page).unwrap();
229        }
230        tmp.flush().unwrap();
231        tmp
232    }
233
234    #[test]
235    fn test_create_backup() {
236        let mut tmp = NamedTempFile::new().unwrap();
237        tmp.write_all(b"test data").unwrap();
238        tmp.flush().unwrap();
239
240        let path = tmp.path().to_str().unwrap();
241        let backup = create_backup(path).unwrap();
242        assert!(backup.exists());
243        assert_eq!(fs::read(&backup).unwrap(), b"test data");
244
245        // Second backup gets .bak.1
246        let backup2 = create_backup(path).unwrap();
247        assert!(backup2.exists());
248        assert_ne!(backup, backup2);
249
250        // Cleanup
251        let _ = fs::remove_file(&backup);
252        let _ = fs::remove_file(&backup2);
253    }
254
255    #[test]
256    fn test_read_page_raw_roundtrip() {
257        let page0 = build_fsp_page(42, 2, 0, 1000, PS, ChecksumAlgorithm::Crc32c);
258        let page1 = make_test_page(1, 42, 2000);
259        let tmp = write_temp_tablespace(&[page0.clone(), page1.clone()]);
260        let path = tmp.path().to_str().unwrap();
261
262        let read0 = read_page_raw(path, 0, PS).unwrap();
263        assert_eq!(read0, page0);
264
265        let read1 = read_page_raw(path, 1, PS).unwrap();
266        assert_eq!(read1, page1);
267    }
268
269    #[test]
270    fn test_write_page_modifies_correct_offset() {
271        let page0 = build_fsp_page(42, 2, 0, 1000, PS, ChecksumAlgorithm::Crc32c);
272        let page1 = make_test_page(1, 42, 2000);
273        let tmp = write_temp_tablespace(&[page0, page1]);
274        let path = tmp.path().to_str().unwrap();
275
276        // Overwrite page 1 with a new page
277        let new_page1 = make_test_page(1, 42, 9999);
278        write_page(path, 1, PS, &new_page1).unwrap();
279
280        // Verify page 1 changed
281        let read1 = read_page_raw(path, 1, PS).unwrap();
282        assert_eq!(read1, new_page1);
283
284        // Verify page 0 untouched
285        let read0 = read_page_raw(path, 0, PS).unwrap();
286        let result = validate_checksum(&read0, PS, None);
287        assert!(result.valid);
288    }
289
290    #[test]
291    fn test_build_fsp_page_valid() {
292        let page = build_fsp_page(42, 5, 0, 1000, PS, ChecksumAlgorithm::Crc32c);
293        assert_eq!(page.len(), PS as usize);
294
295        // Validate checksum
296        let result = validate_checksum(&page, PS, None);
297        assert!(result.valid);
298
299        // Validate LSN consistency
300        assert!(validate_lsn(&page, PS));
301
302        // Check space_id in FIL header
303        assert_eq!(BigEndian::read_u32(&page[FIL_PAGE_SPACE_ID..]), 42);
304
305        // Check FSP header
306        let fsp = FIL_PAGE_DATA;
307        assert_eq!(BigEndian::read_u32(&page[fsp + FSP_SPACE_ID..]), 42);
308        assert_eq!(BigEndian::read_u32(&page[fsp + FSP_SIZE..]), 5);
309    }
310
311    #[test]
312    fn test_build_fsp_page_opens_as_tablespace() {
313        let page0 = build_fsp_page(42, 3, 0, 1000, PS, ChecksumAlgorithm::Crc32c);
314        let page1 = make_test_page(1, 42, 2000);
315        let page2 = make_test_page(2, 42, 3000);
316        let tmp = write_temp_tablespace(&[page0, page1, page2]);
317
318        let ts = Tablespace::open(tmp.path()).unwrap();
319        assert_eq!(ts.page_size(), PS);
320        assert_eq!(ts.page_count(), 3);
321    }
322
323    #[test]
324    fn test_fix_page_checksum_crc32c() {
325        let mut page = make_test_page(1, 42, 5000);
326
327        // Corrupt checksum
328        BigEndian::write_u32(&mut page[FIL_PAGE_SPACE_OR_CHKSUM..], 0xDEAD);
329        assert!(!validate_checksum(&page, PS, None).valid);
330
331        // Fix
332        let (old, new) = fix_page_checksum(&mut page, PS, ChecksumAlgorithm::Crc32c);
333        assert_eq!(old, 0xDEAD);
334        assert_ne!(new, 0xDEAD);
335        assert!(validate_checksum(&page, PS, None).valid);
336        assert!(validate_lsn(&page, PS));
337    }
338
339    #[test]
340    fn test_fix_page_checksum_fixes_trailer_lsn() {
341        let ps = PS as usize;
342        let mut page = make_test_page(1, 42, 5000);
343
344        // Corrupt trailer LSN
345        let trailer = ps - SIZE_FIL_TRAILER;
346        BigEndian::write_u32(&mut page[trailer + 4..], 0xAAAA);
347        assert!(!validate_lsn(&page, PS));
348
349        // Fix
350        fix_page_checksum(&mut page, PS, ChecksumAlgorithm::Crc32c);
351        assert!(validate_lsn(&page, PS));
352        assert!(validate_checksum(&page, PS, None).valid);
353    }
354
355    #[test]
356    fn test_write_tablespace_creates_file() {
357        let page0 = build_fsp_page(42, 2, 0, 1000, PS, ChecksumAlgorithm::Crc32c);
358        let page1 = make_test_page(1, 42, 2000);
359
360        let tmp = tempfile::NamedTempFile::new().unwrap();
361        let path = tmp.path().to_str().unwrap().to_string();
362        drop(tmp); // Delete so write_tablespace creates it
363
364        write_tablespace(&path, &[page0.clone(), page1.clone()]).unwrap();
365
366        // Verify we can open it
367        let ts = Tablespace::open(&path).unwrap();
368        assert_eq!(ts.page_size(), PS);
369        assert_eq!(ts.page_count(), 2);
370
371        let _ = fs::remove_file(&path);
372    }
373
374    #[test]
375    fn test_detect_algorithm_crc32c() {
376        let page = make_test_page(1, 42, 5000);
377        let algo = detect_algorithm(&page, PS, None);
378        assert_eq!(algo, ChecksumAlgorithm::Crc32c);
379    }
380}