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}