steam_vdf_parser/binary/
parser.rs

1//! Binary VDF parser implementation.
2//!
3//! Supports Steam's binary VDF formats:
4//! - shortcuts.vdf (simple binary format)
5//! - appinfo.vdf (with optional string table)
6
7use alloc::borrow::Cow;
8use alloc::format;
9use alloc::string::{String, ToString};
10use alloc::vec::Vec;
11use core::str;
12
13use crate::binary::byte_reader::{read_u32_le, read_u64_le};
14use crate::binary::types::{
15    APPINFO_MAGIC_40, APPINFO_MAGIC_41, BinaryType, PACKAGEINFO_MAGIC_39, PACKAGEINFO_MAGIC_40,
16    PACKAGEINFO_MAGIC_BASE,
17};
18use crate::error::{Error, Result, with_offset};
19use crate::value::{Obj, Value, Vdf};
20
21// ===== Appinfo Header Constants =====
22
23/// Size of the appinfo entry header (up to and including the size field).
24const APPINFO_HEADER_SIZE: usize = 8;
25
26/// Size of the header after the size field (60 bytes).
27const APPINFO_HEADER_AFTER_SIZE: usize = 60;
28
29/// Total size of the appinfo entry header.
30const APPINFO_ENTRY_HEADER_SIZE: usize = APPINFO_HEADER_SIZE + APPINFO_HEADER_AFTER_SIZE;
31
32/// Offset where VDF data starts within an appinfo entry.
33const APPINFO_VDF_DATA_OFFSET: usize = APPINFO_ENTRY_HEADER_SIZE;
34
35// ===== Helper Functions =====
36
37/// Read a little-endian u32 from the start of a slice, returning an error if too small.
38fn ensure_read_u32_le(input: &[u8]) -> Result<(&[u8], u32)> {
39    read_u32_le(input)
40        .map(|value| (&input[4..], value))
41        .ok_or(Error::UnexpectedEndOfInput {
42            context: "reading u32",
43            offset: 0,
44            expected: 4,
45            actual: input.len(),
46        })
47}
48
49/// Parse configuration for binary VDF formats.
50///
51/// Encapsulates the differences between shortcuts.vdf and appinfo.vdf formats.
52#[derive(Clone, Copy, Debug, PartialEq, Default)]
53struct ParseConfig<'input, 'table> {
54    /// Strategy for parsing keys
55    key_mode: KeyMode<'input, 'table>,
56}
57
58/// Key parsing strategy for binary VDF formats.
59#[derive(Clone, Copy, Debug, PartialEq, Default)]
60enum KeyMode<'input, 'table> {
61    /// Parse keys as null-terminated UTF-8 strings (v40, shortcuts)
62    #[default]
63    NullTerminated,
64    /// Parse keys as u32 indices into string table (v41)
65    StringTableIndex {
66        string_table: &'table StringTable<'input>,
67    },
68}
69
70/// String table for v41 appinfo format.
71///
72/// Encapsulates pre-extracted strings from the string table section,
73/// enabling O(1) lookups by index.
74#[derive(Clone, Debug, PartialEq)]
75struct StringTable<'a> {
76    strings: Vec<&'a str>,
77}
78
79impl<'a> StringTable<'a> {
80    /// Get a string by index.
81    fn get(&self, index: usize) -> Result<&'a str> {
82        self.strings
83            .get(index)
84            .copied()
85            .ok_or(Error::InvalidStringIndex {
86                index,
87                max: self.strings.len(),
88            })
89    }
90}
91
92impl<'a> KeyMode<'a, '_> {
93    /// Parse a key from input according to this mode.
94    fn parse_key(&self, input: &'a [u8]) -> Result<(&'a [u8], Cow<'a, str>)> {
95        match self {
96            KeyMode::NullTerminated => {
97                let (rest, s) = parse_null_terminated_string_borrowed(input)?;
98                Ok((rest, Cow::Borrowed(s)))
99            }
100            KeyMode::StringTableIndex { string_table } => {
101                let (rest, index) = ensure_read_u32_le(input)?;
102                let s = string_table.get(index as usize)?;
103                Ok((rest, Cow::Borrowed(s)))
104            }
105        }
106    }
107}
108
109/// Parse binary VDF data (autodetects format).
110///
111/// Attempts to parse as appinfo.vdf first, then falls back to shortcuts.vdf format.
112/// For shortcuts format, returns zero-copy data borrowed from input.
113/// For appinfo format, returns mixed data: root key and app ID keys are owned,
114/// but actual parsed values (including string table entries) are borrowed.
115/// For packageinfo format, returns mixed data similar to appinfo.
116pub fn parse(input: &[u8]) -> Result<Vdf<'_>> {
117    // Check if this looks like appinfo or packageinfo format (starts with magic)
118    if let Some(magic) = read_u32_le(input) {
119        if magic == APPINFO_MAGIC_40 || magic == APPINFO_MAGIC_41 {
120            return parse_appinfo(input);
121        }
122        if magic == PACKAGEINFO_MAGIC_39 || magic == PACKAGEINFO_MAGIC_40 {
123            return parse_packageinfo(input);
124        }
125    }
126
127    // Otherwise, parse as shortcuts format (zero-copy)
128    parse_shortcuts(input)
129}
130
131/// Parse shortcuts.vdf format binary data.
132///
133/// This is the simpler binary format used by Steam for shortcuts and other data.
134///
135/// This function returns zero-copy data - strings are borrowed from the input buffer.
136///
137/// Format:
138/// - Each entry starts with a type byte
139/// - Type 0x00: Object start (key is the object name)
140/// - Type 0x01: String value
141/// - Type 0x02: Int32 value
142/// - Type 0x08: Object end
143///
144/// All strings are null-terminated.
145pub fn parse_shortcuts(input: &[u8]) -> Result<Vdf<'_>> {
146    let config = ParseConfig::default();
147    let (_rest, obj) = parse_object(input, &config)?;
148
149    Ok(Vdf::new("root", Value::Obj(obj)))
150}
151
152/// Parse appinfo.vdf format binary data.
153///
154/// This function returns zero-copy data where possible - strings are borrowed from
155/// the input buffer (including string table entries in v41 format).
156///
157/// Format:
158/// - 4 bytes: Magic number (0x07564428 or 0x07564429)
159/// - 4 bytes: Universe
160/// - If magic == 0x07564429: 8 bytes: String table offset
161/// - Apps continue until EOF (or string table for v41)
162/// - For each app:
163///   - 4 bytes: App ID
164///   - 4 bytes: Size (remaining data size for this entry)
165///   - 4 bytes: InfoState
166///   - 4 bytes: LastUpdated (Unix timestamp)
167///   - 8 bytes: AccessToken
168///   - 20 bytes: SHA1 of text data
169///   - 4 bytes: ChangeNumber
170///   - 20 bytes: SHA1 of binary data
171///   - Then the VDF data for the app (starts with 0x00)
172/// - String table (if magic == 0x07564429, at string_table_offset)
173///
174/// App entry header is `APPINFO_ENTRY_HEADER_SIZE` (68) bytes.
175pub fn parse_appinfo(input: &[u8]) -> Result<Vdf<'_>> {
176    if input.len() < 16 {
177        return Err(Error::UnexpectedEndOfInput {
178            context: "reading appinfo header",
179            offset: input.len(),
180            expected: 16,
181            actual: input.len(),
182        });
183    }
184
185    let Some(magic) = read_u32_le(input) else {
186        return Err(Error::UnexpectedEndOfInput {
187            context: "reading magic number",
188            offset: 0,
189            expected: 4,
190            actual: input.len(),
191        });
192    };
193    let Some(universe) = read_u32_le(&input[4..]) else {
194        return Err(Error::UnexpectedEndOfInput {
195            context: "reading universe",
196            offset: 4,
197            expected: 4,
198            actual: input.len() - 4,
199        });
200    };
201
202    let (string_table_offset, mut rest) = match magic {
203        APPINFO_MAGIC_40 => (None, &input[8..]),
204        APPINFO_MAGIC_41 => {
205            let Some(offset) = read_u64_le(&input[8..]) else {
206                return Err(Error::UnexpectedEndOfInput {
207                    context: "reading string table offset",
208                    offset: 8,
209                    expected: 8,
210                    actual: input.len() - 8,
211                });
212            };
213            (Some(offset as usize), &input[16..])
214        }
215        _ => {
216            return Err(Error::InvalidMagic {
217                found: magic,
218                expected: &[APPINFO_MAGIC_40, APPINFO_MAGIC_41],
219            });
220        }
221    };
222
223    // Parse the string table if present
224    let string_table = if let Some(offset) = string_table_offset {
225        if offset >= input.len() {
226            return Err(Error::UnexpectedEndOfInput {
227                context: "reading string table",
228                offset,
229                expected: 4,
230                actual: input.len() - offset,
231            });
232        }
233        Some(parse_string_table(&input[offset..]).map_err(with_offset(offset))?)
234    } else {
235        None
236    };
237
238    let mut obj = Obj::new();
239
240    // Calculate where apps end (at string table for v41, or EOF for v40)
241    let apps_end_offset = string_table_offset.unwrap_or(input.len());
242
243    // Use v41 format (string table) if string_table_offset is Some
244    let config = ParseConfig {
245        key_mode: if let Some(string_table) = &string_table {
246            KeyMode::StringTableIndex { string_table }
247        } else {
248            KeyMode::NullTerminated
249        },
250    };
251
252    loop {
253        // Check if we've reached the end of apps section
254        let current_offset = input.len() - rest.len();
255        if current_offset >= apps_end_offset {
256            break;
257        }
258
259        // Not enough data for an app entry header.
260        if rest.len() < APPINFO_ENTRY_HEADER_SIZE {
261            return Err(Error::UnexpectedEndOfInput {
262                context: "reading app entry header",
263                offset: current_offset,
264                expected: APPINFO_ENTRY_HEADER_SIZE,
265                actual: rest.len(),
266            });
267        }
268
269        // App ID (offset 0)
270        let Some(app_id) = read_u32_le(rest) else {
271            return Err(Error::UnexpectedEndOfInput {
272                context: "reading app id",
273                offset: current_offset,
274                expected: 4,
275                actual: rest.len(),
276            });
277        };
278        if app_id == 0 {
279            break;
280        }
281
282        // Size (offset 4) - includes everything AFTER this field (APPINFO_HEADER_AFTER_SIZE bytes + VDF data)
283        let Some(size) = read_u32_le(&rest[4..]) else {
284            return Err(Error::UnexpectedEndOfInput {
285                context: "reading entry size",
286                offset: current_offset + 4,
287                expected: 4,
288                actual: rest.len() - 4,
289            });
290        };
291        let size = size as usize;
292
293        // VDF data starts after the header
294        let vdf_size = size - APPINFO_HEADER_AFTER_SIZE;
295        let vdf_end = APPINFO_VDF_DATA_OFFSET + vdf_size;
296
297        if vdf_end > rest.len() {
298            return Err(Error::UnexpectedEndOfInput {
299                context: "reading VDF data",
300                offset: current_offset + vdf_end,
301                expected: vdf_end,
302                actual: rest.len(),
303            });
304        }
305
306        let vdf_data = &rest[APPINFO_VDF_DATA_OFFSET..vdf_end];
307        let vdf_offset = current_offset + APPINFO_VDF_DATA_OFFSET;
308
309        let (_vdf_rest, app_obj) =
310            parse_object(vdf_data, &config).map_err(with_offset(vdf_offset))?;
311
312        // Insert with app ID as key
313        obj.insert(Cow::Owned(app_id.to_string()), Value::Obj(app_obj));
314        rest = &rest[vdf_end..];
315    }
316
317    Ok(Vdf::new(
318        format!("appinfo_universe_{}", universe),
319        Value::Obj(obj),
320    ))
321}
322
323/// Parses an object from binary VDF data.
324///
325/// This function implements a state machine that:
326/// 1. Reads a type byte to determine the entry type
327/// 2. Parses a key (format depends on `config.key_mode`)
328/// 3. Parses the value based on the type byte
329/// 4. Inserts the key-value pair into the object
330/// 5. Returns on `ObjectEnd` (0x08) marker
331///
332/// # Parameters
333/// - `input`: Binary data to parse
334/// - `config`: Parse configuration including string table reference
335///
336/// # Returns
337/// A tuple of remaining input and the parsed object.
338fn parse_object<'a>(input: &'a [u8], config: &ParseConfig<'a, '_>) -> Result<(&'a [u8], Obj<'a>)> {
339    let mut obj = Obj::new();
340    let mut rest = input;
341
342    loop {
343        match rest {
344            [] => {
345                // At root level, EOF is acceptable - file may end without trailing 0x08
346                break Ok((rest, obj));
347            }
348            [type_byte, remainder @ ..] => {
349                let type_byte = *type_byte;
350                let typ = BinaryType::from_byte(type_byte);
351                let offset = input.len() - remainder.len();
352                rest = remainder;
353
354                match typ {
355                    Some(BinaryType::ObjectEnd) => {
356                        // Consume the end marker and return
357                        return Ok((rest, obj));
358                    }
359                    Some(BinaryType::None) => {
360                        // Map entry: 0x00 [key] { ... entries ... }
361                        let key_offset = input.len() - rest.len();
362                        let (new_rest, key) = config
363                            .key_mode
364                            .parse_key(rest)
365                            .map_err(with_offset(key_offset))?;
366                        let (new_rest, nested_obj) = parse_object(new_rest, config)?;
367                        obj.insert(key, Value::Obj(nested_obj));
368                        rest = new_rest;
369                    }
370                    Some(BinaryType::String) => {
371                        // String entry: 0x01 [key] [value]
372                        // VALUE is ALWAYS inline null-terminated string (never from string table!)
373                        let key_offset = input.len() - rest.len();
374                        let (new_rest, key) = config
375                            .key_mode
376                            .parse_key(rest)
377                            .map_err(with_offset(key_offset))?;
378                        let value_offset = input.len() - new_rest.len();
379                        let (new_rest, value) = parse_null_terminated_string_borrowed(new_rest)
380                            .map_err(with_offset(value_offset))?;
381                        obj.insert(key, Value::Str(Cow::Borrowed(value)));
382                        rest = new_rest;
383                    }
384                    Some(BinaryType::Int32) => {
385                        let key_offset = input.len() - rest.len();
386                        let (new_rest, key) = config
387                            .key_mode
388                            .parse_key(rest)
389                            .map_err(with_offset(key_offset))?;
390                        let value_offset = input.len() - new_rest.len();
391                        let (new_rest, value) =
392                            parse_value_int32(new_rest).map_err(with_offset(value_offset))?;
393                        obj.insert(key, value);
394                        rest = new_rest;
395                    }
396                    Some(BinaryType::UInt64) => {
397                        let key_offset = input.len() - rest.len();
398                        let (new_rest, key) = config
399                            .key_mode
400                            .parse_key(rest)
401                            .map_err(with_offset(key_offset))?;
402                        let value_offset = input.len() - new_rest.len();
403                        let (new_rest, value) =
404                            parse_value_uint64(new_rest).map_err(with_offset(value_offset))?;
405                        obj.insert(key, value);
406                        rest = new_rest;
407                    }
408                    Some(BinaryType::Float) => {
409                        let key_offset = input.len() - rest.len();
410                        let (new_rest, key) = config
411                            .key_mode
412                            .parse_key(rest)
413                            .map_err(with_offset(key_offset))?;
414                        let value_offset = input.len() - new_rest.len();
415                        let (new_rest, value) =
416                            parse_value_float(new_rest).map_err(with_offset(value_offset))?;
417                        obj.insert(key, value);
418                        rest = new_rest;
419                    }
420                    Some(BinaryType::Ptr) => {
421                        let key_offset = input.len() - rest.len();
422                        let (new_rest, key) = config
423                            .key_mode
424                            .parse_key(rest)
425                            .map_err(with_offset(key_offset))?;
426                        let value_offset = input.len() - new_rest.len();
427                        let (new_rest, value) =
428                            parse_value_ptr(new_rest).map_err(with_offset(value_offset))?;
429                        obj.insert(key, value);
430                        rest = new_rest;
431                    }
432                    Some(BinaryType::WString) => {
433                        let key_offset = input.len() - rest.len();
434                        let (new_rest, key) = config
435                            .key_mode
436                            .parse_key(rest)
437                            .map_err(with_offset(key_offset))?;
438                        let value_offset = input.len() - new_rest.len();
439                        let (new_rest, value) =
440                            parse_value_wstring(new_rest).map_err(with_offset(value_offset))?;
441                        obj.insert(key, value);
442                        rest = new_rest;
443                    }
444                    Some(BinaryType::Color) => {
445                        let key_offset = input.len() - rest.len();
446                        let (new_rest, key) = config
447                            .key_mode
448                            .parse_key(rest)
449                            .map_err(with_offset(key_offset))?;
450                        let value_offset = input.len() - new_rest.len();
451                        let (new_rest, value) =
452                            parse_value_color(new_rest).map_err(with_offset(value_offset))?;
453                        obj.insert(key, value);
454                        rest = new_rest;
455                    }
456                    None => {
457                        // Unknown type byte
458                        return Err(Error::UnknownType { type_byte, offset });
459                    }
460                }
461            }
462        }
463    }
464}
465
466// ===== Value Parser Functions =====
467
468/// Parse an Int32 value (4 bytes, little-endian).
469fn parse_value_int32<'a>(input: &'a [u8]) -> Result<(&'a [u8], Value<'a>)> {
470    let arr = <[u8; 4]>::try_from(input.get(..4).ok_or(Error::UnexpectedEndOfInput {
471        context: "reading int32",
472        offset: 0,
473        expected: 4,
474        actual: input.len(),
475    })?)
476    .map_err(|_| Error::UnexpectedEndOfInput {
477        context: "reading int32",
478        offset: 0,
479        expected: 4,
480        actual: input.len(),
481    })?;
482    let value = i32::from_le_bytes(arr);
483    Ok((&input[4..], Value::I32(value)))
484}
485
486/// Parse a UInt64 value (8 bytes, little-endian).
487fn parse_value_uint64<'a>(input: &'a [u8]) -> Result<(&'a [u8], Value<'a>)> {
488    let arr = <[u8; 8]>::try_from(input.get(..8).ok_or(Error::UnexpectedEndOfInput {
489        context: "reading uint64",
490        offset: 0,
491        expected: 8,
492        actual: input.len(),
493    })?)
494    .map_err(|_| Error::UnexpectedEndOfInput {
495        context: "reading uint64",
496        offset: 0,
497        expected: 8,
498        actual: input.len(),
499    })?;
500    let value = u64::from_le_bytes(arr);
501    Ok((&input[8..], Value::U64(value)))
502}
503
504/// Parse a Float value (4 bytes, little-endian).
505fn parse_value_float<'a>(input: &'a [u8]) -> Result<(&'a [u8], Value<'a>)> {
506    let arr = <[u8; 4]>::try_from(input.get(..4).ok_or(Error::UnexpectedEndOfInput {
507        context: "reading float",
508        offset: 0,
509        expected: 4,
510        actual: input.len(),
511    })?)
512    .map_err(|_| Error::UnexpectedEndOfInput {
513        context: "reading float",
514        offset: 0,
515        expected: 4,
516        actual: input.len(),
517    })?;
518    let value = f32::from_le_bytes(arr);
519    Ok((&input[4..], Value::Float(value)))
520}
521
522/// Parse a Pointer value (4 bytes, little-endian).
523fn parse_value_ptr<'a>(input: &'a [u8]) -> Result<(&'a [u8], Value<'a>)> {
524    let (rest, value) = ensure_read_u32_le(input)?;
525    Ok((rest, Value::Pointer(value)))
526}
527
528/// Parse a WideString value (UTF-16LE, null-terminated).
529fn parse_value_wstring<'a>(input: &'a [u8]) -> Result<(&'a [u8], Value<'a>)> {
530    let (rest, string) = parse_null_terminated_wstring(input)?;
531    Ok((rest, Value::Str(Cow::Owned(string))))
532}
533
534/// Parse a Color value (4 bytes RGBA).
535fn parse_value_color<'a>(input: &'a [u8]) -> Result<(&'a [u8], Value<'a>)> {
536    let arr = <[u8; 4]>::try_from(input.get(..4).ok_or(Error::UnexpectedEndOfInput {
537        context: "reading color",
538        offset: 0,
539        expected: 4,
540        actual: input.len(),
541    })?)
542    .map_err(|_| Error::UnexpectedEndOfInput {
543        context: "reading color",
544        offset: 0,
545        expected: 4,
546        actual: input.len(),
547    })?;
548    Ok((&input[4..], Value::Color(arr)))
549}
550
551// ===== String Parsing Functions =====
552
553/// Parse a null-terminated string (UTF-8), returning a borrowed slice.
554///
555/// This is the zero-copy version that borrows from the input when possible.
556fn parse_null_terminated_string_borrowed(input: &[u8]) -> Result<(&[u8], &str)> {
557    let null_pos = input
558        .iter()
559        .position(|&b| b == 0)
560        .ok_or(Error::UnexpectedEndOfInput {
561            context: "reading null-terminated string",
562            offset: 0,
563            expected: 1,
564            actual: input.len(),
565        })?;
566
567    let bytes = &input[..null_pos];
568    let string = core::str::from_utf8(bytes).map_err(|e| Error::InvalidUtf8 {
569        offset: e.valid_up_to(),
570    })?;
571
572    Ok((&input[null_pos + 1..], string))
573}
574
575/// Parse a null-terminated wide string (UTF-16LE).
576///
577/// WideString is terminated by two zero bytes (0x00 0x00).
578/// Note: This allocates due to UTF-16 to UTF-8 conversion.
579fn parse_null_terminated_wstring(input: &[u8]) -> Result<(&[u8], String)> {
580    // Find the double-null terminator
581    let mut i = 0;
582    while i + 1 < input.len() {
583        if input[i] == 0 && input[i + 1] == 0 {
584            break;
585        }
586        i += 2;
587    }
588
589    if i + 1 >= input.len() {
590        return Err(Error::UnexpectedEndOfInput {
591            context: "reading null-terminated wide string",
592            offset: i,
593            expected: 2,
594            actual: input.len().saturating_sub(i),
595        });
596    }
597
598    // Convert UTF-16LE to u16 code units
599    let utf16_units = input[..i]
600        .chunks_exact(2)
601        .map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]));
602
603    // Decode UTF-16 to char and then to String
604    let string: String = char::decode_utf16(utf16_units)
605        .enumerate()
606        .map(|(pos, r)| {
607            r.map_err(|_| Error::InvalidUtf16 {
608                offset: pos * 2,
609                position: pos,
610            })
611        })
612        .collect::<core::result::Result<_, _>>()?;
613
614    Ok((&input[i + 2..], string))
615}
616
617/// Parse the string table section (v41 format).
618///
619/// Returns a `StringTable` containing pre-extracted strings for O(1) lookups.
620///
621/// Format:
622/// - 4 bytes: string_count (little-endian u32)
623/// - Then string_count null-terminated UTF-8 strings
624fn parse_string_table(input: &[u8]) -> Result<StringTable<'_>> {
625    let (mut rest, string_count) = ensure_read_u32_le(input)?;
626    let string_count = string_count as usize;
627
628    let mut strings = Vec::with_capacity(string_count);
629
630    // Extract each null-terminated string
631    for _ in 0..string_count {
632        if rest.is_empty() {
633            return Err(Error::UnexpectedEndOfInput {
634                context: "reading string table entry",
635                offset: input.len() - rest.len(),
636                expected: 1,
637                actual: 0,
638            });
639        }
640        let (new_rest, string) = parse_null_terminated_string_borrowed(rest)?;
641        strings.push(string);
642        rest = new_rest;
643    }
644
645    Ok(StringTable { strings })
646}
647
648/// Header size for packageinfo entries (package_id + hash + change_number + token).
649const PACKAGEINFO_ENTRY_HEADER_SIZE_V39: usize = 4 + 20 + 4; // package_id + hash + change_number
650const PACKAGEINFO_ENTRY_HEADER_SIZE_V40: usize = 4 + 20 + 4 + 8; // + token
651
652/// Parse packageinfo.vdf format binary data.
653///
654/// This function returns zero-copy data where possible - strings are borrowed from
655/// the input buffer.
656///
657/// Format:
658/// - 4 bytes: Magic number + version (0x06565527 for v39, 0x06565528 for v40)
659///   - Upper 3 bytes: 0x065655 (magic)
660///   - Lower 1 byte: version (27 = 39, 28 = 40)
661/// - 4 bytes: Universe
662/// - Repeated package entries until package_id == 0xFFFFFFFF:
663///   - 4 bytes: Package ID (uint32)
664///   - 20 bytes: SHA-1 hash
665///   - 4 bytes: Change number (uint32)
666///   - 8 bytes: PICS token (uint64, only in v40+)
667///   - Binary VDF blob (KeyValues1 binary) with package metadata
668pub fn parse_packageinfo(input: &[u8]) -> Result<Vdf<'_>> {
669    if input.len() < 8 {
670        return Err(Error::UnexpectedEndOfInput {
671            context: "reading packageinfo header",
672            offset: input.len(),
673            expected: 8,
674            actual: input.len(),
675        });
676    }
677
678    let Some(magic) = read_u32_le(input) else {
679        return Err(Error::UnexpectedEndOfInput {
680            context: "reading magic number",
681            offset: 0,
682            expected: 4,
683            actual: input.len(),
684        });
685    };
686
687    // Extract version from lower byte and magic from upper 3 bytes
688    let version = magic & 0xFF;
689    let magic_base = magic >> 8;
690
691    if magic_base != PACKAGEINFO_MAGIC_BASE {
692        return Err(Error::InvalidMagic {
693            found: magic,
694            expected: &[PACKAGEINFO_MAGIC_39, PACKAGEINFO_MAGIC_40],
695        });
696    }
697
698    if version != 39 && version != 40 {
699        return Err(Error::InvalidMagic {
700            found: magic,
701            expected: &[PACKAGEINFO_MAGIC_39, PACKAGEINFO_MAGIC_40],
702        });
703    }
704
705    let Some(universe) = read_u32_le(&input[4..]) else {
706        return Err(Error::UnexpectedEndOfInput {
707            context: "reading universe",
708            offset: 4,
709            expected: 4,
710            actual: input.len() - 4,
711        });
712    };
713
714    let has_token = version >= 40;
715    let header_size = if has_token {
716        PACKAGEINFO_ENTRY_HEADER_SIZE_V40
717    } else {
718        PACKAGEINFO_ENTRY_HEADER_SIZE_V39
719    };
720
721    let mut rest = &input[8..];
722    let mut obj = Obj::new();
723
724    loop {
725        // Check if we have at least 4 bytes for the package ID
726        if rest.len() < 4 {
727            // At EOF or termination marker, exit gracefully
728            break;
729        }
730
731        // Read package ID
732        let Some(package_id) = read_u32_le(rest) else {
733            break;
734        };
735
736        // Check for termination marker
737        if package_id == 0xFFFFFFFF {
738            break;
739        }
740
741        // Now ensure we have enough data for the full header
742        if rest.len() < header_size {
743            return Err(Error::UnexpectedEndOfInput {
744                context: "reading package entry header",
745                offset: input.len() - rest.len(),
746                expected: header_size,
747                actual: rest.len(),
748            });
749        }
750
751        // Skip hash (20 bytes), read change number
752        let hash_offset = 4;
753        let change_number_offset = hash_offset + 20;
754
755        let Some(change_number) = read_u32_le(&rest[change_number_offset..]) else {
756            return Err(Error::UnexpectedEndOfInput {
757                context: "reading change number",
758                offset: input.len() - rest.len() + change_number_offset,
759                expected: 4,
760                actual: rest.len() - change_number_offset,
761            });
762        };
763
764        // Skip token if present (8 bytes after change_number)
765        let vdf_data_offset = if has_token {
766            change_number_offset + 4 + 8
767        } else {
768            change_number_offset + 4
769        };
770
771        // Parse the VDF data for this package
772        let vdf_data = &rest[vdf_data_offset..];
773
774        let config = ParseConfig::default(); // Uses null-terminated keys like shortcuts
775
776        let (_vdf_rest, package_obj) =
777            parse_object(vdf_data, &config).map_err(with_offset(input.len() - vdf_data.len()))?;
778
779        // Create metadata object for this package
780        let mut package_with_meta = Obj::new();
781
782        // Add metadata fields
783        package_with_meta.insert(Cow::Borrowed("packageid"), Value::I32(package_id as i32));
784        package_with_meta.insert(
785            Cow::Borrowed("change_number"),
786            Value::U64(change_number as u64),
787        );
788        package_with_meta.insert(
789            Cow::Borrowed("sha1"),
790            Value::Str(Cow::Owned(hex::encode(
791                &rest[hash_offset..hash_offset + 20],
792            ))),
793        );
794
795        // Merge the parsed VDF data
796        for (key, value) in package_obj.iter() {
797            package_with_meta.insert(key.clone(), value.clone());
798        }
799
800        // Insert with package ID as key
801        obj.insert(
802            Cow::Owned(package_id.to_string()),
803            Value::Obj(package_with_meta),
804        );
805
806        // Find the end of this VDF object to move to the next entry
807        // _vdf_rest from the first parse_object call above tells us where VDF data ended
808        let vdf_end = vdf_data.len() - _vdf_rest.len();
809        rest = &rest[vdf_data_offset + vdf_end..];
810    }
811
812    Ok(Vdf::new(
813        format!("packageinfo_universe_{}", universe),
814        Value::Obj(obj),
815    ))
816}
817
818#[cfg(test)]
819mod tests {
820    use super::*;
821
822    #[test]
823    fn test_parse_simple_object() {
824        // Simple binary VDF: "test" { "key" "value" }
825        let data: &[u8] = &[
826            0x00, // Object start
827            b't', b'e', b's', b't', 0x00, // Key "test"
828            0x01, // String type
829            b'k', b'e', b'y', 0x00, // Key "key"
830            b'v', b'a', b'l', b'u', b'e', 0x00, // Value "value"
831            0x08, // Object end
832        ];
833
834        let result = parse_shortcuts(data);
835        assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
836
837        let vdf = result.unwrap();
838        assert_eq!(vdf.key(), "root");
839
840        let obj = vdf.as_obj().unwrap();
841        let test_obj = obj.get("test").and_then(|v| v.as_obj());
842        assert!(test_obj.is_some());
843
844        let test_obj = test_obj.unwrap();
845        let value = test_obj.get("key").and_then(|v| v.as_str());
846        assert_eq!(value, Some("value"));
847    }
848
849    #[test]
850    fn test_parse_nested_objects() {
851        // Nested objects: "outer" { "inner" { "key" "value" } }
852        let data: &[u8] = &[
853            0x00, // Object start
854            b'o', b'u', b't', b'e', b'r', 0x00, // Key "outer"
855            0x00, // Nested object start
856            b'i', b'n', b'n', b'e', b'r', 0x00, // Key "inner"
857            0x01, // String type
858            b'k', b'e', b'y', 0x00, // Key "key"
859            b'v', b'a', b'l', b'u', b'e', 0x00, // Value "value"
860            0x08, // End inner object
861            0x08, // End outer object
862        ];
863
864        let result = parse_shortcuts(data);
865        assert!(result.is_ok());
866
867        let vdf = result.unwrap();
868        let obj = vdf.as_obj().unwrap();
869        let outer = obj.get("outer").and_then(|v| v.as_obj()).unwrap();
870        let inner = outer.get("inner").and_then(|v| v.as_obj()).unwrap();
871        let value = inner.get("key").and_then(|v| v.as_str());
872        assert_eq!(value, Some("value"));
873    }
874
875    #[test]
876    fn test_parse_int32_value() {
877        // Int32 value: "root" { "number" "42" }
878        let data: &[u8] = &[
879            0x00, // Object start
880            b'r', b'o', b'o', b't', 0x00, // Key "root"
881            0x02, // Int32 type
882            b'n', b'u', b'm', b'b', b'e', b'r', 0x00, // Key "number"
883            42, 0, 0, 0,    // Value 42 (little-endian)
884            0x08, // Object end
885        ];
886
887        let result = parse_shortcuts(data);
888        assert!(result.is_ok());
889
890        let vdf = result.unwrap();
891        let obj = vdf.as_obj().unwrap();
892        let root = obj.get("root").and_then(|v| v.as_obj()).unwrap();
893        let value = root.get("number").and_then(|v| v.as_i32());
894        assert_eq!(value, Some(42));
895    }
896
897    #[test]
898    fn test_parse_uint64_value() {
899        // UInt64 value
900        let data: &[u8] = &[
901            0x00, // Object start
902            b'r', b'o', b'o', b't', 0x00, // Key "root"
903            0x07, // UInt64 type
904            b'n', b'u', b'm', b'b', b'e', b'r', 0x00, // Key "number"
905            0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, // Value u32::MAX as u64
906            0x08, // Object end
907        ];
908
909        let result = parse_shortcuts(data);
910        assert!(result.is_ok());
911
912        let vdf = result.unwrap();
913        let obj = vdf.as_obj().unwrap();
914        let root = obj.get("root").and_then(|v| v.as_obj()).unwrap();
915        let value = root.get("number").and_then(|v| v.as_u64());
916        assert_eq!(value, Some(4294967295));
917    }
918
919    #[test]
920    fn test_parse_float_value() {
921        // Float value
922        let data: &[u8] = &[
923            0x00, // Object start
924            b'r', b'o', b'o', b't', 0x00, // Key "root"
925            0x03, // Float type
926            b'v', b'a', b'l', 0x00, // Key "val"
927            0x00, 0x00, 0x80, 0x3F, // Value 1.0 (little-endian)
928            0x08, // Object end
929        ];
930
931        let result = parse_shortcuts(data);
932        assert!(result.is_ok());
933
934        let vdf = result.unwrap();
935        let obj = vdf.as_obj().unwrap();
936        let root = obj.get("root").and_then(|v| v.as_obj()).unwrap();
937        let value = root.get("val").and_then(|v| v.as_float());
938        assert_eq!(value, Some(1.0));
939    }
940
941    #[test]
942    fn test_parse_ptr_value() {
943        // Pointer value
944        let data: &[u8] = &[
945            0x00, // Object start
946            b'r', b'o', b'o', b't', 0x00, // Key "root"
947            0x04, // Ptr type
948            b'p', b't', b'r', 0x00, // Key "ptr"
949            0xAB, 0xCD, 0xEF, 0x12, // Value 0x12EFCDAB
950            0x08, // Object end
951        ];
952
953        let result = parse_shortcuts(data);
954        assert!(result.is_ok());
955
956        let vdf = result.unwrap();
957        let obj = vdf.as_obj().unwrap();
958        let root = obj.get("root").and_then(|v| v.as_obj()).unwrap();
959        let value = root.get("ptr").and_then(|v| v.as_pointer());
960        assert_eq!(value, Some(0x12efcdab));
961    }
962
963    #[test]
964    fn test_parse_color_value() {
965        // Color value: RGBA (255, 0, 0, 255) = "25500255"
966        let data: &[u8] = &[
967            0x00, // Object start
968            b'r', b'o', b'o', b't', 0x00, // Key "root"
969            0x06, // Color type
970            b'c', b'o', b'l', 0x00, // Key "col"
971            0xFF, 0x00, 0x00, 0xFF, // RGBA: red, opaque
972            0x08, // Object end
973        ];
974
975        let result = parse_shortcuts(data);
976        assert!(result.is_ok());
977
978        let vdf = result.unwrap();
979        let obj = vdf.as_obj().unwrap();
980        let root = obj.get("root").and_then(|v| v.as_obj()).unwrap();
981        let value = root.get("col").and_then(|v| v.as_color());
982        assert_eq!(value, Some([255, 0, 0, 255]));
983    }
984
985    // ===== Error Path Tests =====
986
987    #[test]
988    fn test_parse_unknown_type_byte() {
989        let data: &[u8] = &[
990            0x00, b't', b'e', b's', b't', 0x00, 0xFF, // Invalid type byte
991            b'k', b'e', b'y', 0x00,
992        ];
993        assert!(matches!(
994            parse_shortcuts(data),
995            Err(Error::UnknownType {
996                type_byte: 0xFF,
997                ..
998            })
999        ));
1000    }
1001
1002    #[test]
1003    fn test_parse_truncated_object_start() {
1004        let data: &[u8] = &[0x00]; // Incomplete object start
1005        assert!(matches!(
1006            parse_shortcuts(data),
1007            Err(Error::UnexpectedEndOfInput { .. })
1008        ));
1009    }
1010
1011    #[test]
1012    fn test_parse_truncated_string_value() {
1013        let data: &[u8] = &[
1014            0x00, b't', b'e', b's', b't', 0x00, 0x01, // String type
1015            b'k', b'e', b'y', 0x00,
1016            // Missing null terminator
1017        ];
1018        assert!(matches!(
1019            parse_shortcuts(data),
1020            Err(Error::UnexpectedEndOfInput { .. })
1021        ));
1022    }
1023
1024    #[test]
1025    fn test_parse_invalid_utf8_string() {
1026        let data: &[u8] = &[
1027            0x00, b't', b'e', b's', b't', 0x00, 0x01, // String type
1028            b'k', b'e', b'y', 0x00, 0xFF, 0xFF, 0x00, // Invalid UTF-8 followed by null
1029        ];
1030        assert!(matches!(
1031            parse_shortcuts(data),
1032            Err(Error::InvalidUtf8 { .. })
1033        ));
1034    }
1035
1036    #[test]
1037    fn test_parse_truncated_int32_value() {
1038        let data: &[u8] = &[
1039            0x00, b't', b'e', b's', b't', 0x00, 0x02, // Int32 type
1040            b'k', b'e', b'y', 0x00, 0x01, 0x02, // Only 2 bytes instead of 4
1041        ];
1042        assert!(matches!(
1043            parse_shortcuts(data),
1044            Err(Error::UnexpectedEndOfInput { .. })
1045        ));
1046    }
1047
1048    #[test]
1049    fn test_parse_truncated_uint64_value() {
1050        let data: &[u8] = &[
1051            0x00, b't', b'e', b's', b't', 0x00, 0x07, // UInt64 type
1052            b'k', b'e', b'y', 0x00, 0x01, 0x02, 0x03, 0x04, // Only 4 bytes instead of 8
1053        ];
1054        assert!(matches!(
1055            parse_shortcuts(data),
1056            Err(Error::UnexpectedEndOfInput { .. })
1057        ));
1058    }
1059
1060    #[test]
1061    fn test_parse_truncated_float_value() {
1062        let data: &[u8] = &[
1063            0x00, b't', b'e', b's', b't', 0x00, 0x03, // Float type
1064            b'k', b'e', b'y', 0x00, 0x01, 0x02, // Only 2 bytes instead of 4
1065        ];
1066        assert!(matches!(
1067            parse_shortcuts(data),
1068            Err(Error::UnexpectedEndOfInput { .. })
1069        ));
1070    }
1071
1072    #[test]
1073    fn test_parse_truncated_color_value() {
1074        let data: &[u8] = &[
1075            0x00, b't', b'e', b's', b't', 0x00, 0x06, // Color type
1076            b'k', b'e', b'y', 0x00, 0xFF, 0x00, // Only 2 bytes instead of 4
1077        ];
1078        assert!(matches!(
1079            parse_shortcuts(data),
1080            Err(Error::UnexpectedEndOfInput { .. })
1081        ));
1082    }
1083
1084    #[test]
1085    fn test_parse_wstring_unpaired_surrogate() {
1086        // WideString with unpaired surrogate - Rust's decode_utf16 replaces with
1087        // replacement character rather than erroring, so this should parse successfully
1088        let data: &[u8] = &[
1089            0x00, b't', b'e', b's', b't', 0x00, 0x05, // WideString type
1090            b'k', b'e', b'y', 0x00, 0xD8, 0x00, 0x00,
1091            0x00, // Unpaired surrogate (UTF-16) - gets replaced
1092        ];
1093        assert!(parse_shortcuts(data).is_ok());
1094    }
1095
1096    #[test]
1097    fn test_parse_appinfo_invalid_magic() {
1098        let data: &[u8] = &[
1099            0xDE, 0xAD, 0xBE, 0xEF, // Invalid magic
1100            0x00, 0x00, 0x00, 0x00, // universe
1101            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // padding to meet minimum size
1102            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1103        ];
1104        assert!(matches!(
1105            parse_appinfo(data),
1106            Err(Error::InvalidMagic { .. })
1107        ));
1108    }
1109
1110    #[test]
1111    fn test_parse_appinfo_truncated_header() {
1112        let data: &[u8] = &[
1113            0x28, 0x44, 0x56, 0x07, // APPINFO_MAGIC_41 first byte
1114        ];
1115        assert!(matches!(
1116            parse_appinfo(data),
1117            Err(Error::UnexpectedEndOfInput { .. })
1118        ));
1119    }
1120
1121    #[test]
1122    fn test_parse_appinfo_v41_invalid_string_table_offset() {
1123        // v41 with string table offset beyond file length
1124        let data: &[u8] = &[
1125            0x28, 0x44, 0x56, 0x07, // APPINFO_MAGIC_41
1126            0x00, 0x00, 0x00, 0x00, // universe
1127            0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1128            0x00, // string table offset (255, beyond EOF)
1129        ];
1130        assert!(matches!(
1131            parse_appinfo(data),
1132            Err(Error::UnexpectedEndOfInput { .. })
1133        ));
1134    }
1135
1136    #[test]
1137    fn test_parse_packageinfo_invalid_magic() {
1138        let data: &[u8] = &[0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x00, 0x00, 0x00];
1139        assert!(matches!(
1140            parse_packageinfo(data),
1141            Err(Error::InvalidMagic { .. })
1142        ));
1143    }
1144
1145    #[test]
1146    fn test_parse_packageinfo_truncated_header() {
1147        let data: &[u8] = &[0x27]; // Partial magic
1148        assert!(matches!(
1149            parse_packageinfo(data),
1150            Err(Error::UnexpectedEndOfInput { .. })
1151        ));
1152    }
1153
1154    #[test]
1155    fn test_parse_appinfo_with_terminator() {
1156        // v40 format with immediate app_id terminator (no apps)
1157        // Need 68 bytes minimum (APPINFO_ENTRY_HEADER_SIZE) to pass the size check
1158        let data: &[u8] = &[
1159            0x28, 0x44, 0x56, 0x07, // APPINFO_MAGIC_40
1160            0x00, 0x00, 0x00, 0x00, // universe
1161            0x00, 0x00, 0x00, 0x00, // app_id = 0 (terminator)
1162            // Padding to meet APPINFO_ENTRY_HEADER_SIZE (68 bytes total)
1163            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1164            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1165            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1166            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1167            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1168            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1169            0x00, 0x00, 0x00, 0x00,
1170        ];
1171        let result = parse_appinfo(data);
1172        if let Err(e) = &result {
1173            panic!("parse_appinfo failed with: {:?}", e);
1174        }
1175        assert!(
1176            result.is_ok(),
1177            "Appinfo with terminator should parse successfully"
1178        );
1179        let vdf = result.unwrap();
1180        let obj = vdf.as_obj().unwrap();
1181        assert_eq!(obj.len(), 0, "Should have no apps");
1182    }
1183
1184    #[test]
1185    fn test_parse_autodetect_fallback_to_shortcuts() {
1186        // Data that doesn't look like appinfo should be parsed as shortcuts
1187        let data: &[u8] = &[
1188            0x00, // Object start (not appinfo magic)
1189            b't', b'e', b's', b't', 0x00, 0x01, // String type
1190            b'k', b'e', b'y', 0x00, b'v', b'a', b'l', b'u', b'e', 0x00, 0x08, // Object end
1191        ];
1192        let result = parse(data);
1193        assert!(result.is_ok());
1194    }
1195
1196    // ===== packageinfo tests =====
1197
1198    #[test]
1199    fn test_parse_packageinfo_v39_invalid_magic_base() {
1200        // Correct version (39 = 0x27) but wrong magic base
1201        let data: &[u8] = &[
1202            0x27, 0xBE, 0xBA, 0xFE, // Wrong magic base (0xFEBAFE instead of 0x065655)
1203            0x00, 0x00, 0x00, 0x00, // universe
1204        ];
1205        assert!(matches!(
1206            parse_packageinfo(data),
1207            Err(Error::InvalidMagic { .. })
1208        ));
1209    }
1210
1211    #[test]
1212    fn test_parse_packageinfo_invalid_version() {
1213        // Correct magic base but wrong version (38 instead of 39/40)
1214        // 0x06565526 = version 38
1215        let data: &[u8] = &[
1216            0x26, 0x55, 0x56, 0x06, // magic with version 38
1217            0x00, 0x00, 0x00, 0x00, // universe
1218        ];
1219        assert!(matches!(
1220            parse_packageinfo(data),
1221            Err(Error::InvalidMagic { .. })
1222        ));
1223    }
1224
1225    #[test]
1226    fn test_parse_packageinfo_v39_truncated_universe() {
1227        // v39 magic but missing universe bytes
1228        let data: &[u8] = &[
1229            0x27, 0x55, 0x56, 0x06, // PACKAGEINFO_MAGIC_39
1230            0x00, 0x00, // incomplete universe (only 2 bytes)
1231        ];
1232        assert!(matches!(
1233            parse_packageinfo(data),
1234            Err(Error::UnexpectedEndOfInput { .. })
1235        ));
1236    }
1237
1238    #[test]
1239    fn test_parse_packageinfo_v39_with_terminator() {
1240        // v39 format with immediate termination marker (no packages)
1241        let data: &[u8] = &[
1242            0x27, 0x55, 0x56, 0x06, // PACKAGEINFO_MAGIC_39 (v39)
1243            0x01, 0x00, 0x00, 0x00, // universe = 1
1244            0xFF, 0xFF, 0xFF, 0xFF, // package_id = 0xFFFFFFFF (terminator)
1245        ];
1246        let result = parse_packageinfo(data);
1247        assert!(
1248            result.is_ok(),
1249            "parse_packageinfo failed: {:?}",
1250            result.err()
1251        );
1252        let vdf = result.unwrap();
1253        assert_eq!(vdf.key(), "packageinfo_universe_1");
1254        let obj = vdf.as_obj().unwrap();
1255        assert_eq!(obj.len(), 0, "Should have no packages");
1256    }
1257
1258    #[test]
1259    fn test_parse_packageinfo_v40_with_terminator() {
1260        // v40 format with immediate termination marker (no packages)
1261        let data: &[u8] = &[
1262            0x28, 0x55, 0x56, 0x06, // PACKAGEINFO_MAGIC_40 (v40)
1263            0x01, 0x00, 0x00, 0x00, // universe = 1
1264            0xFF, 0xFF, 0xFF, 0xFF, // package_id = 0xFFFFFFFF (terminator)
1265        ];
1266        let result = parse_packageinfo(data);
1267        assert!(
1268            result.is_ok(),
1269            "parse_packageinfo failed: {:?}",
1270            result.err()
1271        );
1272        let vdf = result.unwrap();
1273        assert_eq!(vdf.key(), "packageinfo_universe_1");
1274        let obj = vdf.as_obj().unwrap();
1275        assert_eq!(obj.len(), 0, "Should have no packages");
1276    }
1277
1278    #[test]
1279    fn test_parse_packageinfo_v39_truncated_entry_header() {
1280        // v39 format with package_id but incomplete header
1281        // Header size for v39 is 4 + 20 + 4 = 28 bytes (package_id + hash + change_number)
1282        let data: &[u8] = &[
1283            0x27, 0x55, 0x56, 0x06, // PACKAGEINFO_MAGIC_39
1284            0x00, 0x00, 0x00, 0x00, // universe
1285            0x01, 0x00, 0x00, 0x00, // package_id = 1
1286            // Only 10 bytes of hash (need 20), missing change_number
1287            0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A,
1288        ];
1289        assert!(matches!(
1290            parse_packageinfo(data),
1291            Err(Error::UnexpectedEndOfInput { context, .. }) if context == "reading package entry header"
1292        ));
1293    }
1294
1295    #[test]
1296    fn test_parse_packageinfo_v40_truncated_entry_header() {
1297        // v40 format with package_id but incomplete header
1298        // Header size for v40 is 4 + 20 + 4 + 8 = 36 bytes (+ token)
1299        let data: &[u8] = &[
1300            0x28, 0x55, 0x56, 0x06, // PACKAGEINFO_MAGIC_40
1301            0x00, 0x00, 0x00, 0x00, // universe
1302            0x01, 0x00, 0x00, 0x00, // package_id = 1
1303            // Only hash (20 bytes), missing change_number and token
1304            0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x01, 0x02, 0x03, 0x04,
1305            0x05, 0x06, 0x07, 0x08, 0x09, 0x0A,
1306        ];
1307        assert!(matches!(
1308            parse_packageinfo(data),
1309            Err(Error::UnexpectedEndOfInput { context, .. }) if context == "reading package entry header"
1310        ));
1311    }
1312
1313    #[test]
1314    fn test_parse_packageinfo_v39_with_minimal_vdf() {
1315        // v39 format with minimal VDF that tests basic parsing
1316        let data: &[u8] = &[
1317            0x27, 0x55, 0x56, 0x06, // PACKAGEINFO_MAGIC_39
1318            0x00, 0x00, 0x00, 0x00, // universe = 0
1319            // Package entry
1320            0x01, 0x00, 0x00, 0x00, // package_id = 1
1321            // SHA-1 hash (20 bytes)
1322            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1323            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2A, 0x00, 0x00, 0x00, // change_number = 42
1324            // VDF: simple object with one string entry { "k": "value" }
1325            0x01, // String type
1326            b'k', 0x00, // Key "k"
1327            b'v', b'a', b'l', b'u', b'e', 0x00, // Value "value"
1328            0x08, // Object end
1329            // Termination marker
1330            0xFF, 0xFF, 0xFF, 0xFF,
1331        ];
1332        let result = parse_packageinfo(data);
1333        assert!(
1334            result.is_ok(),
1335            "parse_packageinfo failed: {:?}",
1336            result.err()
1337        );
1338        let vdf = result.unwrap();
1339        assert_eq!(vdf.key(), "packageinfo_universe_0");
1340
1341        let obj = vdf.as_obj().unwrap();
1342        assert_eq!(obj.len(), 1);
1343        let package = obj.get("1").and_then(|v| v.as_obj()).unwrap();
1344        assert_eq!(package.get("k").and_then(|v| v.as_str()), Some("value"));
1345    }
1346
1347    #[test]
1348    fn test_parse_packageinfo_v40_with_minimal_vdf() {
1349        // v40 format with minimal VDF
1350        let data: &[u8] = &[
1351            0x28, 0x55, 0x56, 0x06, // PACKAGEINFO_MAGIC_40
1352            0x00, 0x00, 0x00, 0x00, // universe = 0
1353            // Package entry
1354            0x01, 0x00, 0x00, 0x00, // package_id = 1
1355            // SHA-1 hash (20 bytes)
1356            0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
1357            0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0x2A, 0x00, 0x00, 0x00, // change_number = 42
1358            // PICS token (8 bytes)
1359            0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
1360            // VDF: simple object with one int32 entry { "x": 5 }
1361            0x02, // Int32 type
1362            b'x', 0x00, // Key "x"
1363            0x05, 0x00, 0x00, 0x00, // Value 5
1364            0x08, // Object end
1365            // Termination marker
1366            0xFF, 0xFF, 0xFF, 0xFF,
1367        ];
1368        let result = parse_packageinfo(data);
1369        assert!(
1370            result.is_ok(),
1371            "parse_packageinfo failed: {:?}",
1372            result.err()
1373        );
1374        let vdf = result.unwrap();
1375        assert_eq!(vdf.key(), "packageinfo_universe_0");
1376
1377        let obj = vdf.as_obj().unwrap();
1378        assert_eq!(obj.len(), 1);
1379        let package = obj.get("1").and_then(|v| v.as_obj()).unwrap();
1380        assert_eq!(package.get("x").and_then(|v| v.as_i32()), Some(5));
1381    }
1382
1383    #[test]
1384    fn test_parse_packageinfo_multiple_packages() {
1385        // v39 format with multiple packages
1386        let data: &[u8] = &[
1387            0x27, 0x55, 0x56, 0x06, // PACKAGEINFO_MAGIC_39
1388            0x00, 0x00, 0x00, 0x00, // universe = 0
1389            // First package
1390            0x01, 0x00, 0x00, 0x00, // package_id = 1
1391            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1392            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // hash
1393            0x01, 0x00, 0x00, 0x00, // change_number = 1
1394            // VDF: { "x": 1 }
1395            0x02, 0x01, 0x00, 0x00, 0x00, b'x', 0x00, 0x08, // Second package
1396            0x02, 0x00, 0x00, 0x00, // package_id = 2
1397            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1398            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // hash
1399            0x02, 0x00, 0x00, 0x00, // change_number = 2
1400            // VDF: { "a": 2 }
1401            0x02, 0x02, 0x00, 0x00, 0x00, b'a', 0x00, 0x08, // Termination marker
1402            0xFF, 0xFF, 0xFF, 0xFF,
1403        ];
1404        let result = parse_packageinfo(data);
1405        assert!(
1406            result.is_ok(),
1407            "parse_packageinfo failed: {:?}",
1408            result.err()
1409        );
1410        let vdf = result.unwrap();
1411        let obj = vdf.as_obj().unwrap();
1412        assert_eq!(obj.len(), 2);
1413        assert!(obj.get("1").is_some());
1414        assert!(obj.get("2").is_some());
1415    }
1416
1417    #[test]
1418    fn test_parse_packageinfo_empty_input() {
1419        // Completely empty input
1420        let data: &[u8] = &[];
1421        assert!(matches!(
1422            parse_packageinfo(data),
1423            Err(Error::UnexpectedEndOfInput { context, .. }) if context == "reading packageinfo header"
1424        ));
1425    }
1426
1427    #[test]
1428    fn test_parse_packageinfo_only_magic() {
1429        // Only magic bytes, no universe
1430        let data: &[u8] = &[
1431            0x27, 0x55, 0x56, 0x06, // PACKAGEINFO_MAGIC_39
1432        ];
1433        assert!(matches!(
1434            parse_packageinfo(data),
1435            Err(Error::UnexpectedEndOfInput { .. })
1436        ));
1437    }
1438}