1pub mod metapage;
2
3pub mod fulltext_index;
5
6pub mod encryption;
8
9pub mod maintenance;
11
12pub mod wal;
14
15pub mod csr;
17
18pub mod node_store;
20
21pub mod property_index;
23
24pub mod text_index;
26
27pub mod edge_store;
29
30use std::os::unix::fs::FileExt;
31use std::path::{Path, PathBuf};
32
33use sparrowdb_common::{Error, PageId, Result};
34
35use crate::encryption::EncryptionContext;
36
37pub fn crc32_of(buf: &[u8]) -> u32 {
39 crc32c::crc32c(buf)
40}
41
42pub fn crc32_zeroed_at(
48 buf: &[u8],
49 zeroed_offset: usize,
50 zeroed_len: usize,
51) -> sparrowdb_common::Result<u32> {
52 let end = zeroed_offset.checked_add(zeroed_len).ok_or_else(|| {
54 sparrowdb_common::Error::InvalidArgument("zeroed range overflows usize".into())
55 })?;
56 if end > buf.len() {
57 return Err(sparrowdb_common::Error::InvalidArgument(format!(
58 "zeroed range {}..{} out of bounds for buffer of length {}",
59 zeroed_offset,
60 end,
61 buf.len()
62 )));
63 }
64 let crc = crc32c::crc32c(&buf[..zeroed_offset]);
66 const CHUNK: usize = 64;
68 let zeros = [0u8; CHUNK];
69 let mut crc = crc;
70 let mut remaining = zeroed_len;
71 while remaining > 0 {
72 let n = remaining.min(CHUNK);
73 crc = crc32c::crc32c_append(crc, &zeros[..n]);
74 remaining -= n;
75 }
76 Ok(crc32c::crc32c_append(crc, &buf[end..]))
77}
78
79pub struct PageStore {
101 file: std::fs::File,
107 page_size: usize,
109 enc: EncryptionContext,
111 _path: PathBuf,
113}
114
115impl PageStore {
116 pub fn open(path: &Path) -> Result<Self> {
121 Self::open_inner(path, 4096, EncryptionContext::none())
122 }
123
124 pub fn open_with_page_size(path: &Path, page_size: usize) -> Result<Self> {
126 Self::open_inner(path, page_size, EncryptionContext::none())
127 }
128
129 pub fn open_encrypted(path: &Path, page_size: usize, key: [u8; 32]) -> Result<Self> {
137 Self::open_inner(path, page_size, EncryptionContext::with_key(key))
138 }
139
140 fn open_inner(path: &Path, page_size: usize, enc: EncryptionContext) -> Result<Self> {
141 let file = std::fs::OpenOptions::new()
142 .read(true)
143 .write(true)
144 .create(true)
145 .truncate(false)
146 .open(path)?;
147 Ok(PageStore {
148 file,
149 page_size,
150 enc,
151 _path: path.to_path_buf(),
152 })
153 }
154
155 fn page_offset(&self, id: PageId) -> u64 {
157 let stride = if self.enc.is_encrypted() {
158 self.page_size + 40
159 } else {
160 self.page_size
161 };
162 id.0 * stride as u64
163 }
164
165 pub fn read_page(&self, id: PageId, buf: &mut [u8]) -> Result<()> {
175 if buf.len() != self.page_size {
176 return Err(Error::InvalidArgument(format!(
177 "read_page: buf len {} != page_size {}",
178 buf.len(),
179 self.page_size
180 )));
181 }
182
183 let offset = self.page_offset(id);
184 let on_disk_size = if self.enc.is_encrypted() {
185 self.page_size + 40
186 } else {
187 self.page_size
188 };
189
190 let mut on_disk_buf = vec![0u8; on_disk_size];
191 self.file.read_exact_at(&mut on_disk_buf, offset)?;
192
193 if self.enc.is_encrypted() {
194 let plaintext = self.enc.decrypt_page(id.0, &on_disk_buf)?;
195 buf.copy_from_slice(&plaintext);
196 } else {
197 buf.copy_from_slice(&on_disk_buf);
198 }
199
200 Ok(())
201 }
202
203 pub fn write_page(&self, id: PageId, buf: &[u8]) -> Result<()> {
211 if buf.len() != self.page_size {
212 return Err(Error::InvalidArgument(format!(
213 "write_page: buf len {} != page_size {}",
214 buf.len(),
215 self.page_size
216 )));
217 }
218
219 let on_disk_data = if self.enc.is_encrypted() {
220 self.enc.encrypt_page(id.0, buf)?
221 } else {
222 buf.to_vec()
223 };
224
225 let offset = self.page_offset(id);
226 self.file.write_all_at(&on_disk_data, offset)?;
227 Ok(())
228 }
229
230 pub fn fsync(&self) -> Result<()> {
232 self.file.sync_all()?;
233 Ok(())
234 }
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240
241 #[test]
242 fn page_store_type_exists() {
243 let _: fn(&Path) -> Result<PageStore> = PageStore::open;
245 }
246
247 #[test]
248 fn page_store_plaintext_roundtrip() {
249 let dir = tempfile::tempdir().unwrap();
250 let path = dir.path().join("pages.bin");
251 let store = PageStore::open_with_page_size(&path, 512).unwrap();
252
253 let write_buf = vec![0x42u8; 512];
254 store.write_page(PageId(0), &write_buf).unwrap();
255
256 let mut read_buf = vec![0u8; 512];
257 store.read_page(PageId(0), &mut read_buf).unwrap();
258 assert_eq!(read_buf, write_buf);
259 }
260
261 #[test]
262 fn page_store_encrypted_roundtrip() {
263 let dir = tempfile::tempdir().unwrap();
264 let path = dir.path().join("enc_pages.bin");
265 let key = [0x11u8; 32];
266 let store = PageStore::open_encrypted(&path, 512, key).unwrap();
267
268 let write_buf = vec![0xAAu8; 512];
269 store.write_page(PageId(3), &write_buf).unwrap();
270
271 let mut read_buf = vec![0u8; 512];
272 store.read_page(PageId(3), &mut read_buf).unwrap();
273 assert_eq!(read_buf, write_buf);
274 }
275
276 #[test]
277 fn page_store_wrong_key_returns_auth_failed() {
278 let dir = tempfile::tempdir().unwrap();
279 let path = dir.path().join("enc_pages.bin");
280
281 {
283 let store = PageStore::open_encrypted(&path, 512, [0xAAu8; 32]).unwrap();
284 store.write_page(PageId(0), &vec![0x55u8; 512]).unwrap();
285 }
286
287 let store = PageStore::open_encrypted(&path, 512, [0xBBu8; 32]).unwrap();
289 let mut buf = vec![0u8; 512];
290 let result = store.read_page(PageId(0), &mut buf);
291 assert!(
292 matches!(result, Err(Error::EncryptionAuthFailed)),
293 "expected EncryptionAuthFailed, got {:?}",
294 result
295 );
296 }
297
298 #[test]
299 fn page_store_multiple_pages_encrypted() {
300 let dir = tempfile::tempdir().unwrap();
301 let path = dir.path().join("multi.bin");
302 let key = [0x33u8; 32];
303 let store = PageStore::open_encrypted(&path, 256, key).unwrap();
304
305 for i in 0u64..4 {
306 let buf = vec![i as u8; 256];
307 store.write_page(PageId(i), &buf).unwrap();
308 }
309
310 for i in 0u64..4 {
311 let mut buf = vec![0u8; 256];
312 store.read_page(PageId(i), &mut buf).unwrap();
313 assert!(buf.iter().all(|&b| b == i as u8));
314 }
315 }
316
317 #[test]
318 fn crc32_zeroed_at_matches_manual_zeroing() {
319 let mut buf = [0xABu8; 16];
320 buf[4..8].copy_from_slice(&0xDEADBEEFu32.to_le_bytes());
322 let mut manual = buf;
324 manual[4..8].copy_from_slice(&[0u8; 4]);
325 let expected = crc32c::crc32c(&manual);
326 let actual = crc32_zeroed_at(&buf, 4, 4).unwrap();
327 assert_eq!(actual, expected);
328 }
329
330 #[test]
331 fn crc32_zeroed_at_rejects_out_of_bounds() {
332 let buf = [0u8; 16];
333 assert!(crc32_zeroed_at(&buf, 14, 4).is_err()); assert!(crc32_zeroed_at(&buf, usize::MAX, 1).is_err()); }
336}