Skip to main content

chat4n6_sqlite_forensics/
freelist.rs

1pub struct Freeblock {
2    pub offset: usize,
3    pub size: usize,
4    pub data: Vec<u8>,
5}
6
7/// Walk the freeblock linked list within a single page.
8///
9/// The B-tree page header for a leaf page (8 bytes) has:
10///   +0: page type
11///   +1-2: first freeblock offset (0 = none)
12///   +3-4: cell count
13///   +5-6: cell content area start
14///   +7: fragmented bytes count
15///
16/// Each freeblock starts with: [next: u16][size: u16][data: size-4 bytes]
17/// The chain ends when next == 0.
18pub fn parse_freeblock_chain(page: &[u8], page_size: usize) -> Vec<Freeblock> {
19    if page.len() < 4 {
20        return Vec::new();
21    }
22    let first_fb = u16::from_be_bytes([page[1], page[2]]) as usize;
23
24    let mut freeblocks = Vec::new();
25    let mut fb_offset = first_fb;
26    let mut visited = std::collections::HashSet::new();
27
28    while fb_offset != 0 && fb_offset + 4 <= page.len() {
29        if fb_offset >= page_size {
30            break; // freeblock cannot start outside the declared page bounds
31        }
32        if !visited.insert(fb_offset) {
33            break; // cycle guard
34        }
35        let next = u16::from_be_bytes([page[fb_offset], page[fb_offset + 1]]) as usize;
36        let size = u16::from_be_bytes([page[fb_offset + 2], page[fb_offset + 3]]) as usize;
37
38        // size must include its own 4-byte header and must fit within the page
39        if size < 4 || fb_offset + size > page.len() {
40            break;
41        }
42
43        let data = page[fb_offset + 4..fb_offset + size].to_vec();
44        freeblocks.push(Freeblock {
45            offset: fb_offset,
46            size,
47            data,
48        });
49        fb_offset = next;
50    }
51
52    freeblocks
53}
54
55/// Walk the freelist trunk→leaf chain.
56///
57/// SQLite freelist format:
58///   Trunk page layout (from byte 0 of page):
59///     +0-3: next trunk page number (0 = last trunk)
60///     +4-7: number of leaf page numbers on this trunk
61///     +8..: leaf page numbers (4 bytes each)
62///
63/// Returns all freed page numbers (trunk and leaf pages included).
64pub fn walk_freelist_chain(db: &[u8], trunk_page: u32, page_size: u32) -> Vec<u32> {
65    let mut pages = Vec::new();
66    if trunk_page == 0 || db.is_empty() {
67        return pages;
68    }
69
70    let mut current = trunk_page;
71    let mut visited = std::collections::HashSet::new();
72
73    while current != 0 {
74        if !visited.insert(current) {
75            break; // cycle guard
76        }
77        let page_start = (current as usize - 1) * page_size as usize;
78        let page_end = page_start + page_size as usize;
79        let Some(page) = db.get(page_start..page_end) else {
80            break;
81        };
82
83        if page.len() < 8 {
84            break;
85        }
86
87        let next_trunk = u32::from_be_bytes([page[0], page[1], page[2], page[3]]);
88        let leaf_count = u32::from_be_bytes([page[4], page[5], page[6], page[7]]) as usize;
89
90        pages.push(current); // trunk page itself is a freed page
91
92        // Read leaf page numbers
93        for i in 0..leaf_count {
94            let leaf_off = 8 + i * 4;
95            if leaf_off + 4 > page.len() {
96                break;
97            }
98            let leaf_page = u32::from_be_bytes([
99                page[leaf_off],
100                page[leaf_off + 1],
101                page[leaf_off + 2],
102                page[leaf_off + 3],
103            ]);
104            if leaf_page != 0 {
105                pages.push(leaf_page);
106            }
107        }
108
109        current = next_trunk;
110    }
111
112    pages
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn test_freelist_chain_empty() {
121        let pages = walk_freelist_chain(&[], 0, 4096);
122        assert!(pages.is_empty());
123    }
124
125    #[test]
126    fn test_walk_freelist_chain_with_trunk_and_leaves() {
127        let page_size: usize = 4096;
128        // 3 pages: page 1 unused, page 2 = trunk, pages 3+4 = leaves (not in db slice)
129        let mut db = vec![0u8; page_size * 2];
130        let trunk_start = page_size; // (page 2 - 1) * page_size
131                                     // next_trunk = 0
132        db[trunk_start..trunk_start + 4].copy_from_slice(&0u32.to_be_bytes());
133        // leaf_count = 2
134        db[trunk_start + 4..trunk_start + 8].copy_from_slice(&2u32.to_be_bytes());
135        // leaf pages: 3 and 4
136        db[trunk_start + 8..trunk_start + 12].copy_from_slice(&3u32.to_be_bytes());
137        db[trunk_start + 12..trunk_start + 16].copy_from_slice(&4u32.to_be_bytes());
138
139        let pages = walk_freelist_chain(&db, 2, 4096);
140        assert_eq!(pages, vec![2, 3, 4]);
141    }
142
143    #[test]
144    fn test_freelist_chain_cycle_guard() {
145        let page_size: usize = 4096;
146        // Two trunk pages that point to each other: page 2 → page 3 → page 2 (cycle)
147        let mut db = vec![0u8; page_size * 3];
148        let p2 = page_size;
149        let p3 = page_size * 2;
150        // Page 2: next_trunk = 3, leaf_count = 0
151        db[p2..p2 + 4].copy_from_slice(&3u32.to_be_bytes());
152        db[p2 + 4..p2 + 8].copy_from_slice(&0u32.to_be_bytes());
153        // Page 3: next_trunk = 2 (cycle), leaf_count = 0
154        db[p3..p3 + 4].copy_from_slice(&2u32.to_be_bytes());
155        db[p3 + 4..p3 + 8].copy_from_slice(&0u32.to_be_bytes());
156
157        let pages = walk_freelist_chain(&db, 2, 4096);
158        assert_eq!(pages, vec![2, 3]); // cycle stops after visiting both once
159    }
160
161    #[test]
162    fn test_freeblock_chain_parse() {
163        let mut page = vec![0u8; 4096];
164        page[0] = 0x0d; // table leaf page type
165                        // first freeblock at offset 200
166        page[1] = 0x00;
167        page[2] = 0xc8;
168        // freeblock at 200: next=0, size=20
169        page[200] = 0x00;
170        page[201] = 0x00; // next = 0
171        page[202] = 0x00;
172        page[203] = 0x14; // size = 20
173        for i in 204..220 {
174            page[i] = (i % 256) as u8;
175        }
176
177        let freeblocks = parse_freeblock_chain(&page, 4096);
178        assert_eq!(freeblocks.len(), 1);
179        assert_eq!(freeblocks[0].offset, 200);
180        assert_eq!(freeblocks[0].size, 20);
181        assert_eq!(freeblocks[0].data.len(), 16); // size - 4 header bytes
182    }
183
184    #[test]
185    fn test_freeblock_chain_two_blocks() {
186        let mut page = vec![0u8; 4096];
187        page[0] = 0x0d;
188        // first freeblock at 200
189        page[1] = 0x00;
190        page[2] = 0xc8; // first_fb = 200
191                        // freeblock at 200: next=300, size=10
192        page[200] = 0x01;
193        page[201] = 0x2c; // next = 300
194        page[202] = 0x00;
195        page[203] = 0x0a; // size = 10
196                          // freeblock at 300: next=0, size=8
197        page[300] = 0x00;
198        page[301] = 0x00; // next = 0
199        page[302] = 0x00;
200        page[303] = 0x08; // size = 8
201
202        let freeblocks = parse_freeblock_chain(&page, 4096);
203        assert_eq!(freeblocks.len(), 2);
204        assert_eq!(freeblocks[0].offset, 200);
205        assert_eq!(freeblocks[0].size, 10);
206        assert_eq!(freeblocks[1].offset, 300);
207        assert_eq!(freeblocks[1].size, 8);
208    }
209
210    #[test]
211    fn test_freeblock_chain_self_cycle() {
212        let mut page = vec![0u8; 4096];
213        page[0] = 0x0d;
214        // first freeblock at 100
215        page[1] = 0x00;
216        page[2] = 0x64; // first_fb = 100
217                        // freeblock at 100: next=100 (self-referential cycle), size=8
218        page[100] = 0x00;
219        page[101] = 0x64; // next = 100 (cycle!)
220        page[102] = 0x00;
221        page[103] = 0x08; // size = 8
222
223        let freeblocks = parse_freeblock_chain(&page, 4096);
224        assert_eq!(freeblocks.len(), 1); // parse one block, then stop on cycle
225    }
226}