141 lines
4.0 KiB
Rust
141 lines
4.0 KiB
Rust
use crate::error::Error;
|
|
use std::convert::{TryFrom, TryInto};
|
|
use std::num::NonZeroU32;
|
|
|
|
static HEADER_STRING: &[u8] = &[
|
|
//S q l i t e ` ` f o r m a t ` ` 3 \u{0}
|
|
83, 81, 76, 105, 116, 101, 32, 102, 111, 114, 109, 97, 116, 32, 51, 0,
|
|
];
|
|
|
|
/// A value stored as a Write Format Verson or
|
|
/// Read Format Version
|
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
|
|
pub enum FormatVersion {
|
|
/// Represents the rollback journal mode
|
|
Legacy,
|
|
/// Represents the Write Ahead Log mode
|
|
WriteAheadLog,
|
|
/// Represents any mode not 1 or 2, the value
|
|
/// will be provided
|
|
Unknown(u8),
|
|
}
|
|
|
|
impl From<u8> for FormatVersion {
|
|
fn from(v: u8) -> Self {
|
|
match v {
|
|
1 => Self::Legacy,
|
|
2 => Self::WriteAheadLog,
|
|
_ => Self::Unknown(v),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// This struct will wrap our 32bit number allowing us
|
|
/// to control how it gets used later.
|
|
#[derive(Debug)]
|
|
pub struct PageSize(u32);
|
|
|
|
#[derive(Debug)]
|
|
pub struct DatabaseHeader {
|
|
pub page_size: PageSize,
|
|
pub write_version: FormatVersion,
|
|
pub read_version: FormatVersion,
|
|
pub reserved_bytes: u8,
|
|
pub change_counter: u32,
|
|
pub database_size: Option<NonZeroU32>,
|
|
}
|
|
|
|
impl TryFrom<u16> for PageSize {
|
|
type Error = Error;
|
|
|
|
fn try_from(v: u16) -> Result<PageSize, Self::Error> {
|
|
match v {
|
|
// Special case for the largest page size
|
|
1 => Ok(PageSize(65_536u32)),
|
|
|
|
// < 512, != 1
|
|
0 | 2..=511 => Err(Error::InvalidPageSize(format!(
|
|
"value must be >= 512, found: {}",
|
|
v
|
|
))),
|
|
|
|
_ => {
|
|
if v.is_power_of_two() {
|
|
Ok(PageSize(v as u32))
|
|
} else {
|
|
Err(Error::InvalidPageSize(format!(
|
|
"value must be a power of 2, found: {}",
|
|
v
|
|
)))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn validate_fraction(byte: u8, target: u8, name: &str) -> Result<(), Error> {
|
|
if byte != target {
|
|
Err(Error::InvalidFraction(format!(
|
|
"{} must be {}, found: {}",
|
|
name, target, byte
|
|
)))
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
pub fn parse_header(bytes: &[u8]) -> Result<DatabaseHeader, Error> {
|
|
// Check that the first 16 bytes match the header string
|
|
validate_magic_string(&bytes)?;
|
|
// capture the page size
|
|
let page_size = parse_page_size(bytes)?;
|
|
// capture the write format version
|
|
let write_version = FormatVersion::from(bytes[18]);
|
|
// capture the read format version
|
|
let read_version = FormatVersion::from(bytes[19]);
|
|
let reserved_bytes = bytes[20];
|
|
validate_fraction(bytes[21], 64, "Maximum payload fraction")?;
|
|
validate_fraction(bytes[22], 32, "Minimum payload fraction")?;
|
|
validate_fraction(bytes[23], 32, "Leaf fraction")?;
|
|
|
|
let change_counter =
|
|
crate::try_parse_u32(&bytes[24..28]).map_err(|msg| Error::InvalidChangeCounter(msg))?;
|
|
|
|
let database_size = crate::try_parse_u32(&bytes[28..32])
|
|
.map(NonZeroU32::new)
|
|
.ok()
|
|
.flatten();
|
|
|
|
Ok(DatabaseHeader {
|
|
page_size,
|
|
write_version,
|
|
read_version,
|
|
reserved_bytes,
|
|
change_counter,
|
|
database_size,
|
|
})
|
|
}
|
|
|
|
/// Validate that the bytes provided match the special string
|
|
/// at the start of Sqlite3 files
|
|
pub fn validate_magic_string(bytes: &[u8]) -> Result<(), Error> {
|
|
let buf = &bytes[0..16];
|
|
if buf != HEADER_STRING {
|
|
return Err(Error::HeaderString(
|
|
String::from_utf8_lossy(buf).to_string(),
|
|
));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Attempt to generate the appropriate `PageSize` struct
|
|
pub fn parse_page_size(bytes: &[u8]) -> Result<PageSize, Error> {
|
|
// Convert into array, and convert error if needed, so that the `?` operator works
|
|
let page_size_bytes: [u8; 2] = bytes[16..18].try_into().map_err(|_| {
|
|
Error::InvalidPageSize(format!("expected a 2 byte slice, found: {:?}", bytes))
|
|
})?;
|
|
|
|
u16::from_be_bytes(page_size_bytes).try_into()
|
|
}
|