diff --git a/src/error.rs b/src/error.rs index ca56d57..47eda2d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -9,6 +9,11 @@ pub enum Error { HeaderString(String), /// An error parsing the page size InvalidPageSize(String), + /// An error parsing the maximum/minimum payload fraction + /// or leaf fraction + InvalidFraction(String), + /// The change counter failed to parse + InvalidChangeCounter(String), } impl fmt::Display for Error { @@ -17,6 +22,8 @@ impl fmt::Display for Error { match self { Self::HeaderString(v) => write!(f, "Unexpected bytes at start of file, expected the magic string 'SQLite format 3\u{0}', found {:?}", v), Self::InvalidPageSize(msg) => write!(f, "Invalid page size, {}", msg), + Self::InvalidFraction(msg) => write!(f, "{}", msg), + Self::InvalidChangeCounter(msg) => write!(f, "Invalid change counter: {}", msg), } } } diff --git a/src/header.rs b/src/header.rs index 5aef665..1b28237 100644 --- a/src/header.rs +++ b/src/header.rs @@ -1,5 +1,6 @@ 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} @@ -34,6 +35,16 @@ impl From for FormatVersion { #[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, +} + impl TryFrom for PageSize { type Error = Error; @@ -62,7 +73,18 @@ impl TryFrom for PageSize { } } -pub fn parse_header(bytes: &[u8]) -> Result<(PageSize, FormatVersion, FormatVersion), Error> { +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 { // Check that the first 16 bytes match the header string validate_magic_string(&bytes)?; // capture the page size @@ -71,8 +93,27 @@ pub fn parse_header(bytes: &[u8]) -> Result<(PageSize, FormatVersion, FormatVers 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")?; - Ok((page_size, write_version, read_version)) + 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 diff --git a/src/lib.rs b/src/lib.rs index 8f708c2..e52d6ff 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,2 +1,26 @@ pub mod error; pub mod header; + +// A little strange but since this might end up being +// used in a large number of places, we can use a +// String in the error position of our result. This +// will allow the caller to insert their own error +// with more context +fn try_parse_u32(bytes: &[u8]) -> Result { + use std::convert::TryInto; + + // Just like with our u16, we are going to need to convert + // a slice into an array of 4 bytes. Using the `try_into` + // method on a slice, we will fail if the slice isn't exactly + // 4 bytes. We can use `map_err` to build our string only if + // it fails + let arr: [u8; 4] = bytes.try_into().map_err(|_| { + format!( + "expected a 4 byte slice, found a {} byte slice", + bytes.len() + ) + })?; + + // Finally we can use the `from_be_bytes` constructor for a u32 + Ok(u32::from_be_bytes(arr)) +} diff --git a/src/main.rs b/src/main.rs index ba902eb..4b7c22d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,18 +1,14 @@ -use sqlite_parser::{ - header::parse_header, - error::Error, -}; +use sqlite_parser::{error::Error, header::parse_header}; use std::fs::read; fn main() -> Result<(), Error> { // first, read in all the bytes of our file // using unwrap to just panic if this fails - let contents = read("data.sqlite") - .expect("Failed to read data.sqlite"); + let contents = read("data.sqlite").expect("Failed to read data.sqlite"); - let (page_size, write_format, read_format) = parse_header(&contents[0..100])?; + let db_header = parse_header(&contents[0..100])?; - println!("page_size {:?}, write_format {:?}, read_format {:?}", page_size, write_format, read_format); + println!("{:#?}", db_header); Ok(()) }