Skip to main content

vhdx/medium/
create.rs

1//! Create-options builder implementation.
2
3use bitvec::prelude::*;
4use crc32c::crc32c;
5use std::io::{BufWriter, Read, Seek, Write};
6
7use super::{
8    CreateOptions, Len, LogReplayPolicy, Medium, ParentCreateInfo, ParentMedium, SetLen, SyncData,
9    read_exact_at, write_all_at,
10};
11use crate::constants::{
12    BAT_REGION_GUID, HEADER_BUFFER_SIZE, HEADER_SIZE, HEADER1_OFFSET, HEADER2_OFFSET, LOG_OFFSET,
13    METADATA_REGION_GUID, MIB, VHDX_SIGNATURE_BYTES,
14};
15use crate::constants::{
16    BAT_REGION_OFFSET, KV_ENTRY_SIZE, LOCATOR_HEADER_SIZE, LOG_LENGTH, METADATA_REGION_SIZE,
17    METADATA_TABLE_SIZE, REGION_TABLE_SIZE, REGION_TABLE1_OFFSET, REGION_TABLE2_OFFSET,
18    TABLE_ENTRY_SIZE, TABLE_HEADER_SIZE, TIB,
19};
20use crate::error::{Error, Result};
21use crate::types::{self, Guid};
22use std::sync::atomic::AtomicU64;
23
24struct MetadataEntryMeta {
25    guid: Guid,
26    rel_offset: u32,
27    length: u32,
28    flags: u32,
29}
30
31impl<T> CreateOptions<T> {
32    // -- Builder methods ----------------------------------------------------
33
34    /// Set the virtual disk size in bytes (required).
35    ///
36    /// Must be a multiple of `logical_sector_size` and at most 64 TB.
37    #[must_use]
38    pub fn size(mut self, virtual_size: u64) -> Self {
39        self.virtual_size = virtual_size;
40        self
41    }
42
43    /// Set whether this is a fixed-size disk (default: dynamic).
44    #[must_use]
45    pub fn fixed(mut self, fixed: bool) -> Self {
46        self.fixed = fixed;
47        self
48    }
49
50    /// Set the payload block size in bytes (default: 32 MB).
51    ///
52    /// Must be in `[1 MB, 256 MB]` and a power of two.
53    #[must_use]
54    pub fn block_size(mut self, size: u32) -> Self {
55        self.block_size = size;
56        self
57    }
58
59    /// Set the logical sector size in bytes (default: 4096).
60    ///
61    /// Must be 512 or 4096.
62    #[must_use]
63    pub fn logical_sector_size(mut self, size: u32) -> Self {
64        self.logical_sector_size = size;
65        self
66    }
67
68    /// Set the physical sector size in bytes (default: 4096).
69    ///
70    /// Must be 512 or 4096.
71    #[must_use]
72    pub fn physical_sector_size(mut self, size: u32) -> Self {
73        self.physical_sector_size = size;
74        self
75    }
76
77    /// Set caller-provided parent metadata for a differencing disk.
78    ///
79    /// # Errors
80    ///
81    /// Returns an error if the parent medium's Data Write GUID cannot be read.
82    pub fn parent<P>(
83        mut self, parent: &mut Medium<P>, relative_path: impl AsRef<std::path::Path>,
84    ) -> Result<Self>
85    where
86        P: Read + Seek,
87    {
88        self.parent = Some(ParentCreateInfo {
89            relative_path: relative_path.as_ref().to_path_buf(),
90            data_write_guid: parent.data_write_guid()?,
91        });
92        Ok(self)
93    }
94
95    // -- Validation ---------------------------------------------------------
96
97    fn validate(&self) -> Result<()> {
98        if self.virtual_size == 0 {
99            return Err(Error::InvalidParameter(
100                "virtual disk size must be set".into(),
101            ));
102        }
103
104        if self.virtual_size > 64 * TIB {
105            return Err(Error::InvalidParameter(
106                "virtual disk size must not exceed 64 TB".into(),
107            ));
108        }
109
110        if !self
111            .virtual_size
112            .is_multiple_of(u64::from(self.logical_sector_size))
113        {
114            return Err(Error::InvalidParameter(
115                "virtual disk size must be a multiple of logical sector size".into(),
116            ));
117        }
118
119        if self.block_size < MIB || self.block_size > 256 * MIB {
120            return Err(Error::InvalidParameter(
121                "block size must be between 1 MB and 256 MB".into(),
122            ));
123        }
124        if !self.block_size.is_power_of_two() {
125            return Err(Error::InvalidParameter(
126                "block size must be a power of 2".into(),
127            ));
128        }
129
130        if !matches!(self.logical_sector_size, 512 | 4096) {
131            return Err(Error::InvalidParameter(
132                "logical sector size must be 512 or 4096".into(),
133            ));
134        }
135
136        if !matches!(self.physical_sector_size, 512 | 4096) {
137            return Err(Error::InvalidParameter(
138                "physical sector size must be 512 or 4096".into(),
139            ));
140        }
141
142        if self.fixed && self.parent.is_some() {
143            return Err(Error::InvalidParameter(
144                "fixed disk cannot have a parent".into(),
145            ));
146        }
147
148        Ok(())
149    }
150
151    // -- Finalisation -------------------------------------------------------
152
153    /// Create the VHDX on the caller-provided medium.
154    ///
155    /// Writes the File Type Identifier, both Headers, both Region Tables,
156    /// the BAT region (initialised per disk type), the full Metadata table
157    /// and items, and — for fixed disks — pre-allocates and zero-fills all
158    /// payload blocks.
159    ///
160    /// # Errors
161    ///
162    /// Returns an error if validation fails, the file cannot be created,
163    /// or any write operation fails.
164    ///
165    /// # Panics
166    ///
167    /// Panics if this builder has already had its inner medium taken.
168    pub fn finish(mut self) -> Result<Medium<T>>
169    where
170        T: Read + Write + Seek + Len + SetLen + SyncData,
171    {
172        self.validate()?;
173
174        let inner = self
175            .inner
176            .take()
177            .expect("CreateOptions always owns a medium before finish");
178        let mut w = BufWriter::new(inner);
179
180        let bat_size =
181            Self::calculate_bat_size(self.virtual_size, self.block_size, self.logical_sector_size);
182        let metadata_offset = u64::from(BAT_REGION_OFFSET) + u64::from(bat_size);
183
184        // 1. File Type Identifier (offset 0)
185        Self::write_file_type_identifier(&mut w)?;
186
187        // 2. Headers (offsets 64 KB and 128 KB)
188        let file_write_guid = Guid::new_v4();
189        let data_write_guid = Guid::new_v4();
190        let log_guid = Guid::zero(); // No active log for fresh file (MS-VHDX §2.2.1)
191
192        // Header 1 with sequence number 0
193        let header1 = Self::build_header(0, &file_write_guid, &data_write_guid, &log_guid);
194        write_all_at(&mut w, u64::from(HEADER1_OFFSET), &header1)?;
195
196        // Header 2 with sequence number 1 (different from header1 to satisfy §2.2.2)
197        let header2 = Self::build_header(1, &file_write_guid, &data_write_guid, &log_guid);
198        write_all_at(&mut w, u64::from(HEADER2_OFFSET), &header2)?;
199
200        // 3. Region Tables (offsets 192 KB and 256 KB)
201        let region = Self::build_region_table(bat_size, metadata_offset);
202
203        write_all_at(&mut w, u64::from(REGION_TABLE1_OFFSET), &region)?;
204
205        write_all_at(&mut w, u64::from(REGION_TABLE2_OFFSET), &region)?;
206
207        // 4. Extend file to cover log + BAT + metadata (zero-filled)
208        //    For fixed disks, also pre-allocate all payload blocks.
209        //    The first payload offset must be block_size-aligned for the
210        //    validator's payload-offset alignment check (MS-VHDX §2.5.1.1).
211        let _first_payload_offset_mb = if self.fixed {
212            let (num_payload, _num_sb, _total_entries, _chunk_ratio) =
213                Self::compute_bat_entry_counts(
214                    self.virtual_size,
215                    self.block_size,
216                    self.logical_sector_size,
217                );
218            let payload_align = u64::from(self.block_size / MIB);
219            let raw_first_mb =
220                (metadata_offset + u64::from(METADATA_REGION_SIZE)).div_ceil(u64::from(MIB));
221            let first_payload_offset_mb = raw_first_mb.div_ceil(payload_align) * payload_align;
222            let total_payload = num_payload * u64::from(self.block_size);
223            let end = first_payload_offset_mb * u64::from(MIB) + total_payload;
224            w.flush()?;
225            w.get_mut().set_len(end)?;
226            first_payload_offset_mb
227        } else {
228            let end = metadata_offset + u64::from(METADATA_REGION_SIZE);
229            w.flush()?;
230            w.get_mut().set_len(end)?;
231            0
232        };
233
234        // 5. Write BAT entries
235        Self::write_bat_entries(
236            &mut w,
237            self.virtual_size,
238            self.block_size,
239            self.logical_sector_size,
240            self.fixed,
241            metadata_offset,
242        )?;
243
244        let parent_data_write_guid = self.parent.as_ref().map(|parent| parent.data_write_guid);
245
246        // 6. Write metadata table + items
247        self.write_metadata(&mut w, metadata_offset, parent_data_write_guid)?;
248
249        w.flush()?;
250        w.get_mut().sync_data()?;
251        let mut inner = w
252            .into_inner()
253            .map_err(std::io::IntoInnerError::into_error)?;
254
255        // Re-read header buffer from the start of the medium.
256        let mut header_buf = vec![0u8; HEADER_BUFFER_SIZE];
257        read_exact_at(&mut inner, 0, &mut header_buf)?;
258
259        Ok(Medium {
260            inner: std::sync::Mutex::new(inner),
261            header_buf: std::sync::RwLock::new(Some(super::CacheEntry::new(
262                0,
263                std::sync::Arc::from(header_buf),
264            ))),
265            bat_buf: std::sync::RwLock::new(None),
266            metadata_buf: std::sync::RwLock::new(None),
267            log_buf: std::sync::RwLock::new(None),
268            generation: AtomicU64::new(0),
269            write: true,
270            strict: true,
271            log_replay_policy: LogReplayPolicy::Require,
272            replay_overlay: None,
273            parent_resolver: std::sync::Mutex::new(None),
274            validator_buf: std::sync::RwLock::new(None),
275        })
276    }
277
278    // -- Internal helpers ---------------------------------------------------
279
280    pub(crate) fn calculate_bat_size(
281        virtual_size: u64, block_size: u32, logical_sector_size: u32,
282    ) -> u32 {
283        let (_num_payload, _num_sb, total_entries, _chunk_ratio) =
284            Self::compute_bat_entry_counts(virtual_size, block_size, logical_sector_size);
285        let bat_bytes = total_entries * 8;
286        let bat_mb = std::cmp::max(bat_bytes.div_ceil(u64::from(MIB)), 1);
287        u32::try_from(bat_mb).unwrap() * (1024 * 1024)
288    }
289
290    fn write_file_type_identifier(w: &mut (impl Write + Seek)) -> Result<()> {
291        let mut creator = [0u8; 512];
292        let ident = "vhdx-rs\0";
293        for (i, ch) in ident.encode_utf16().enumerate() {
294            let off = i * 2;
295            if off + 1 < 512 {
296                creator[off..off + 2].copy_from_slice(&ch.to_le_bytes());
297            }
298        }
299        write_all_at(w, 0, &VHDX_SIGNATURE_BYTES.into_inner().to_le_bytes())?;
300        write_all_at(w, 8, &creator)?;
301        Ok(())
302    }
303
304    fn build_header(
305        sequence_number: u64, file_write_guid: &Guid, data_write_guid: &Guid, log_guid: &Guid,
306    ) -> [u8; HEADER_SIZE as usize] {
307        let mut buf = [0u8; HEADER_SIZE as usize];
308        buf[..4].copy_from_slice(b"head");
309        buf[4..8].copy_from_slice(&0u32.to_le_bytes());
310        buf[8..16].copy_from_slice(&sequence_number.to_le_bytes());
311        buf[16..32].copy_from_slice(&file_write_guid.to_bytes());
312        buf[32..48].copy_from_slice(&data_write_guid.to_bytes());
313        buf[48..64].copy_from_slice(&log_guid.to_bytes());
314        buf[64..66].copy_from_slice(&0u16.to_le_bytes());
315        buf[66..68].copy_from_slice(&1u16.to_le_bytes());
316        buf[68..72].copy_from_slice(&LOG_LENGTH.to_le_bytes());
317        buf[72..80].copy_from_slice(&u64::from(LOG_OFFSET).to_le_bytes());
318
319        let checksum = crc32c(&buf);
320        buf[4..8].copy_from_slice(&checksum.to_le_bytes());
321
322        buf
323    }
324
325    fn build_region_table(bat_size: u32, metadata_offset: u64) -> Vec<u8> {
326        let mut buf = vec![0u8; REGION_TABLE_SIZE as usize];
327        buf[..4].copy_from_slice(b"regi");
328        buf[4..8].copy_from_slice(&0u32.to_le_bytes());
329        buf[8..12].copy_from_slice(&2u32.to_le_bytes());
330        buf[12..16].copy_from_slice(&0u32.to_le_bytes());
331
332        buf[16..32].copy_from_slice(&BAT_REGION_GUID.to_bytes());
333        buf[32..40].copy_from_slice(&u64::from(BAT_REGION_OFFSET).to_le_bytes());
334        buf[40..44].copy_from_slice(&bat_size.to_le_bytes());
335        buf[44..48].view_bits_mut::<Lsb0>().set(0, true); // Required
336
337        buf[48..64].copy_from_slice(&METADATA_REGION_GUID.to_bytes());
338        buf[64..72].copy_from_slice(&metadata_offset.to_le_bytes());
339        buf[72..76].copy_from_slice(&METADATA_REGION_SIZE.to_le_bytes());
340        buf[76..80].view_bits_mut::<Lsb0>().set(0, true); // Required
341
342        let checksum = crc32c(&buf);
343        buf[4..8].copy_from_slice(&checksum.to_le_bytes());
344
345        buf
346    }
347
348    /// Compute BAT entry counts and chunk ratio.
349    ///
350    /// Returns `(num_payload_blocks, num_sector_bitmap_blocks, total_entries, chunk_ratio)`.
351    pub(crate) fn compute_bat_entry_counts(
352        virtual_size: u64, block_size: u32, logical_sector_size: u32,
353    ) -> (u64, u64, u64, u64) {
354        let num_payload = virtual_size.div_ceil(u64::from(block_size));
355        let chunk_ratio = (1u64 << 23) * u64::from(logical_sector_size) / u64::from(block_size);
356        let num_sb = num_payload.div_ceil(chunk_ratio);
357        let total = num_payload + num_sb;
358        (num_payload, num_sb, total, chunk_ratio)
359    }
360
361    /// Write BAT entries at [`BAT_REGION_OFFSET`].
362    ///
363    /// - Dynamic disk: all entries = 0 (`PAYLOAD_BLOCK_NOT_PRESENT`).
364    /// - Fixed disk: payload entries = `FullyPresent` with sequential
365    ///   `FileOffsetMB`; sector-bitmap entries = 0.
366    fn write_bat_entries(
367        w: &mut (impl Write + Seek), virtual_size: u64, block_size: u32, logical_sector_size: u32,
368        fixed: bool, metadata_offset: u64,
369    ) -> Result<()> {
370        let (_num_payload, num_sb, total_entries, chunk_ratio) =
371            Self::compute_bat_entry_counts(virtual_size, block_size, logical_sector_size);
372
373        if !fixed {
374            // Dynamic disk: BAT region is already zero-filled by set_len.
375            return Ok(());
376        }
377
378        // Fixed disk: write interleaved payload + sector-bitmap entries.
379        // Align first payload to block_size boundary so that the validator's
380        // payload-offset alignment check passes (MS-VHDX §2.5.1.1).
381        let payload_align = u64::from(block_size / MIB);
382        let raw_first_payload_mb =
383            (metadata_offset + u64::from(METADATA_REGION_SIZE)).div_ceil(u64::from(MIB));
384        let first_payload_offset_mb = raw_first_payload_mb.div_ceil(payload_align) * payload_align;
385
386        let mut sb_written: u64 = 0;
387        for i in 0..total_entries {
388            let entry_offset = u64::from(BAT_REGION_OFFSET)
389                .checked_add(i.checked_mul(8).expect("BAT entry offset fits u64"))
390                .expect("BAT entry offset fits u64");
391            // Determine if this entry is a sector bitmap based on how many
392            // payload entries have been written since the last SB entry.
393            // SB entries appear after every chunk_ratio payload entries.
394            let payloads_written = i - sb_written;
395            let is_sb = payloads_written > 0
396                && payloads_written.is_multiple_of(chunk_ratio)
397                && sb_written < num_sb;
398            if is_sb {
399                // Sector bitmap entry: NotPresent
400                write_all_at(w, entry_offset, &0u64.to_le_bytes())?;
401                sb_written += 1;
402            } else {
403                // Payload entry: FullyPresent at sequential offset
404                let payload_idx = payloads_written;
405                let offset_mb = first_payload_offset_mb + payload_idx * u64::from(block_size / MIB);
406                let mut raw_bytes = [0u8; 8];
407                let bits = raw_bytes.view_bits_mut::<Lsb0>();
408                bits[0..3].store::<u8>(6u8); // FullyPresent
409                bits[20..64].store::<u64>(offset_mb);
410                write_all_at(w, entry_offset, &raw_bytes)?;
411            }
412        }
413
414        Ok(())
415    }
416
417    /// Write the full metadata table (64 KB header + entries) followed by all
418    /// required metadata items at `metadata_offset`.
419    fn write_metadata(
420        &self, w: &mut (impl Write + Seek), metadata_offset: u64,
421        parent_data_write_guid: Option<Guid>,
422    ) -> Result<()> {
423        let has_parent = self.parent.is_some();
424        let (items_buf, item_metas) =
425            self.build_metadata_items(has_parent, parent_data_write_guid)?;
426        let table = Self::build_metadata_table(if has_parent { 6 } else { 5 }, &item_metas);
427        write_all_at(w, metadata_offset, &table)?;
428        write_all_at(
429            w,
430            metadata_offset + u64::from(METADATA_TABLE_SIZE),
431            &items_buf,
432        )?;
433        Ok(())
434    }
435
436    fn rel_metadata_offset(items_buf: &[u8]) -> Result<u32> {
437        let base = METADATA_TABLE_SIZE;
438        let rel = u32::try_from(items_buf.len())
439            .map_err(|_| Error::InvalidParameter("metadata items buffer too large".into()))?;
440        base.checked_add(rel)
441            .ok_or_else(|| Error::InvalidParameter("metadata relative offset overflow".into()))
442    }
443
444    fn metadata_flags(is_virtual_disk: bool, is_required: bool) -> u32 {
445        let mut buf = [0u8; 4];
446        let bits = buf.view_bits_mut::<Lsb0>();
447        bits.set(1, is_virtual_disk);
448        bits.set(2, is_required);
449        u32::from_le_bytes(buf)
450    }
451
452    fn build_metadata_items(
453        &self, has_parent: bool, parent_data_write_guid: Option<Guid>,
454    ) -> Result<(Vec<u8>, Vec<MetadataEntryMeta>)> {
455        let virtual_disk_id = Guid::new_v4();
456        let mut items_buf = Vec::new();
457        let mut item_metas = Vec::with_capacity(if has_parent { 6 } else { 5 });
458        self.push_file_parameters_item(&mut items_buf, &mut item_metas, has_parent)?;
459        Self::push_simple_item(
460            &mut items_buf,
461            &mut item_metas,
462            types::StandardItems::VIRTUAL_DISK_SIZE,
463            &self.virtual_size.to_le_bytes(),
464            true,
465        )?;
466        Self::push_simple_item(
467            &mut items_buf,
468            &mut item_metas,
469            types::StandardItems::VIRTUAL_DISK_ID,
470            &virtual_disk_id.to_bytes(),
471            true,
472        )?;
473        Self::push_simple_item(
474            &mut items_buf,
475            &mut item_metas,
476            types::StandardItems::LOGICAL_SECTOR_SIZE,
477            &self.logical_sector_size.to_le_bytes(),
478            true,
479        )?;
480        Self::push_simple_item(
481            &mut items_buf,
482            &mut item_metas,
483            types::StandardItems::PHYSICAL_SECTOR_SIZE,
484            &self.physical_sector_size.to_le_bytes(),
485            true,
486        )?;
487        if has_parent {
488            self.push_parent_locator_item(
489                &mut items_buf,
490                &mut item_metas,
491                parent_data_write_guid
492                    .expect("parent_data_write_guid must be set when has_parent is true"),
493            )?;
494        }
495        Ok((items_buf, item_metas))
496    }
497
498    fn push_file_parameters_item(
499        &self, items_buf: &mut Vec<u8>, metas: &mut Vec<MetadataEntryMeta>, has_parent: bool,
500    ) -> Result<()> {
501        let rel = Self::rel_metadata_offset(items_buf)?;
502        let mut fp_buf = [0u8; 8];
503        let fp_bits = fp_buf.view_bits_mut::<Lsb0>();
504        fp_bits[0..32].store_le::<u32>(self.block_size);
505        fp_bits.set(32, self.fixed);
506        fp_bits.set(33, has_parent);
507        items_buf.extend_from_slice(&fp_buf);
508        metas.push(MetadataEntryMeta {
509            guid: types::StandardItems::FILE_PARAMETERS,
510            rel_offset: rel,
511            length: 8,
512            flags: Self::metadata_flags(false, true),
513        });
514        Ok(())
515    }
516
517    fn push_simple_item(
518        items_buf: &mut Vec<u8>, metas: &mut Vec<MetadataEntryMeta>, guid: Guid, bytes: &[u8],
519        is_virtual_disk: bool,
520    ) -> Result<()> {
521        let rel = Self::rel_metadata_offset(items_buf)?;
522        items_buf.extend_from_slice(bytes);
523        metas.push(MetadataEntryMeta {
524            guid,
525            rel_offset: rel,
526            length: u32::try_from(bytes.len()).expect("metadata item length fits u32"),
527            flags: Self::metadata_flags(is_virtual_disk, true),
528        });
529        Ok(())
530    }
531
532    fn push_parent_locator_item(
533        &self, items_buf: &mut Vec<u8>, metas: &mut Vec<MetadataEntryMeta>, parent_guid: Guid,
534    ) -> Result<()> {
535        let rel = Self::rel_metadata_offset(items_buf)?;
536        let pl_data = self.build_parent_locator(parent_guid);
537        let pl_length = u32::try_from(pl_data.len())
538            .map_err(|_| Error::InvalidParameter("parent locator metadata too large".into()))?;
539        items_buf.extend_from_slice(&pl_data);
540        metas.push(MetadataEntryMeta {
541            guid: types::StandardItems::PARENT_LOCATOR,
542            rel_offset: rel,
543            length: pl_length,
544            flags: Self::metadata_flags(false, true),
545        });
546        Ok(())
547    }
548
549    fn build_metadata_table(entry_count: u16, item_metas: &[MetadataEntryMeta]) -> Vec<u8> {
550        let mut table = vec![0u8; METADATA_TABLE_SIZE as usize];
551        table[0..8].copy_from_slice(b"metadata");
552        table[10..12].copy_from_slice(&entry_count.to_le_bytes());
553        let mut entry_off: usize = TABLE_HEADER_SIZE as usize;
554        for meta in item_metas {
555            table[entry_off..entry_off + 16].copy_from_slice(&meta.guid.to_bytes());
556            table[entry_off + 16..entry_off + 20].copy_from_slice(&meta.rel_offset.to_le_bytes());
557            table[entry_off + 20..entry_off + 24].copy_from_slice(&meta.length.to_le_bytes());
558            table[entry_off + 24..entry_off + 28].copy_from_slice(&meta.flags.to_le_bytes());
559            entry_off += TABLE_ENTRY_SIZE as usize;
560        }
561        table
562    }
563
564    /// Build the parent locator metadata item for differencing disks.
565    ///
566    /// Layout: 20-byte header + 2×12-byte KV entries + UTF-16LE key/value data.
567    ///
568    /// `parent_data_write_guid` is the `DataWriteGuid` read from the parent file's
569    /// header. Per MS-VHDX §2.6.2.6.3, the `parent_linkage` value MUST be the
570    /// parent's `DataWriteGuid`, formatted as a lowercase GUID string with braces.
571    ///
572    /// # Panics
573    ///
574    /// Panics if any of the 8 `u32::try_from` / `u16::try_from` offset or length
575    /// conversions overflow. This should not happen with valid UTF-16 key/value
576    /// data within reasonable size limits.
577    fn build_parent_locator(&self, parent_data_write_guid: Guid) -> Vec<u8> {
578        // Format parent_linkage as the parent's DataWriteGuid with braces:
579        // "{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}"
580        let guid_str = parent_data_write_guid.to_uuid().hyphenated().to_string();
581        let parent_linkage_str = format!("{{{guid_str}}}");
582        let relative_path = self
583            .parent
584            .as_ref()
585            .map(|p| p.relative_path.to_string_lossy().to_string())
586            .unwrap_or_default();
587
588        let key1 = "parent_linkage";
589        let key2 = "relative_path";
590
591        let key1_utf16: Vec<u8> = key1.encode_utf16().flat_map(u16::to_le_bytes).collect();
592        let val1_utf16: Vec<u8> = parent_linkage_str
593            .encode_utf16()
594            .flat_map(u16::to_le_bytes)
595            .collect();
596        let key2_utf16: Vec<u8> = key2.encode_utf16().flat_map(u16::to_le_bytes).collect();
597        let val2_utf16: Vec<u8> = relative_path
598            .encode_utf16()
599            .flat_map(u16::to_le_bytes)
600            .collect();
601
602        let kv_data_start = LOCATOR_HEADER_SIZE as usize + 2 * KV_ENTRY_SIZE as usize;
603
604        let key1_off = kv_data_start;
605        let val1_off = key1_off + key1_utf16.len();
606        let key2_off = val1_off + val1_utf16.len();
607        let val2_off = key2_off + key2_utf16.len();
608
609        let total_len = val2_off + val2_utf16.len();
610        let mut buf = vec![0u8; total_len];
611
612        // Locator header (20 bytes)
613        buf[0..16].copy_from_slice(&types::StandardItems::LOCATOR_TYPE_VHDX.to_bytes());
614        // reserved (2 bytes) = 0
615        buf[18..20].copy_from_slice(&2u16.to_le_bytes()); // 2 KV entries
616
617        // KV entry 0: parent_linkage
618        let kv0_off = LOCATOR_HEADER_SIZE as usize;
619        buf[kv0_off..kv0_off + 4].copy_from_slice(&u32::try_from(key1_off).unwrap().to_le_bytes());
620        buf[kv0_off + 4..kv0_off + 8]
621            .copy_from_slice(&u32::try_from(val1_off).unwrap().to_le_bytes());
622        buf[kv0_off + 8..kv0_off + 10]
623            .copy_from_slice(&u16::try_from(key1_utf16.len()).unwrap().to_le_bytes());
624        buf[kv0_off + 10..kv0_off + 12]
625            .copy_from_slice(&u16::try_from(val1_utf16.len()).unwrap().to_le_bytes());
626
627        // KV entry 1: relative_path
628        let kv1_off = LOCATOR_HEADER_SIZE as usize + KV_ENTRY_SIZE as usize;
629        buf[kv1_off..kv1_off + 4].copy_from_slice(&u32::try_from(key2_off).unwrap().to_le_bytes());
630        buf[kv1_off + 4..kv1_off + 8]
631            .copy_from_slice(&u32::try_from(val2_off).unwrap().to_le_bytes());
632        buf[kv1_off + 8..kv1_off + 10]
633            .copy_from_slice(&u16::try_from(key2_utf16.len()).unwrap().to_le_bytes());
634        buf[kv1_off + 10..kv1_off + 12]
635            .copy_from_slice(&u16::try_from(val2_utf16.len()).unwrap().to_le_bytes());
636
637        // Key/value data
638        buf[key1_off..key1_off + key1_utf16.len()].copy_from_slice(&key1_utf16);
639        buf[val1_off..val1_off + val1_utf16.len()].copy_from_slice(&val1_utf16);
640        buf[key2_off..key2_off + key2_utf16.len()].copy_from_slice(&key2_utf16);
641        buf[val2_off..val2_off + val2_utf16.len()].copy_from_slice(&val2_utf16);
642
643        buf
644    }
645}