Skip to main content

fsqlite_types/
encoding.rs

1//! Canonical endian helpers for on-disk/wire encodings (ยง1.5, bd-22n.7).
2//!
3//! SQLite-compatible structures use big-endian integers.
4//! FrankenSQLite-native ECS structures use little-endian integers.
5
6#[inline]
7#[must_use]
8pub fn read_u16_be(src: &[u8]) -> Option<u16> {
9    Some(u16::from_be_bytes(src.get(..2)?.try_into().ok()?))
10}
11
12#[inline]
13#[must_use]
14pub fn read_u32_be(src: &[u8]) -> Option<u32> {
15    Some(u32::from_be_bytes(src.get(..4)?.try_into().ok()?))
16}
17
18#[inline]
19#[must_use]
20pub fn read_i32_be(src: &[u8]) -> Option<i32> {
21    Some(i32::from_be_bytes(src.get(..4)?.try_into().ok()?))
22}
23
24#[inline]
25#[must_use]
26pub fn read_u32_le(src: &[u8]) -> Option<u32> {
27    Some(u32::from_le_bytes(src.get(..4)?.try_into().ok()?))
28}
29
30#[inline]
31#[must_use]
32pub fn read_u16_le(src: &[u8]) -> Option<u16> {
33    Some(u16::from_le_bytes(src.get(..2)?.try_into().ok()?))
34}
35
36#[inline]
37#[must_use]
38pub fn read_u64_le(src: &[u8]) -> Option<u64> {
39    Some(u64::from_le_bytes(src.get(..8)?.try_into().ok()?))
40}
41
42#[inline]
43pub fn write_u16_be(dst: &mut [u8], value: u16) -> Option<()> {
44    dst.get_mut(..2)?.copy_from_slice(&value.to_be_bytes());
45    Some(())
46}
47
48#[inline]
49pub fn write_u32_be(dst: &mut [u8], value: u32) -> Option<()> {
50    dst.get_mut(..4)?.copy_from_slice(&value.to_be_bytes());
51    Some(())
52}
53
54#[inline]
55pub fn write_i32_be(dst: &mut [u8], value: i32) -> Option<()> {
56    dst.get_mut(..4)?.copy_from_slice(&value.to_be_bytes());
57    Some(())
58}
59
60#[inline]
61pub fn write_u32_le(dst: &mut [u8], value: u32) -> Option<()> {
62    dst.get_mut(..4)?.copy_from_slice(&value.to_le_bytes());
63    Some(())
64}
65
66#[inline]
67pub fn write_u16_le(dst: &mut [u8], value: u16) -> Option<()> {
68    dst.get_mut(..2)?.copy_from_slice(&value.to_le_bytes());
69    Some(())
70}
71
72#[inline]
73pub fn write_u64_le(dst: &mut [u8], value: u64) -> Option<()> {
74    dst.get_mut(..8)?.copy_from_slice(&value.to_le_bytes());
75    Some(())
76}
77
78#[inline]
79#[must_use]
80pub fn read_u64_be(src: &[u8]) -> Option<u64> {
81    Some(u64::from_be_bytes(src.get(..8)?.try_into().ok()?))
82}
83
84#[inline]
85pub fn write_u64_be(dst: &mut [u8], value: u64) -> Option<()> {
86    dst.get_mut(..8)?.copy_from_slice(&value.to_be_bytes());
87    Some(())
88}
89
90#[inline]
91pub fn append_u16_be(buf: &mut Vec<u8>, value: u16) {
92    buf.extend_from_slice(&value.to_be_bytes());
93}
94
95#[inline]
96pub fn append_u32_be(buf: &mut Vec<u8>, value: u32) {
97    buf.extend_from_slice(&value.to_be_bytes());
98}
99
100#[inline]
101pub fn append_u64_be(buf: &mut Vec<u8>, value: u64) {
102    buf.extend_from_slice(&value.to_be_bytes());
103}
104
105#[inline]
106pub fn append_u32_le(buf: &mut Vec<u8>, value: u32) {
107    let mut scratch = [0u8; 4];
108    write_u32_le(&mut scratch, value).expect("fixed scratch width");
109    buf.extend_from_slice(&scratch);
110}
111
112#[inline]
113pub fn append_u16_le(buf: &mut Vec<u8>, value: u16) {
114    let mut scratch = [0u8; 2];
115    write_u16_le(&mut scratch, value).expect("fixed scratch width");
116    buf.extend_from_slice(&scratch);
117}
118
119#[inline]
120pub fn append_u64_le(buf: &mut Vec<u8>, value: u64) {
121    let mut scratch = [0u8; 8];
122    write_u64_le(&mut scratch, value).expect("fixed scratch width");
123    buf.extend_from_slice(&scratch);
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    use crate::ecs::{PatchKind, VersionPointer};
130    use crate::{DatabaseHeader, ObjectId};
131
132    #[test]
133    fn test_sqlite_structures_big_endian() {
134        let header = DatabaseHeader {
135            change_counter: 0x0102_0304,
136            page_count: 0x1112_1314,
137            page_size: crate::PageSize::new(4096).expect("valid page size"),
138            ..DatabaseHeader::default()
139        };
140        let bytes = header.to_bytes().expect("header encodes");
141
142        assert_eq!(read_u16_be(&bytes[16..18]), Some(4096));
143        assert_eq!(read_u32_be(&bytes[24..28]), Some(0x0102_0304));
144        assert_eq!(read_u32_be(&bytes[28..32]), Some(0x1112_1314));
145    }
146
147    #[test]
148    fn test_native_ecs_structures_little_endian() {
149        let pointer = VersionPointer {
150            commit_seq: 0x0102_0304_0506_0708,
151            patch_object: ObjectId::from_bytes([7u8; 16]),
152            patch_kind: PatchKind::FullImage,
153            base_hint: None,
154        };
155        let bytes = pointer.to_bytes();
156        assert_eq!(
157            read_u64_le(&bytes[..8]),
158            Some(0x0102_0304_0506_0708),
159            "version pointer commit_seq must be little-endian"
160        );
161    }
162
163    #[test]
164    fn test_canonical_encoding_unique() {
165        let header = DatabaseHeader {
166            change_counter: 42,
167            page_count: 7,
168            ..DatabaseHeader::default()
169        };
170        let a = header.to_bytes().expect("encodes");
171        let b = header.to_bytes().expect("encodes");
172        assert_eq!(a, b, "same sqlite header must encode identically");
173
174        let pointer = VersionPointer {
175            commit_seq: 99,
176            patch_object: ObjectId::from_bytes([3u8; 16]),
177            patch_kind: PatchKind::SparseXor,
178            base_hint: Some(ObjectId::from_bytes([4u8; 16])),
179        };
180        assert_eq!(pointer.to_bytes(), pointer.to_bytes());
181    }
182
183    #[test]
184    fn test_roundtrip_encode_decode() {
185        let header = DatabaseHeader {
186            change_counter: 123,
187            page_count: 456,
188            ..DatabaseHeader::default()
189        };
190        let header_bytes = header.to_bytes().expect("encodes");
191        let parsed = DatabaseHeader::from_bytes(&header_bytes).expect("decodes");
192        assert_eq!(parsed, header);
193
194        let pointer = VersionPointer {
195            commit_seq: 777,
196            patch_object: ObjectId::from_bytes([9u8; 16]),
197            patch_kind: PatchKind::FullImage,
198            base_hint: Some(ObjectId::from_bytes([10u8; 16])),
199        };
200        let bytes = pointer.to_bytes();
201        let decoded = VersionPointer::from_bytes(&bytes).expect("pointer decodes");
202        assert_eq!(decoded, pointer);
203    }
204
205    #[test]
206    fn test_mixed_endian_udp_documented() {
207        // Header fields in network-byte-order (big-endian), payload metadata
208        // in little-endian is intentional and explicitly encoded via helpers.
209        let mut packet = [0u8; 10];
210        write_u16_be(&mut packet[0..2], 9443).expect("u16 be");
211        write_u16_be(&mut packet[2..4], 9444).expect("u16 be");
212        write_u32_le(&mut packet[4..8], 128).expect("u32 le");
213        write_u16_be(&mut packet[8..10], 1).expect("u16 be");
214
215        assert_eq!(read_u16_be(&packet[0..2]), Some(9443));
216        assert_eq!(read_u16_be(&packet[2..4]), Some(9444));
217        assert_eq!(read_u32_le(&packet[4..8]), Some(128));
218        assert_eq!(read_u16_be(&packet[8..10]), Some(1));
219    }
220}