1use std::{
2 fs::{File, OpenOptions},
3 io::{Read, Seek, SeekFrom, Write},
4 path::Path,
5};
6
7use crate::error::MqdbError;
8
9pub const PAGE_SIZE: usize = 8192;
10pub const PAGE_HEADER_SIZE: usize = 16;
11pub const PAGE_BODY_SIZE: usize = PAGE_SIZE - PAGE_HEADER_SIZE;
12
13pub const PAGE_TYPE_FREE: u32 = 0;
14pub(crate) const PAGE_TYPE_FILE_HEADER: u32 = 1;
15pub(crate) const PAGE_TYPE_CATALOG: u32 = 2;
16pub(crate) const PAGE_TYPE_BLOCK_DATA: u32 = 3;
17pub(crate) const PAGE_TYPE_OVERFLOW: u32 = 4;
18pub(crate) const PAGE_TYPE_INDEX: u32 = 5;
19pub(crate) const PAGE_TYPE_TABLE_DATA: u32 = 6;
20
21const FILE_MAGIC: u32 = 0x4D51_4442;
22pub const FILE_VERSION: u32 = 5;
23const LEGACY_VERSIONS: &[u32] = &[4];
24const CATALOG_START_PAGE: u32 = 1;
25
26fn invalid_data(message: impl Into<String>) -> MqdbError {
27 MqdbError::Storage(message.into())
28}
29
30fn file_header_body(num_pages: u32) -> [u8; PAGE_BODY_SIZE] {
31 let mut body = [0u8; PAGE_BODY_SIZE];
32 body[0..4].copy_from_slice(&FILE_MAGIC.to_le_bytes());
33 body[4..8].copy_from_slice(&FILE_VERSION.to_le_bytes());
34 body[8..12].copy_from_slice(&(PAGE_SIZE as u32).to_le_bytes());
35 body[12..16].copy_from_slice(&num_pages.to_le_bytes());
36 body[16..20].copy_from_slice(&CATALOG_START_PAGE.to_le_bytes());
37 body
38}
39
40pub fn compute_checksum(page: &[u8; PAGE_SIZE]) -> u32 {
41 let mut checksum = 0u32;
42 for byte in &page[0..4] {
43 checksum = checksum.wrapping_add(u32::from(*byte));
44 }
45 for byte in &page[8..PAGE_SIZE] {
46 checksum = checksum.wrapping_add(u32::from(*byte));
47 }
48 checksum
49}
50
51pub fn verify_checksum(page: &[u8; PAGE_SIZE]) -> bool {
52 let (_, checksum, _, _) = parse_page_header(page);
53 checksum == compute_checksum(page)
54}
55
56pub fn parse_page_header(page: &[u8; PAGE_SIZE]) -> (u32, u32, u32, u32) {
57 let page_type = u32::from_le_bytes(page[0..4].try_into().expect("page type slice"));
58 let checksum = u32::from_le_bytes(page[4..8].try_into().expect("checksum slice"));
59 let page_id = u32::from_le_bytes(page[8..12].try_into().expect("page id slice"));
60 let next_page = u32::from_le_bytes(page[12..16].try_into().expect("next page slice"));
61 (page_type, checksum, page_id, next_page)
62}
63
64pub fn make_page(page_type: u32, page_id: u32, next_page: u32, body: &[u8]) -> [u8; PAGE_SIZE] {
65 let mut page = [0u8; PAGE_SIZE];
66 page[0..4].copy_from_slice(&page_type.to_le_bytes());
67 page[8..12].copy_from_slice(&page_id.to_le_bytes());
68 page[12..16].copy_from_slice(&next_page.to_le_bytes());
69
70 let copy_len = body.len().min(PAGE_BODY_SIZE);
71 page[PAGE_HEADER_SIZE..PAGE_HEADER_SIZE + copy_len].copy_from_slice(&body[..copy_len]);
72
73 let checksum = compute_checksum(&page);
74 page[4..8].copy_from_slice(&checksum.to_le_bytes());
75 page
76}
77
78pub struct PageFile {
79 file: File,
80 pub num_pages: u32,
81 header_dirty: bool,
84 pub version: u32,
88}
89
90impl PageFile {
91 pub fn create(path: &Path) -> Result<Self, MqdbError> {
92 let file = OpenOptions::new()
93 .read(true)
94 .write(true)
95 .create(true)
96 .truncate(true)
97 .open(path)?;
98 let mut page_file = Self {
99 file,
100 num_pages: 1,
101 header_dirty: false,
102 version: FILE_VERSION,
103 };
104 page_file.write_file_header()?;
105 Ok(page_file)
106 }
107
108 pub fn open(path: &Path) -> Result<Self, MqdbError> {
109 let mut file = OpenOptions::new().read(true).write(true).open(path)?;
110 let mut page = [0u8; PAGE_SIZE];
111 file.seek(SeekFrom::Start(0))?;
112 file.read_exact(&mut page)?;
113
114 if !verify_checksum(&page) {
115 return Err(invalid_data("invalid file header checksum"));
116 }
117
118 let (page_type, _, page_id, _) = parse_page_header(&page);
119 if page_type != PAGE_TYPE_FILE_HEADER || page_id != 0 {
120 return Err(invalid_data("page 0 is not a valid file header page"));
121 }
122
123 let body = &page[PAGE_HEADER_SIZE..];
124 let magic = u32::from_le_bytes(body[0..4].try_into().expect("magic slice"));
125 let version = u32::from_le_bytes(body[4..8].try_into().expect("version slice"));
126 let page_size = u32::from_le_bytes(body[8..12].try_into().expect("page size slice"));
127 let num_pages = u32::from_le_bytes(body[12..16].try_into().expect("num pages slice"));
128 let catalog_start = u32::from_le_bytes(body[16..20].try_into().expect("catalog slice"));
129
130 if magic != FILE_MAGIC {
131 return Err(invalid_data("invalid MQDB magic number"));
132 }
133 if version != FILE_VERSION && !LEGACY_VERSIONS.contains(&version) {
134 return Err(invalid_data(format!(
135 "unsupported file version {version} (expected {FILE_VERSION}); run `mq-db index` to recreate the store"
136 )));
137 }
138 if page_size != PAGE_SIZE as u32 {
139 return Err(invalid_data(format!("unsupported page size: {page_size}")));
140 }
141 if catalog_start != CATALOG_START_PAGE {
142 return Err(invalid_data(format!(
143 "unexpected catalog start page: {catalog_start}"
144 )));
145 }
146 if num_pages == 0 {
147 return Err(invalid_data("invalid page count 0 in file header"));
148 }
149
150 let file_len = file.metadata()?.len();
151 if file_len % PAGE_SIZE as u64 != 0 {
152 return Err(invalid_data(
153 "database file size is not aligned to page size",
154 ));
155 }
156 if file_len < u64::from(num_pages) * PAGE_SIZE as u64 {
157 return Err(invalid_data(
158 "database file is shorter than file header page count",
159 ));
160 }
161
162 Ok(Self {
163 file,
164 num_pages,
165 header_dirty: false,
166 version,
167 })
168 }
169
170 pub fn read_page(&mut self, page_id: u32) -> Result<[u8; PAGE_SIZE], MqdbError> {
171 if page_id >= self.num_pages {
172 return Err(invalid_data(format!("page out of bounds: {page_id}")));
173 }
174
175 let mut page = [0u8; PAGE_SIZE];
176 self.file
177 .seek(SeekFrom::Start(u64::from(page_id) * PAGE_SIZE as u64))?;
178 self.file.read_exact(&mut page)?;
179
180 if !verify_checksum(&page) {
181 return Err(invalid_data(format!(
182 "checksum mismatch for page {page_id}"
183 )));
184 }
185
186 let (_, _, stored_page_id, _) = parse_page_header(&page);
187 if stored_page_id != page_id {
188 return Err(invalid_data(format!(
189 "page header id mismatch: expected {page_id}, found {stored_page_id}"
190 )));
191 }
192
193 Ok(page)
194 }
195
196 pub fn write_page(&mut self, page_id: u32, data: &[u8; PAGE_SIZE]) -> Result<(), MqdbError> {
197 if page_id >= self.num_pages {
198 return Err(invalid_data(format!("page out of bounds: {page_id}")));
199 }
200
201 self.file
202 .seek(SeekFrom::Start(u64::from(page_id) * PAGE_SIZE as u64))?;
203 self.file.write_all(data)?;
204 Ok(())
205 }
206
207 pub fn append_page(&mut self, data: &[u8; PAGE_SIZE]) -> Result<u32, MqdbError> {
208 let page_id = self.num_pages;
209 self.file.seek(SeekFrom::End(0))?;
210 self.file.write_all(data)?;
211 self.num_pages = self
212 .num_pages
213 .checked_add(1)
214 .ok_or_else(|| invalid_data("page count overflow"))?;
215 self.header_dirty = true;
216 Ok(page_id)
217 }
218
219 pub fn sync_header(&mut self) -> Result<(), MqdbError> {
222 if self.header_dirty {
223 self.write_file_header()?;
224 self.header_dirty = false;
225 }
226 Ok(())
227 }
228
229 fn write_file_header(&mut self) -> Result<(), MqdbError> {
230 let body = file_header_body(self.num_pages);
231 let page = make_page(PAGE_TYPE_FILE_HEADER, 0, 0, &body);
232 self.file.seek(SeekFrom::Start(0))?;
233 self.file.write_all(&page)?;
234 Ok(())
235 }
236}