use wire_repr::{ExactWidthError, wire_repr};
const SQLITE_MAGIC: [u8; 16] = *b"SQLite format 3\0";
wire_repr! {
pub absolute layout Header {
field magic: bytes(16) { offset: 0; }
field page_size: BeU16 { offset: 16; }
field write_version: U8 { offset: 18; }
field read_version: U8 { offset: 19; }
field reserved_space: U8 { offset: 20; }
field max_payload_fraction: U8 { offset: 21; }
field min_payload_fraction: U8 { offset: 22; }
field leaf_payload_fraction: U8 { offset: 23; }
field file_change_counter: BeU32 { offset: 24; }
field database_size_pages: BeU32 { offset: 28; }
field first_freelist_trunk_page: BeU32 { offset: 32; }
field freelist_page_count: BeU32 { offset: 36; }
field schema_cookie: BeU32 { offset: 40; }
field schema_format_number: BeU32 { offset: 44; }
field suggested_cache_size: BeU32 { offset: 48; }
field largest_root_btree_page: BeU32 { offset: 52; }
field text_encoding: BeU32 { offset: 56; }
field user_version: BeU32 { offset: 60; }
field incremental_vacuum_mode: BeU32 { offset: 64; }
field application_id: BeU32 { offset: 68; }
field reserved_expansion: bytes(20) { offset: 72; }
field version_valid_for: BeU32 { offset: 92; }
field sqlite_version_number: BeU32 { offset: 96; }
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum HeaderSemanticError {
WrongMagic,
NonZeroReservedExpansion,
}
fn validate_sqlite_header(view: &HeaderView<'_>) -> Result<(), HeaderSemanticError> {
if view.magic() != SQLITE_MAGIC {
Err(HeaderSemanticError::WrongMagic)
} else if view.reserved_expansion().iter().any(|byte| *byte != 0) {
Err(HeaderSemanticError::NonZeroReservedExpansion)
} else {
Ok(())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum PageSizeError {
InvalidRaw(u16),
}
fn page_size_from_raw(raw: u16) -> Result<u32, PageSizeError> {
if raw == 1 {
Ok(65_536)
} else if (512..=32_768).contains(&raw) && raw.is_power_of_two() {
Ok(u32::from(raw))
} else {
Err(PageSizeError::InvalidRaw(raw))
}
}
fn read_be_u16(bytes: &[u8], offset: usize) -> u16 {
u16::from_be_bytes([bytes[offset], bytes[offset + 1]])
}
fn read_be_u32(bytes: &[u8], offset: usize) -> u32 {
u32::from_be_bytes([
bytes[offset],
bytes[offset + 1],
bytes[offset + 2],
bytes[offset + 3],
])
}
fn write_be_u16(bytes: &mut [u8], offset: usize, value: u16) {
bytes[offset..offset + 2].copy_from_slice(&value.to_be_bytes());
}
fn write_be_u32(bytes: &mut [u8], offset: usize, value: u32) {
bytes[offset..offset + 4].copy_from_slice(&value.to_be_bytes());
}
fn sqlite_header() -> Vec<u8> {
let mut bytes = vec![0xa5; 100];
bytes[0..16].copy_from_slice(&SQLITE_MAGIC);
write_be_u16(&mut bytes, 16, 4_096);
bytes[18] = 1;
bytes[19] = 1;
bytes[20] = 32;
bytes[21] = 64;
bytes[22] = 32;
bytes[23] = 32;
write_be_u32(&mut bytes, 24, 0x0102_0304);
write_be_u32(&mut bytes, 28, 0x0000_0102);
write_be_u32(&mut bytes, 32, 0x0000_0203);
write_be_u32(&mut bytes, 36, 0x0000_0304);
write_be_u32(&mut bytes, 40, 0x1122_3344);
write_be_u32(&mut bytes, 44, 4);
write_be_u32(&mut bytes, 48, 0xffff_ff80);
write_be_u32(&mut bytes, 52, 0x0000_0506);
write_be_u32(&mut bytes, 56, 1);
write_be_u32(&mut bytes, 60, 0x5566_7788);
write_be_u32(&mut bytes, 64, 1);
write_be_u32(&mut bytes, 68, 0x0a0b_0c0d);
bytes[72..92].fill(0);
write_be_u32(&mut bytes, 92, 0x99aa_bbcc);
write_be_u32(&mut bytes, 96, 3_045_000);
bytes
}
#[test]
fn real_header_parses_as_an_exact_absolute_view_with_consumer_semantics_and_a_disjoint_suffix() {
let header = sqlite_header();
let suffix = [0xde, 0xad, 0xbe, 0xef];
let mut input = header.clone();
input.extend_from_slice(&suffix);
let (view, parsed_suffix) = HeaderView::parse_prefix(&input).expect("real header parses");
assert_eq!(HeaderView::WIDTH, 100);
assert_eq!(view.as_bytes(), header.as_slice());
assert_eq!(parsed_suffix, suffix);
assert_eq!(view.as_bytes().as_ptr(), input.as_ptr());
assert_eq!(parsed_suffix.as_ptr(), input[100..].as_ptr());
assert_eq!(view.magic(), SQLITE_MAGIC.as_slice());
assert_eq!(view.reserved_expansion(), [0; 20]);
assert_eq!(validate_sqlite_header(&view), Ok(()));
assert_eq!(view.page_size(), read_be_u16(&header, 16));
assert_eq!(view.write_version(), header[18]);
assert_eq!(view.max_payload_fraction(), header[21]);
assert_eq!(view.file_change_counter(), read_be_u32(&header, 24));
assert_eq!(view.schema_cookie(), read_be_u32(&header, 40));
assert_eq!(view.suggested_cache_size(), read_be_u32(&header, 48));
assert_eq!(view.application_id(), read_be_u32(&header, 68));
assert_eq!(view.version_valid_for(), read_be_u32(&header, 92));
assert_eq!(view.sqlite_version_number(), read_be_u32(&header, 96));
assert!(matches!(
HeaderView::parse_exact(&input),
Err(HeaderError::TrailingBytes {
expected: 100,
actual: 104
})
));
assert!(matches!(
HeaderView::parse_exact(&header[..99]),
Err(HeaderError::InputTooShort {
expected: 100,
actual: 99
})
));
}
#[test]
fn consumer_semantics_reject_wrong_magic_after_structural_parse() {
let mut bytes = sqlite_header();
bytes[0] = b'X';
let view = HeaderView::parse_exact(&bytes).expect("all exact-layout bytes parse");
assert_eq!(view.magic(), &bytes[0..16]);
assert_eq!(
validate_sqlite_header(&view),
Err(HeaderSemanticError::WrongMagic)
);
}
#[test]
fn consumer_semantics_reject_nonzero_reserved_expansion_after_structural_parse() {
let mut bytes = sqlite_header();
bytes[80] = 1;
let view = HeaderView::parse_exact(&bytes).expect("all exact-layout bytes parse");
assert_eq!(view.reserved_expansion(), &bytes[72..92]);
assert_eq!(
validate_sqlite_header(&view),
Err(HeaderSemanticError::NonZeroReservedExpansion)
);
}
#[test]
fn sqlite_page_size_semantics_remain_handwritten_consumer_logic() {
assert_eq!(page_size_from_raw(1), Ok(65_536));
assert_eq!(page_size_from_raw(4_096), Ok(4_096));
for raw in [0, 511, 513, 32_769] {
assert_eq!(page_size_from_raw(raw), Err(PageSizeError::InvalidRaw(raw)));
}
let mut structurally_valid = sqlite_header();
write_be_u16(&mut structurally_valid, 16, 513);
let view = HeaderView::parse_exact(&structurally_valid)
.expect("layout parsing does not impose SQLite page-size semantics");
assert_eq!(view.page_size(), 513);
assert_eq!(
page_size_from_raw(view.page_size()),
Err(PageSizeError::InvalidRaw(513))
);
}
#[test]
fn mutable_views_change_only_declared_field_spans() {
let mut bytes = sqlite_header();
let before = bytes.clone();
let mut view = HeaderViewMut::parse_exact_mut(&mut bytes).expect("real mutable header parses");
view.set_page_size(8_192).expect("built-in plan succeeds");
view.set_schema_cookie(0xaabb_ccdd)
.expect("built-in plan succeeds");
view.set_sqlite_version_number(3_046_000)
.expect("built-in plan succeeds");
assert_eq!(view.page_size(), 8_192);
assert_eq!(view.schema_cookie(), 0xaabb_ccdd);
assert_eq!(view.sqlite_version_number(), 3_046_000);
for index in 0..bytes.len() {
let changed =
(16..18).contains(&index) || (40..44).contains(&index) || (96..100).contains(&index);
if !changed {
assert_eq!(
bytes[index], before[index],
"unexpected write at byte {index}"
);
}
}
assert_eq!(bytes[72..92], before[72..92]);
}
#[test]
fn builder_writes_a_complete_header_without_touching_the_suffix() {
let mut output = vec![0x5a; 104];
let suffix_before = output[100..].to_vec();
let mut expected = sqlite_header();
let (mut view, suffix) = HeaderBuilder::new()
.magic(&SQLITE_MAGIC)
.page_size(4_096)
.write_version(1)
.read_version(1)
.reserved_space(32)
.max_payload_fraction(64)
.min_payload_fraction(32)
.leaf_payload_fraction(32)
.file_change_counter(0x0102_0304)
.database_size_pages(0x0000_0102)
.first_freelist_trunk_page(0x0000_0203)
.freelist_page_count(0x0000_0304)
.schema_cookie(0x1122_3344)
.schema_format_number(4)
.suggested_cache_size(0xffff_ff80)
.largest_root_btree_page(0x0000_0506)
.text_encoding(1)
.user_version(0x5566_7788)
.incremental_vacuum_mode(1)
.application_id(0x0a0b_0c0d)
.reserved_expansion(&[0; 20])
.version_valid_for(0x99aa_bbcc)
.sqlite_version_number(3_045_000)
.build_into(&mut output)
.expect("complete SQLite header builder");
assert_eq!(view.as_bytes().len(), 100);
assert_eq!(view.as_bytes(), expected.as_slice());
assert_eq!(&*suffix, suffix_before.as_slice());
assert_eq!(view.page_size(), 4_096);
assert_eq!(view.application_id(), 0x0a0b_0c0d);
assert_eq!(view.sqlite_version_number(), 3_045_000);
view.set_user_version(7)
.expect("built view remains mutable");
expected[60..64].copy_from_slice(&7_u32.to_be_bytes());
assert_eq!(view.as_bytes(), expected.as_slice());
assert_eq!(output[72..92], [0; 20]);
assert_eq!(output[100..], suffix_before);
assert_eq!(read_be_u32(&output, 60), 7);
}
#[test]
fn wrong_width_builder_bytes_leave_output_unchanged() {
let mut output = [0x3c; 100];
let before = output;
assert!(matches!(
HeaderBuilder::new()
.magic(&SQLITE_MAGIC[..15])
.page_size(4_096)
.write_version(1)
.read_version(1)
.reserved_space(32)
.max_payload_fraction(64)
.min_payload_fraction(32)
.leaf_payload_fraction(32)
.file_change_counter(1)
.database_size_pages(2)
.first_freelist_trunk_page(3)
.freelist_page_count(4)
.schema_cookie(5)
.schema_format_number(4)
.suggested_cache_size(6)
.largest_root_btree_page(7)
.text_encoding(1)
.user_version(8)
.incremental_vacuum_mode(0)
.application_id(9)
.reserved_expansion(&[0; 20])
.version_valid_for(10)
.sqlite_version_number(3_045_000)
.build_into(&mut output),
Err(HeaderWriteError::FieldMagic(error))
if error == ExactWidthError::new(16, 15)
));
assert_eq!(output, before);
}
#[test]
fn short_builder_output_is_unchanged() {
let mut output = [0x3c; 99];
assert!(matches!(
HeaderBuilder::new()
.magic(&SQLITE_MAGIC)
.page_size(4_096)
.write_version(1)
.read_version(1)
.reserved_space(32)
.max_payload_fraction(64)
.min_payload_fraction(32)
.leaf_payload_fraction(32)
.file_change_counter(1)
.database_size_pages(2)
.first_freelist_trunk_page(3)
.freelist_page_count(4)
.schema_cookie(5)
.schema_format_number(4)
.suggested_cache_size(6)
.largest_root_btree_page(7)
.text_encoding(1)
.user_version(8)
.incremental_vacuum_mode(0)
.application_id(9)
.reserved_expansion(&[0; 20])
.version_valid_for(10)
.sqlite_version_number(3_045_000)
.build_into(&mut output),
Err(HeaderWriteError::OutputTooShort {
needed: 100,
available: 99
})
));
assert_eq!(output, [0x3c; 99]);
}