///|
/// Text encodings allowed by the SQLite database header.
pub(all) enum TextEncoding {
Utf8
Utf16Le
Utf16Be
Unspecified
} derive(Eq, Debug)
///|
pub fn TextEncoding::from_header(
value : UInt64,
) -> TextEncoding raise ParseError {
match value {
0UL => Unspecified
1UL => Utf8
2UL => Utf16Le
3UL => Utf16Be
_ => raise InvalidValue(56, "text encoding", value.to_string())
}
}
///|
pub fn TextEncoding::label(self : TextEncoding) -> String {
match self {
Utf8 => "UTF-8"
Utf16Le => "UTF-16LE"
Utf16Be => "UTF-16BE"
Unspecified => "unspecified"
}
}
///|
/// Parsed representation of SQLite's 100-byte database header.
pub(all) struct DatabaseHeader {
page_size : Int
write_version : Int
read_version : Int
reserved_bytes : Int
max_embedded_payload_fraction : Int
min_embedded_payload_fraction : Int
leaf_payload_fraction : Int
file_change_counter : UInt64
database_size_pages : UInt64
first_freelist_trunk_page : UInt64
total_freelist_pages : UInt64
schema_cookie : UInt64
schema_format : UInt64
default_page_cache_size : Int64
largest_root_btree_page : UInt64
text_encoding : TextEncoding
user_version : UInt64
incremental_vacuum : UInt64
application_id : UInt64
version_valid_for : UInt64
sqlite_version_number : UInt64
} derive(Eq, Debug)
///|
pub fn DatabaseHeader::usable_page_size(self : DatabaseHeader) -> Int {
self.page_size - self.reserved_bytes
}
///|
pub fn DatabaseHeader::is_wal_mode(self : DatabaseHeader) -> Bool {
self.write_version == 2 || self.read_version == 2
}
///|
pub fn DatabaseHeader::effective_database_pages(
self : DatabaseHeader,
file_length : Int,
) -> UInt64 {
if self.database_size_pages > 0UL {
self.database_size_pages
} else if self.page_size > 0 {
(file_length / self.page_size).to_uint64()
} else {
0UL
}
}
///|
fn valid_page_size(size : Int) -> Bool {
size >= 512 && size <= 65536 && (size & (size - 1)) == 0
}
///|
fn validate_format_version(
value : Int,
offset : Int,
field : String,
) -> Unit raise ParseError {
guard value == 1 || value == 2 else {
raise InvalidValue(offset, field, value.to_string())
}
}
///|
fn require_header_byte(
actual : Int,
expected : Int,
offset : Int,
field : String,
) -> Unit raise ParseError {
guard actual == expected else {
raise InvalidValue(offset, field, actual.to_string())
}
}
///|
/// Parse and strictly validate the SQLite database header.
pub fn parse_database_header(data : Bytes) -> DatabaseHeader raise ParseError {
let reader = BinaryReader::named(data, "SQLite database header")
guard data.length() >= 100 else {
raise UnexpectedEnd(0, 100, data.length(), "SQLite database header")
}
let magic = reader.read_bytes(16)
let expected = b"SQLite format 3\x00"
guard bytes_equal(magic, expected) else {
raise InvalidMagic(bytes_hex(expected), bytes_hex(magic))
}
let raw_page_size = reader.read_u16_be()
let page_size = if raw_page_size == 1 { 65536 } else { raw_page_size }
guard valid_page_size(page_size) else {
raise InvalidValue(16, "page size", raw_page_size.to_string())
}
let write_version = reader.read_u8().to_int()
validate_format_version(write_version, 18, "file format write version")
let read_version = reader.read_u8().to_int()
validate_format_version(read_version, 19, "file format read version")
let reserved_bytes = reader.read_u8().to_int()
guard reserved_bytes < page_size else {
raise InvalidValue(20, "reserved bytes", reserved_bytes.to_string())
}
let max_embedded_payload_fraction = reader.read_u8().to_int()
require_header_byte(
max_embedded_payload_fraction, 64, 21, "maximum embedded payload fraction",
)
let min_embedded_payload_fraction = reader.read_u8().to_int()
require_header_byte(
min_embedded_payload_fraction, 32, 22, "minimum embedded payload fraction",
)
let leaf_payload_fraction = reader.read_u8().to_int()
require_header_byte(leaf_payload_fraction, 32, 23, "leaf payload fraction")
let file_change_counter = reader.read_u32_be()
let database_size_pages = reader.read_u32_be()
let first_freelist_trunk_page = reader.read_u32_be()
let total_freelist_pages = reader.read_u32_be()
let schema_cookie = reader.read_u32_be()
let schema_format = reader.read_u32_be()
// Zero is valid for a database that has no schema yet. This matters when a
// WAL contains the transaction that creates the first schema objects.
guard schema_format <= 4UL else {
raise InvalidValue(44, "schema format", schema_format.to_string())
}
let default_page_cache_size = reader.read_i32_be()
let largest_root_btree_page = reader.read_u32_be()
let text_encoding = TextEncoding::from_header(reader.read_u32_be())
let user_version = reader.read_u32_be()
let incremental_vacuum = reader.read_u32_be()
let application_id = reader.read_u32_be()
reader.skip(20)
let version_valid_for = reader.read_u32_be()
let sqlite_version_number = reader.read_u32_be()
{
page_size,
write_version,
read_version,
reserved_bytes,
max_embedded_payload_fraction,
min_embedded_payload_fraction,
leaf_payload_fraction,
file_change_counter,
database_size_pages,
first_freelist_trunk_page,
total_freelist_pages,
schema_cookie,
schema_format,
default_page_cache_size,
largest_root_btree_page,
text_encoding,
user_version,
incremental_vacuum,
application_id,
version_valid_for,
sqlite_version_number,
}
}
///|
/// Return non-fatal relationships that cannot be decided from one field alone.
pub fn validate_database_header(
header : DatabaseHeader,
file_length : Int,
) -> Array[Diagnostic] {
let diagnostics : Array[Diagnostic] = []
if file_length % header.page_size != 0 {
diagnostics.push(
Diagnostic::warning(
"HEADER_FILE_LENGTH",
"file length is not an exact multiple of the declared page size",
offset=16,
),
)
}
let physical_pages = if header.page_size > 0 {
(file_length / header.page_size).to_uint64()
} else {
0UL
}
if header.database_size_pages > physical_pages {
diagnostics.push(
Diagnostic::error(
"HEADER_DATABASE_SIZE",
"header database size exceeds pages available in the file",
offset=28,
),
)
}
if header.first_freelist_trunk_page == 0UL &&
header.total_freelist_pages != 0UL {
diagnostics.push(
Diagnostic::warning(
"HEADER_FREELIST_ROOT",
"freelist count is non-zero but the first trunk page is zero",
offset=32,
),
)
}
if header.first_freelist_trunk_page > physical_pages &&
header.first_freelist_trunk_page != 0UL {
diagnostics.push(
Diagnostic::error(
"HEADER_FREELIST_RANGE",
"first freelist trunk page is outside the database file",
offset=32,
page_number=header.first_freelist_trunk_page,
),
)
}
if header.incremental_vacuum != 0UL && header.largest_root_btree_page == 0UL {
diagnostics.push(
Diagnostic::warning(
"HEADER_INCREMENTAL_VACUUM",
"incremental-vacuum flag is set without an auto-vacuum root page",
offset=64,
),
)
}
if header.version_valid_for != header.file_change_counter {
diagnostics.push(
Diagnostic::info(
"HEADER_VERSION_VALIDITY",
"database size field may be stale because version-valid-for differs from the change counter",
offset=92,
),
)
}
diagnostics
}