#![cfg(feature = "cli")]
use byteorder::{BigEndian, ByteOrder};
use std::io::Write;
use tempfile::NamedTempFile;
use idb::innodb::checksum::{recalculate_checksum, ChecksumAlgorithm};
use idb::innodb::constants::*;
use idb::innodb::write;
const PAGE_SIZE: u32 = 16384;
const PS: usize = PAGE_SIZE as usize;
fn build_fsp_hdr_page(space_id: u32, total_pages: u32) -> Vec<u8> {
write::build_fsp_page(
space_id,
total_pages,
0,
1000,
PAGE_SIZE,
ChecksumAlgorithm::Crc32c,
)
}
fn build_index_page(page_num: u32, space_id: u32, lsn: u64, prev: u32, next: u32) -> Vec<u8> {
let mut page = vec![0u8; PS];
BigEndian::write_u32(&mut page[FIL_PAGE_OFFSET..], page_num);
BigEndian::write_u32(&mut page[FIL_PAGE_PREV..], prev);
BigEndian::write_u32(&mut page[FIL_PAGE_NEXT..], next);
BigEndian::write_u64(&mut page[FIL_PAGE_LSN..], lsn);
BigEndian::write_u16(&mut page[FIL_PAGE_TYPE..], 17855); BigEndian::write_u32(&mut page[FIL_PAGE_SPACE_ID..], space_id);
let ph = FIL_PAGE_DATA;
BigEndian::write_u16(&mut page[ph + PAGE_N_DIR_SLOTS..], 2);
BigEndian::write_u16(&mut page[ph + PAGE_HEAP_TOP..], 200);
BigEndian::write_u16(&mut page[ph + PAGE_N_HEAP..], 0x8002); BigEndian::write_u16(&mut page[ph + PAGE_N_RECS..], 1);
BigEndian::write_u16(&mut page[ph + PAGE_LEVEL..], 0); BigEndian::write_u64(&mut page[ph + PAGE_INDEX_ID..], 100);
let trailer = PS - SIZE_FIL_TRAILER;
BigEndian::write_u32(&mut page[trailer + 4..], (lsn & 0xFFFFFFFF) as u32);
recalculate_checksum(&mut page, PAGE_SIZE, ChecksumAlgorithm::Crc32c);
page
}
fn write_tablespace(pages: &[Vec<u8>]) -> NamedTempFile {
let mut tmp = NamedTempFile::new().unwrap();
for page in pages {
tmp.write_all(page).unwrap();
}
tmp.flush().unwrap();
tmp
}
#[test]
fn test_verify_passes_valid_tablespace() {
let page0 = build_fsp_hdr_page(42, 4);
let page1 = build_index_page(1, 42, 2000, FIL_NULL, 2);
let page2 = build_index_page(2, 42, 3000, 1, FIL_NULL);
let page3 = build_index_page(3, 42, 4000, FIL_NULL, FIL_NULL);
let tmp = write_tablespace(&[page0, page1, page2, page3]);
let path = tmp.path().to_str().unwrap();
let mut output = Vec::new();
let result = idb::cli::verify::execute(
&idb::cli::verify::VerifyOptions {
file: path.to_string(),
verbose: false,
json: false,
page_size: None,
keyring: None,
mmap: false,
redo: None,
chain: vec![],
backup_meta: None,
},
&mut output,
);
assert!(
result.is_ok(),
"Expected verify to pass on valid tablespace"
);
let text = String::from_utf8(output).unwrap();
assert!(text.contains("PASS"));
assert!(text.contains("Structural Verification"));
}
#[test]
fn test_verify_fails_wrong_page_number() {
let page0 = build_fsp_hdr_page(42, 3);
let mut page1 = build_index_page(5, 42, 2000, FIL_NULL, FIL_NULL);
recalculate_checksum(&mut page1, PAGE_SIZE, ChecksumAlgorithm::Crc32c);
let page2 = build_index_page(2, 42, 3000, FIL_NULL, FIL_NULL);
let tmp = write_tablespace(&[page0, page1, page2]);
let path = tmp.path().to_str().unwrap();
let mut output = Vec::new();
let result = idb::cli::verify::execute(
&idb::cli::verify::VerifyOptions {
file: path.to_string(),
verbose: true,
json: false,
page_size: None,
keyring: None,
mmap: false,
redo: None,
chain: vec![],
backup_meta: None,
},
&mut output,
);
assert!(
result.is_err(),
"Expected verify to fail on wrong page number"
);
let text = String::from_utf8(output).unwrap();
assert!(text.contains("FAIL"));
assert!(text.contains("page_number_sequence"));
assert!(text.contains("Page 1 has page_number 5"));
}
#[test]
fn test_verify_fails_mixed_space_ids() {
let page0 = build_fsp_hdr_page(42, 3);
let page1 = build_index_page(1, 99, 2000, FIL_NULL, FIL_NULL);
let page2 = build_index_page(2, 42, 3000, FIL_NULL, FIL_NULL);
let tmp = write_tablespace(&[page0, page1, page2]);
let path = tmp.path().to_str().unwrap();
let mut output = Vec::new();
let result = idb::cli::verify::execute(
&idb::cli::verify::VerifyOptions {
file: path.to_string(),
verbose: true,
json: false,
page_size: None,
keyring: None,
mmap: false,
redo: None,
chain: vec![],
backup_meta: None,
},
&mut output,
);
assert!(
result.is_err(),
"Expected verify to fail on mixed space IDs"
);
let text = String::from_utf8(output).unwrap();
assert!(text.contains("space_id_consistency"));
assert!(text.contains("space_id 99"));
}
#[test]
fn test_verify_fails_chain_out_of_bounds() {
let page0 = build_fsp_hdr_page(42, 3);
let page1 = build_index_page(1, 42, 2000, FIL_NULL, 999);
let page2 = build_index_page(2, 42, 3000, FIL_NULL, FIL_NULL);
let tmp = write_tablespace(&[page0, page1, page2]);
let path = tmp.path().to_str().unwrap();
let mut output = Vec::new();
let result = idb::cli::verify::execute(
&idb::cli::verify::VerifyOptions {
file: path.to_string(),
verbose: true,
json: false,
page_size: None,
keyring: None,
mmap: false,
redo: None,
chain: vec![],
backup_meta: None,
},
&mut output,
);
assert!(
result.is_err(),
"Expected verify to fail on out-of-bounds chain pointer"
);
let text = String::from_utf8(output).unwrap();
assert!(text.contains("page_chain_bounds"));
assert!(text.contains("next pointer 999"));
}
#[test]
fn test_verify_fails_trailer_lsn_mismatch() {
let page0 = build_fsp_hdr_page(42, 2);
let mut page1 = vec![0u8; PS];
let lsn: u64 = 5000;
BigEndian::write_u32(&mut page1[FIL_PAGE_OFFSET..], 1);
BigEndian::write_u32(&mut page1[FIL_PAGE_PREV..], FIL_NULL);
BigEndian::write_u32(&mut page1[FIL_PAGE_NEXT..], FIL_NULL);
BigEndian::write_u64(&mut page1[FIL_PAGE_LSN..], lsn);
BigEndian::write_u16(&mut page1[FIL_PAGE_TYPE..], 17855);
BigEndian::write_u32(&mut page1[FIL_PAGE_SPACE_ID..], 42);
let trailer = PS - SIZE_FIL_TRAILER;
BigEndian::write_u32(&mut page1[trailer + 4..], 0xDEADBEEF);
recalculate_checksum(&mut page1, PAGE_SIZE, ChecksumAlgorithm::Crc32c);
let tmp = write_tablespace(&[page0, page1]);
let path = tmp.path().to_str().unwrap();
let mut output = Vec::new();
let result = idb::cli::verify::execute(
&idb::cli::verify::VerifyOptions {
file: path.to_string(),
verbose: true,
json: false,
page_size: None,
keyring: None,
mmap: false,
redo: None,
chain: vec![],
backup_meta: None,
},
&mut output,
);
assert!(
result.is_err(),
"Expected verify to fail on trailer LSN mismatch"
);
let text = String::from_utf8(output).unwrap();
assert!(text.contains("trailer_lsn_match"));
}
#[test]
fn test_verify_json_output_valid_tablespace() {
let page0 = build_fsp_hdr_page(42, 3);
let page1 = build_index_page(1, 42, 2000, FIL_NULL, FIL_NULL);
let page2 = build_index_page(2, 42, 3000, FIL_NULL, FIL_NULL);
let tmp = write_tablespace(&[page0, page1, page2]);
let path = tmp.path().to_str().unwrap();
let mut output = Vec::new();
let result = idb::cli::verify::execute(
&idb::cli::verify::VerifyOptions {
file: path.to_string(),
verbose: false,
json: true,
page_size: None,
keyring: None,
mmap: false,
redo: None,
chain: vec![],
backup_meta: None,
},
&mut output,
);
assert!(result.is_ok());
let json: serde_json::Value = serde_json::from_slice(&output).unwrap();
assert!(json.get("file").is_some());
assert_eq!(json["total_pages"], 3);
assert_eq!(json["page_size"], 16384);
assert_eq!(json["passed"], true);
assert!(json.get("findings").is_none());
assert!(!json["summary"].as_array().unwrap().is_empty());
for s in json["summary"].as_array().unwrap() {
assert!(s.get("kind").is_some());
assert!(s.get("pages_checked").is_some());
assert!(s.get("issues_found").is_some());
assert!(s.get("passed").is_some());
}
}
#[test]
fn test_verify_json_output_with_failures() {
let page0 = build_fsp_hdr_page(42, 2);
let page1 = build_index_page(1, 99, 2000, FIL_NULL, FIL_NULL);
let tmp = write_tablespace(&[page0, page1]);
let path = tmp.path().to_str().unwrap();
let mut output = Vec::new();
let _result = idb::cli::verify::execute(
&idb::cli::verify::VerifyOptions {
file: path.to_string(),
verbose: false,
json: true,
page_size: None,
keyring: None,
mmap: false,
redo: None,
chain: vec![],
backup_meta: None,
},
&mut output,
);
let json: serde_json::Value = serde_json::from_slice(&output).unwrap();
assert_eq!(json["passed"], false);
assert!(!json["findings"].as_array().unwrap().is_empty());
let findings = json["findings"].as_array().unwrap();
let has_space_id_finding = findings.iter().any(|f| f["kind"] == "SpaceIdConsistency");
assert!(
has_space_id_finding,
"Expected to find SpaceIdConsistency finding in JSON"
);
let summaries = json["summary"].as_array().unwrap();
let space_id_summary = summaries
.iter()
.find(|s| s["kind"] == "SpaceIdConsistency")
.unwrap();
assert_eq!(space_id_summary["passed"], false);
assert!(space_id_summary["issues_found"].as_u64().unwrap() > 0);
}
#[test]
fn test_verify_passes_with_empty_pages() {
let page0 = build_fsp_hdr_page(42, 3);
let page1 = vec![0u8; PS]; let page2 = build_index_page(2, 42, 3000, FIL_NULL, FIL_NULL);
let tmp = write_tablespace(&[page0, page1, page2]);
let path = tmp.path().to_str().unwrap();
let mut output = Vec::new();
let result = idb::cli::verify::execute(
&idb::cli::verify::VerifyOptions {
file: path.to_string(),
verbose: false,
json: false,
page_size: None,
keyring: None,
mmap: false,
redo: None,
chain: vec![],
backup_meta: None,
},
&mut output,
);
assert!(
result.is_ok(),
"Expected verify to pass when empty pages are present"
);
}
#[test]
fn test_verify_fails_prev_pointer_out_of_bounds() {
let page0 = build_fsp_hdr_page(42, 2);
let page1 = build_index_page(1, 42, 2000, 500, FIL_NULL);
let tmp = write_tablespace(&[page0, page1]);
let path = tmp.path().to_str().unwrap();
let mut output = Vec::new();
let result = idb::cli::verify::execute(
&idb::cli::verify::VerifyOptions {
file: path.to_string(),
verbose: true,
json: false,
page_size: None,
keyring: None,
mmap: false,
redo: None,
chain: vec![],
backup_meta: None,
},
&mut output,
);
assert!(
result.is_err(),
"Expected verify to fail on prev pointer out of bounds"
);
let text = String::from_utf8(output).unwrap();
assert!(text.contains("prev pointer 500"));
}
#[test]
fn test_chain_contiguous_passes() {
let page0a = build_fsp_hdr_page(42, 2);
let page1a = build_index_page(1, 42, 1000, FIL_NULL, FIL_NULL);
let file_a = write_tablespace(&[page0a, page1a]);
let page0b = build_fsp_hdr_page(42, 2);
let page1b = build_index_page(1, 42, 2000, FIL_NULL, FIL_NULL);
let file_b = write_tablespace(&[page0b, page1b]);
let mut output = Vec::new();
let result = idb::cli::verify::execute(
&idb::cli::verify::VerifyOptions {
file: file_a.path().to_str().unwrap().to_string(),
verbose: false,
json: false,
page_size: None,
keyring: None,
mmap: false,
redo: None,
chain: vec![
file_a.path().to_str().unwrap().to_string(),
file_b.path().to_str().unwrap().to_string(),
],
backup_meta: None,
},
&mut output,
);
assert!(result.is_ok(), "Expected chain to pass");
let text = String::from_utf8(output).unwrap();
assert!(text.contains("PASS"));
}
#[test]
fn test_chain_json_output() {
let page0a = build_fsp_hdr_page(42, 2);
let page1a = build_index_page(1, 42, 1000, FIL_NULL, FIL_NULL);
let file_a = write_tablespace(&[page0a, page1a]);
let page0b = build_fsp_hdr_page(42, 2);
let page1b = build_index_page(1, 42, 2000, FIL_NULL, FIL_NULL);
let file_b = write_tablespace(&[page0b, page1b]);
let mut output = Vec::new();
let result = idb::cli::verify::execute(
&idb::cli::verify::VerifyOptions {
file: file_a.path().to_str().unwrap().to_string(),
verbose: false,
json: true,
page_size: None,
keyring: None,
mmap: false,
redo: None,
chain: vec![
file_a.path().to_str().unwrap().to_string(),
file_b.path().to_str().unwrap().to_string(),
],
backup_meta: None,
},
&mut output,
);
assert!(result.is_ok());
let text = String::from_utf8(output).unwrap();
let json: serde_json::Value = serde_json::from_str(&text).unwrap();
assert_eq!(json["contiguous"], true);
assert_eq!(json["consistent_space_id"], true);
assert_eq!(json["files"].as_array().unwrap().len(), 2);
}
#[test]
fn test_chain_mixed_space_ids_fails() {
let page0a = build_fsp_hdr_page(42, 2);
let page1a = build_index_page(1, 42, 1000, FIL_NULL, FIL_NULL);
let file_a = write_tablespace(&[page0a, page1a]);
let page0b = build_fsp_hdr_page(99, 2);
let page1b = build_index_page(1, 99, 2000, FIL_NULL, FIL_NULL);
let file_b = write_tablespace(&[page0b, page1b]);
let mut output = Vec::new();
let result = idb::cli::verify::execute(
&idb::cli::verify::VerifyOptions {
file: file_a.path().to_str().unwrap().to_string(),
verbose: false,
json: false,
page_size: None,
keyring: None,
mmap: false,
redo: None,
chain: vec![
file_a.path().to_str().unwrap().to_string(),
file_b.path().to_str().unwrap().to_string(),
],
backup_meta: None,
},
&mut output,
);
assert!(
result.is_err(),
"Expected chain to fail with mixed space IDs"
);
let text = String::from_utf8(output).unwrap();
assert!(text.contains("FAIL"));
}
#[test]
fn test_chain_requires_two_files() {
let page0 = build_fsp_hdr_page(42, 2);
let page1 = build_index_page(1, 42, 1000, FIL_NULL, FIL_NULL);
let file_a = write_tablespace(&[page0, page1]);
let mut output = Vec::new();
let result = idb::cli::verify::execute(
&idb::cli::verify::VerifyOptions {
file: file_a.path().to_str().unwrap().to_string(),
verbose: false,
json: false,
page_size: None,
keyring: None,
mmap: false,
redo: None,
chain: vec![file_a.path().to_str().unwrap().to_string()],
backup_meta: None,
},
&mut output,
);
assert!(result.is_err());
}
#[test]
fn test_verify_backup_meta_within_window() {
use std::io::Write as IoWrite;
let page0 = build_fsp_hdr_page(42, 3);
let page1 = build_index_page(1, 42, 2000, FIL_NULL, FIL_NULL);
let page2 = build_index_page(2, 42, 3000, FIL_NULL, FIL_NULL);
let ts_file = write_tablespace(&[page0, page1, page2]);
let mut ckpt = tempfile::NamedTempFile::new().unwrap();
writeln!(ckpt, "backup_type = full-backuped").unwrap();
writeln!(ckpt, "from_lsn = 0").unwrap();
writeln!(ckpt, "to_lsn = 5000").unwrap();
writeln!(ckpt, "last_lsn = 5000").unwrap();
ckpt.flush().unwrap();
let mut output = Vec::new();
let result = idb::cli::verify::execute(
&idb::cli::verify::VerifyOptions {
file: ts_file.path().to_str().unwrap().to_string(),
verbose: false,
json: true,
page_size: None,
keyring: None,
mmap: false,
redo: None,
chain: vec![],
backup_meta: Some(ckpt.path().to_str().unwrap().to_string()),
},
&mut output,
);
assert!(
result.is_ok(),
"Expected verify to pass when LSNs within window"
);
let json: serde_json::Value = serde_json::from_slice(&output).unwrap();
let meta = &json["backup_meta"];
assert_eq!(meta["passed"], true);
assert_eq!(meta["from_lsn"], 0);
assert_eq!(meta["to_lsn"], 5000);
assert!(meta["pages_before_window"].as_array().unwrap().is_empty());
assert!(meta["pages_after_window"].as_array().unwrap().is_empty());
}
#[test]
fn test_verify_backup_meta_pages_outside_window() {
use std::io::Write as IoWrite;
let page0 = build_fsp_hdr_page(42, 3);
let page1 = build_index_page(1, 42, 2000, FIL_NULL, FIL_NULL);
let page2 = build_index_page(2, 42, 9000, FIL_NULL, FIL_NULL);
let ts_file = write_tablespace(&[page0, page1, page2]);
let mut ckpt = tempfile::NamedTempFile::new().unwrap();
writeln!(ckpt, "backup_type = full-backuped").unwrap();
writeln!(ckpt, "from_lsn = 1500").unwrap();
writeln!(ckpt, "to_lsn = 5000").unwrap();
writeln!(ckpt, "last_lsn = 5000").unwrap();
ckpt.flush().unwrap();
let mut output = Vec::new();
let result = idb::cli::verify::execute(
&idb::cli::verify::VerifyOptions {
file: ts_file.path().to_str().unwrap().to_string(),
verbose: true,
json: true,
page_size: None,
keyring: None,
mmap: false,
redo: None,
chain: vec![],
backup_meta: Some(ckpt.path().to_str().unwrap().to_string()),
},
&mut output,
);
assert!(
result.is_err(),
"Expected verify to fail with pages outside window"
);
let json: serde_json::Value = serde_json::from_slice(&output).unwrap();
let meta = &json["backup_meta"];
assert_eq!(meta["passed"], false);
assert!(!meta["pages_after_window"].as_array().unwrap().is_empty());
let after = meta["pages_after_window"].as_array().unwrap();
let page2_issue = after.iter().find(|p| p["page_number"] == 2);
assert!(
page2_issue.is_some(),
"Expected page 2 in pages_after_window"
);
assert_eq!(page2_issue.unwrap()["lsn"], 9000);
}