Skip to main content

vhdx/metadata/
core.rs

1use bitvec::prelude::*;
2
3use crate::constants::{
4    FP_BITFIELDS, FP_BITS_END, FP_BLOCK_SIZE, FP_HAS_PARENT, FP_LEAVE_BLOCK_ALLOCATED,
5    FP_RESERVED_START, KV_ENTRY_SIZE, LOCATOR_HEADER_SIZE, METADATA_SIGNATURE, METADATA_TABLE_SIZE,
6    TABLE_ENTRY_SIZE, TABLE_HEADER_SIZE,
7};
8use crate::error::{Error, Result, SignaturePosition};
9use crate::types::Guid;
10pub use crate::types::StandardItems;
11
12// ---------------------------------------------------------------------------
13// Metadata (top-level wrapper)
14// ---------------------------------------------------------------------------
15
16/// Wrapper around the entire metadata region buffer.
17///
18/// Layout: 64 KB metadata table followed by variable-length metadata items.
19#[derive(Clone, Copy)]
20pub struct Metadata<'a> {
21    data: &'a [u8],
22}
23
24impl<'a> Metadata<'a> {
25    /// Create a new `Metadata` view over the metadata region bytes.
26    ///
27    /// The buffer must be at least 64 KB (the fixed table size).
28    ///
29    /// # Errors
30    ///
31    /// Returns [`Error::InvalidMetadata`] if the buffer is smaller than 64 KB.
32    pub(crate) fn new(data: &'a [u8]) -> Result<Self> {
33        if data.len() < METADATA_TABLE_SIZE as usize {
34            return Err(Error::InvalidMetadata(format!(
35                "metadata region too small: {} bytes, need at least {METADATA_TABLE_SIZE}",
36                data.len()
37            )));
38        }
39        Ok(Self { data })
40    }
41
42    /// Access the 64 KB metadata table.
43    #[must_use]
44    pub fn table(&self) -> MetadataTable<'a> {
45        MetadataTable {
46            data: &self.data[..METADATA_TABLE_SIZE as usize],
47        }
48    }
49
50    /// Access metadata items (region after the 64 KB table).
51    #[must_use]
52    pub fn items(&self) -> MetadataItems<'a> {
53        MetadataItems {
54            table: self.table(),
55            items_data: self.data,
56        }
57    }
58}
59
60// ---------------------------------------------------------------------------
61// MetadataTable
62// ---------------------------------------------------------------------------
63
64/// The fixed 64 KB metadata table: a 32-byte header followed by 32-byte entries.
65pub struct MetadataTable<'a> {
66    data: &'a [u8],
67}
68
69impl<'a> MetadataTable<'a> {
70    /// Access the table header.
71    #[must_use]
72    pub fn header(&self) -> TableHeader<'a> {
73        TableHeader {
74            data: &self.data[..TABLE_HEADER_SIZE as usize],
75        }
76    }
77
78    /// Look up a table entry by GUID.
79    ///
80    /// Returns `Err(Error::MetadataNotFound)` if no entry matches.
81    ///
82    /// # Errors
83    ///
84    /// Returns [`Error::MetadataNotFound`] when the GUID is not present.
85    pub fn entry(&self, item_id: &Guid) -> Result<TableEntry<'a>> {
86        for e in self.entries() {
87            if e.item_id() == *item_id {
88                return Ok(e);
89            }
90        }
91        Err(Error::MetadataNotFound { guid: *item_id })
92    }
93
94    /// Iterate over all table entries (zero-copy views).
95    pub fn entries(&self) -> impl Iterator<Item = TableEntry<'a>> + 'a {
96        let count = self.header().entry_count() as usize;
97        let data = self.data;
98        (0..count).map(move |i| {
99            let start = TABLE_HEADER_SIZE as usize + i * TABLE_ENTRY_SIZE as usize;
100            TableEntry {
101                data: &data[start..start + TABLE_ENTRY_SIZE as usize],
102            }
103        })
104    }
105}
106
107// ---------------------------------------------------------------------------
108// TableHeader
109// ---------------------------------------------------------------------------
110
111/// 32-byte metadata table header.
112pub struct TableHeader<'a> {
113    data: &'a [u8],
114}
115
116impl<'a> TableHeader<'a> {
117    /// Signature: 8 bytes, must be "metadata".
118    ///
119    /// # Panics
120    ///
121    /// Panics if the header slice is shorter than 8 bytes.
122    #[must_use]
123    pub fn signature(&self) -> &'a [u8; 8] {
124        self.data[..8]
125            .try_into()
126            .expect("header has 8 signature bytes")
127    }
128
129    /// Reserved: 2 bytes (must be 0).
130    ///
131    /// # Panics
132    ///
133    /// Panics if the header slice is shorter than 10 bytes.
134    #[must_use]
135    pub fn reserved(&self) -> &'a [u8; 2] {
136        self.data[8..10]
137            .try_into()
138            .expect("header has 2 reserved bytes")
139    }
140
141    /// Number of table entries (must be <= 2047).
142    ///
143    /// # Panics
144    ///
145    /// Panics if the header slice is shorter than 12 bytes.
146    #[must_use]
147    pub fn entry_count(&self) -> u16 {
148        u16::from_le_bytes(self.data[10..12].try_into().unwrap())
149    }
150
151    /// Reserved2: 20 bytes (must be 0).
152    ///
153    /// # Panics
154    ///
155    /// Panics if the header slice is shorter than 32 bytes.
156    #[must_use]
157    pub fn reserved2(&self) -> &'a [u8; 20] {
158        self.data[12..32]
159            .try_into()
160            .expect("header has 20 reserved2 bytes")
161    }
162
163    /// Check that the signature matches "metadata".
164    ///
165    /// # Errors
166    ///
167    /// Returns [`Error::InvalidSignature`] if the signature does not match.
168    pub(crate) fn validate_signature(&self) -> Result<()> {
169        let signature = *self.signature();
170        if signature.view_bits::<Lsb0>() != *METADATA_SIGNATURE {
171            return Err(Error::InvalidSignature {
172                position: SignaturePosition::MetadataTable,
173                expected: METADATA_SIGNATURE.into_inner().to_le_bytes(),
174                found: signature,
175            });
176        }
177        Ok(())
178    }
179}
180
181// ---------------------------------------------------------------------------
182// TableEntry
183// ---------------------------------------------------------------------------
184
185/// 32-byte metadata table entry.
186pub struct TableEntry<'a> {
187    data: &'a [u8],
188}
189
190impl TableEntry<'_> {
191    /// Item ID (16-byte GUID).
192    ///
193    /// # Panics
194    ///
195    /// Panics if the entry slice is shorter than 16 bytes.
196    #[must_use]
197    pub fn item_id(&self) -> Guid {
198        let bytes: [u8; 16] = self.data[..16].try_into().expect("entry has 16 guid bytes");
199        Guid::from_bytes(bytes)
200    }
201
202    /// Byte offset of the metadata item (relative to start of metadata region).
203    ///
204    /// # Panics
205    ///
206    /// Panics if the entry slice is shorter than 20 bytes.
207    #[must_use]
208    pub fn offset(&self) -> u32 {
209        u32::from_le_bytes(self.data[16..20].try_into().unwrap())
210    }
211
212    /// Length of the metadata item in bytes.
213    ///
214    /// # Panics
215    ///
216    /// Panics if the entry slice is shorter than 24 bytes.
217    #[must_use]
218    pub fn length(&self) -> u32 {
219        u32::from_le_bytes(self.data[20..24].try_into().unwrap())
220    }
221
222    /// Raw flags bits (4 bytes).
223    ///
224    /// # Panics
225    ///
226    /// Panics if the entry slice is shorter than 28 bytes.
227    #[must_use]
228    pub fn flags_bits(&self) -> u32 {
229        u32::from_le_bytes(self.data[24..28].try_into().unwrap())
230    }
231
232    /// Reserved field (4 bytes).
233    ///
234    /// # Panics
235    ///
236    /// Panics if the entry slice is shorter than 32 bytes.
237    #[must_use]
238    pub fn reserved(&self) -> u32 {
239        u32::from_le_bytes(self.data[28..32].try_into().unwrap())
240    }
241
242    /// Parsed flags.
243    #[must_use]
244    pub fn flags(&self) -> EntryFlags<'_> {
245        EntryFlags {
246            data: &self.data[24..28],
247        }
248    }
249}
250
251// ---------------------------------------------------------------------------
252// EntryFlags
253// ---------------------------------------------------------------------------
254
255/// Bitfield flags for a metadata table entry.
256///
257/// Per MS-VHDX §2.6.1.2 diagram: A=IsUser(bit0), B=IsVirtualDisk(bit1),
258/// C=IsRequired(bit2), bits 3-31 Reserved and MUST be 0.
259#[derive(Clone, Copy, Debug)]
260pub struct EntryFlags<'a> {
261    pub(super) data: &'a [u8],
262}
263
264impl EntryFlags<'_> {
265    /// Create an `EntryFlags` view from a 4-byte flags slice.
266    #[cfg(test)]
267    pub(crate) fn new(data: &[u8]) -> EntryFlags<'_> {
268        EntryFlags { data }
269    }
270
271    /// `IsUser` (bit 0): user metadata vs system metadata.
272    #[must_use]
273    pub fn is_user(&self) -> bool {
274        self.data.view_bits::<Lsb0>()[0]
275    }
276
277    /// `IsVirtualDisk` (bit 1): virtual disk metadata vs file metadata.
278    #[must_use]
279    pub fn is_virtual_disk(&self) -> bool {
280        self.data.view_bits::<Lsb0>()[1]
281    }
282
283    /// `IsRequired` (bit 2): implementation must understand this item.
284    #[must_use]
285    pub fn is_required(&self) -> bool {
286        self.data.view_bits::<Lsb0>()[2]
287    }
288
289    /// Whether any reserved bits (3-31) are set.
290    pub(crate) fn has_reserved_bits(&self) -> bool {
291        self.data.view_bits::<Lsb0>()[3..=31].any()
292    }
293}
294
295// ---------------------------------------------------------------------------
296// MetadataItems
297// ---------------------------------------------------------------------------
298
299/// Accessor for metadata items by well-known GUID.
300pub struct MetadataItems<'a> {
301    table: MetadataTable<'a>,
302    items_data: &'a [u8],
303}
304
305impl<'a> MetadataItems<'a> {
306    /// Resolve the item data slice for a given GUID.
307    ///
308    /// # Errors
309    ///
310    /// Returns [`Error::MetadataRequiredMissing`] if the GUID is not found in
311    /// the table or if the offset + length overflows or exceeds the metadata
312    /// region bounds.
313    fn item_data(&self, guid: &Guid) -> Result<&'a [u8]> {
314        let Ok(entry) = self.table.entry(guid) else {
315            return Err(Error::MetadataRequiredMissing { guid: *guid });
316        };
317        let offset = entry.offset() as usize;
318        let length = entry.length() as usize;
319        if length == 0 {
320            // Present but empty
321            return Ok(&[]);
322        }
323        let end = offset
324            .checked_add(length)
325            .ok_or(Error::MetadataRequiredMissing { guid: *guid })?;
326        if end > self.items_data.len() {
327            return Err(Error::MetadataRequiredMissing { guid: *guid });
328        }
329        Ok(&self.items_data[offset..end])
330    }
331
332    /// File Parameters metadata item.
333    ///
334    /// # Errors
335    ///
336    /// Returns an error if the item is missing or has an invalid extent.
337    pub fn file_parameters(&self) -> Result<FileParameters<'a>> {
338        let data = self.item_data(&StandardItems::FILE_PARAMETERS)?;
339        // FileParameters is 8 bytes; tolerate shorter (empty) items
340        Ok(FileParameters { data })
341    }
342
343    /// Virtual disk size in bytes (8 bytes, little-endian u64).
344    ///
345    /// # Errors
346    ///
347    /// Returns an error if the item is missing or shorter than 8 bytes.
348    ///
349    /// # Panics
350    ///
351    /// Panics only if the internal length check is violated before converting
352    /// the 8-byte slice.
353    pub fn virtual_disk_size(&self) -> Result<u64> {
354        let data = self.item_data(&StandardItems::VIRTUAL_DISK_SIZE)?;
355        if data.len() < 8 {
356            return Err(Error::MetadataRequiredMissing {
357                guid: StandardItems::VIRTUAL_DISK_SIZE,
358            });
359        }
360        Ok(u64::from_le_bytes(data[..8].try_into().unwrap()))
361    }
362
363    /// Virtual disk identifier (16-byte GUID).
364    ///
365    /// # Errors
366    ///
367    /// Returns an error if the item is missing or shorter than 16 bytes.
368    pub fn virtual_disk_id(&self) -> Result<Guid> {
369        let data = self.item_data(&StandardItems::VIRTUAL_DISK_ID)?;
370        if data.len() < 16 {
371            return Err(Error::MetadataRequiredMissing {
372                guid: StandardItems::VIRTUAL_DISK_ID,
373            });
374        }
375        let bytes: [u8; 16] =
376            data[..16]
377                .try_into()
378                .map_err(|_| Error::MetadataRequiredMissing {
379                    guid: StandardItems::VIRTUAL_DISK_ID,
380                })?;
381        Ok(Guid::from_bytes(bytes))
382    }
383
384    /// Logical sector size in bytes (4 bytes, little-endian u32).
385    ///
386    /// # Errors
387    ///
388    /// Returns an error if the item is missing or shorter than 4 bytes.
389    ///
390    /// # Panics
391    ///
392    /// Panics only if the internal length check is violated before converting
393    /// the 4-byte slice.
394    pub fn logical_sector_size(&self) -> Result<u32> {
395        let data = self.item_data(&StandardItems::LOGICAL_SECTOR_SIZE)?;
396        if data.len() < 4 {
397            return Err(Error::MetadataRequiredMissing {
398                guid: StandardItems::LOGICAL_SECTOR_SIZE,
399            });
400        }
401        Ok(u32::from_le_bytes(data[..4].try_into().unwrap()))
402    }
403
404    /// Physical sector size in bytes (4 bytes, little-endian u32).
405    ///
406    /// # Errors
407    ///
408    /// Returns an error if the item is missing or shorter than 4 bytes.
409    ///
410    /// # Panics
411    ///
412    /// Panics only if the internal length check is violated before converting
413    /// the 4-byte slice.
414    pub fn physical_sector_size(&self) -> Result<u32> {
415        let data = self.item_data(&StandardItems::PHYSICAL_SECTOR_SIZE)?;
416        if data.len() < 4 {
417            return Err(Error::MetadataRequiredMissing {
418                guid: StandardItems::PHYSICAL_SECTOR_SIZE,
419            });
420        }
421        Ok(u32::from_le_bytes(data[..4].try_into().unwrap()))
422    }
423
424    /// Parent locator (differencing disks).
425    ///
426    /// # Errors
427    ///
428    /// Returns an error if the item is missing or has an invalid extent.
429    pub fn parent_locator(&self) -> Result<ParentLocator<'a>> {
430        let data = self.item_data(&StandardItems::PARENT_LOCATOR)?;
431        Ok(ParentLocator { data })
432    }
433}
434
435// ---------------------------------------------------------------------------
436// FileParameters
437// ---------------------------------------------------------------------------
438
439/// File Parameters metadata item (8 bytes).
440///
441/// Layout per MS-VHDX §2.6.2.1:
442/// ```text
443///  Bytes 0-3: BlockSize (u32 LE)
444///  Bytes 4-7: BitFields (u32 LE)
445///    Bit 0: LeaveBlockAllocated
446///    Bit 1: HasParent
447///    Bits 2-31: Reserved (MUST be 0)
448/// ```
449pub struct FileParameters<'a> {
450    data: &'a [u8],
451}
452
453impl FileParameters<'_> {
454    /// Block size in bytes (first u32 per MS-VHDX §2.6.2.1).
455    #[must_use]
456    pub fn block_size(&self) -> u32 {
457        if self.data.len() < 8 {
458            return 0;
459        }
460        self.data.view_bits::<Lsb0>()[FP_BLOCK_SIZE].load_le::<u32>()
461    }
462
463    /// Raw bitfields word (second u32 per MS-VHDX §2.6.2.1).
464    pub(crate) fn flags(&self) -> u32 {
465        if self.data.len() < 8 {
466            return 0;
467        }
468        self.data.view_bits::<Lsb0>()[FP_BITFIELDS].load_le::<u32>()
469    }
470
471    /// Whether blocks should remain allocated (fixed disk) — bit 0 of `BitFields`.
472    #[must_use]
473    pub fn leave_block_allocated(&self) -> bool {
474        if self.data.len() < 8 {
475            return false;
476        }
477        self.data.view_bits::<Lsb0>()[FP_LEAVE_BLOCK_ALLOCATED]
478    }
479
480    /// Whether this file has a parent (differencing disk) — bit 1 of `BitFields`.
481    #[must_use]
482    pub fn has_parent(&self) -> bool {
483        if self.data.len() < 8 {
484            return false;
485        }
486        self.data.view_bits::<Lsb0>()[FP_HAS_PARENT]
487    }
488
489    /// Whether any reserved bits (bits 2-31 of `BitFields`) are set.
490    ///
491    /// Per MS-VHDX §2.6.2.1, bits 2-31 MUST be 0.
492    pub(crate) fn has_reserved_bits_set(&self) -> bool {
493        if self.data.len() < 8 {
494            return false;
495        }
496        self.data.view_bits::<Lsb0>()[FP_RESERVED_START..FP_BITS_END].any()
497    }
498}
499
500// ---------------------------------------------------------------------------
501// ParentLocator
502// ---------------------------------------------------------------------------
503
504/// Parent Locator metadata item for differencing disks.
505///
506/// Layout: 20-byte header + N × 12-byte key-value entry table + key/value data.
507pub struct ParentLocator<'a> {
508    data: &'a [u8],
509}
510
511impl<'a> ParentLocator<'a> {
512    /// Access the 20-byte locator header.
513    #[must_use]
514    pub fn header(&self) -> LocatorHeader<'a> {
515        LocatorHeader {
516            data: &self.data[..(LOCATOR_HEADER_SIZE as usize).min(self.data.len())],
517        }
518    }
519
520    /// Get a key-value entry by index.
521    ///
522    /// # Errors
523    ///
524    /// Returns an error if the index is out of range or entry bytes are truncated.
525    pub fn entry(&self, index: usize) -> Result<KeyValueEntry<'a>> {
526        let count = self.header().key_value_count() as usize;
527        if index >= count {
528            return Err(Error::InvalidParameter(format!(
529                "parent locator entry index {index} out of range (count={count})"
530            )));
531        }
532        let start = LOCATOR_HEADER_SIZE as usize + index * KV_ENTRY_SIZE as usize;
533        let end = start + KV_ENTRY_SIZE as usize;
534        if end > self.data.len() {
535            return Err(Error::InvalidParentLocator(
536                "parent locator data too short for entries".into(),
537            ));
538        }
539        Ok(KeyValueEntry {
540            data: &self.data[start..end],
541        })
542    }
543
544    /// Iterate over all key-value entries (zero-copy).
545    pub fn entries(&self) -> impl Iterator<Item = KeyValueEntry<'a>> + 'a {
546        let count = self.header().key_value_count() as usize;
547        let data = self.data;
548        (0..count).filter_map(move |i| {
549            let start = LOCATOR_HEADER_SIZE as usize + i * KV_ENTRY_SIZE as usize;
550            let end = start + KV_ENTRY_SIZE as usize;
551            if end <= data.len() {
552                Some(KeyValueEntry {
553                    data: &data[start..end],
554                })
555            } else {
556                None
557            }
558        })
559    }
560
561    /// The raw parent locator item data (including the 20-byte header and entry table).
562    ///
563    /// Offsets in [`KeyValueEntry`] are relative to the start of this data.
564    #[must_use]
565    pub fn key_value_data(&self) -> &'a [u8] {
566        self.data
567    }
568
569    /// Resolve the preferred parent path candidate.
570    ///
571    /// Path candidates are selected in VHDX order: `relative_path`, then
572    /// `volume_path`, then `absolute_win32_path`. Key and value strings are
573    /// decoded from the parent locator's UTF-16LE key-value data.
574    ///
575    /// # Errors
576    ///
577    /// Returns an error if a key or value cannot be decoded, or if none of the
578    /// standard parent path keys is present.
579    pub fn resolve_parent_path(&self) -> Result<std::path::PathBuf> {
580        const PATH_KEYS: [&str; 3] = ["relative_path", "volume_path", "absolute_win32_path"];
581
582        let data = self.key_value_data();
583        let mut paths: [Option<std::path::PathBuf>; 3] = [None, None, None];
584        for entry in self.entries() {
585            let key = entry.key(data)?;
586            if let Some(index) = PATH_KEYS.iter().position(|candidate| *candidate == key) {
587                paths[index] = Some(std::path::PathBuf::from(entry.value(data)?));
588            }
589        }
590
591        paths
592            .into_iter()
593            .flatten()
594            .next()
595            .ok_or(Error::ParentNotFound)
596    }
597}
598
599// ---------------------------------------------------------------------------
600// LocatorHeader
601// ---------------------------------------------------------------------------
602
603/// 20-byte parent locator header.
604pub struct LocatorHeader<'a> {
605    data: &'a [u8],
606}
607
608impl LocatorHeader<'_> {
609    /// Locator type GUID (16 bytes).
610    ///
611    /// # Panics
612    ///
613    /// Panics only if an internal 16-byte guard is violated.
614    #[must_use]
615    pub fn locator_type(&self) -> Guid {
616        let bytes: [u8; 16] = if self.data.len() >= 16 {
617            self.data[..16].try_into().expect("16 bytes")
618        } else {
619            [0u8; 16]
620        };
621        Guid::from_bytes(bytes)
622    }
623
624    /// Reserved (2 bytes, must be 0).
625    ///
626    /// # Panics
627    ///
628    /// Panics only if an internal 2-byte guard is violated.
629    #[must_use]
630    pub fn reserved(&self) -> u16 {
631        if self.data.len() >= 18 {
632            u16::from_le_bytes(self.data[16..18].try_into().unwrap())
633        } else {
634            0
635        }
636    }
637
638    /// Number of key-value entries.
639    ///
640    /// # Panics
641    ///
642    /// Panics only if an internal 2-byte guard is violated.
643    #[must_use]
644    pub fn key_value_count(&self) -> u16 {
645        if self.data.len() >= 20 {
646            u16::from_le_bytes(self.data[18..20].try_into().unwrap())
647        } else {
648            0
649        }
650    }
651}
652
653// ---------------------------------------------------------------------------
654// KeyValueEntry
655// ---------------------------------------------------------------------------
656
657/// 12-byte key-value entry in a parent locator.
658pub struct KeyValueEntry<'a> {
659    data: &'a [u8],
660}
661
662impl KeyValueEntry<'_> {
663    /// Key offset within the parent locator item.
664    ///
665    /// # Panics
666    ///
667    /// Panics if the entry slice is shorter than 4 bytes.
668    #[must_use]
669    pub fn key_offset(&self) -> u32 {
670        u32::from_le_bytes(self.data[..4].try_into().unwrap())
671    }
672
673    /// Value offset within the parent locator item.
674    ///
675    /// # Panics
676    ///
677    /// Panics if the entry slice is shorter than 8 bytes.
678    #[must_use]
679    pub fn value_offset(&self) -> u32 {
680        u32::from_le_bytes(self.data[4..8].try_into().unwrap())
681    }
682
683    /// Key length in bytes.
684    ///
685    /// # Panics
686    ///
687    /// Panics if the entry slice is shorter than 10 bytes.
688    #[must_use]
689    pub fn key_length(&self) -> u16 {
690        u16::from_le_bytes(self.data[8..10].try_into().unwrap())
691    }
692
693    /// Value length in bytes.
694    ///
695    /// # Panics
696    ///
697    /// Panics if the entry slice is shorter than 12 bytes.
698    #[must_use]
699    pub fn value_length(&self) -> u16 {
700        u16::from_le_bytes(self.data[10..12].try_into().unwrap())
701    }
702
703    /// Decode the key string (UTF-16LE) from the locator data.
704    ///
705    /// # Errors
706    ///
707    /// Returns an error if offset/length are invalid or UTF-16 decoding fails.
708    pub fn key(&self, data: &[u8]) -> Result<String> {
709        decode_utf16le(data, self.key_offset() as usize, self.key_length() as usize)
710    }
711
712    /// Decode the value string (UTF-16LE) from the locator data.
713    ///
714    /// # Errors
715    ///
716    /// Returns an error if offset/length are invalid or UTF-16 decoding fails.
717    pub fn value(&self, data: &[u8]) -> Result<String> {
718        decode_utf16le(
719            data,
720            self.value_offset() as usize,
721            self.value_length() as usize,
722        )
723    }
724}
725
726/// Decode a UTF-16LE string from a byte slice at the given offset and byte-length.
727pub(crate) fn decode_utf16le(data: &[u8], offset: usize, byte_len: usize) -> Result<String> {
728    let end = offset
729        .checked_add(byte_len)
730        .ok_or_else(|| Error::InvalidParentLocator("key/value offset+length overflow".into()))?;
731    if end > data.len() {
732        return Err(Error::InvalidParentLocator(format!(
733            "key/value data out of bounds: offset={offset}, len={byte_len}, data_len={}",
734            data.len()
735        )));
736    }
737    if !byte_len.is_multiple_of(2) {
738        return Err(Error::InvalidParentLocator(
739            "UTF-16LE string has odd byte length".into(),
740        ));
741    }
742    let units: Vec<u16> = data[offset..end]
743        .chunks_exact(2)
744        .map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
745        .collect();
746    String::from_utf16(&units)
747        .map_err(|e| Error::InvalidParentLocator(format!("invalid UTF-16LE string: {e}")))
748}