Skip to main content

winreg_format/
cells.rs

1//! Cell types and the `CellOffset` newtype.
2
3use crate::flags::{KeyFlags, ValueFlags, ValueType};
4use binrw::BinRead;
5
6/// Offset to a cell within hive bins data.
7///
8/// All cell offsets in the REGF format are relative to the start of the hive
9/// bins data area (which begins at file offset 4096). This newtype prevents
10/// accidentally mixing cell offsets with file offsets.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, BinRead)]
12#[br(little)]
13pub struct CellOffset(pub u32);
14
15impl CellOffset {
16    /// Null/empty sentinel value (0xFFFFFFFF).
17    pub const NULL: Self = Self(0xFFFF_FFFF);
18
19    /// Convert a cell offset to an absolute file offset.
20    ///
21    /// `file_offset = 4096 + cell_offset`
22    pub fn file_offset(self) -> u64 {
23        4096 + u64::from(self.0)
24    }
25
26    /// Check if this is a null/empty reference.
27    pub fn is_null(self) -> bool {
28        self.0 == 0xFFFF_FFFF
29    }
30}
31
32impl std::fmt::Display for CellOffset {
33    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34        if self.is_null() {
35            write!(f, "NULL")
36        } else {
37            write!(f, "0x{:08X}", self.0)
38        }
39    }
40}
41
42/// Raw cell header — the first 4 bytes of every cell.
43///
44/// Cell size is a signed i32:
45/// - **Negative** = allocated cell (use absolute value for size)
46/// - **Positive** = free/unallocated cell
47///
48/// All cell sizes are 8-byte aligned.
49#[derive(Debug, Clone, Copy)]
50pub struct CellHeader {
51    /// Raw size field (negative = allocated, positive = free).
52    pub raw_size: i32,
53}
54
55impl CellHeader {
56    /// Parse cell header from 4 bytes.
57    pub fn from_bytes(bytes: &[u8; 4]) -> Self {
58        Self {
59            raw_size: i32::from_le_bytes(*bytes),
60        }
61    }
62
63    /// Whether this cell is allocated.
64    pub fn is_allocated(&self) -> bool {
65        self.raw_size < 0
66    }
67
68    /// Absolute cell size in bytes (including the 4-byte size field).
69    pub fn size(&self) -> u32 {
70        self.raw_size.unsigned_abs()
71    }
72}
73
74/// Two-byte cell signature identifying the cell type.
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76pub enum CellSignature {
77    /// `nk` — Key Node
78    KeyNode,
79    /// `vk` — Key Value
80    KeyValue,
81    /// `sk` — Security Key
82    SecurityKey,
83    /// `lf` — Fast Leaf (subkey index with name hints)
84    FastLeaf,
85    /// `lh` — Hash Leaf (subkey index with name hashes)
86    HashLeaf,
87    /// `li` — Index Leaf (simple subkey index)
88    IndexLeaf,
89    /// `ri` — Root Index (index of subkey indices)
90    RootIndex,
91    /// `db` — Big Data
92    BigData,
93}
94
95impl CellSignature {
96    /// Parse a 2-byte signature.
97    pub fn from_bytes(bytes: &[u8; 2]) -> Option<Self> {
98        match bytes {
99            b"nk" => Some(Self::KeyNode),
100            b"vk" => Some(Self::KeyValue),
101            b"sk" => Some(Self::SecurityKey),
102            b"lf" => Some(Self::FastLeaf),
103            b"lh" => Some(Self::HashLeaf),
104            b"li" => Some(Self::IndexLeaf),
105            b"ri" => Some(Self::RootIndex),
106            b"db" => Some(Self::BigData),
107            _ => None,
108        }
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn cell_offset_file_conversion() {
118        let offset = CellOffset(0x20);
119        assert_eq!(offset.file_offset(), 4096 + 0x20);
120    }
121
122    #[test]
123    fn cell_offset_null() {
124        assert!(CellOffset::NULL.is_null());
125        assert!(!CellOffset(0).is_null());
126    }
127
128    #[test]
129    fn cell_offset_display() {
130        assert_eq!(format!("{}", CellOffset::NULL), "NULL");
131        assert_eq!(format!("{}", CellOffset(0x20)), "0x00000020");
132    }
133
134    #[test]
135    fn cell_header_allocated() {
136        let bytes = (-128i32).to_le_bytes();
137        let header = CellHeader::from_bytes(&bytes);
138        assert!(header.is_allocated());
139        assert_eq!(header.size(), 128);
140    }
141
142    #[test]
143    fn cell_header_free() {
144        let bytes = 64i32.to_le_bytes();
145        let header = CellHeader::from_bytes(&bytes);
146        assert!(!header.is_allocated());
147        assert_eq!(header.size(), 64);
148    }
149
150    #[test]
151    fn cell_signatures() {
152        assert_eq!(
153            CellSignature::from_bytes(b"nk"),
154            Some(CellSignature::KeyNode)
155        );
156        assert_eq!(
157            CellSignature::from_bytes(b"vk"),
158            Some(CellSignature::KeyValue)
159        );
160        assert_eq!(
161            CellSignature::from_bytes(b"sk"),
162            Some(CellSignature::SecurityKey)
163        );
164        assert_eq!(
165            CellSignature::from_bytes(b"lf"),
166            Some(CellSignature::FastLeaf)
167        );
168        assert_eq!(
169            CellSignature::from_bytes(b"lh"),
170            Some(CellSignature::HashLeaf)
171        );
172        assert_eq!(
173            CellSignature::from_bytes(b"li"),
174            Some(CellSignature::IndexLeaf)
175        );
176        assert_eq!(
177            CellSignature::from_bytes(b"ri"),
178            Some(CellSignature::RootIndex)
179        );
180        assert_eq!(
181            CellSignature::from_bytes(b"db"),
182            Some(CellSignature::BigData)
183        );
184        assert_eq!(CellSignature::from_bytes(b"xx"), None);
185    }
186}
187
188/// Raw NK (Key Node) cell data — parsed from bytes after the cell size field.
189///
190/// Fixed header: 76 bytes (0x4C) + variable-length key name.
191#[derive(Debug, Clone)]
192pub struct RawKeyNode {
193    pub flags: KeyFlags,
194    pub last_written: u64,
195    pub access_bits: u32,
196    pub parent: CellOffset,
197    pub subkey_count: u32,
198    pub volatile_subkey_count: u32,
199    pub subkeys_list_offset: CellOffset,
200    pub volatile_subkeys_list_offset: CellOffset,
201    pub value_count: u32,
202    pub values_list_offset: CellOffset,
203    pub security_offset: CellOffset,
204    pub class_name_offset: CellOffset,
205    pub max_subkey_name_compound: u32,
206    pub max_subkey_class_len: u32,
207    pub max_value_name_len: u32,
208    pub max_value_data_size: u32,
209    pub work_var: u32,
210    pub key_name_len: u16,
211    pub class_name_len: u16,
212    pub key_name_raw: Vec<u8>,
213}
214
215impl RawKeyNode {
216    pub const HEADER_SIZE: usize = 0x4C;
217
218    /// Parse an NK cell from a byte slice (starting after the 2-byte "nk" signature).
219    pub fn parse(data: &[u8]) -> Option<Self> {
220        if data.len() < Self::HEADER_SIZE - 2 {
221            return None;
222        }
223        let flags = KeyFlags::from_bits_truncate(u16::from_le_bytes([data[0], data[1]]));
224        let last_written = u64::from_le_bytes(data[2..10].try_into().ok()?);
225        let access_bits = u32::from_le_bytes(data[10..14].try_into().ok()?);
226        let parent = CellOffset(u32::from_le_bytes(data[14..18].try_into().ok()?));
227        let subkey_count = u32::from_le_bytes(data[18..22].try_into().ok()?);
228        let volatile_subkey_count = u32::from_le_bytes(data[22..26].try_into().ok()?);
229        let subkeys_list_offset = CellOffset(u32::from_le_bytes(data[26..30].try_into().ok()?));
230        let volatile_subkeys_list_offset =
231            CellOffset(u32::from_le_bytes(data[30..34].try_into().ok()?));
232        let value_count = u32::from_le_bytes(data[34..38].try_into().ok()?);
233        let values_list_offset = CellOffset(u32::from_le_bytes(data[38..42].try_into().ok()?));
234        let security_offset = CellOffset(u32::from_le_bytes(data[42..46].try_into().ok()?));
235        let class_name_offset = CellOffset(u32::from_le_bytes(data[46..50].try_into().ok()?));
236        let max_subkey_name_compound = u32::from_le_bytes(data[50..54].try_into().ok()?);
237        let max_subkey_class_len = u32::from_le_bytes(data[54..58].try_into().ok()?);
238        let max_value_name_len = u32::from_le_bytes(data[58..62].try_into().ok()?);
239        let max_value_data_size = u32::from_le_bytes(data[62..66].try_into().ok()?);
240        let work_var = u32::from_le_bytes(data[66..70].try_into().ok()?);
241        let key_name_len = u16::from_le_bytes([data[70], data[71]]);
242        let class_name_len = u16::from_le_bytes([data[72], data[73]]);
243
244        let name_start = 74;
245        let name_end = name_start + usize::from(key_name_len);
246        if data.len() < name_end {
247            return None;
248        }
249        let key_name_raw = data[name_start..name_end].to_vec();
250
251        Some(Self {
252            flags,
253            last_written,
254            access_bits,
255            parent,
256            subkey_count,
257            volatile_subkey_count,
258            subkeys_list_offset,
259            volatile_subkeys_list_offset,
260            value_count,
261            values_list_offset,
262            security_offset,
263            class_name_offset,
264            max_subkey_name_compound,
265            max_subkey_class_len,
266            max_value_name_len,
267            max_value_data_size,
268            work_var,
269            key_name_len,
270            class_name_len,
271            key_name_raw,
272        })
273    }
274
275    pub fn key_name(&self) -> String {
276        if self.flags.contains(KeyFlags::COMP_NAME) {
277            self.key_name_raw.iter().map(|&b| b as char).collect()
278        } else {
279            let u16s: Vec<u16> = self
280                .key_name_raw
281                .chunks_exact(2)
282                .map(|c| u16::from_le_bytes([c[0], c[1]]))
283                .collect();
284            String::from_utf16_lossy(&u16s)
285        }
286    }
287
288    pub fn is_root(&self) -> bool {
289        self.flags.contains(KeyFlags::HIVE_ENTRY)
290    }
291}
292
293/// Raw VK (Key Value) cell data — parsed from bytes after the cell size field.
294///
295/// Fixed header: 20 bytes (0x14) + variable-length value name.
296#[derive(Debug, Clone)]
297pub struct RawKeyValue {
298    pub name_len: u16,
299    pub data_size_raw: u32,
300    pub data_offset_raw: u32,
301    pub data_type: ValueType,
302    pub flags: ValueFlags,
303    pub name_raw: Vec<u8>,
304}
305
306impl RawKeyValue {
307    pub const HEADER_SIZE: usize = 0x14;
308
309    pub fn parse(data: &[u8]) -> Option<Self> {
310        if data.len() < Self::HEADER_SIZE - 2 {
311            return None;
312        }
313        let name_len = u16::from_le_bytes([data[0], data[1]]);
314        let data_size_raw = u32::from_le_bytes(data[2..6].try_into().ok()?);
315        let data_offset_raw = u32::from_le_bytes(data[6..10].try_into().ok()?);
316        let data_type = ValueType::from_raw(u32::from_le_bytes(data[10..14].try_into().ok()?));
317        let flags = ValueFlags::from_bits_truncate(u16::from_le_bytes([data[14], data[15]]));
318
319        let name_start = 18;
320        let name_end = name_start + usize::from(name_len);
321        if data.len() < name_end {
322            return None;
323        }
324        let name_raw = data[name_start..name_end].to_vec();
325
326        Some(Self {
327            name_len,
328            data_size_raw,
329            data_offset_raw,
330            data_type,
331            flags,
332            name_raw,
333        })
334    }
335
336    pub fn is_resident(&self) -> bool {
337        self.data_size_raw & 0x8000_0000 != 0
338    }
339
340    pub fn data_size(&self) -> u32 {
341        self.data_size_raw & 0x7FFF_FFFF
342    }
343
344    pub fn data_offset(&self) -> CellOffset {
345        CellOffset(self.data_offset_raw)
346    }
347
348    pub fn inline_data(&self) -> Vec<u8> {
349        let size = self.data_size() as usize;
350        let bytes = self.data_offset_raw.to_le_bytes();
351        bytes[..size.min(4)].to_vec()
352    }
353
354    pub fn value_name(&self) -> String {
355        if self.name_len == 0 {
356            return String::new();
357        }
358        if self.flags.contains(ValueFlags::COMP_NAME) {
359            self.name_raw.iter().map(|&b| b as char).collect()
360        } else {
361            let u16s: Vec<u16> = self
362                .name_raw
363                .chunks_exact(2)
364                .map(|c| u16::from_le_bytes([c[0], c[1]]))
365                .collect();
366            String::from_utf16_lossy(&u16s)
367        }
368    }
369}
370
371#[cfg(test)]
372mod nk_vk_tests {
373    use super::*;
374    use crate::flags::KeyFlags;
375
376    fn build_nk_bytes(name: &str, flags: KeyFlags, subkey_count: u32, value_count: u32) -> Vec<u8> {
377        let name_bytes = name.as_bytes();
378        let mut buf = vec![0u8; 74 + name_bytes.len()];
379        buf[0..2].copy_from_slice(&flags.bits().to_le_bytes());
380        buf[2..10].copy_from_slice(&1000u64.to_le_bytes());
381        buf[14..18].copy_from_slice(&0x20u32.to_le_bytes());
382        buf[18..22].copy_from_slice(&subkey_count.to_le_bytes());
383        let sk_offset = if subkey_count > 0 {
384            0x100u32
385        } else {
386            0xFFFF_FFFFu32
387        };
388        buf[26..30].copy_from_slice(&sk_offset.to_le_bytes());
389        buf[34..38].copy_from_slice(&value_count.to_le_bytes());
390        let vl_offset = if value_count > 0 {
391            0x200u32
392        } else {
393            0xFFFF_FFFFu32
394        };
395        buf[38..42].copy_from_slice(&vl_offset.to_le_bytes());
396        buf[42..46].copy_from_slice(&0x300u32.to_le_bytes());
397        buf[46..50].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
398        buf[70..72].copy_from_slice(&(name_bytes.len() as u16).to_le_bytes());
399        buf[74..74 + name_bytes.len()].copy_from_slice(name_bytes);
400        buf
401    }
402
403    #[test]
404    fn parse_nk_root_key() {
405        let data = build_nk_bytes(
406            "CMI-CreateHive{2A7FB991}",
407            KeyFlags::HIVE_ENTRY | KeyFlags::COMP_NAME,
408            3,
409            0,
410        );
411        let nk = RawKeyNode::parse(&data).unwrap();
412        assert!(nk.is_root());
413        assert_eq!(nk.key_name(), "CMI-CreateHive{2A7FB991}");
414        assert_eq!(nk.subkey_count, 3);
415        assert_eq!(nk.value_count, 0);
416        assert!(nk.flags.contains(KeyFlags::COMP_NAME));
417    }
418
419    #[test]
420    fn parse_nk_child_key() {
421        let data = build_nk_bytes("Software", KeyFlags::COMP_NAME, 0, 2);
422        let nk = RawKeyNode::parse(&data).unwrap();
423        assert!(!nk.is_root());
424        assert_eq!(nk.key_name(), "Software");
425        assert_eq!(nk.value_count, 2);
426    }
427
428    #[test]
429    fn nk_rejects_truncated_data() {
430        let data = vec![0u8; 10];
431        assert!(RawKeyNode::parse(&data).is_none());
432    }
433
434    fn build_vk_bytes(name: &str, data_type: u32, data_size: u32, data_offset: u32) -> Vec<u8> {
435        let name_bytes = name.as_bytes();
436        let comp_flag: u16 = if name.is_empty() { 0 } else { 0x0001 };
437        let mut buf = vec![0u8; 18 + name_bytes.len()];
438        buf[0..2].copy_from_slice(&(name_bytes.len() as u16).to_le_bytes());
439        buf[2..6].copy_from_slice(&data_size.to_le_bytes());
440        buf[6..10].copy_from_slice(&data_offset.to_le_bytes());
441        buf[10..14].copy_from_slice(&data_type.to_le_bytes());
442        buf[14..16].copy_from_slice(&comp_flag.to_le_bytes());
443        buf[18..18 + name_bytes.len()].copy_from_slice(name_bytes);
444        buf
445    }
446
447    #[test]
448    fn parse_vk_dword_resident() {
449        let data = build_vk_bytes("Start", 4, 0x8000_0004, 0x0000_0003);
450        let vk = RawKeyValue::parse(&data).unwrap();
451        assert_eq!(vk.value_name(), "Start");
452        assert!(matches!(vk.data_type, ValueType::Dword));
453        assert!(vk.is_resident());
454        assert_eq!(vk.data_size(), 4);
455        assert_eq!(vk.inline_data(), vec![3, 0, 0, 0]);
456    }
457
458    #[test]
459    fn parse_vk_string_non_resident() {
460        let data = build_vk_bytes("ImagePath", 1, 42, 0x500);
461        let vk = RawKeyValue::parse(&data).unwrap();
462        assert_eq!(vk.value_name(), "ImagePath");
463        assert!(matches!(vk.data_type, ValueType::Sz));
464        assert!(!vk.is_resident());
465        assert_eq!(vk.data_size(), 42);
466        assert_eq!(vk.data_offset(), CellOffset(0x500));
467    }
468
469    #[test]
470    fn parse_vk_unnamed_default_value() {
471        let data = build_vk_bytes("", 1, 10, 0x600);
472        let vk = RawKeyValue::parse(&data).unwrap();
473        assert_eq!(vk.value_name(), "");
474        assert_eq!(vk.name_len, 0);
475    }
476
477    #[test]
478    fn vk_rejects_truncated_data() {
479        let data = vec![0u8; 5];
480        assert!(RawKeyValue::parse(&data).is_none());
481    }
482}
483
484/// LF (Fast Leaf) element: key node offset + 4-byte name hint.
485#[derive(Debug, Clone, Copy)]
486pub struct LfElement {
487    pub key_offset: CellOffset,
488    /// First 4 ASCII characters of key name (uppercase).
489    pub name_hint: [u8; 4],
490}
491
492/// LH (Hash Leaf) element: key node offset + 32-bit name hash.
493#[derive(Debug, Clone, Copy)]
494pub struct LhElement {
495    pub key_offset: CellOffset,
496    /// Hash: H = 37*H + C[i] over uppercase key name.
497    pub name_hash: u32,
498}
499
500/// Parsed subkey index — dispatches across LF, LH, LI, RI.
501#[derive(Debug, Clone)]
502pub enum SubkeyIndex {
503    FastLeaf(Vec<LfElement>),
504    HashLeaf(Vec<LhElement>),
505    IndexLeaf(Vec<CellOffset>),
506    RootIndex(Vec<CellOffset>),
507}
508
509impl SubkeyIndex {
510    pub fn parse_lf(data: &[u8]) -> Option<Self> {
511        if data.len() < 2 {
512            return None;
513        }
514        let count = u16::from_le_bytes([data[0], data[1]]) as usize;
515        let elements_data = &data[2..];
516        if elements_data.len() < count * 8 {
517            return None;
518        }
519        let elements = (0..count)
520            .map(|i| {
521                let base = i * 8;
522                LfElement {
523                    key_offset: CellOffset(crate::bytes::le_u32(elements_data, base)),
524                    name_hint: crate::bytes::read4(elements_data, base + 4),
525                }
526            })
527            .collect();
528        Some(Self::FastLeaf(elements))
529    }
530
531    pub fn parse_lh(data: &[u8]) -> Option<Self> {
532        if data.len() < 2 {
533            return None;
534        }
535        let count = u16::from_le_bytes([data[0], data[1]]) as usize;
536        let elements_data = &data[2..];
537        if elements_data.len() < count * 8 {
538            return None;
539        }
540        let elements = (0..count)
541            .map(|i| {
542                let base = i * 8;
543                LhElement {
544                    key_offset: CellOffset(crate::bytes::le_u32(elements_data, base)),
545                    name_hash: crate::bytes::le_u32(elements_data, base + 4),
546                }
547            })
548            .collect();
549        Some(Self::HashLeaf(elements))
550    }
551
552    pub fn parse_li(data: &[u8]) -> Option<Self> {
553        if data.len() < 2 {
554            return None;
555        }
556        let count = u16::from_le_bytes([data[0], data[1]]) as usize;
557        let elements_data = &data[2..];
558        if elements_data.len() < count * 4 {
559            return None;
560        }
561        let offsets = (0..count)
562            .map(|i| {
563                let base = i * 4;
564                CellOffset(crate::bytes::le_u32(elements_data, base))
565            })
566            .collect();
567        Some(Self::IndexLeaf(offsets))
568    }
569
570    pub fn parse_ri(data: &[u8]) -> Option<Self> {
571        if data.len() < 2 {
572            return None;
573        }
574        let count = u16::from_le_bytes([data[0], data[1]]) as usize;
575        let elements_data = &data[2..];
576        if elements_data.len() < count * 4 {
577            return None;
578        }
579        let offsets = (0..count)
580            .map(|i| {
581                let base = i * 4;
582                CellOffset(crate::bytes::le_u32(elements_data, base))
583            })
584            .collect();
585        Some(Self::RootIndex(offsets))
586    }
587}
588
589/// Compute LH name hash: H = 37*H + C[i] over uppercase name.
590pub fn lh_hash(name: &str) -> u32 {
591    let mut h: u32 = 0;
592    for c in name.to_ascii_uppercase().bytes() {
593        h = h.wrapping_mul(37).wrapping_add(u32::from(c));
594    }
595    h
596}
597
598/// Raw SK (Security Key) cell data.
599#[derive(Debug, Clone)]
600pub struct RawSecurityKey {
601    pub flink: CellOffset,
602    pub blink: CellOffset,
603    pub reference_count: u32,
604    pub descriptor_size: u32,
605    pub descriptor: Vec<u8>,
606}
607
608impl RawSecurityKey {
609    pub fn parse(data: &[u8]) -> Option<Self> {
610        if data.len() < 18 {
611            return None;
612        }
613        let flink = CellOffset(u32::from_le_bytes(data[2..6].try_into().ok()?));
614        let blink = CellOffset(u32::from_le_bytes(data[6..10].try_into().ok()?));
615        let reference_count = u32::from_le_bytes(data[10..14].try_into().ok()?);
616        let descriptor_size = u32::from_le_bytes(data[14..18].try_into().ok()?);
617        let desc_end = 18 + descriptor_size as usize;
618        if data.len() < desc_end {
619            return None;
620        }
621        let descriptor = data[18..desc_end].to_vec();
622        Some(Self {
623            flink,
624            blink,
625            reference_count,
626            descriptor_size,
627            descriptor,
628        })
629    }
630}
631
632/// Raw DB (Big Data) cell.
633#[derive(Debug, Clone)]
634pub struct RawBigData {
635    pub segment_count: u16,
636    pub segment_list_offset: CellOffset,
637}
638
639impl RawBigData {
640    pub fn parse(data: &[u8]) -> Option<Self> {
641        if data.len() < 6 {
642            return None;
643        }
644        let segment_count = u16::from_le_bytes([data[0], data[1]]);
645        let segment_list_offset = CellOffset(u32::from_le_bytes(data[2..6].try_into().ok()?));
646        Some(Self {
647            segment_count,
648            segment_list_offset,
649        })
650    }
651}
652
653#[cfg(test)]
654mod index_tests {
655    use super::*;
656
657    #[test]
658    fn parse_lh_with_two_elements() {
659        let mut data = vec![0u8; 2 + 2 * 8];
660        data[0..2].copy_from_slice(&2u16.to_le_bytes());
661        data[2..6].copy_from_slice(&0x100u32.to_le_bytes());
662        data[6..10].copy_from_slice(&0xABCDu32.to_le_bytes());
663        data[10..14].copy_from_slice(&0x200u32.to_le_bytes());
664        data[14..18].copy_from_slice(&0x1234u32.to_le_bytes());
665        let index = SubkeyIndex::parse_lh(&data).unwrap();
666        if let SubkeyIndex::HashLeaf(elements) = index {
667            assert_eq!(elements.len(), 2);
668            assert_eq!(elements[0].key_offset, CellOffset(0x100));
669            assert_eq!(elements[0].name_hash, 0xABCD);
670            assert_eq!(elements[1].key_offset, CellOffset(0x200));
671        } else {
672            panic!("expected HashLeaf");
673        }
674    }
675
676    #[test]
677    fn parse_li_with_three_offsets() {
678        let mut data = vec![0u8; 2 + 3 * 4];
679        data[0..2].copy_from_slice(&3u16.to_le_bytes());
680        data[2..6].copy_from_slice(&0x100u32.to_le_bytes());
681        data[6..10].copy_from_slice(&0x200u32.to_le_bytes());
682        data[10..14].copy_from_slice(&0x300u32.to_le_bytes());
683        let index = SubkeyIndex::parse_li(&data).unwrap();
684        if let SubkeyIndex::IndexLeaf(offsets) = index {
685            assert_eq!(offsets.len(), 3);
686            assert_eq!(offsets[0], CellOffset(0x100));
687        } else {
688            panic!("expected IndexLeaf");
689        }
690    }
691
692    #[test]
693    fn lh_hash_algorithm() {
694        let hash = lh_hash("SOFTWARE");
695        assert_eq!(hash, lh_hash("software")); // case-insensitive
696    }
697
698    #[test]
699    fn parse_sk_cell() {
700        let mut data = vec![0u8; 18 + 20];
701        data[2..6].copy_from_slice(&0x100u32.to_le_bytes());
702        data[6..10].copy_from_slice(&0x200u32.to_le_bytes());
703        data[10..14].copy_from_slice(&3u32.to_le_bytes());
704        data[14..18].copy_from_slice(&20u32.to_le_bytes());
705        data[18..38].fill(0xAA);
706        let sk = RawSecurityKey::parse(&data).unwrap();
707        assert_eq!(sk.flink, CellOffset(0x100));
708        assert_eq!(sk.reference_count, 3);
709        assert_eq!(sk.descriptor.len(), 20);
710    }
711
712    #[test]
713    fn parse_db_cell() {
714        let mut data = vec![0u8; 6];
715        data[0..2].copy_from_slice(&3u16.to_le_bytes());
716        data[2..6].copy_from_slice(&0x500u32.to_le_bytes());
717        let db = RawBigData::parse(&data).unwrap();
718        assert_eq!(db.segment_count, 3);
719        assert_eq!(db.segment_list_offset, CellOffset(0x500));
720    }
721
722    #[test]
723    fn empty_index_parses() {
724        let data = vec![0u8; 2];
725        let index = SubkeyIndex::parse_lh(&data).unwrap();
726        if let SubkeyIndex::HashLeaf(elements) = index {
727            assert!(elements.is_empty());
728        } else {
729            panic!("expected empty HashLeaf");
730        }
731    }
732}