Skip to main content

fsqlite_types/
flags.rs

1use bitflags::bitflags;
2
3/// Canonical big-endian encoding for on-disk SQLite-compatible structures.
4pub trait BigEndianEncode {
5    type Bytes;
6
7    fn to_be_canonical(self) -> Self::Bytes;
8}
9
10/// Canonical big-endian decoding for on-disk SQLite-compatible structures.
11pub trait BigEndianDecode: Sized {
12    type Bytes;
13
14    fn from_be_canonical(bytes: Self::Bytes) -> Self;
15}
16
17/// Canonical little-endian encoding for FrankenSQLite-native ECS structures.
18pub trait LittleEndianEncode {
19    type Bytes;
20
21    fn to_le_canonical(self) -> Self::Bytes;
22}
23
24/// Canonical little-endian decoding for FrankenSQLite-native ECS structures.
25pub trait LittleEndianDecode: Sized {
26    type Bytes;
27
28    fn from_le_canonical(bytes: Self::Bytes) -> Self;
29}
30
31macro_rules! impl_endian_codecs {
32    ($ty:ty, $len:expr) => {
33        impl BigEndianEncode for $ty {
34            type Bytes = [u8; $len];
35
36            fn to_be_canonical(self) -> Self::Bytes {
37                self.to_be_bytes()
38            }
39        }
40
41        impl BigEndianDecode for $ty {
42            type Bytes = [u8; $len];
43
44            fn from_be_canonical(bytes: Self::Bytes) -> Self {
45                Self::from_be_bytes(bytes)
46            }
47        }
48
49        impl LittleEndianEncode for $ty {
50            type Bytes = [u8; $len];
51
52            fn to_le_canonical(self) -> Self::Bytes {
53                self.to_le_bytes()
54            }
55        }
56
57        impl LittleEndianDecode for $ty {
58            type Bytes = [u8; $len];
59
60            fn from_le_canonical(bytes: Self::Bytes) -> Self {
61                Self::from_le_bytes(bytes)
62            }
63        }
64    };
65}
66
67impl_endian_codecs!(u16, 2);
68impl_endian_codecs!(u32, 4);
69impl_endian_codecs!(u64, 8);
70impl_endian_codecs!(i32, 4);
71
72fn read_array<const N: usize>(input: &[u8], offset: usize) -> Option<[u8; N]> {
73    let end = offset.checked_add(N)?;
74    input.get(offset..end)?.try_into().ok()
75}
76
77fn write_array<const N: usize>(output: &mut [u8], offset: usize, bytes: [u8; N]) -> bool {
78    let Some(end) = offset.checked_add(N) else {
79        return false;
80    };
81    let Some(dst) = output.get_mut(offset..end) else {
82        return false;
83    };
84    dst.copy_from_slice(&bytes);
85    true
86}
87
88#[must_use]
89pub fn read_u16_be(input: &[u8], offset: usize) -> Option<u16> {
90    read_array::<2>(input, offset).map(u16::from_be_canonical)
91}
92
93#[must_use]
94pub fn read_u32_be(input: &[u8], offset: usize) -> Option<u32> {
95    read_array::<4>(input, offset).map(u32::from_be_canonical)
96}
97
98#[must_use]
99pub fn read_u64_be(input: &[u8], offset: usize) -> Option<u64> {
100    read_array::<8>(input, offset).map(u64::from_be_canonical)
101}
102
103#[must_use]
104pub fn read_i32_be(input: &[u8], offset: usize) -> Option<i32> {
105    read_array::<4>(input, offset).map(i32::from_be_canonical)
106}
107
108#[must_use]
109pub fn read_u16_le(input: &[u8], offset: usize) -> Option<u16> {
110    read_array::<2>(input, offset).map(u16::from_le_canonical)
111}
112
113#[must_use]
114pub fn read_u32_le(input: &[u8], offset: usize) -> Option<u32> {
115    read_array::<4>(input, offset).map(u32::from_le_canonical)
116}
117
118#[must_use]
119pub fn read_u64_le(input: &[u8], offset: usize) -> Option<u64> {
120    read_array::<8>(input, offset).map(u64::from_le_canonical)
121}
122
123#[must_use]
124pub fn write_u16_be(output: &mut [u8], offset: usize, value: u16) -> bool {
125    write_array(output, offset, value.to_be_canonical())
126}
127
128#[must_use]
129pub fn write_u32_be(output: &mut [u8], offset: usize, value: u32) -> bool {
130    write_array(output, offset, value.to_be_canonical())
131}
132
133#[must_use]
134pub fn write_u64_be(output: &mut [u8], offset: usize, value: u64) -> bool {
135    write_array(output, offset, value.to_be_canonical())
136}
137
138#[must_use]
139pub fn write_i32_be(output: &mut [u8], offset: usize, value: i32) -> bool {
140    write_array(output, offset, value.to_be_canonical())
141}
142
143#[must_use]
144pub fn write_u16_le(output: &mut [u8], offset: usize, value: u16) -> bool {
145    write_array(output, offset, value.to_le_canonical())
146}
147
148#[must_use]
149pub fn write_u32_le(output: &mut [u8], offset: usize, value: u32) -> bool {
150    write_array(output, offset, value.to_le_canonical())
151}
152
153#[must_use]
154pub fn write_u64_le(output: &mut [u8], offset: usize, value: u64) -> bool {
155    write_array(output, offset, value.to_le_canonical())
156}
157
158bitflags! {
159    /// Flags for `sqlite3_open_v2()`.
160    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
161    pub struct OpenFlags: u32 {
162        /// Open for reading only.
163        const READONLY       = 0x0000_0001;
164        /// Open for reading and writing.
165        const READWRITE      = 0x0000_0002;
166        /// Create the database if it doesn't exist.
167        const CREATE         = 0x0000_0004;
168        /// Interpret the filename as a URI.
169        const URI            = 0x0000_0040;
170        /// Open an in-memory database.
171        const MEMORY         = 0x0000_0080;
172        /// Use the main database only (no attached databases).
173        const NOMUTEX        = 0x0000_8000;
174        /// Use the serialized threading mode.
175        const FULLMUTEX      = 0x0001_0000;
176        /// Use shared cache mode.
177        const SHAREDCACHE    = 0x0002_0000;
178        /// Use private cache mode.
179        const PRIVATECACHE   = 0x0004_0000;
180        /// Do not follow symlinks.
181        const NOFOLLOW       = 0x0100_0000;
182    }
183}
184
185impl Default for OpenFlags {
186    fn default() -> Self {
187        Self::READWRITE | Self::CREATE
188    }
189}
190
191bitflags! {
192    /// Flags for VFS file sync operations.
193    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
194    pub struct SyncFlags: u8 {
195        /// Normal sync (SQLITE_SYNC_NORMAL).
196        const NORMAL   = 0x02;
197        /// Full sync (SQLITE_SYNC_FULL).
198        const FULL     = 0x03;
199        /// Sync the data only, not the directory (SQLITE_SYNC_DATAONLY).
200        const DATAONLY = 0x10;
201    }
202}
203
204bitflags! {
205    /// Flags for VFS file open operations.
206    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
207    pub struct VfsOpenFlags: u32 {
208        /// Main database file.
209        const MAIN_DB          = 0x0000_0100;
210        /// Main journal file.
211        const MAIN_JOURNAL     = 0x0000_0800;
212        /// Temporary database file.
213        const TEMP_DB          = 0x0000_0200;
214        /// Temporary journal file.
215        const TEMP_JOURNAL     = 0x0000_1000;
216        /// Sub-journal file.
217        const SUBJOURNAL       = 0x0000_2000;
218        /// Super-journal file (formerly master journal).
219        const SUPER_JOURNAL    = 0x0000_4000;
220        /// WAL file.
221        const WAL              = 0x0008_0000;
222        /// Open for exclusive access.
223        const EXCLUSIVE        = 0x0000_0010;
224        /// Create the file if it doesn't exist.
225        const CREATE           = 0x0000_0004;
226        /// Open for reading and writing.
227        const READWRITE        = 0x0000_0002;
228        /// Open for reading only.
229        const READONLY         = 0x0000_0001;
230        /// Delete on close.
231        const DELETEONCLOSE    = 0x0000_0008;
232    }
233}
234
235bitflags! {
236    /// Flags for VFS access checks.
237    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
238    pub struct AccessFlags: u8 {
239        /// Check if the file exists.
240        const EXISTS    = 0;
241        /// Check if the file is readable and writable.
242        const READWRITE = 1;
243        /// Check if the file is readable.
244        const READ      = 2;
245    }
246}
247
248bitflags! {
249    /// Prepare statement flags.
250    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
251    pub struct PrepareFlags: u32 {
252        /// The statement is persistent (will be reused).
253        const PERSISTENT = 0x01;
254        /// The statement may be normalized.
255        const NORMALIZE  = 0x02;
256        /// Do not scan the schema before preparing.
257        const NO_VTAB    = 0x04;
258    }
259}
260
261bitflags! {
262    /// Internal flags on the Mem/sqlite3_value structure.
263    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
264    pub struct MemFlags: u16 {
265        /// Value is NULL.
266        const NULL      = 0x0001;
267        /// Value is a string.
268        const STR       = 0x0002;
269        /// Value is an integer.
270        const INT       = 0x0004;
271        /// Value is a real (float).
272        const REAL      = 0x0008;
273        /// Value is a BLOB.
274        const BLOB      = 0x0010;
275        /// Value is an integer that should be treated as real (optimization).
276        const INT_REAL  = 0x0020;
277        /// Auxiliary data attached.
278        const AFF_MASK  = 0x003F;
279        /// Memory needs to be freed.
280        const DYN       = 0x0040;
281        /// Value is a static string (no free needed).
282        const STATIC    = 0x0080;
283        /// Value is stored in an ephemeral buffer.
284        const EPHEM     = 0x0100;
285        /// Value has been cleared/invalidated.
286        const CLEARED   = 0x0200;
287        /// String has a NUL terminator.
288        const TERM      = 0x0400;
289        /// Has a subtype value.
290        const SUBTYPE   = 0x0800;
291        /// Zero-filled blob.
292        const ZERO      = 0x1000;
293    }
294}
295
296#[cfg(test)]
297mod tests {
298    use super::*;
299    use std::fs::File;
300    use std::io::{Read, Seek};
301    use std::process::Command;
302    use std::sync::atomic::{AtomicUsize, Ordering};
303
304    #[test]
305    fn open_flags_default() {
306        let flags = OpenFlags::default();
307        assert!(flags.contains(OpenFlags::READWRITE));
308        assert!(flags.contains(OpenFlags::CREATE));
309        assert!(!flags.contains(OpenFlags::READONLY));
310    }
311
312    #[test]
313    fn open_flags_combinations() {
314        let flags = OpenFlags::READONLY | OpenFlags::URI;
315        assert!(flags.contains(OpenFlags::READONLY));
316        assert!(flags.contains(OpenFlags::URI));
317        assert!(!flags.contains(OpenFlags::CREATE));
318    }
319
320    #[test]
321    fn sync_flags() {
322        let flags = SyncFlags::FULL | SyncFlags::DATAONLY;
323        assert!(flags.contains(SyncFlags::FULL));
324        assert!(flags.contains(SyncFlags::DATAONLY));
325    }
326
327    #[test]
328    fn vfs_open_flags() {
329        let flags = VfsOpenFlags::MAIN_DB | VfsOpenFlags::CREATE | VfsOpenFlags::READWRITE;
330        assert!(flags.contains(VfsOpenFlags::MAIN_DB));
331        assert!(flags.contains(VfsOpenFlags::CREATE));
332    }
333
334    #[test]
335    fn prepare_flags() {
336        let flags = PrepareFlags::PERSISTENT | PrepareFlags::NORMALIZE;
337        assert!(flags.contains(PrepareFlags::PERSISTENT));
338        assert!(flags.contains(PrepareFlags::NORMALIZE));
339    }
340
341    #[test]
342    fn mem_flags() {
343        let flags = MemFlags::INT | MemFlags::STATIC;
344        assert!(flags.contains(MemFlags::INT));
345        assert!(flags.contains(MemFlags::STATIC));
346        assert!(!flags.contains(MemFlags::NULL));
347    }
348
349    #[test]
350    fn test_sqlite_structures_big_endian() {
351        let header = crate::DatabaseHeader {
352            page_size: crate::PageSize::new(4096).expect("valid page size"),
353            change_counter: 0x0102_0304,
354            page_count: 0x0A0B_0C0D,
355            default_cache_size: -2000,
356            ..crate::DatabaseHeader::default()
357        };
358
359        let bytes = header
360            .to_bytes()
361            .expect("header serialization must succeed");
362        assert_eq!(read_u16_be(&bytes, 16), Some(4096));
363        assert_eq!(read_u32_be(&bytes, 24), Some(header.change_counter));
364        assert_eq!(read_u32_be(&bytes, 28), Some(header.page_count));
365        assert_eq!(read_i32_be(&bytes, 48), Some(header.default_cache_size));
366
367        let mut page = vec![0u8; header.page_size.as_usize()];
368        page[0] = crate::BTreePageType::LeafTable as u8;
369        assert!(write_u16_be(&mut page, 1, 0));
370        assert!(write_u16_be(&mut page, 3, 1));
371        assert!(write_u16_be(&mut page, 5, 400));
372        page[7] = 0;
373
374        let parsed = crate::BTreePageHeader::parse(&page, header.page_size, 0, false)
375            .expect("btree header parsing must succeed");
376        assert_eq!(parsed.cell_count, 1);
377        assert_eq!(read_u16_be(&page, 3), Some(parsed.cell_count));
378        assert_eq!(read_u16_be(&page, 5), Some(400));
379    }
380
381    #[test]
382    fn test_mixed_endian_udp_documented() {
383        // Intentional protocol split: network header uses big-endian while
384        // payload scalar fields use little-endian.
385        let mut packet = [0u8; 12];
386        assert!(write_u16_be(&mut packet, 0, 0xBEEF));
387        assert!(write_u16_be(&mut packet, 2, 8)); // payload bytes
388        assert!(write_u64_le(&mut packet, 4, 0x1122_3344_5566_7788));
389
390        assert_eq!(read_u16_be(&packet, 0), Some(0xBEEF));
391        assert_eq!(read_u16_be(&packet, 2), Some(8));
392        assert_eq!(read_u64_le(&packet, 4), Some(0x1122_3344_5566_7788));
393    }
394
395    #[test]
396    fn test_e2e_canonical_bytes_match_sqlite_where_required() {
397        static COUNTER: AtomicUsize = AtomicUsize::new(0);
398
399        if Command::new("sqlite3").arg("--version").output().is_err() {
400            return;
401        }
402
403        let mut path = std::env::temp_dir();
404        path.push(format!(
405            "fsqlite_bd_22n_7_{}_{}.sqlite",
406            std::process::id(),
407            COUNTER.fetch_add(1, Ordering::Relaxed)
408        ));
409
410        let status = Command::new("sqlite3")
411            .arg(&path)
412            .arg("CREATE TABLE t(x); INSERT INTO t VALUES(1);")
413            .status()
414            .expect("sqlite3 execution must succeed");
415        assert!(status.success(), "sqlite3 command failed");
416
417        let mut file = File::open(&path).expect("must open sqlite file");
418        let mut header_bytes = [0u8; crate::DATABASE_HEADER_SIZE];
419        file.read_exact(&mut header_bytes)
420            .expect("must read sqlite header");
421
422        let parsed =
423            crate::DatabaseHeader::from_bytes(&header_bytes).expect("header parse must succeed");
424        let rewritten = parsed.to_bytes().expect("header encode must succeed");
425        assert_eq!(header_bytes, rewritten, "canonical bytes must roundtrip");
426        assert_eq!(
427            parsed
428                .open_mode(crate::MAX_FILE_FORMAT_VERSION)
429                .expect("open mode derivation must succeed"),
430            crate::DatabaseOpenMode::ReadWrite
431        );
432
433        let encoded_page_size = if parsed.page_size.get() == 65_536 {
434            1
435        } else {
436            u16::try_from(parsed.page_size.get()).expect("page size <= u16")
437        };
438        assert_eq!(read_u16_be(&header_bytes, 16), Some(encoded_page_size));
439        assert_eq!(read_u32_be(&header_bytes, 24), Some(parsed.change_counter));
440        assert_eq!(read_u32_be(&header_bytes, 28), Some(parsed.page_count));
441
442        let mut page1 = vec![0u8; parsed.page_size.as_usize()];
443        file.rewind().expect("rewind to file start");
444        file.read_exact(&mut page1).expect("read page 1");
445        let btree =
446            crate::BTreePageHeader::parse(&page1, parsed.page_size, parsed.reserved_per_page, true)
447                .expect("parse page1 btree header");
448
449        assert_eq!(page1[crate::DATABASE_HEADER_SIZE], btree.page_type as u8);
450        assert_eq!(
451            read_u16_be(&page1, crate::DATABASE_HEADER_SIZE + 3),
452            Some(btree.cell_count)
453        );
454    }
455}