zlayer-agent 0.12.1

Container runtime agent using libcontainer/youki
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
//! Safe Rust wrappers for HCS layer-storage operations.
//!
//! HCS exposes a set of synchronous functions in `computestorage.dll` for
//! materializing container image layers on disk, creating scratch (writable)
//! sandbox layers, and attaching/detaching the WCIFS filter driver to present
//! a layered root filesystem to a container. This module wraps them with
//! Rust-friendly paths, serialized `LayerData` JSON, and typed errors.
//!
//! All of these operations are synchronous — unlike the asynchronous
//! compute-system lifecycle APIs in [`zlayer_hcs`] — so they live in this
//! module rather than in the HCS crate.
//!
//! The higher-level orchestration (OCI tar → wclayer directory layout, parent
//! chain management, VHD open/close for scratch setup) lives in
//! `windows::unpacker` and `windows::scratch`; this module only provides the
//! 1:1 FFI bindings.

#![cfg(target_os = "windows")]
#![allow(unsafe_code)]

use std::io;
use std::os::windows::ffi::OsStrExt;
use std::path::Path;

use serde::Serialize;
use windows::core::{HSTRING, PCWSTR};
use windows::Win32::System::HostComputeSystem::{HcsDestroyLayer, HcsExportLayer, HcsImportLayer};

use zlayer_hcs::schema::{Layer, SchemaVersion};

// ---------------------------------------------------------------------------
// Shared FFI types for `vmcompute.dll` legacy `wclayer` APIs.
//
// `CreateSandboxLayer`, `ActivateLayer`, `PrepareLayer`, `UnprepareLayer`,
// `DeactivateLayer`, and `GetLayerMountPath` all take a `WC_DRIVER_INFO*` as
// their first argument. `PrepareLayer` and `CreateSandboxLayer` additionally
// take an array of `WC_LAYER_DESCRIPTOR`. These structs and the empty
// home-dir buffer are shared across every wrapper below.
// ---------------------------------------------------------------------------

/// `WC_DRIVER_INFO`. See [`create_sandbox_layer`] for the full ABI write-up
/// (flavour discriminant width, `info_buffer` null-pointer trap, etc.).
#[repr(C)]
struct WcDriverInfo {
    flavour: u32,
    info_buffer: *const u16,
}

/// `WC_LAYER_DESCRIPTOR`: 16-byte GUID + 4-byte flags + 4-byte pad + 8-byte
/// path pointer = 32 bytes on x64. Matches hcsshim's `WC_LAYER_DESCRIPTOR`
/// and the .NET `LayerDescriptor` reference.
#[repr(C)]
struct WcLayerDescriptor {
    layer_id: windows::core::GUID,
    flags: u32,
    _flags_pad: u32,
    path: *const u16,
}

/// Empty null-terminated UTF-16 string the driver-info points at. Static so
/// the pointer is stable for any caller's `WcDriverInfo` literal — matches
/// hcsshim's `var utf16EmptyString uint16` + `&utf16EmptyString`.
static EMPTY_HOME_DIR: [u16; 1] = [0];

/// Build a `WcDriverInfo` configured for the filter-driver flavour (the only
/// one we ever use). The returned struct borrows `&EMPTY_HOME_DIR` which has
/// `'static` lifetime, so the returned value is safe to pass through to FFI.
fn driver_info() -> WcDriverInfo {
    WcDriverInfo {
        flavour: 1,
        info_buffer: EMPTY_HOME_DIR.as_ptr(),
    }
}

/// Convert a parent chain into the parallel `(path_buffers, descriptors)`
/// vectors expected by `CreateSandboxLayer` / `PrepareLayer`.
///
/// `path_buffers` owns the wide-string path data that each descriptor's
/// `path` pointer refers to; it MUST outlive the descriptor slice across the
/// FFI call.
fn parent_chain_to_descriptors(
    parent_chain: &LayerChain,
) -> io::Result<(Vec<Vec<u16>>, Vec<WcLayerDescriptor>)> {
    let path_buffers: Vec<Vec<u16>> = parent_chain
        .0
        .iter()
        .map(|l| {
            let mut w: Vec<u16> = Path::new(&l.path).as_os_str().encode_wide().collect();
            w.push(0);
            w
        })
        .collect();
    let descriptors: Vec<WcLayerDescriptor> = parent_chain
        .0
        .iter()
        .zip(path_buffers.iter())
        .map(|(l, buf)| {
            let id = parse_guid_str(&l.id)?;
            Ok(WcLayerDescriptor {
                layer_id: id,
                flags: 0,
                _flags_pad: 0,
                path: buf.as_ptr(),
            })
        })
        .collect::<io::Result<Vec<_>>>()?;
    Ok((path_buffers, descriptors))
}

// ---------------------------------------------------------------------------
// Parent chain + LayerData serialization
// ---------------------------------------------------------------------------

/// Parent-layer chain passed to HCS layer-storage operations.
///
/// Order is **child-to-parent**: the first element is the immediate parent of
/// the layer being imported, attached, or set up; the last is the base OS
/// layer. An empty chain is valid for base-layer import operations.
#[derive(Debug, Default, Clone)]
pub struct LayerChain(pub Vec<Layer>);

impl LayerChain {
    /// Build a new chain from an owned `Vec<Layer>`.
    #[must_use]
    pub fn new(layers: Vec<Layer>) -> Self {
        Self(layers)
    }

    /// Serialize this chain into the `LayerData` JSON document shape HCS
    /// expects. The document always carries a schema-version tag; the
    /// `Layers` array is omitted when the chain is empty.
    fn to_layer_data_json(&self) -> io::Result<String> {
        // `LayerData` is a schema-v2.1 document with a `SchemaVersion` tag
        // and an optional `Layers` array. HCS tolerates both presentations
        // (empty array or missing key), but the hcsshim reference omits the
        // key entirely when there are no parents.
        #[derive(Serialize)]
        #[serde(rename_all = "PascalCase")]
        struct LayerData<'a> {
            schema_version: SchemaVersion,
            #[serde(skip_serializing_if = "<[Layer]>::is_empty")]
            layers: &'a [Layer],
        }
        let data = LayerData {
            schema_version: SchemaVersion::default(),
            layers: &self.0,
        };
        serde_json::to_string(&data)
            .map_err(|e| io::Error::other(format!("serialize LayerData: {e}")))
    }
}

/// Convert a filesystem [`Path`] into the wide-string form HCS expects.
///
/// Windows HCS APIs take `PCWSTR` for every path. `HSTRING::from(OsStr)` does
/// the WTF-8 → UTF-16 conversion for us and the resulting `HSTRING` implements
/// `Param<PCWSTR>`, so it can be passed to the FFI call directly.
fn path_to_hstring(path: &Path) -> HSTRING {
    HSTRING::from(path.as_os_str())
}

// ---------------------------------------------------------------------------
// Layer-storage wrappers (1:1 with HCS functions)
// ---------------------------------------------------------------------------

/// Import a previously-staged layer directory at `layer_path` with the given
/// parent chain. The `source_folder` contains the unpacked OCI tar converted
/// into the wclayer folder layout (`Files/`, `Hives/`, `tombstones.txt`, ...);
/// on success, HCS has materialized the layer into `layer_path`.
///
/// Callers must hold `SeBackupPrivilege` + `SeRestorePrivilege` on the current
/// process token — see [`crate::windows::layer::enable_backup_restore_privileges`].
///
/// # Errors
///
/// Returns an [`io::Error`] if HCS returns a non-success HRESULT.
pub fn import_layer(
    layer_path: &Path,
    source_folder: &Path,
    parent_chain: &LayerChain,
) -> io::Result<()> {
    let layer_data = parent_chain.to_layer_data_json()?;
    let lp = path_to_hstring(layer_path);
    let sf = path_to_hstring(source_folder);
    let ld = HSTRING::from(layer_data);
    // SAFETY: All three arguments are live `HSTRING`s that outlive the call.
    unsafe {
        HcsImportLayer(&lp, &sf, &ld)
            .map_err(|e| io::Error::other(format!("HcsImportLayer: {e}")))?;
    }
    Ok(())
}

/// Export a materialized layer at `layer_path` back into `export_folder` in
/// the wclayer directory layout. Empty `options` is the common case.
///
/// # Errors
///
/// Returns an [`io::Error`] if HCS returns a non-success HRESULT.
pub fn export_layer(
    layer_path: &Path,
    export_folder: &Path,
    parent_chain: &LayerChain,
    options_json: &str,
) -> io::Result<()> {
    let layer_data = parent_chain.to_layer_data_json()?;
    let lp = path_to_hstring(layer_path);
    let ef = path_to_hstring(export_folder);
    let ld = HSTRING::from(layer_data);
    let opts = HSTRING::from(options_json);
    // SAFETY: All four arguments are live `HSTRING`s that outlive the call.
    unsafe {
        HcsExportLayer(&lp, &ef, &ld, &opts)
            .map_err(|e| io::Error::other(format!("HcsExportLayer: {e}")))?;
    }
    Ok(())
}

/// Destroy the layer directory at `layer_path`. This tears down the on-disk
/// representation (including any VHD backing store HCS created during
/// import/init) and is the correct cleanup for both read-only and writable
/// layers.
///
/// # Errors
///
/// Returns an [`io::Error`] if HCS returns a non-success HRESULT.
pub fn destroy_layer(layer_path: &Path) -> io::Result<()> {
    let lp = path_to_hstring(layer_path);
    // SAFETY: `lp` is a live `HSTRING` that outlives the call.
    unsafe {
        HcsDestroyLayer(&lp).map_err(|e| io::Error::other(format!("HcsDestroyLayer: {e}")))?;
    }
    Ok(())
}

/// Activate a layer at `layer_path` (mount its sandbox.vhdx via the layer
/// filter driver, allowing subsequent `PrepareLayer` + `GetLayerMountPath`).
///
/// Wraps `vmcompute.dll!ActivateLayer`. Matches hcsshim's
/// `internal/wclayer/activatelayer.go::ActivateLayer`. Pair every successful
/// `activate_layer` with a `deactivate_layer` once the consumer is done.
///
/// # Errors
///
/// Returns an [`io::Error`] if `ActivateLayer` returns a non-success HRESULT.
pub fn activate_layer(layer_path: &Path) -> io::Result<()> {
    windows::core::link!(
        "vmcompute.dll" "system" fn ActivateLayer(
            info: *const WcDriverInfo,
            id: PCWSTR,
        ) -> windows::core::HRESULT
    );
    let info = driver_info();
    let lp = HSTRING::from(layer_path.as_os_str());
    // SAFETY: `info` borrows `EMPTY_HOME_DIR` (static); `lp` is a live
    // `HSTRING` whose null-terminated buffer outlives the call.
    let hr = unsafe { ActivateLayer(&info, PCWSTR::from_raw(lp.as_ptr())) };
    hr.ok()
        .map_err(|e| io::Error::other(format!("ActivateLayer: {e}")))?;
    Ok(())
}

/// Deactivate a layer previously activated via [`activate_layer`]. Must be
/// called after `unprepare_layer` and before `destroy_layer`.
///
/// Wraps `vmcompute.dll!DeactivateLayer`. Matches hcsshim's
/// `internal/wclayer/deactivatelayer.go::DeactivateLayer`.
///
/// # Errors
///
/// Returns an [`io::Error`] if `DeactivateLayer` returns a non-success HRESULT.
pub fn deactivate_layer(layer_path: &Path) -> io::Result<()> {
    windows::core::link!(
        "vmcompute.dll" "system" fn DeactivateLayer(
            info: *const WcDriverInfo,
            id: PCWSTR,
        ) -> windows::core::HRESULT
    );
    let info = driver_info();
    let lp = HSTRING::from(layer_path.as_os_str());
    // SAFETY: same as `activate_layer`.
    let hr = unsafe { DeactivateLayer(&info, PCWSTR::from_raw(lp.as_ptr())) };
    hr.ok()
        .map_err(|e| io::Error::other(format!("DeactivateLayer: {e}")))?;
    Ok(())
}

/// Prepare a writable layer at `layer_path` over `parent_chain` so that the
/// host (or a container) can read/write through the layered view. Must be
/// called after [`activate_layer`] and before [`get_layer_mount_path`].
///
/// Wraps `vmcompute.dll!PrepareLayer`. Matches hcsshim's
/// `internal/wclayer/preparelayer.go::PrepareLayer`. Pair every successful
/// `prepare_layer` with an `unprepare_layer` once the consumer is done.
///
/// # Errors
///
/// Returns an [`io::Error`] if `PrepareLayer` returns a non-success HRESULT.
pub fn prepare_layer(layer_path: &Path, parent_chain: &LayerChain) -> io::Result<()> {
    windows::core::link!(
        "vmcompute.dll" "system" fn PrepareLayer(
            info: *const WcDriverInfo,
            id: PCWSTR,
            descriptors: *const WcLayerDescriptor,
            descriptor_count: usize,
        ) -> windows::core::HRESULT
    );
    let info = driver_info();
    let lp = HSTRING::from(layer_path.as_os_str());
    let (_path_buffers, descriptors) = parent_chain_to_descriptors(parent_chain)?;
    // SAFETY: `info` borrows `EMPTY_HOME_DIR` (static); `lp` is a live
    // `HSTRING`; `descriptors` and the wide-string buffers it points into
    // (`_path_buffers`) outlive the call.
    let hr = unsafe {
        PrepareLayer(
            &info,
            PCWSTR::from_raw(lp.as_ptr()),
            descriptors.as_ptr(),
            descriptors.len(),
        )
    };
    hr.ok()
        .map_err(|e| io::Error::other(format!("PrepareLayer: {e}")))?;
    Ok(())
}

/// Tear down the prepared state of a layer at `layer_path`. Pair with
/// [`prepare_layer`].
///
/// Wraps `vmcompute.dll!UnprepareLayer`. Matches hcsshim's
/// `internal/wclayer/unpreparelayer.go::UnprepareLayer`.
///
/// # Errors
///
/// Returns an [`io::Error`] if `UnprepareLayer` returns a non-success HRESULT.
pub fn unprepare_layer(layer_path: &Path) -> io::Result<()> {
    windows::core::link!(
        "vmcompute.dll" "system" fn UnprepareLayer(
            info: *const WcDriverInfo,
            id: PCWSTR,
        ) -> windows::core::HRESULT
    );
    let info = driver_info();
    let lp = HSTRING::from(layer_path.as_os_str());
    // SAFETY: same as `activate_layer`.
    let hr = unsafe { UnprepareLayer(&info, PCWSTR::from_raw(lp.as_ptr())) };
    hr.ok()
        .map_err(|e| io::Error::other(format!("UnprepareLayer: {e}")))?;
    Ok(())
}

/// Materialize the read-only base OS layer at `layer_path` after a successful
/// [`import_layer`] of a base layer (i.e. one imported with an empty
/// parent chain). This is the rough equivalent of hcsshim's
/// `wclayer.ProcessBaseLayer` — it translates the staged `Hives/*` registry
/// hive exports into the live `Files\Windows\System32\config\*` files that
/// HCS expects when a child layer chain-walks back to this base.
///
/// Without this call, importing a child layer that lists this base in its
/// parent chain can fail with `0x80070002` ("file not found") because the
/// derived `config\` materializations don't exist yet and HCS's
/// `NtQueryDirectoryFile` walk hits an absent directory.
///
/// Backing FFI: `vmcompute.dll!ProcessBaseImage(path: *uint16) -> HRESULT`
/// (matches `hcsshim/internal/wclayer/zsyscall_windows.go`). This symbol is
/// not exposed by `windows::Win32::System::HostComputeSystem` in
/// `windows-rs 0.62`, so we declare the link inline.
///
/// # Errors
///
/// Returns an [`io::Error`] if the syscall returns a non-success HRESULT.
pub fn process_base_layer(layer_path: &Path) -> io::Result<()> {
    let lp = path_to_hstring(layer_path);
    // Link directly to vmcompute.dll's `ProcessBaseImage`. Single PCWSTR in,
    // HRESULT out.
    windows::core::link!(
        "vmcompute.dll" "system" fn ProcessBaseImage(path: PCWSTR) -> windows::core::HRESULT
    );
    tracing::debug!(
        target: "zlayer_agent::wclayer",
        path = %layer_path.display(),
        "calling vmcompute.dll!ProcessBaseImage",
    );
    // SAFETY: `lp` is a live `HSTRING` whose backing buffer is null-terminated
    // UTF-16 (per `HSTRING`'s `Deref<Target = [u16]>` invariant) and outlives
    // the call. `ProcessBaseImage` only reads the wide-string path argument
    // and returns an HRESULT; no out-pointers or shared resources.
    let hr = unsafe { ProcessBaseImage(PCWSTR::from_raw(lp.as_ptr())) };
    tracing::debug!(
        target: "zlayer_agent::wclayer",
        path = %layer_path.display(),
        hr = ?hr,
        "vmcompute.dll!ProcessBaseImage returned",
    );
    hr.ok()
        .map_err(|e| io::Error::other(format!("ProcessBaseImage: {e}")))?;
    Ok(())
}

/// Materialize a base OS layer's Utility VM (`<layer>\UtilityVM`) so the
/// derived `UtilityVM\SystemTemplate.vhdx` and `UtilityVM\SystemTemplateBase.vhdx`
/// artifacts exist on disk. Must be called **after** [`process_base_layer`]
/// on the same layer, and only when `<layer>\UtilityVM\Files\` actually
/// exists (process-only sideloaded images may not ship a UVM payload).
///
/// Without this call, [`crate::windows::unpacker::locate_uvm_boot_files`]
/// rejects the layer with "missing SystemTemplate.vhdx", and
/// `Uvm::create` cannot stage the per-UVM sandbox VHDX, blocking every
/// Hyper-V-isolated container boot. Equivalent to hcsshim's
/// `internal/wclayer/processimage.go::ProcessUtilityVMImage` — the
/// containerd-shim-runhcs-v1 snapshotter calls this on every base layer
/// during unpack.
///
/// Backing FFI: `vmcompute.dll!ProcessUtilityImage(path: *uint16) -> HRESULT`.
/// Not exposed by `windows::Win32::System::HostComputeSystem` in
/// `windows-rs 0.62`, so we declare the link inline.
///
/// # Errors
///
/// Returns an [`io::Error`] if the syscall returns a non-success HRESULT.
pub fn process_utility_vm_image(utility_vm_path: &Path) -> io::Result<()> {
    let lp = path_to_hstring(utility_vm_path);
    windows::core::link!(
        "vmcompute.dll" "system" fn ProcessUtilityImage(path: PCWSTR) -> windows::core::HRESULT
    );
    tracing::debug!(
        target: "zlayer_agent::wclayer",
        path = %utility_vm_path.display(),
        "calling vmcompute.dll!ProcessUtilityImage",
    );
    // SAFETY: `lp` is a live `HSTRING` whose backing buffer is null-terminated
    // UTF-16 (per `HSTRING`'s `Deref<Target = [u16]>` invariant) and outlives
    // the call. `ProcessUtilityImage` only reads the wide-string path
    // argument and returns an HRESULT; no out-pointers or shared resources.
    let hr = unsafe { ProcessUtilityImage(PCWSTR::from_raw(lp.as_ptr())) };
    tracing::debug!(
        target: "zlayer_agent::wclayer",
        path = %utility_vm_path.display(),
        hr = ?hr,
        "vmcompute.dll!ProcessUtilityImage returned",
    );
    hr.ok()
        .map_err(|e| io::Error::other(format!("ProcessUtilityImage: {e}")))?;
    Ok(())
}

/// Create a fresh scratch (writable) layer on disk via
/// `vmcompute.dll!CreateSandboxLayer`.
///
/// This is the canonical scratch-layer-creation path for Windows containers:
/// matches `hcsshim/internal/wclayer/createscratchlayer.go::CreateScratchLayer`,
/// which is the production code path used by hcsshim, containerd-shim-runhcs,
/// runhcs, and Moby. `CreateSandboxLayer` produces a fully-formatted
/// `sandbox.vhdx` and supporting metadata inside `layer_path` in a single
/// call — there is no separate `Format` step. To make the layer mountable,
/// the caller chains [`activate_layer`] → [`prepare_layer`] →
/// [`get_layer_mount_path`], mirroring hcsshim's
/// `internal/layers/wcow_mount.go::mountProcessIsolatedWCIFSLayers`.
///
/// `parent_chain` is child-to-parent ordered (first entry = immediate parent,
/// last entry = base OS layer).
///
/// # ABI notes
///
/// * `WC_DRIVER_INFO` carries a 4-byte flavour discriminant (.NET reference
///   `int Type; IntPtr Path;`) and an 8-byte pointer to a UTF-16 home-dir
///   string. Both hcsshim and the .NET reference initialise it with
///   `{flavour: 1, info_buffer: &""}` — `FilterDriver` flavour with a
///   pointer to an *empty UTF-16 string*, NOT a NULL pointer. `vmcompute`
///   unconditionally dereferences `info_buffer`; NULL crashes inside
///   `vmcompute.dll` with `STATUS_ACCESS_VIOLATION` at ~offset 0x30e6a.
/// * `WC_LAYER_DESCRIPTOR` is 16-byte GUID + 4-byte `Flags` + 4-byte pad +
///   8-byte path pointer = 32 bytes on x64.
/// * Descriptor count is passed as `usize` to match hcsshim's
///   `internal/wclayer/wclayer.go` Go binding; the underlying C signature
///   is `ULONG count`, and both bindings place a correctly-extended 4-byte
///   value in the stack slot.
///
/// # Errors
///
/// Returns an [`io::Error`] if `CreateSandboxLayer` returns a non-success
/// HRESULT or if the descriptor `id` field cannot be parsed as a GUID.
pub fn create_sandbox_layer(layer_path: &Path, parent_chain: &LayerChain) -> io::Result<()> {
    windows::core::link!(
        "vmcompute.dll" "system" fn CreateSandboxLayer(
            info: *const WcDriverInfo,
            id: PCWSTR,
            parent: usize,
            descriptors: *const WcLayerDescriptor,
            descriptor_count: usize,
        ) -> windows::core::HRESULT
    );

    let info = driver_info();
    let layer_path_w = HSTRING::from(layer_path.as_os_str());
    let (_path_buffers, descriptors) = parent_chain_to_descriptors(parent_chain)?;

    // SAFETY: `info` borrows `EMPTY_HOME_DIR` (static); `layer_path_w` is a
    // live `HSTRING`; `descriptors` and the wide-string buffers it points
    // into (`_path_buffers`) outlive the call. `CreateSandboxLayer` only
    // reads through these pointers and returns an HRESULT.
    let hr = unsafe {
        CreateSandboxLayer(
            &info,
            PCWSTR::from_raw(layer_path_w.as_ptr()),
            0,
            descriptors.as_ptr(),
            descriptors.len(),
        )
    };
    hr.ok()
        .map_err(|e| io::Error::other(format!("CreateSandboxLayer: {e}")))?;
    Ok(())
}

/// Create a hardlink at `link_abs` pointing at `target_abs`, ensuring the
/// link's parent directory exists first. Mirrors the shape of hcsshim's
/// `safefile.LinkRelative(target, targetRoot, name, root)` — both endpoints
/// must already be absolute paths under `dest_root` (i.e. the imported layer
/// directory) so they remain inside the layer boundary.
///
/// `dest_root` is the imported layer's root; passed in only so the function
/// can record a verification that both endpoints are descendants of it, which
/// would catch a path-resolution bug in the caller before the syscall fires.
///
/// # Errors
///
/// Returns an [`io::Error`] when either endpoint is outside `dest_root`,
/// when the link's parent directory cannot be materialised, or when
/// `std::fs::hard_link` itself fails (most commonly because the target does
/// not exist on disk — which is the precise signal that
/// `wclayer::import_layer` did not produce the expected merged view).
pub fn link_relative(dest_root: &Path, target_abs: &Path, link_abs: &Path) -> io::Result<()> {
    if !target_abs.starts_with(dest_root) {
        return Err(io::Error::other(format!(
            "link target {} is outside layer root {}",
            target_abs.display(),
            dest_root.display(),
        )));
    }
    if !link_abs.starts_with(dest_root) {
        return Err(io::Error::other(format!(
            "link path {} is outside layer root {}",
            link_abs.display(),
            dest_root.display(),
        )));
    }
    if let Some(parent) = link_abs.parent() {
        // Some link paths (Catroot, WinSxS, etc.) blow past MAX_PATH; fall
        // back to the `\\?\`-prefixed dir helper if std fails. Matches the
        // strategy used by `unpacker::create_long_path_dir_all`.
        if std::fs::create_dir_all(parent).is_err() {
            let mut to_create: Vec<&Path> = parent.ancestors().collect();
            to_create.reverse();
            for component in to_create {
                if component.as_os_str().is_empty() || component.is_dir() {
                    continue;
                }
                crate::windows::layer::create_long_path_dir(component)?;
            }
        }
    }
    std::fs::hard_link(target_abs, link_abs)
}

/// Parse a canonical-format GUID string (lowercase 8-4-4-4-12) into a
/// `windows::core::GUID`. Returns an error if `s` is not exactly 36 chars in
/// the expected layout. The `layer_id_for_path` helper produces this format.
fn parse_guid_str(s: &str) -> io::Result<windows::core::GUID> {
    let bytes = s.as_bytes();
    if bytes.len() != 36
        || bytes[8] != b'-'
        || bytes[13] != b'-'
        || bytes[18] != b'-'
        || bytes[23] != b'-'
    {
        return Err(io::Error::other(format!(
            "parse_guid_str: malformed GUID {s:?}"
        )));
    }
    let d1 = u32::from_str_radix(&s[0..8], 16)
        .map_err(|e| io::Error::other(format!("parse_guid_str d1: {e}")))?;
    let d2 = u16::from_str_radix(&s[9..13], 16)
        .map_err(|e| io::Error::other(format!("parse_guid_str d2: {e}")))?;
    let d3 = u16::from_str_radix(&s[14..18], 16)
        .map_err(|e| io::Error::other(format!("parse_guid_str d3: {e}")))?;
    let mut d4 = [0u8; 8];
    d4[0] = u8::from_str_radix(&s[19..21], 16)
        .map_err(|e| io::Error::other(format!("parse_guid_str d4[0]: {e}")))?;
    d4[1] = u8::from_str_radix(&s[21..23], 16)
        .map_err(|e| io::Error::other(format!("parse_guid_str d4[1]: {e}")))?;
    for i in 0..6 {
        let start = 24 + i * 2;
        d4[2 + i] = u8::from_str_radix(&s[start..start + 2], 16)
            .map_err(|e| io::Error::other(format!("parse_guid_str d4[{}]: {e}", 2 + i)))?;
    }
    Ok(windows::core::GUID {
        data1: d1,
        data2: d2,
        data3: d3,
        data4: d4,
    })
}

/// Derive the HCS layer-id for a given on-disk layer path.
///
/// HCS keys parent layers by `NameToGuid(basename(layer_path))`, NOT by any
/// caller-supplied id. The id field in a `LayerData` JSON record MUST be the
/// GUID returned here for HCS to find the parent's backing VHD during
/// `HcsImportLayer` / `CreateSandboxLayer` / `HcsAttachLayerStorageFilter`
/// chain walks. Passing an unrelated UUID yields `ERROR_PATH_NOT_FOUND`
/// (`0x80070003`) the moment HCS tries to resolve a parent.
///
/// Equivalent to hcsshim's `internal/wclayer/layerid.go::LayerID(path)`. Must
/// be called for every layer path that goes into a [`LayerChain`] consumed by
/// HCS.
///
/// Backing FFI: `vmcompute.dll!NameToGuid(name: PCWSTR, guid: *mut GUID)
/// -> HRESULT` — not surfaced by `windows::Win32::System::HostComputeSystem`
/// in `windows-rs 0.62`, so the link is declared inline.
///
/// # Errors
///
/// Returns an [`io::Error`] if the path has no basename or if `NameToGuid`
/// returns a non-success HRESULT.
pub fn layer_id_for_path(layer_path: &Path) -> io::Result<String> {
    let basename = layer_path.file_name().ok_or_else(|| {
        io::Error::other(format!(
            "layer_id_for_path: no basename in {}",
            layer_path.display()
        ))
    })?;
    let name_w = HSTRING::from(basename);
    windows::core::link!(
        "vmcompute.dll" "system" fn NameToGuid(name: PCWSTR, guid: *mut windows::core::GUID) -> windows::core::HRESULT
    );
    let mut guid = windows::core::GUID::zeroed();
    // SAFETY: `name_w` is a live `HSTRING` whose null-terminated UTF-16 buffer
    // outlives the call; `&mut guid` is a valid, exclusively-borrowed out-pointer.
    let hr = unsafe { NameToGuid(PCWSTR::from_raw(name_w.as_ptr()), &mut guid) };
    hr.ok()
        .map_err(|e| io::Error::other(format!("NameToGuid({basename:?}): {e}")))?;
    // Canonical lowercase 8-4-4-4-12 form (matches hcsshim's `LayerID` output).
    Ok(format!(
        "{:08x}-{:04x}-{:04x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
        guid.data1,
        guid.data2,
        guid.data3,
        guid.data4[0],
        guid.data4[1],
        guid.data4[2],
        guid.data4[3],
        guid.data4[4],
        guid.data4[5],
        guid.data4[6],
        guid.data4[7],
    ))
}

/// Retrieve the host mount path of a layer at `layer_path`. The layer must
/// already be activated + prepared ([`activate_layer`] + [`prepare_layer`]).
///
/// Wraps `vmcompute.dll!GetLayerMountPath`. Matches hcsshim's
/// `internal/wclayer/getlayermountpath.go::GetLayerMountPath`: a two-call
/// pattern (first call with `buffer=NULL` to learn the required length,
/// second call to fill an allocated buffer of that length).
///
/// Returns an empty string when the layer has no mount path (e.g. an
/// activated read-only layer that has not been prepared).
///
/// # Errors
///
/// Returns an [`io::Error`] if either call returns a non-success HRESULT.
pub fn get_layer_mount_path(layer_path: &Path) -> io::Result<String> {
    windows::core::link!(
        "vmcompute.dll" "system" fn GetLayerMountPath(
            info: *const WcDriverInfo,
            id: PCWSTR,
            length: *mut usize,
            buffer: *mut u16,
        ) -> windows::core::HRESULT
    );
    let info = driver_info();
    let lp = HSTRING::from(layer_path.as_os_str());

    // Pass 1: discover the required wide-char length (HCS writes it through
    // `length`; `buffer=NULL` signals "size query only", same as hcsshim's
    // first `_getLayerMountPath` call).
    let mut length: usize = 0;
    // SAFETY: `info` borrows `EMPTY_HOME_DIR` (static); `lp` is live; `&mut
    // length` is an exclusively-borrowed out-pointer; the buffer is
    // explicitly null which `GetLayerMountPath` accepts for the size query.
    let hr = unsafe {
        GetLayerMountPath(
            &info,
            PCWSTR::from_raw(lp.as_ptr()),
            &mut length,
            std::ptr::null_mut(),
        )
    };
    hr.ok()
        .map_err(|e| io::Error::other(format!("GetLayerMountPath(size query): {e}")))?;

    if length == 0 {
        return Ok(String::new());
    }

    // Pass 2: allocate `length` u16s and fill them. `length` already includes
    // the null terminator.
    let mut buf = vec![0u16; length];
    // SAFETY: same as pass 1 plus `buf.as_mut_ptr()` is a valid writable
    // pointer to a `length`-element u16 buffer that outlives the call.
    let hr = unsafe {
        GetLayerMountPath(
            &info,
            PCWSTR::from_raw(lp.as_ptr()),
            &mut length,
            buf.as_mut_ptr(),
        )
    };
    hr.ok()
        .map_err(|e| io::Error::other(format!("GetLayerMountPath(fill): {e}")))?;

    // Decode up to the first null (HCS guarantees one within `length`).
    let end = buf.iter().position(|&c| c == 0).unwrap_or(buf.len());
    String::from_utf16(&buf[..end])
        .map_err(|e| io::Error::other(format!("UTF-16 mount path decode: {e}")))
}

/// Grant the per-VM virtual SID (`NT VIRTUAL MACHINE\<vm_id>`) read/write
/// access to `file_path` so a Hyper-V UVM with that VM ID can open the file
/// after `HcsStartComputeSystem`. Without this, the VM's effective identity
/// has no ACL on the host-owned file and `Start` fails with
/// `0x80070005 (Access is denied)` from the synthetic storage device's
/// `PowerOnCold` step.
///
/// hcsshim calls this on every host file projected into a UVM: the sandbox
/// VHDX, every read-only parent layer dir surfaced as a VSMB share, the
/// `UtilityVM\Files` boot tree, and the original `SystemTemplate.vhdx` (used
/// as the SOURCE of the sandbox copy — even though we copy it, hcsshim's
/// convention is to grant on the source too in case any code path re-opens).
///
/// `vm_id` is the runtime GUID we pass to `HcsCreateComputeSystem` as the
/// system id. `file_path` must be an absolute Windows path.
///
/// Backing FFI: `vmcompute.dll!GrantVmAccess(VmId: PCWSTR, FilePath: PCWSTR)
/// -> HRESULT`. Not exposed by `windows-rs 0.62`, so we declare the link inline.
/// Mirrors hcsshim `internal/wclayer/grantvmaccess.go::GrantVmAccess`.
///
/// # Errors
///
/// Returns an [`io::Error`] if the syscall returns a non-success HRESULT.
pub fn grant_vm_access(vm_id: windows::core::GUID, file_path: &Path) -> io::Result<()> {
    let vm_id_str = format!(
        "{:08x}-{:04x}-{:04x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
        vm_id.data1,
        vm_id.data2,
        vm_id.data3,
        vm_id.data4[0],
        vm_id.data4[1],
        vm_id.data4[2],
        vm_id.data4[3],
        vm_id.data4[4],
        vm_id.data4[5],
        vm_id.data4[6],
        vm_id.data4[7],
    );
    let vm_id_h = windows::core::HSTRING::from(vm_id_str.as_str());
    let path_h = path_to_hstring(file_path);
    windows::core::link!(
        "vmcompute.dll" "system" fn GrantVmAccess(
            vm_id: windows::core::PCWSTR,
            file_path: windows::core::PCWSTR,
        ) -> windows::core::HRESULT
    );
    tracing::debug!(
        target: "zlayer_agent::wclayer",
        vm_id = %vm_id_str,
        path = %file_path.display(),
        "calling vmcompute.dll!GrantVmAccess",
    );
    // SAFETY: both HSTRINGs hold live null-terminated UTF-16 buffers that
    // outlive the call. The FFI only reads the two PCWSTRs and returns an
    // HRESULT; no out-pointers, no shared resources.
    let hr = unsafe {
        GrantVmAccess(
            windows::core::PCWSTR::from_raw(vm_id_h.as_ptr()),
            windows::core::PCWSTR::from_raw(path_h.as_ptr()),
        )
    };
    tracing::debug!(
        target: "zlayer_agent::wclayer",
        vm_id = %vm_id_str,
        path = %file_path.display(),
        hr = ?hr,
        "vmcompute.dll!GrantVmAccess returned",
    );
    hr.ok().map_err(|e| {
        io::Error::other(format!(
            "GrantVmAccess({vm_id_str}, {}): {e}",
            file_path.display()
        ))
    })?;
    Ok(())
}

// ---------------------------------------------------------------------------
// Tests (no HCS calls)
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn empty_chain_serializes_without_layers_key() {
        let chain = LayerChain::default();
        let json = chain.to_layer_data_json().expect("serialize");
        // Must always carry the schema tag, and must omit the empty array.
        assert!(json.contains("\"SchemaVersion\""));
        assert!(!json.contains("\"Layers\""));
    }

    #[test]
    fn non_empty_chain_serializes_parents_in_order() {
        let chain = LayerChain::new(vec![
            Layer {
                id: "1111".into(),
                path: r"C:\layers\a".into(),
            },
            Layer {
                id: "2222".into(),
                path: r"C:\layers\b".into(),
            },
        ]);
        let json = chain.to_layer_data_json().expect("serialize");
        assert!(json.contains("\"Layers\""));
        // Child-to-parent ordering preserved.
        let a = json.find("1111").expect("first id present");
        let b = json.find("2222").expect("second id present");
        assert!(a < b, "child layer must appear before parent");
        // PascalCase field names (matches HCS expectations).
        assert!(json.contains("\"Id\""));
        assert!(json.contains("\"Path\""));
    }
}