Skip to main content

acyclic_fs/native_mount/
materialize.rs

1//! Explicit native materialization of one authenticated checkout.
2
3#[cfg(any(target_os = "macos", windows))]
4use crate::ObjectId;
5use crate::kernel::{
6    ExtentKind, FileKind, FileMetadata, FilePayload, LogicalName, NameEncoding, NamespacePath,
7};
8use crate::native_host::{HostDirectory, HostRoot};
9use crate::{
10    AsyncAuthorityStore, AsyncObjectStore, ByteRange, CancellationToken, Checkout, FileId,
11    OperationFailure, OperationReceipt, PinnedReader, ResolvedFileRangeReadRequest, WorkBudget,
12    WorkCounters, WorkError,
13};
14use bytes::Bytes;
15use std::collections::HashMap;
16#[cfg(unix)]
17use std::ffi::OsStr;
18use std::ffi::OsString;
19use std::fs::File;
20use std::path::{Path, PathBuf};
21use std::sync::Arc;
22use std::sync::atomic::{AtomicU64, Ordering};
23use thiserror::Error;
24
25static RESTORE_SEQUENCE: AtomicU64 = AtomicU64::new(0);
26
27/// Exact hard bounds and destination for one materialization.
28#[derive(Clone, Debug, Eq, PartialEq)]
29pub struct MaterializeOptions {
30    /// Existing empty directory owned by the caller.
31    pub destination: PathBuf,
32    /// Maximum directory entries fetched per authenticated page.
33    pub maximum_directory_entries: u32,
34    /// Maximum sparse spans fetched per authenticated range plan.
35    pub maximum_extent_spans: u32,
36    /// Maximum bytes read or zero-filled in one allocation.
37    pub transfer_bytes: u64,
38}
39
40/// Both outputs preserve canonical metadata in the SDK. Host-generated Unix
41/// timestamps are view-local and are not required to match the physical view.
42#[derive(Clone, Copy, Debug, Eq, PartialEq)]
43pub(crate) enum MaterializeMode {
44    DurableOutput,
45    ReconstructibleView,
46}
47
48impl MaterializeMode {
49    fn host_metadata(metadata: FileMetadata) -> FileMetadata {
50        #[cfg(unix)]
51        let mut metadata = metadata;
52        #[cfg(unix)]
53        {
54            metadata.changed_ns = crate::kernel::MetadataField::Unavailable;
55        }
56        #[cfg(target_os = "linux")]
57        {
58            metadata.created_ns = crate::kernel::MetadataField::Unavailable;
59        }
60        metadata
61    }
62}
63
64impl MaterializeOptions {
65    /// Creates the canonical bounded native materialization policy.
66    #[must_use]
67    pub fn native(destination: impl Into<PathBuf>) -> Self {
68        Self {
69            destination: destination.into(),
70            maximum_directory_entries: 1_024,
71            maximum_extent_spans: 65_536,
72            transfer_bytes: 8 * 1024 * 1024,
73        }
74    }
75}
76
77/// Exact result and host-side movement facts.
78#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
79pub struct MaterializationReceipt {
80    /// Materialized regular-file or hard-link bindings.
81    pub files: u64,
82    /// Materialized directory bindings, excluding the supplied root.
83    pub directories: u64,
84    /// Materialized symbolic-link bindings.
85    pub symbolic_links: u64,
86    /// Materialized FIFO, socket, character-device, or block-device bindings.
87    pub special_files: u64,
88    /// Logical regular-file bytes represented at the destination.
89    pub logical_file_bytes: u64,
90    /// Bytes physically written for content and allocated-zero spans.
91    pub written_bytes: u64,
92    /// Complete canonical-engine and host-movement work.
93    pub work: WorkCounters,
94}
95
96/// How a materialized host path replaces its current destination.
97#[derive(Clone, Copy, Debug, Eq, PartialEq)]
98pub enum HostPathReplacement {
99    /// Exchange an existing entry atomically on the same volume.
100    Atomic,
101    /// Route ordinary renames through a live kernel mount.
102    LiveMount,
103}
104
105/// Exact result of restoring one authenticated path into a host tree.
106#[derive(Clone, Copy, Debug, Eq, PartialEq)]
107pub enum HostPathRestore {
108    /// The authenticated entry was materialized and published.
109    Restored,
110    /// The authenticated path was absent and the host entry was removed.
111    Removed,
112}
113
114/// Fail-closed explicit materialization errors.
115#[derive(Debug, Error)]
116pub enum MaterializeError {
117    /// Destination is absent, non-directory, a symlink, or non-empty.
118    #[error("materialization destination must be an existing empty real directory")]
119    InvalidDestination,
120    /// A canonical name cannot be represented exactly on this host.
121    #[error("canonical name cannot be represented exactly on this host")]
122    UnrepresentableName,
123    /// The requested authenticated path is absent from the checkout.
124    #[error("requested path is absent from the checkout")]
125    MissingPath,
126    /// Host path is empty, absolute, escapes the root, or cannot be encoded.
127    #[error("requested host path is not a valid relative path")]
128    InvalidPath,
129    /// This host adapter cannot recreate the authenticated kind exactly.
130    #[error("file kind {0:?} cannot be materialized exactly on this host")]
131    UnsupportedKind(FileKind),
132    /// This host adapter cannot reproduce an authenticated metadata field.
133    #[error("metadata field {0} cannot be materialized exactly on this host")]
134    UnsupportedMetadata(&'static str),
135    /// Supplied operation bounds are zero or exceed native addressability.
136    #[error("materialization options are invalid")]
137    InvalidOptions,
138    /// Selected paths overlap, so pathwise staging would be order-dependent.
139    #[error("materialization paths must be unique and non-overlapping")]
140    OverlappingPaths,
141    /// Canonical engine operation failed.
142    #[error("filesystem engine failed: {0}")]
143    Engine(String),
144    /// Host filesystem operation failed.
145    #[error("host filesystem operation failed: {0}")]
146    Io(#[from] std::io::Error),
147    /// Exact work overflowed or exceeded the admitted budget.
148    #[error(transparent)]
149    Work(#[from] WorkError),
150}
151
152/// Explicitly materializes one checkout without changing or publishing it.
153///
154/// Sparse files are pre-sized and only authenticated content or allocated-zero
155/// spans are written. Hole spans issue no body I/O. Hard links are recreated by
156/// stable file identity, and directory enumeration remains bounded and paged.
157/// This function is only invoked by explicit materialization requests.
158///
159/// # Errors
160///
161/// Fails before traversal for an invalid destination or zero bounds. During
162/// traversal it fails closed on unrepresentable names, unsupported special
163/// kinds, canonical engine errors, host I/O, cancellation, or work exhaustion.
164pub async fn materialize_checkout<A: AsyncAuthorityStore, O: AsyncObjectStore>(
165    checkout: &mut Checkout<A, O>,
166    options: &MaterializeOptions,
167    budget: WorkBudget,
168    cancellation: &CancellationToken,
169) -> Result<OperationReceipt<MaterializationReceipt>, OperationFailure<MaterializeError>> {
170    materialize_checkout_with_mode(
171        checkout,
172        options,
173        budget,
174        cancellation,
175        MaterializeMode::DurableOutput,
176    )
177    .await
178}
179
180pub(crate) async fn materialize_checkout_with_mode<A: AsyncAuthorityStore, O: AsyncObjectStore>(
181    checkout: &mut Checkout<A, O>,
182    options: &MaterializeOptions,
183    budget: WorkBudget,
184    cancellation: &CancellationToken,
185    mode: MaterializeMode,
186) -> Result<OperationReceipt<MaterializationReceipt>, OperationFailure<MaterializeError>> {
187    let root =
188        NamespacePath::new(Vec::new(), checkout.volume_config().limits).map_err(|error| {
189            OperationFailure::before_work(MaterializeError::Engine(error.to_string()))
190        })?;
191    materialize_checkout_paths_with_mode(
192        checkout,
193        std::slice::from_ref(&root),
194        options,
195        budget,
196        cancellation,
197        mode,
198    )
199    .await
200}
201
202/// Materializes one authenticated path into an empty destination directory.
203///
204/// The destination receives the same relative logical path below its root;
205/// for example, `src/main.rs` is written to
206/// `options.destination/src/main.rs`.  This lets callers stage a path in a
207/// sibling directory and perform their own atomic host-side exchange without
208/// reimplementing authenticated lookup, sparse-file handling, hard links, or
209/// platform name conversion.  Parent directories created for the selected
210/// path are not included in the receipt; the selected node and its subtree
211/// are. Hard links within that subtree retain their shared identity; a link
212/// to a node outside the selected subtree becomes an independent copy.
213#[allow(clippy::too_many_lines)]
214pub async fn materialize_checkout_path<A: AsyncAuthorityStore, O: AsyncObjectStore>(
215    checkout: &mut Checkout<A, O>,
216    path: &NamespacePath,
217    options: &MaterializeOptions,
218    budget: WorkBudget,
219    cancellation: &CancellationToken,
220) -> Result<OperationReceipt<MaterializationReceipt>, OperationFailure<MaterializeError>> {
221    materialize_checkout_paths(
222        checkout,
223        std::slice::from_ref(path),
224        options,
225        budget,
226        cancellation,
227    )
228    .await
229}
230
231/// Materializes multiple non-overlapping authenticated paths in one traversal.
232///
233/// All selected paths share one destination capability, cumulative work
234/// receipt, and file-identity table, preserving hard links across siblings.
235#[allow(clippy::too_many_lines)]
236pub async fn materialize_checkout_paths<A: AsyncAuthorityStore, O: AsyncObjectStore>(
237    checkout: &mut Checkout<A, O>,
238    paths: &[NamespacePath],
239    options: &MaterializeOptions,
240    budget: WorkBudget,
241    cancellation: &CancellationToken,
242) -> Result<OperationReceipt<MaterializationReceipt>, OperationFailure<MaterializeError>> {
243    materialize_checkout_paths_with_mode(
244        checkout,
245        paths,
246        options,
247        budget,
248        cancellation,
249        MaterializeMode::DurableOutput,
250    )
251    .await
252}
253
254#[allow(clippy::too_many_lines)]
255pub(crate) async fn materialize_checkout_paths_with_mode<
256    A: AsyncAuthorityStore,
257    O: AsyncObjectStore,
258>(
259    checkout: &mut Checkout<A, O>,
260    paths: &[NamespacePath],
261    options: &MaterializeOptions,
262    budget: WorkBudget,
263    cancellation: &CancellationToken,
264    mode: MaterializeMode,
265) -> Result<OperationReceipt<MaterializationReceipt>, OperationFailure<MaterializeError>> {
266    let host_root = Arc::new(validate_options(options).map_err(OperationFailure::before_work)?);
267    let reader = checkout.pinned_reader().map_err(|error| {
268        OperationFailure::before_work(MaterializeError::Engine(error.to_string()))
269    })?;
270    if paths.is_empty()
271        || (paths.len() != 1 && paths.iter().any(|path| path.components().is_empty()))
272    {
273        return Err(OperationFailure::before_work(
274            MaterializeError::InvalidOptions,
275        ));
276    }
277    let mut selected_paths = paths.to_vec();
278    selected_paths.sort_unstable();
279    if selected_paths
280        .windows(2)
281        .any(|pair| matches!(pair, [ancestor, path] if path.is_within(ancestor)))
282    {
283        return Err(OperationFailure::before_work(
284            MaterializeError::OverlappingPaths,
285        ));
286    }
287    let mut receipt = MaterializationReceipt::default();
288    let mut known_files = HashMap::<FileId, PathBuf>::new();
289    #[cfg(any(target_os = "macos", windows))]
290    let mut known_payloads = HashMap::<(u64, ObjectId, ObjectId), PathBuf>::new();
291    #[cfg(any(target_os = "macos", windows))]
292    let clone_available = crate::probe_native_storage_capabilities(&options.destination)
293        .is_ok_and(|capabilities| capabilities.block_cloning);
294    let mut pending = Vec::new();
295    let mut deferred_directory_metadata = Vec::new();
296    for path in &selected_paths {
297        if path.components().is_empty() {
298            pending.push((path.clone(), PathBuf::new()));
299            deferred_directory_metadata.push((path.clone(), PathBuf::new()));
300            continue;
301        }
302        cancellation.check().map_err(|error| {
303            OperationFailure::new(MaterializeError::Engine(error.to_string()), receipt.work)
304        })?;
305        let remaining = receipt
306            .work
307            .remaining(budget)
308            .map_err(|error| OperationFailure::new(MaterializeError::Work(error), receipt.work))?;
309        let lookup = checkout
310            .lookup_no_follow(path, remaining, cancellation)
311            .await
312            .map_err(|failure| map_engine_failure(failure, receipt.work))?;
313        receipt.work = add_work(receipt.work, lookup.work)?;
314        let record = lookup
315            .value
316            .record
317            .ok_or_else(|| OperationFailure::new(MaterializeError::MissingPath, receipt.work))?;
318        let mut host_path = PathBuf::new();
319        for component in path.components() {
320            host_path.push(
321                host_name(component).map_err(|error| OperationFailure::new(error, receipt.work))?,
322            );
323        }
324        if let Some(parent) = host_path
325            .parent()
326            .filter(|parent| !parent.as_os_str().is_empty())
327        {
328            host_root
329                .create_dir_all_held(parent)
330                .map_err(|error| OperationFailure::new(error.into(), receipt.work))?;
331        }
332        if record.kind == FileKind::Directory {
333            deferred_directory_metadata.push((path.clone(), host_path.clone()));
334        }
335        materialize_entry(
336            &reader,
337            &host_root,
338            path,
339            &host_path,
340            record.file_id,
341            record.kind,
342            record.payload,
343            #[cfg(any(target_os = "macos", windows))]
344            record.metadata,
345            options,
346            budget,
347            cancellation,
348            &mut known_files,
349            #[cfg(any(target_os = "macos", windows))]
350            &mut known_payloads,
351            #[cfg(any(target_os = "macos", windows))]
352            clone_available,
353            &mut pending,
354            &mut receipt,
355            mode,
356        )
357        .await?;
358    }
359
360    while let Some((directory, host_directory)) = pending.pop() {
361        cancellation.check().map_err(|error| {
362            OperationFailure::new(MaterializeError::Engine(error.to_string()), receipt.work)
363        })?;
364        let mut after = None;
365        loop {
366            cancellation.check().map_err(|error| {
367                OperationFailure::new(MaterializeError::Engine(error.to_string()), receipt.work)
368            })?;
369            let remaining = receipt.work.remaining(budget).map_err(|error| {
370                OperationFailure::new(MaterializeError::Work(error), receipt.work)
371            })?;
372            let page = checkout
373                .list_directory_records(
374                    &directory,
375                    after.as_ref(),
376                    options.maximum_directory_entries,
377                    remaining,
378                    cancellation,
379                )
380                .await
381                .map_err(|failure| map_engine_failure(failure, receipt.work))?;
382            receipt.work = add_work(receipt.work, page.work)?;
383            if page.value.has_more && page.value.entries.is_empty() {
384                return Err(OperationFailure::new(
385                    MaterializeError::Engine("directory pagination made no progress".to_owned()),
386                    receipt.work,
387                ));
388            }
389            for entry in page.value.entries {
390                let child = append_path(
391                    &directory,
392                    entry.name.clone(),
393                    checkout.volume_config().limits,
394                )
395                .map_err(|error| OperationFailure::new(error, receipt.work))?;
396                let child_host = host_directory.join(
397                    host_name(&entry.name)
398                        .map_err(|error| OperationFailure::new(error, receipt.work))?,
399                );
400                if entry.record.kind == FileKind::Directory {
401                    deferred_directory_metadata.push((child.clone(), child_host.clone()));
402                }
403                materialize_entry(
404                    &reader,
405                    &host_root,
406                    &child,
407                    &child_host,
408                    entry.record.file_id,
409                    entry.record.kind,
410                    entry.record.payload,
411                    #[cfg(any(target_os = "macos", windows))]
412                    entry.record.metadata,
413                    options,
414                    budget,
415                    cancellation,
416                    &mut known_files,
417                    #[cfg(any(target_os = "macos", windows))]
418                    &mut known_payloads,
419                    #[cfg(any(target_os = "macos", windows))]
420                    clone_available,
421                    &mut pending,
422                    &mut receipt,
423                    mode,
424                )
425                .await?;
426                after = Some(entry.name);
427            }
428            if !page.value.has_more {
429                break;
430            }
431        }
432    }
433    for (directory, host_directory) in deferred_directory_metadata.into_iter().rev() {
434        apply_metadata(
435            &reader,
436            &host_root,
437            &directory,
438            &host_directory,
439            budget,
440            cancellation,
441            &mut receipt,
442        )
443        .await?;
444    }
445    Ok(OperationReceipt {
446        value: receipt,
447        work: receipt.work,
448    })
449}
450
451/// Materializes a host-relative path using the checkout's configured name
452/// profile. Native consumers do not need to construct logical names or know
453/// whether this platform stores them as bytes or UTF-16 units.
454pub async fn materialize_checkout_host_path<A: AsyncAuthorityStore, O: AsyncObjectStore>(
455    checkout: &mut Checkout<A, O>,
456    relative: &Path,
457    options: &MaterializeOptions,
458    budget: WorkBudget,
459    cancellation: &CancellationToken,
460) -> Result<OperationReceipt<MaterializationReceipt>, OperationFailure<MaterializeError>> {
461    let limits = checkout.volume_config().limits;
462    let profile = checkout.volume_config().profile;
463    let mut names = Vec::new();
464    for component in relative.components() {
465        let std::path::Component::Normal(name) = component else {
466            return Err(OperationFailure::before_work(MaterializeError::InvalidPath));
467        };
468        let (encoding, bytes) =
469            crate::native_name::host_name_bytes(name, profile, limits.maximum_component_bytes)
470                .map_err(|_| OperationFailure::before_work(MaterializeError::InvalidPath))?;
471        let logical = LogicalName::new(encoding, bytes, limits.maximum_component_bytes)
472            .map_err(|_| OperationFailure::before_work(MaterializeError::InvalidPath))?;
473        names.push(logical);
474    }
475    if names.is_empty() {
476        return Err(OperationFailure::before_work(MaterializeError::InvalidPath));
477    }
478    let path = NamespacePath::new(names, limits)
479        .map_err(|_| OperationFailure::before_work(MaterializeError::InvalidPath))?;
480    materialize_checkout_path(checkout, &path, options, budget, cancellation).await
481}
482
483/// Restores one authenticated host-relative path without touching siblings.
484///
485/// Materialization occurs in a private same-volume stage. Existing entries are
486/// exchanged atomically or replaced through ordinary mount-visible renames.
487/// An absent authenticated path removes the current host entry.
488pub async fn restore_checkout_host_path<A: AsyncAuthorityStore, O: AsyncObjectStore>(
489    checkout: &mut Checkout<A, O>,
490    relative: &Path,
491    replacement: HostPathReplacement,
492    options: &MaterializeOptions,
493    budget: WorkBudget,
494    cancellation: &CancellationToken,
495) -> Result<OperationReceipt<HostPathRestore>, OperationFailure<MaterializeError>> {
496    validate_host_relative(relative).map_err(OperationFailure::before_work)?;
497    validate_bounds(options).map_err(OperationFailure::before_work)?;
498    if cancellation.is_cancelled() {
499        return Err(OperationFailure::before_work(MaterializeError::Engine(
500            "materialization cancelled".into(),
501        )));
502    }
503    let destination_root = options.destination.clone();
504    let relative = relative.to_path_buf();
505    let (destination, stage_root, _restore_lock) = tokio::task::spawn_blocking({
506        let destination_root = destination_root.clone();
507        let relative = relative.clone();
508        move || prepare_restore(&destination_root, &relative, replacement)
509    })
510    .await
511    .map_err(|error| OperationFailure::before_work(MaterializeError::Engine(error.to_string())))?
512    .map_err(OperationFailure::before_work)?;
513    let staged = stage_root.join(&relative);
514    let materialize_options = MaterializeOptions {
515        destination: stage_root.clone(),
516        maximum_directory_entries: options.maximum_directory_entries,
517        maximum_extent_spans: options.maximum_extent_spans,
518        transfer_bytes: options.transfer_bytes,
519    };
520    let materialized = materialize_checkout_host_path(
521        checkout,
522        &relative,
523        &materialize_options,
524        budget,
525        cancellation,
526    )
527    .await;
528    let receipt = match materialized {
529        Ok(receipt) => receipt,
530        Err(failure) if matches!(failure.error, MaterializeError::MissingPath) => {
531            let work = *failure.work;
532            tokio::task::spawn_blocking(move || {
533                let parent =
534                    held_parent(&destination_root, &relative).map_err(materialize_io_error)?;
535                let name = relative
536                    .file_name()
537                    .ok_or_else(|| std::io::Error::other("restore path has no leaf"))?;
538                remove_any(&stage_root)?;
539                remove_restored_path(&parent, Path::new(name), &destination_root, &relative)
540            })
541            .await
542            .map_err(|error| {
543                OperationFailure::new(MaterializeError::Engine(error.to_string()), work)
544            })?
545            .map_err(|error| OperationFailure::new(MaterializeError::Io(error), work))?;
546            return Ok(OperationReceipt {
547                value: HostPathRestore::Removed,
548                work,
549            });
550        }
551        Err(failure) => {
552            let _ = tokio::task::spawn_blocking(move || remove_any(&stage_root)).await;
553            return Err(failure);
554        }
555    };
556    tokio::task::spawn_blocking(move || {
557        publish_restore(
558            &destination_root,
559            &relative,
560            &destination,
561            &staged,
562            &stage_root,
563            replacement,
564        )
565    })
566    .await
567    .map_err(|error| {
568        OperationFailure::new(MaterializeError::Engine(error.to_string()), receipt.work)
569    })?
570    .map_err(|error| OperationFailure::new(error, receipt.work))?;
571    Ok(OperationReceipt {
572        value: HostPathRestore::Restored,
573        work: receipt.work,
574    })
575}
576
577fn materialize_io_error(error: MaterializeError) -> std::io::Error {
578    match error {
579        MaterializeError::Io(error) => error,
580        error => std::io::Error::other(error),
581    }
582}
583
584fn prepare_restore(
585    destination_root: &Path,
586    relative: &Path,
587    replacement: HostPathReplacement,
588) -> Result<(PathBuf, PathBuf, File), MaterializeError> {
589    let destination = destination_root.join(relative);
590    let destination_parent = held_parent(destination_root, relative)?;
591    let destination_name = relative.file_name().ok_or(MaterializeError::InvalidPath)?;
592    let restore_lock = acquire_restore_lock(relative, &destination, true)?;
593    cleanup_removed_restore(&destination_parent, relative, &destination)?;
594    match replacement {
595        HostPathReplacement::Atomic => {
596            crate::native_exchange::recover_native_entry_exchange(&destination)
597                .map_err(|error| MaterializeError::Engine(error.to_string()))?;
598        }
599        HostPathReplacement::LiveMount => recover_live_mount_replacement(
600            &destination_parent,
601            Path::new(destination_name),
602            relative,
603            &destination,
604        )?,
605    }
606    let stage_parent = match replacement {
607        HostPathReplacement::Atomic => destination_root
608            .parent()
609            .ok_or(MaterializeError::InvalidPath)?
610            .to_path_buf(),
611        HostPathReplacement::LiveMount => destination
612            .parent()
613            .ok_or(MaterializeError::InvalidPath)?
614            .to_path_buf(),
615    };
616    Ok((
617        destination,
618        create_restore_stage(&stage_parent)?,
619        restore_lock,
620    ))
621}
622
623fn publish_restore(
624    destination_root: &Path,
625    relative: &Path,
626    destination: &Path,
627    staged: &Path,
628    stage_root: &Path,
629    replacement: HostPathReplacement,
630) -> Result<(), MaterializeError> {
631    let destination_parent = held_parent(destination_root, relative)?;
632    let destination_name = relative.file_name().ok_or(MaterializeError::InvalidPath)?;
633    let stage_parent = held_parent(stage_root, relative)?;
634    let staged_name = relative.file_name().ok_or(MaterializeError::InvalidPath)?;
635    match destination_parent.symlink_metadata(Path::new(destination_name)) {
636        Ok(_) => match replacement {
637            HostPathReplacement::Atomic => crate::exchange_native_entries(destination, staged)
638                .map_err(|error| MaterializeError::Engine(error.to_string()))?,
639            HostPathReplacement::LiveMount => replace_live_mount(
640                &stage_parent,
641                Path::new(staged_name),
642                &destination_parent,
643                Path::new(destination_name),
644                relative,
645                destination,
646                staged,
647            )?,
648        },
649        Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
650            stage_parent.rename_to(
651                Path::new(staged_name),
652                &destination_parent,
653                Path::new(destination_name),
654            )?;
655            sync_restore_parent(destination)?;
656            sync_restore_parent(staged)?;
657        }
658        Err(error) => return Err(error.into()),
659    }
660    stage_parent.close();
661    drop(destination_parent);
662    remove_any(stage_root)?;
663    Ok(())
664}
665
666fn validate_host_relative(relative: &Path) -> Result<(), MaterializeError> {
667    let mut any = false;
668    for component in relative.components() {
669        let std::path::Component::Normal(_) = component else {
670            return Err(MaterializeError::InvalidPath);
671        };
672        any = true;
673    }
674    if any {
675        Ok(())
676    } else {
677        Err(MaterializeError::InvalidPath)
678    }
679}
680
681fn held_parent(root: &Path, relative: &Path) -> Result<HostDirectory, MaterializeError> {
682    let root = HostRoot::open(root).map_err(|_| MaterializeError::InvalidDestination)?;
683    root.create_dir_all_held(relative.parent().unwrap_or_else(|| Path::new("")))
684        .map_err(|_| MaterializeError::InvalidDestination)
685}
686
687fn create_restore_stage(parent: &Path) -> Result<PathBuf, MaterializeError> {
688    for _ in 0..16 {
689        let sequence = RESTORE_SEQUENCE.fetch_add(1, Ordering::Relaxed);
690        let stage = parent.join(format!(
691            ".acyclic-restore-{}-{sequence}",
692            std::process::id()
693        ));
694        match std::fs::create_dir(&stage) {
695            Ok(()) => return Ok(stage),
696            Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {}
697            Err(error) => return Err(error.into()),
698        }
699    }
700    Err(MaterializeError::Io(std::io::Error::new(
701        std::io::ErrorKind::AlreadyExists,
702        "restore stage names are exhausted",
703    )))
704}
705
706fn restore_artifact_name(prefix: &str, relative: &Path) -> PathBuf {
707    let mut hasher = blake3::Hasher::new();
708    #[cfg(unix)]
709    {
710        use std::os::unix::ffi::OsStrExt as _;
711        hasher.update(relative.as_os_str().as_bytes());
712    }
713    #[cfg(windows)]
714    {
715        use std::os::windows::ffi::OsStrExt as _;
716        for unit in relative.as_os_str().encode_wide() {
717            hasher.update(&unit.to_le_bytes());
718        }
719    }
720    #[cfg(not(any(unix, windows)))]
721    hasher.update(relative.to_string_lossy().as_bytes());
722    PathBuf::from(format!("{prefix}{}", hasher.finalize().to_hex()))
723}
724
725fn acquire_restore_lock(
726    relative: &Path,
727    destination: &Path,
728    blocking: bool,
729) -> Result<File, MaterializeError> {
730    let path = restore_witness_path(".acyclic-restore-lock-", relative, destination);
731    let file = std::fs::OpenOptions::new()
732        .create(true)
733        .truncate(false)
734        .read(true)
735        .write(true)
736        .open(&path)?;
737    let metadata = std::fs::symlink_metadata(&path)?;
738    if metadata.file_type().is_symlink() || !file.metadata().is_ok_and(|value| value.is_file()) {
739        return Err(MaterializeError::InvalidDestination);
740    }
741    if blocking {
742        fs2::FileExt::lock_exclusive(&file)?;
743    } else {
744        fs2::FileExt::try_lock_exclusive(&file)?;
745    }
746    Ok(file)
747}
748
749fn sync_restore_parent(destination: &Path) -> Result<(), MaterializeError> {
750    let parent = destination.parent().ok_or(MaterializeError::InvalidPath)?;
751    acyclic_native_runtime::sync_parent(parent, acyclic_native_runtime::Durability::Full)
752        .map_err(Into::into)
753}
754
755const REMOVE_RESTORE_WITNESS: &[u8] = b"acyclic-remove-restore-v1\n";
756const LIVE_RESTORE_WITNESS: &[u8] = b"acyclic-live-restore-v1\n";
757
758fn restore_witness_path(prefix: &str, relative: &Path, destination: &Path) -> PathBuf {
759    destination
760        .parent()
761        .unwrap_or_else(|| Path::new(""))
762        .join(restore_artifact_name(prefix, relative))
763}
764
765fn create_restore_witness(path: &Path, contents: &[u8]) -> std::io::Result<()> {
766    use std::io::Write as _;
767
768    let mut file = std::fs::OpenOptions::new()
769        .create_new(true)
770        .write(true)
771        .open(path)?;
772    file.write_all(contents)?;
773    file.sync_all()?;
774    acyclic_native_runtime::sync_parent(
775        path.parent()
776            .ok_or_else(|| std::io::Error::other("restore witness has no parent"))?,
777        acyclic_native_runtime::Durability::Full,
778    )
779}
780
781fn validate_restore_witness(path: &Path, expected: &[u8]) -> std::io::Result<bool> {
782    match std::fs::read(path) {
783        Ok(contents) if contents == expected => Ok(true),
784        Ok(_) => Err(std::io::Error::new(
785            std::io::ErrorKind::InvalidData,
786            "restore artifact ownership witness is invalid",
787        )),
788        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(false),
789        Err(error) => Err(error),
790    }
791}
792
793fn remove_restore_witness(path: &Path) -> std::io::Result<()> {
794    match std::fs::remove_file(path) {
795        Ok(()) => acyclic_native_runtime::sync_parent(
796            path.parent()
797                .ok_or_else(|| std::io::Error::other("restore witness has no parent"))?,
798            acyclic_native_runtime::Durability::Full,
799        ),
800        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
801        Err(error) => Err(error),
802    }
803}
804
805fn cleanup_removed_restore(
806    destination_parent: &HostDirectory,
807    relative: &Path,
808    destination: &Path,
809) -> Result<(), MaterializeError> {
810    let removed = restore_artifact_name(".acyclic-restore-removed-", relative);
811    let witness = restore_witness_path(".acyclic-restore-remove-witness-", relative, destination);
812    match destination_parent.symlink_metadata(&removed) {
813        Ok(_) => {
814            if !validate_restore_witness(&witness, REMOVE_RESTORE_WITNESS)? {
815                return Err(MaterializeError::Engine(
816                    "unowned restore-removal artifact collision".into(),
817                ));
818            }
819            destination_parent.remove(&removed)?;
820            sync_restore_parent(destination)?;
821            remove_restore_witness(&witness)?;
822        }
823        Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
824            if validate_restore_witness(&witness, REMOVE_RESTORE_WITNESS)? {
825                remove_restore_witness(&witness)?;
826            }
827        }
828        Err(error) => return Err(error.into()),
829    }
830    Ok(())
831}
832
833fn remove_restored_path(
834    destination_parent: &HostDirectory,
835    destination_name: &Path,
836    destination_root: &Path,
837    relative: &Path,
838) -> std::io::Result<()> {
839    let destination = destination_root.join(relative);
840    let removed = restore_artifact_name(".acyclic-restore-removed-", relative);
841    let witness = restore_witness_path(".acyclic-restore-remove-witness-", relative, &destination);
842    create_restore_witness(&witness, REMOVE_RESTORE_WITNESS)?;
843    match destination_parent.symlink_metadata(destination_name) {
844        Ok(_) => {
845            destination_parent.rename_to(destination_name, destination_parent, &removed)?;
846            acyclic_native_runtime::sync_parent(
847                destination.parent().ok_or_else(|| {
848                    std::io::Error::new(std::io::ErrorKind::InvalidInput, "path has no parent")
849                })?,
850                acyclic_native_runtime::Durability::Full,
851            )?;
852            destination_parent.remove(&removed)?;
853            acyclic_native_runtime::sync_parent(
854                destination.parent().ok_or_else(|| {
855                    std::io::Error::new(std::io::ErrorKind::InvalidInput, "path has no parent")
856                })?,
857                acyclic_native_runtime::Durability::Full,
858            )?;
859            remove_restore_witness(&witness)
860        }
861        Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
862            remove_restore_witness(&witness)
863        }
864        Err(error) => Err(error),
865    }
866}
867
868fn remove_any(path: &Path) -> std::io::Result<()> {
869    match std::fs::symlink_metadata(path) {
870        Ok(metadata) if metadata.is_dir() => std::fs::remove_dir_all(path),
871        Ok(_) => std::fs::remove_file(path),
872        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
873        Err(error) => Err(error),
874    }
875}
876
877fn replace_live_mount(
878    staged_parent: &HostDirectory,
879    staged_name: &Path,
880    destination_parent: &HostDirectory,
881    destination_name: &Path,
882    relative: &Path,
883    destination: &Path,
884    staged: &Path,
885) -> Result<(), MaterializeError> {
886    let backup_name = restore_artifact_name(".acyclic-restore-backup-", relative);
887    recover_live_mount_replacement(destination_parent, destination_name, relative, destination)?;
888    let witness = restore_witness_path(".acyclic-restore-live-witness-", relative, destination);
889    create_restore_witness(&witness, LIVE_RESTORE_WITNESS)?;
890    destination_parent.rename_to(destination_name, destination_parent, &backup_name)?;
891    sync_restore_parent(destination)?;
892    if let Err(error) = staged_parent.rename_to(staged_name, destination_parent, destination_name) {
893        if let Err(rollback) =
894            destination_parent.rename_to(&backup_name, destination_parent, destination_name)
895        {
896            return Err(MaterializeError::Engine(format!(
897                "replacement failed: {error}; displaced entry remains at {} after rollback failed: {rollback}",
898                backup_name.display()
899            )));
900        }
901        sync_restore_parent(destination)?;
902        return Err(error.into());
903    }
904    sync_restore_parent(destination)?;
905    sync_restore_parent(staged)?;
906    destination_parent.remove(&backup_name)?;
907    sync_restore_parent(destination)?;
908    remove_restore_witness(&witness)?;
909    Ok(())
910}
911
912fn recover_live_mount_replacement(
913    destination_parent: &HostDirectory,
914    destination_name: &Path,
915    relative: &Path,
916    destination: &Path,
917) -> Result<(), MaterializeError> {
918    let backup_name = restore_artifact_name(".acyclic-restore-backup-", relative);
919    let witness = restore_witness_path(".acyclic-restore-live-witness-", relative, destination);
920    let backup_exists = match destination_parent.symlink_metadata(&backup_name) {
921        Ok(_) => true,
922        Err(error) if error.kind() == std::io::ErrorKind::NotFound => false,
923        Err(error) => return Err(error.into()),
924    };
925    if !backup_exists {
926        if validate_restore_witness(&witness, LIVE_RESTORE_WITNESS)? {
927            remove_restore_witness(&witness)?;
928        }
929        return Ok(());
930    }
931    if !validate_restore_witness(&witness, LIVE_RESTORE_WITNESS)? {
932        return Err(MaterializeError::Engine(
933            "unowned live-restore backup collision".into(),
934        ));
935    }
936    match destination_parent.symlink_metadata(destination_name) {
937        Ok(_) => destination_parent.remove(&backup_name)?,
938        Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
939            destination_parent.rename_to(&backup_name, destination_parent, destination_name)?;
940        }
941        Err(error) => return Err(error.into()),
942    }
943    sync_restore_parent(destination)?;
944    remove_restore_witness(&witness)?;
945    Ok(())
946}
947
948#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
949async fn materialize_entry<A: AsyncAuthorityStore, O: AsyncObjectStore>(
950    reader: &PinnedReader<A, O>,
951    host_root: &Arc<HostRoot>,
952    path: &NamespacePath,
953    host_path: &Path,
954    file_id: FileId,
955    kind: FileKind,
956    payload: FilePayload,
957    #[cfg(any(target_os = "macos", windows))] metadata_id: ObjectId,
958    options: &MaterializeOptions,
959    budget: WorkBudget,
960    cancellation: &CancellationToken,
961    known_files: &mut HashMap<FileId, PathBuf>,
962    #[cfg(any(target_os = "macos", windows))] known_payloads: &mut HashMap<
963        (u64, ObjectId, ObjectId),
964        PathBuf,
965    >,
966    #[cfg(any(target_os = "macos", windows))] clone_available: bool,
967    pending: &mut Vec<(NamespacePath, PathBuf)>,
968    receipt: &mut MaterializationReceipt,
969    mode: MaterializeMode,
970) -> Result<(), OperationFailure<MaterializeError>> {
971    cancellation.check().map_err(|error| {
972        OperationFailure::new(MaterializeError::Engine(error.to_string()), receipt.work)
973    })?;
974    if let Some(existing) = known_files.get(&file_id) {
975        host_root
976            .hard_link(existing, host_path)
977            .map_err(|error| OperationFailure::new(error.into(), receipt.work))?;
978        receipt.files = receipt.files.checked_add(1).ok_or_else(|| {
979            OperationFailure::new(MaterializeError::Work(WorkError::Overflow), receipt.work)
980        })?;
981        account_materialization(receipt, 0, budget)?;
982        cancellation.check().map_err(|error| {
983            OperationFailure::new(MaterializeError::Engine(error.to_string()), receipt.work)
984        })?;
985        return Ok(());
986    }
987
988    let mut authenticated_metadata = None;
989    match (kind, payload) {
990        (FileKind::Directory, FilePayload::Directory { .. }) => {
991            host_root
992                .create_dir(host_path)
993                .map_err(|error| OperationFailure::new(error.into(), receipt.work))?;
994            known_files.insert(file_id, host_path.to_path_buf());
995            pending.push((path.clone(), host_path.to_path_buf()));
996            receipt.directories = receipt.directories.checked_add(1).ok_or_else(|| {
997                OperationFailure::new(MaterializeError::Work(WorkError::Overflow), receipt.work)
998            })?;
999            account_materialization(receipt, 0, budget)?;
1000            cancellation.check().map_err(|error| {
1001                OperationFailure::new(MaterializeError::Engine(error.to_string()), receipt.work)
1002            })?;
1003            return Ok(());
1004        }
1005        (FileKind::Regular, FilePayload::InlineRegular(data)) => {
1006            let file = create_file(host_root, host_path, receipt.work)?;
1007            file.write_all_batch_async(vec![acyclic_native_runtime::OwnedWrite {
1008                offset: 0,
1009                bytes: Bytes::copy_from_slice(data.as_bytes()),
1010            }])
1011            .await
1012            .map_err(|error| OperationFailure::new(error.into(), receipt.work))?;
1013            if mode == MaterializeMode::DurableOutput {
1014                file.sync_async(acyclic_native_runtime::Durability::Full)
1015                    .await
1016                    .map_err(|error| OperationFailure::new(error.into(), receipt.work))?;
1017            }
1018            known_files.insert(file_id, host_path.to_path_buf());
1019            account_file(
1020                receipt,
1021                data.as_bytes().len() as u64,
1022                data.as_bytes().len() as u64,
1023                budget,
1024            )?;
1025        }
1026        (
1027            FileKind::Regular,
1028            FilePayload::Regular {
1029                logical_bytes,
1030                extents,
1031            },
1032        ) => {
1033            #[cfg(not(any(target_os = "macos", windows)))]
1034            let _ = extents;
1035            #[cfg(any(target_os = "macos", windows))]
1036            let cloned = if clone_available
1037                && let Some(source) = known_payloads.get(&(logical_bytes, extents, metadata_id))
1038            {
1039                host_root
1040                    .clone_file(source, host_path)
1041                    .map_err(|error| OperationFailure::new(error.into(), receipt.work))?
1042            } else {
1043                false
1044            };
1045            #[cfg(not(any(target_os = "macos", windows)))]
1046            let cloned = false;
1047            if !cloned {
1048                let mut file = create_file(host_root, host_path, receipt.work)?;
1049                #[cfg(windows)]
1050                {
1051                    file.control_async(mark_sparse)
1052                        .await
1053                        .map_err(|error| OperationFailure::new(error.into(), receipt.work))?;
1054                }
1055                file.set_len_async(logical_bytes)
1056                    .await
1057                    .map_err(|error| OperationFailure::new(error.into(), receipt.work))?;
1058                authenticated_metadata = Some(
1059                    materialize_sparse_file(
1060                        reader,
1061                        path,
1062                        &mut file,
1063                        logical_bytes,
1064                        options,
1065                        budget,
1066                        cancellation,
1067                        receipt,
1068                    )
1069                    .await?,
1070                );
1071                if mode == MaterializeMode::DurableOutput {
1072                    file.sync_async(acyclic_native_runtime::Durability::Full)
1073                        .await
1074                        .map_err(|error| OperationFailure::new(error.into(), receipt.work))?;
1075                }
1076            }
1077            #[cfg(target_os = "macos")]
1078            if cloned && mode == MaterializeMode::DurableOutput {
1079                let file = host_root
1080                    .open_file(host_path)
1081                    .map(cap_std::fs::File::into_std)
1082                    .map_err(|error| OperationFailure::new(error.into(), receipt.work))?;
1083                acyclic_native_runtime::NativeFile::from_file(file)
1084                    .map_err(|error| OperationFailure::new(error.into(), receipt.work))?
1085                    .sync_async(acyclic_native_runtime::Durability::Full)
1086                    .await
1087                    .map_err(|error| OperationFailure::new(error.into(), receipt.work))?;
1088            }
1089            #[cfg(any(target_os = "macos", windows))]
1090            known_payloads
1091                .entry((logical_bytes, extents, metadata_id))
1092                .or_insert_with(|| host_path.to_path_buf());
1093            known_files.insert(file_id, host_path.to_path_buf());
1094            receipt.files = receipt.files.checked_add(1).ok_or_else(|| {
1095                OperationFailure::new(MaterializeError::Work(WorkError::Overflow), receipt.work)
1096            })?;
1097            receipt.logical_file_bytes = receipt
1098                .logical_file_bytes
1099                .checked_add(logical_bytes)
1100                .ok_or_else(|| {
1101                    OperationFailure::new(MaterializeError::Work(WorkError::Overflow), receipt.work)
1102                })?;
1103            account_materialization(receipt, 0, budget)?;
1104        }
1105        (FileKind::SymbolicLink, FilePayload::SymbolicLink { target_bytes, .. }) => {
1106            let remaining = receipt.work.remaining(budget).map_err(|error| {
1107                OperationFailure::new(MaterializeError::Work(error), receipt.work)
1108            })?;
1109            let target = reader
1110                .read_symbolic_link(path, remaining, cancellation)
1111                .await
1112                .map_err(|failure| map_engine_failure(failure, receipt.work))?;
1113            receipt.work = add_work(receipt.work, target.work)?;
1114            if u64::try_from(target.value.len()).unwrap_or(u64::MAX) != target_bytes {
1115                return Err(OperationFailure::new(
1116                    MaterializeError::Engine("symbolic-link length mismatch".to_owned()),
1117                    receipt.work,
1118                ));
1119            }
1120            create_symlink(host_root, &target.value, host_path)
1121                .map_err(|error| OperationFailure::new(error, receipt.work))?;
1122            known_files.insert(file_id, host_path.to_path_buf());
1123            receipt.symbolic_links = receipt.symbolic_links.checked_add(1).ok_or_else(|| {
1124                OperationFailure::new(MaterializeError::Work(WorkError::Overflow), receipt.work)
1125            })?;
1126            account_materialization(receipt, 0, budget)?;
1127        }
1128        (kind @ (FileKind::Fifo | FileKind::Socket), FilePayload::Empty) => {
1129            create_special(host_root, host_path, kind, None)
1130                .map_err(|error| OperationFailure::new(error, receipt.work))?;
1131            known_files.insert(file_id, host_path.to_path_buf());
1132            receipt.special_files = receipt.special_files.checked_add(1).ok_or_else(|| {
1133                OperationFailure::new(MaterializeError::Work(WorkError::Overflow), receipt.work)
1134            })?;
1135            account_materialization(receipt, 0, budget)?;
1136        }
1137        (
1138            kind @ (FileKind::CharacterDevice | FileKind::BlockDevice),
1139            FilePayload::Device { major, minor },
1140        ) => {
1141            create_special(host_root, host_path, kind, Some((major, minor)))
1142                .map_err(|error| OperationFailure::new(error, receipt.work))?;
1143            known_files.insert(file_id, host_path.to_path_buf());
1144            receipt.special_files = receipt.special_files.checked_add(1).ok_or_else(|| {
1145                OperationFailure::new(MaterializeError::Work(WorkError::Overflow), receipt.work)
1146            })?;
1147            account_materialization(receipt, 0, budget)?;
1148        }
1149        (unsupported, _) => {
1150            return Err(OperationFailure::new(
1151                MaterializeError::UnsupportedKind(unsupported),
1152                receipt.work,
1153            ));
1154        }
1155    }
1156    // Directory metadata is applied after descendants have been populated.
1157    // Applying it here would do the same work twice and can make a directory
1158    // read-only before its children have been created.
1159    if kind != FileKind::Directory {
1160        if let Some(metadata) = authenticated_metadata {
1161            apply_host_metadata_offloaded(
1162                host_root,
1163                host_path,
1164                MaterializeMode::host_metadata(metadata),
1165            )
1166            .await
1167            .map_err(|error| OperationFailure::new(error, receipt.work))?;
1168        } else {
1169            apply_metadata(
1170                reader,
1171                host_root,
1172                path,
1173                host_path,
1174                budget,
1175                cancellation,
1176                receipt,
1177            )
1178            .await?;
1179        }
1180    }
1181    cancellation.check().map_err(|error| {
1182        OperationFailure::new(MaterializeError::Engine(error.to_string()), receipt.work)
1183    })?;
1184    Ok(())
1185}
1186
1187#[cfg(windows)]
1188#[allow(unsafe_code)]
1189fn mark_sparse(file: &File) -> std::io::Result<()> {
1190    use std::os::windows::io::AsRawHandle;
1191    use windows::Win32::Foundation::{ERROR_IO_PENDING, HANDLE};
1192    use windows::Win32::System::IO::{DeviceIoControl, GetOverlappedResult, OVERLAPPED};
1193    use windows::Win32::System::Ioctl::FSCTL_SET_SPARSE;
1194
1195    let handle = HANDLE(file.as_raw_handle());
1196    let mut overlapped = OVERLAPPED::default();
1197    // This runs as the first sequenced control operation, before the native
1198    // completion owner attaches the overlapped file handle to an IOCP. The
1199    // FSCTL has no payload buffers; its OVERLAPPED must nevertheless remain
1200    // live until terminal completion, including when DeviceIoControl pends.
1201    let submitted = unsafe {
1202        DeviceIoControl(
1203            handle,
1204            FSCTL_SET_SPARSE,
1205            None,
1206            0,
1207            None,
1208            0,
1209            None,
1210            Some(&mut overlapped),
1211        )
1212    };
1213    match submitted {
1214        Ok(()) => Ok(()),
1215        Err(error) if error.code() == windows::core::HRESULT::from_win32(ERROR_IO_PENDING.0) => {
1216            let mut transferred = 0;
1217            // SAFETY: the file and OVERLAPPED remain live through this wait.
1218            // With no other I/O on this newly created file, a null hEvent is
1219            // the file handle's own event. If waiting itself fails, completion
1220            // is uncertain and unwinding would free the stack OVERLAPPED.
1221            match unsafe { GetOverlappedResult(handle, &overlapped, &mut transferred, true) } {
1222                Ok(()) => Ok(()),
1223                Err(_) => std::process::abort(),
1224            }
1225        }
1226        Err(error) => Err(std::io::Error::other(error.to_string())),
1227    }
1228}
1229
1230#[allow(clippy::too_many_arguments)]
1231#[allow(clippy::too_many_lines)]
1232async fn materialize_sparse_file<A: AsyncAuthorityStore, O: AsyncObjectStore>(
1233    reader: &PinnedReader<A, O>,
1234    path: &NamespacePath,
1235    file: &mut acyclic_native_runtime::NativeFile,
1236    logical_bytes: u64,
1237    options: &MaterializeOptions,
1238    budget: WorkBudget,
1239    cancellation: &CancellationToken,
1240    receipt: &mut MaterializationReceipt,
1241) -> Result<FileMetadata, OperationFailure<MaterializeError>> {
1242    let remaining = receipt
1243        .work
1244        .remaining(budget)
1245        .map_err(|error| OperationFailure::new(MaterializeError::Work(error), receipt.work))?;
1246    let resolved = reader
1247        .resolve_files(std::slice::from_ref(path), remaining, cancellation)
1248        .await
1249        .map_err(|failure| map_engine_failure(failure, receipt.work))?;
1250    receipt.work = add_work(receipt.work, resolved.work)?;
1251    let resolved = resolved
1252        .value
1253        .into_iter()
1254        .next()
1255        .flatten()
1256        .ok_or_else(|| OperationFailure::new(MaterializeError::MissingPath, receipt.work))?;
1257    let metadata = resolved.description().metadata;
1258    let mut offset = 0_u64;
1259    while offset < logical_bytes {
1260        let length = options.transfer_bytes.min(logical_bytes - offset);
1261        let remaining = receipt
1262            .work
1263            .remaining(budget)
1264            .map_err(|error| OperationFailure::new(MaterializeError::Work(error), receipt.work))?;
1265        let plan = resolved
1266            .plan_extents(
1267                ByteRange { offset, length },
1268                options.maximum_extent_spans,
1269                remaining,
1270                cancellation,
1271            )
1272            .await
1273            .map_err(|failure| map_engine_failure(failure, receipt.work))?;
1274        receipt.work = add_work(receipt.work, plan.work)?;
1275        let Some(plan) = plan.value else {
1276            return Err(OperationFailure::new(
1277                MaterializeError::Engine(
1278                    "non-inline file returned an inline extent plan".to_owned(),
1279                ),
1280                receipt.work,
1281            ));
1282        };
1283        let mut writes = Vec::new();
1284        writes
1285            .try_reserve_exact(plan.spans.len())
1286            .map_err(|_| OperationFailure::new(MaterializeError::InvalidOptions, receipt.work))?;
1287        let mut reads = Vec::new();
1288        reads
1289            .try_reserve_exact(plan.spans.len())
1290            .map_err(|_| OperationFailure::new(MaterializeError::InvalidOptions, receipt.work))?;
1291        for span in plan.spans {
1292            match span.kind {
1293                ExtentKind::Hole => {
1294                    #[cfg(target_os = "macos")]
1295                    file.control_async(move |handle| {
1296                        crate::native_host::punch_hole(handle, span.offset, span.length)
1297                    })
1298                    .await
1299                    .map_err(|error| OperationFailure::new(error.into(), receipt.work))?;
1300                }
1301                ExtentKind::AllocatedZero => {
1302                    queue_zero_writes(
1303                        &mut writes,
1304                        span.offset,
1305                        span.length,
1306                        options.transfer_bytes,
1307                        receipt.work,
1308                    )?;
1309                }
1310                ExtentKind::Content { .. } => {
1311                    reads.push(ResolvedFileRangeReadRequest {
1312                        file: &resolved,
1313                        range: ByteRange {
1314                            offset: span.offset,
1315                            length: span.length,
1316                        },
1317                    });
1318                }
1319            }
1320        }
1321        if !reads.is_empty() {
1322            read_content_spans(
1323                reader,
1324                reads,
1325                options.maximum_extent_spans,
1326                budget,
1327                cancellation,
1328                receipt,
1329                &mut writes,
1330            )
1331            .await?;
1332        }
1333        cancellation.check().map_err(|error| {
1334            OperationFailure::new(MaterializeError::Engine(error.to_string()), receipt.work)
1335        })?;
1336        let written_bytes = writes.iter().try_fold(0_u64, |total, write| {
1337            total
1338                .checked_add(u64::try_from(write.bytes.len()).unwrap_or(u64::MAX))
1339                .ok_or_else(|| {
1340                    OperationFailure::new(MaterializeError::Work(WorkError::Overflow), receipt.work)
1341                })
1342        })?;
1343        let mut write = file.write_all_batch_async(writes);
1344        let mut cancelled = false;
1345        tokio::select! {
1346            result = &mut write => {
1347                result.map_err(|error| OperationFailure::new(error.into(), receipt.work))?;
1348            }
1349            () = cancellation.cancelled() => {
1350                cancelled = true;
1351                write.await.map_err(|error| OperationFailure::new(error.into(), receipt.work))?;
1352            }
1353        }
1354        account_written(receipt, written_bytes, budget)?;
1355        if cancelled {
1356            return Err(OperationFailure::new(
1357                MaterializeError::Engine("operation cancelled".to_owned()),
1358                receipt.work,
1359            ));
1360        }
1361        offset = offset.checked_add(length).ok_or_else(|| {
1362            OperationFailure::new(MaterializeError::Work(WorkError::Overflow), receipt.work)
1363        })?;
1364    }
1365    Ok(metadata)
1366}
1367
1368#[allow(clippy::too_many_arguments)]
1369async fn read_content_spans<A: AsyncAuthorityStore, O: AsyncObjectStore>(
1370    reader: &PinnedReader<A, O>,
1371    reads: Vec<ResolvedFileRangeReadRequest<'_, A, O>>,
1372    maximum_extent_spans: u32,
1373    budget: WorkBudget,
1374    cancellation: &CancellationToken,
1375    receipt: &mut MaterializationReceipt,
1376    writes: &mut Vec<acyclic_native_runtime::OwnedWrite>,
1377) -> Result<(), OperationFailure<MaterializeError>> {
1378    let remaining = receipt
1379        .work
1380        .remaining(budget)
1381        .map_err(|error| OperationFailure::new(MaterializeError::Work(error), receipt.work))?;
1382    let read = reader
1383        .read_resolved_ranges(
1384            &reads,
1385            usize::try_from(maximum_extent_spans).unwrap_or(usize::MAX),
1386            remaining,
1387            cancellation,
1388        )
1389        .await
1390        .map_err(|failure| map_engine_failure(failure, receipt.work))?;
1391    receipt.work = add_work(receipt.work, read.work)?;
1392    writes.extend(reads.into_iter().zip(read.value).map(|(request, result)| {
1393        acyclic_native_runtime::OwnedWrite {
1394            offset: request.range.offset,
1395            bytes: result.bytes,
1396        }
1397    }));
1398    Ok(())
1399}
1400
1401fn queue_zero_writes(
1402    writes: &mut Vec<acyclic_native_runtime::OwnedWrite>,
1403    offset: u64,
1404    length: u64,
1405    transfer_bytes: u64,
1406    work: WorkCounters,
1407) -> Result<(), OperationFailure<MaterializeError>> {
1408    if transfer_bytes == 0 {
1409        return Err(OperationFailure::new(
1410            MaterializeError::InvalidOptions,
1411            work,
1412        ));
1413    }
1414    let capacity = usize::try_from(transfer_bytes.min(length))
1415        .map_err(|_| OperationFailure::new(MaterializeError::InvalidOptions, work))?;
1416    let zeros = bytes::Bytes::from(vec![0_u8; capacity]);
1417    let chunks = length.div_ceil(transfer_bytes);
1418    writes
1419        .try_reserve(usize::try_from(chunks).unwrap_or(usize::MAX))
1420        .map_err(|_| OperationFailure::new(MaterializeError::InvalidOptions, work))?;
1421    let mut remaining = length;
1422    let mut position = offset;
1423    while remaining != 0 {
1424        let count = usize::try_from(remaining.min(transfer_bytes))
1425            .map_err(|_| OperationFailure::new(MaterializeError::InvalidOptions, work))?;
1426        writes.push(acyclic_native_runtime::OwnedWrite {
1427            offset: position,
1428            bytes: zeros.slice(..count),
1429        });
1430        position = position.checked_add(count as u64).ok_or_else(|| {
1431            OperationFailure::new(MaterializeError::Work(WorkError::Overflow), work)
1432        })?;
1433        remaining -= count as u64;
1434    }
1435    Ok(())
1436}
1437
1438#[allow(clippy::too_many_arguments)]
1439async fn apply_metadata<A: AsyncAuthorityStore, O: AsyncObjectStore>(
1440    reader: &PinnedReader<A, O>,
1441    host_root: &Arc<HostRoot>,
1442    path: &NamespacePath,
1443    host_path: &Path,
1444    budget: WorkBudget,
1445    cancellation: &CancellationToken,
1446    receipt: &mut MaterializationReceipt,
1447) -> Result<(), OperationFailure<MaterializeError>> {
1448    let remaining = receipt
1449        .work
1450        .remaining(budget)
1451        .map_err(|error| OperationFailure::new(MaterializeError::Work(error), receipt.work))?;
1452    let metadata = reader
1453        .read_metadata(path, remaining, cancellation)
1454        .await
1455        .map_err(|failure| map_engine_failure(failure, receipt.work))?;
1456    receipt.work = add_work(receipt.work, metadata.work)?;
1457    apply_host_metadata_offloaded(
1458        host_root,
1459        host_path,
1460        MaterializeMode::host_metadata(metadata.value),
1461    )
1462    .await
1463    .map_err(|error| OperationFailure::new(error, receipt.work))
1464}
1465
1466#[cfg(target_os = "linux")]
1467async fn apply_host_metadata_offloaded(
1468    host_root: &Arc<HostRoot>,
1469    host_path: &Path,
1470    metadata: FileMetadata,
1471) -> Result<(), MaterializeError> {
1472    reject_unbaselined_unix_metadata(metadata)?;
1473    if metadata == FileMetadata::default() {
1474        return verify_host_path_offloaded(host_root, host_path).await;
1475    }
1476    let root = Arc::clone(host_root);
1477    let path = host_path.to_path_buf();
1478    // Opening is non-mutating. If its observer is dropped, the worker can
1479    // only release the newly held inode; it cannot alter a reused path.
1480    let target =
1481        acyclic_native_runtime::run_blocking_io(move || root.open_linux_metadata_target(&path))
1482            .await
1483            .map_err(MaterializeError::Io)?
1484            .map_err(linux_metadata_error)?;
1485    // All mutations use the pinned inode. A dropped observer cannot redirect
1486    // this operation to a replacement at the same relative path.
1487    acyclic_native_runtime::run_blocking_io(move || target.apply(metadata))
1488        .await
1489        .map_err(MaterializeError::Io)?
1490        .map_err(linux_metadata_error)
1491}
1492
1493#[cfg(target_os = "macos")]
1494async fn apply_host_metadata_offloaded(
1495    host_root: &Arc<HostRoot>,
1496    host_path: &Path,
1497    metadata: FileMetadata,
1498) -> Result<(), MaterializeError> {
1499    reject_unbaselined_unix_metadata(metadata)?;
1500    if metadata == FileMetadata::default() {
1501        return verify_host_path_offloaded(host_root, host_path).await;
1502    }
1503    let root = Arc::clone(host_root);
1504    let path = host_path.to_path_buf();
1505    let target =
1506        acyclic_native_runtime::run_blocking_io(move || root.open_macos_metadata_target(&path))
1507            .await
1508            .map_err(MaterializeError::Io)?
1509            .map_err(mac_metadata_error)?;
1510    acyclic_native_runtime::run_blocking_io(move || target.apply(metadata))
1511        .await
1512        .map_err(MaterializeError::Io)?
1513        .map_err(mac_metadata_error)
1514}
1515
1516#[cfg(windows)]
1517async fn apply_host_metadata_offloaded(
1518    host_root: &Arc<HostRoot>,
1519    host_path: &Path,
1520    metadata: FileMetadata,
1521) -> Result<(), MaterializeError> {
1522    if metadata == FileMetadata::default() {
1523        return verify_host_path_offloaded(host_root, host_path).await;
1524    }
1525    let root = Arc::clone(host_root);
1526    let path = host_path.to_path_buf();
1527    let target =
1528        acyclic_native_runtime::run_blocking_io(move || root.open_windows_metadata_target(&path))
1529            .await
1530            .map_err(MaterializeError::Io)?
1531            .map_err(windows_metadata_error)?;
1532    acyclic_native_runtime::run_blocking_io(move || target.apply(metadata))
1533        .await
1534        .map_err(MaterializeError::Io)?
1535        .map_err(windows_metadata_error)
1536}
1537
1538async fn verify_host_path_offloaded(
1539    host_root: &Arc<HostRoot>,
1540    host_path: &Path,
1541) -> Result<(), MaterializeError> {
1542    let root = Arc::clone(host_root);
1543    let path = host_path.to_path_buf();
1544    acyclic_native_runtime::run_blocking_io(move || root.symlink_metadata_held(&path).map(|_| ()))
1545        .await
1546        .map_err(MaterializeError::Io)?
1547        .map_err(MaterializeError::Io)
1548}
1549
1550#[cfg(windows)]
1551fn windows_metadata_error(error: crate::native_host::WindowsMetadataError) -> MaterializeError {
1552    match error {
1553        crate::native_host::WindowsMetadataError::Unsupported(field) => {
1554            MaterializeError::UnsupportedMetadata(field)
1555        }
1556        crate::native_host::WindowsMetadataError::Io(error) => MaterializeError::Io(error),
1557    }
1558}
1559
1560#[cfg(unix)]
1561fn reject_unbaselined_unix_metadata(metadata: FileMetadata) -> Result<(), MaterializeError> {
1562    use crate::kernel::MetadataField;
1563    if matches!(metadata.changed_ns, MetadataField::Value(_)) {
1564        return Err(MaterializeError::UnsupportedMetadata(
1565            "changed_ns without native-view baseline",
1566        ));
1567    }
1568    #[cfg(target_os = "linux")]
1569    if matches!(metadata.created_ns, MetadataField::Value(_)) {
1570        return Err(MaterializeError::UnsupportedMetadata(
1571            "created_ns without native-view baseline",
1572        ));
1573    }
1574    Ok(())
1575}
1576
1577#[cfg(target_os = "linux")]
1578fn linux_metadata_error(error: crate::native_host::LinuxMetadataError) -> MaterializeError {
1579    match error {
1580        crate::native_host::LinuxMetadataError::Unsupported(field) => {
1581            MaterializeError::UnsupportedMetadata(field)
1582        }
1583        crate::native_host::LinuxMetadataError::Io(error) => MaterializeError::Io(error),
1584    }
1585}
1586
1587#[cfg(target_os = "macos")]
1588fn mac_metadata_error(error: crate::native_host::MacMetadataError) -> MaterializeError {
1589    match error {
1590        crate::native_host::MacMetadataError::Unsupported(field) => {
1591            MaterializeError::UnsupportedMetadata(field)
1592        }
1593        crate::native_host::MacMetadataError::Io(error) => MaterializeError::Io(error),
1594    }
1595}
1596
1597fn validate_options(options: &MaterializeOptions) -> Result<HostRoot, MaterializeError> {
1598    validate_bounds(options)?;
1599    let root =
1600        HostRoot::open(&options.destination).map_err(|_| MaterializeError::InvalidDestination)?;
1601    if !root
1602        .is_empty()
1603        .map_err(|_| MaterializeError::InvalidDestination)?
1604    {
1605        return Err(MaterializeError::InvalidDestination);
1606    }
1607    Ok(root)
1608}
1609
1610fn validate_bounds(options: &MaterializeOptions) -> Result<(), MaterializeError> {
1611    if options.maximum_directory_entries == 0
1612        || options.maximum_extent_spans == 0
1613        || options.transfer_bytes == 0
1614        || usize::try_from(options.transfer_bytes).is_err()
1615    {
1616        return Err(MaterializeError::InvalidOptions);
1617    }
1618    Ok(())
1619}
1620
1621fn append_path(
1622    parent: &NamespacePath,
1623    name: LogicalName,
1624    limits: crate::model::VolumeLimits,
1625) -> Result<NamespacePath, MaterializeError> {
1626    let mut components = parent.components().to_vec();
1627    components.push(name);
1628    NamespacePath::new(components, limits)
1629        .map_err(|error| MaterializeError::Engine(error.to_string()))
1630}
1631
1632fn create_file(
1633    host_root: &HostRoot,
1634    path: &Path,
1635    work: WorkCounters,
1636) -> Result<acyclic_native_runtime::NativeFile, OperationFailure<MaterializeError>> {
1637    #[cfg(windows)]
1638    {
1639        let file = host_root
1640            .create_overlapped_file(path)
1641            .map_err(|error| OperationFailure::new(error.into(), work))?;
1642        // SAFETY: HostRoot created this handle with FILE_FLAG_OVERLAPPED,
1643        // capability-relative to the authorized root. It is newly created,
1644        // has no completion-port association, and is moved directly into the
1645        // sole native I/O owner without any independent file I/O.
1646        #[allow(unsafe_code)]
1647        unsafe { acyclic_native_runtime::NativeFile::from_overlapped_file_unchecked(file) }
1648            .map_err(|error| OperationFailure::new(error.into(), work))
1649    }
1650    #[cfg(not(windows))]
1651    {
1652        host_root
1653            .create_file(path)
1654            .and_then(acyclic_native_runtime::NativeFile::from_file)
1655            .map_err(|error| OperationFailure::new(error.into(), work))
1656    }
1657}
1658
1659#[allow(clippy::needless_pass_by_value)]
1660fn map_engine_failure<E: std::fmt::Display>(
1661    failure: OperationFailure<E>,
1662    prior: WorkCounters,
1663) -> OperationFailure<MaterializeError> {
1664    match prior.checked_add(*failure.work) {
1665        Ok(work) => {
1666            OperationFailure::new(MaterializeError::Engine(failure.error.to_string()), work)
1667        }
1668        Err(error) => OperationFailure::new(MaterializeError::Work(error), prior),
1669    }
1670}
1671
1672fn add_work(
1673    left: WorkCounters,
1674    right: WorkCounters,
1675) -> Result<WorkCounters, OperationFailure<MaterializeError>> {
1676    left.checked_add(right)
1677        .map_err(|error| OperationFailure::new(error.into(), left))
1678}
1679
1680fn account_written(
1681    receipt: &mut MaterializationReceipt,
1682    bytes: u64,
1683    budget: WorkBudget,
1684) -> Result<(), OperationFailure<MaterializeError>> {
1685    receipt.written_bytes = receipt.written_bytes.checked_add(bytes).ok_or_else(|| {
1686        OperationFailure::new(MaterializeError::Work(WorkError::Overflow), receipt.work)
1687    })?;
1688    receipt.work = add_work(
1689        receipt.work,
1690        WorkCounters {
1691            bytes_copied: bytes,
1692            output_bytes: bytes,
1693            ..WorkCounters::default()
1694        },
1695    )?;
1696    receipt
1697        .work
1698        .verify(budget)
1699        .map_err(|error| OperationFailure::new(MaterializeError::Work(error), receipt.work))
1700}
1701
1702fn account_materialization(
1703    receipt: &mut MaterializationReceipt,
1704    bytes: u64,
1705    budget: WorkBudget,
1706) -> Result<(), OperationFailure<MaterializeError>> {
1707    account_written(receipt, bytes, budget)?;
1708    receipt.work = add_work(
1709        receipt.work,
1710        WorkCounters {
1711            materializations: 1,
1712            ..WorkCounters::default()
1713        },
1714    )?;
1715    receipt
1716        .work
1717        .verify(budget)
1718        .map_err(|error| OperationFailure::new(MaterializeError::Work(error), receipt.work))
1719}
1720
1721fn account_file(
1722    receipt: &mut MaterializationReceipt,
1723    logical: u64,
1724    written: u64,
1725    budget: WorkBudget,
1726) -> Result<(), OperationFailure<MaterializeError>> {
1727    receipt.files = receipt.files.checked_add(1).ok_or_else(|| {
1728        OperationFailure::new(MaterializeError::Work(WorkError::Overflow), receipt.work)
1729    })?;
1730    receipt.logical_file_bytes =
1731        receipt
1732            .logical_file_bytes
1733            .checked_add(logical)
1734            .ok_or_else(|| {
1735                OperationFailure::new(MaterializeError::Work(WorkError::Overflow), receipt.work)
1736            })?;
1737    account_materialization(receipt, written, budget)
1738}
1739
1740#[cfg(unix)]
1741pub(crate) fn host_name(name: &LogicalName) -> Result<OsString, MaterializeError> {
1742    use std::os::unix::ffi::OsStringExt;
1743    match name.encoding() {
1744        NameEncoding::Utf8 | NameEncoding::PosixBytes => {
1745            Ok(OsString::from_vec(name.as_bytes().to_vec()))
1746        }
1747        NameEncoding::WindowsUtf16Le => Err(MaterializeError::UnrepresentableName),
1748    }
1749}
1750
1751#[cfg(windows)]
1752pub(crate) fn host_name(name: &LogicalName) -> Result<OsString, MaterializeError> {
1753    use std::os::windows::ffi::OsStringExt;
1754    match name.encoding() {
1755        NameEncoding::Utf8 | NameEncoding::PosixBytes => std::str::from_utf8(name.as_bytes())
1756            .map(OsString::from)
1757            .map_err(|_| MaterializeError::UnrepresentableName),
1758        NameEncoding::WindowsUtf16Le => {
1759            let units = name
1760                .as_bytes()
1761                .chunks_exact(2)
1762                .map(|pair| {
1763                    pair.try_into()
1764                        .map(u16::from_le_bytes)
1765                        .map_err(|_| MaterializeError::UnrepresentableName)
1766                })
1767                .collect::<Result<Vec<_>, _>>()?;
1768            Ok(OsString::from_wide(&units))
1769        }
1770    }
1771}
1772
1773#[cfg(all(test, windows))]
1774mod windows_name_tests {
1775    use super::*;
1776
1777    #[tokio::test]
1778    async fn overlapped_sparse_control_finishes_before_native_writes()
1779    -> Result<(), Box<dyn std::error::Error>> {
1780        let directory = tempfile::tempdir()?;
1781        let root = HostRoot::open(directory.path())?;
1782        let path = Path::new("sparse-control.bin");
1783        let file = root.create_overlapped_file(path)?;
1784        // SAFETY: the new capability-rooted handle is overlapped, unattached
1785        // to an IOCP, and moved directly to its sole native owner.
1786        #[allow(unsafe_code)]
1787        let native =
1788            unsafe { acyclic_native_runtime::NativeFile::from_overlapped_file_unchecked(file)? };
1789        native.control_async(mark_sparse).await?;
1790        native.set_len_async(1024 * 1024).await?;
1791        native
1792            .write_all_batch_async(vec![acyclic_native_runtime::OwnedWrite {
1793                offset: 1024 * 1024 - 4,
1794                bytes: Bytes::from_static(b"tail"),
1795            }])
1796            .await?;
1797        native
1798            .sync_async(acyclic_native_runtime::Durability::Full)
1799            .await?;
1800        let bytes = std::fs::read(directory.path().join(path))?;
1801        assert_eq!(bytes.len(), 1024 * 1024);
1802        assert_eq!(&bytes[bytes.len() - 4..], b"tail");
1803        Ok(())
1804    }
1805
1806    #[test]
1807    fn posix_profile_names_materialize_through_utf8_on_windows()
1808    -> Result<(), Box<dyn std::error::Error>> {
1809        let valid = LogicalName::new(
1810            NameEncoding::PosixBytes,
1811            "uni-é中.txt".as_bytes().to_vec(),
1812            255,
1813        )?;
1814        assert_eq!(host_name(&valid)?, OsString::from("uni-é中.txt"));
1815
1816        let invalid = LogicalName::new(NameEncoding::PosixBytes, vec![0xff], 255)?;
1817        assert!(matches!(
1818            host_name(&invalid),
1819            Err(MaterializeError::UnrepresentableName)
1820        ));
1821        Ok(())
1822    }
1823}
1824
1825#[cfg(unix)]
1826fn create_symlink(
1827    host_root: &HostRoot,
1828    target: &Bytes,
1829    destination: &Path,
1830) -> Result<(), MaterializeError> {
1831    use std::os::unix::ffi::OsStrExt;
1832    host_root
1833        .symlink(OsStr::from_bytes(target), destination)
1834        .map_err(Into::into)
1835}
1836
1837#[cfg(unix)]
1838#[allow(
1839    clippy::useless_conversion,
1840    reason = "libc file-type constants have different widths on Linux and macOS"
1841)]
1842fn create_special(
1843    host_root: &HostRoot,
1844    destination: &Path,
1845    kind: FileKind,
1846    device: Option<(u32, u32)>,
1847) -> Result<(), MaterializeError> {
1848    match kind {
1849        FileKind::Fifo => host_root.create_fifo_held(destination, 0o600)?,
1850        FileKind::CharacterDevice | FileKind::BlockDevice => {
1851            let Some((major, minor)) = device else {
1852                return Err(MaterializeError::UnsupportedKind(kind));
1853            };
1854            let file_type = if kind == FileKind::CharacterDevice {
1855                u32::from(libc::S_IFCHR)
1856            } else {
1857                u32::from(libc::S_IFBLK)
1858            };
1859            let device_number = match super::device::join_device(major, minor) {
1860                Ok(device_number) => device_number,
1861                Err(errno) => return Err(std::io::Error::from_raw_os_error(errno).into()),
1862            };
1863            host_root.create_device_held(destination, file_type | 0o600, device_number)?;
1864        }
1865        FileKind::Socket => {
1866            host_root.bind_unix_socket(destination)?;
1867        }
1868        _ => return Err(MaterializeError::UnsupportedKind(kind)),
1869    }
1870    Ok(())
1871}
1872
1873#[cfg(not(unix))]
1874fn create_special(
1875    _: &HostRoot,
1876    _: &Path,
1877    kind: FileKind,
1878    _: Option<(u32, u32)>,
1879) -> Result<(), MaterializeError> {
1880    Err(MaterializeError::UnsupportedKind(kind))
1881}
1882
1883#[cfg(windows)]
1884fn create_symlink(
1885    host_root: &HostRoot,
1886    target: &Bytes,
1887    destination: &Path,
1888) -> Result<(), MaterializeError> {
1889    use std::os::windows::ffi::OsStringExt;
1890    if !target.len().is_multiple_of(2) {
1891        return Err(MaterializeError::UnrepresentableName);
1892    }
1893    let units = target
1894        .chunks_exact(2)
1895        .map(|pair| {
1896            pair.try_into()
1897                .map(u16::from_le_bytes)
1898                .map_err(|_| MaterializeError::UnrepresentableName)
1899        })
1900        .collect::<Result<Vec<_>, _>>()?;
1901    host_root
1902        .symlink_file(Path::new(&OsString::from_wide(&units)), destination)
1903        .map_err(Into::into)
1904}
1905
1906#[cfg(test)]
1907mod restore_recovery_tests {
1908    use super::*;
1909
1910    #[tokio::test]
1911    async fn native_materialization_rejects_unrepresentable_metadata()
1912    -> Result<(), Box<dyn std::error::Error>> {
1913        use crate::kernel::MetadataField;
1914
1915        let temporary = tempfile::tempdir()?;
1916        std::fs::write(temporary.path().join("file"), b"data")?;
1917        let root = Arc::new(HostRoot::open(temporary.path())?);
1918        #[cfg(unix)]
1919        for (metadata, expected) in [
1920            (
1921                FileMetadata {
1922                    posix_flags: MetadataField::Value(1),
1923                    ..FileMetadata::default()
1924                },
1925                "posix_flags",
1926            ),
1927            (
1928                FileMetadata {
1929                    changed_ns: MetadataField::Value(123),
1930                    ..FileMetadata::default()
1931                },
1932                "changed_ns without native-view baseline",
1933            ),
1934        ] {
1935            assert!(matches!(
1936                apply_host_metadata_offloaded(&root, Path::new("file"), metadata).await,
1937                Err(MaterializeError::UnsupportedMetadata(field)) if field == expected
1938            ));
1939        }
1940        #[cfg(windows)]
1941        let metadata = FileMetadata {
1942            posix_mode: MetadataField::Value(0o644),
1943            ..FileMetadata::default()
1944        };
1945        #[cfg(windows)]
1946        assert!(matches!(
1947            apply_host_metadata_offloaded(&root, Path::new("file"), metadata).await,
1948            Err(MaterializeError::UnsupportedMetadata(_))
1949        ));
1950        Ok(())
1951    }
1952
1953    #[test]
1954    fn same_path_restores_are_serialized_by_a_process_crash_safe_lock()
1955    -> Result<(), Box<dyn std::error::Error>> {
1956        let temporary = tempfile::tempdir()?;
1957        let root = temporary.path().join("root");
1958        std::fs::create_dir(&root)?;
1959        let relative = Path::new("entry");
1960        let destination = root.join(relative);
1961        let first = acquire_restore_lock(relative, &destination, true)?;
1962
1963        assert!(acquire_restore_lock(relative, &destination, false).is_err());
1964        drop(first);
1965        let _next = acquire_restore_lock(relative, &destination, false)?;
1966        Ok(())
1967    }
1968
1969    #[test]
1970    fn live_mount_recovery_restores_a_displaced_entry() -> Result<(), Box<dyn std::error::Error>> {
1971        let temporary = tempfile::tempdir()?;
1972        let root = temporary.path().join("root");
1973        std::fs::create_dir(&root)?;
1974        let relative = Path::new("entry");
1975        let destination = root.join(relative);
1976        std::fs::write(&destination, b"before")?;
1977        let parent = held_parent(&root, relative)?;
1978        let backup = restore_artifact_name(".acyclic-restore-backup-", relative);
1979        let witness =
1980            restore_witness_path(".acyclic-restore-live-witness-", relative, &destination);
1981        create_restore_witness(&witness, LIVE_RESTORE_WITNESS)?;
1982        parent.rename_to(relative, &parent, &backup)?;
1983
1984        recover_live_mount_replacement(&parent, relative, relative, &destination)?;
1985
1986        assert_eq!(std::fs::read(&destination)?, b"before");
1987        assert!(!root.join(backup).exists());
1988        Ok(())
1989    }
1990
1991    #[test]
1992    fn live_mount_recovery_keeps_a_published_entry() -> Result<(), Box<dyn std::error::Error>> {
1993        let temporary = tempfile::tempdir()?;
1994        let root = temporary.path().join("root");
1995        std::fs::create_dir(&root)?;
1996        let relative = Path::new("entry");
1997        let destination = root.join(relative);
1998        std::fs::write(&destination, b"after")?;
1999        let backup = restore_artifact_name(".acyclic-restore-backup-", relative);
2000        let witness =
2001            restore_witness_path(".acyclic-restore-live-witness-", relative, &destination);
2002        create_restore_witness(&witness, LIVE_RESTORE_WITNESS)?;
2003        std::fs::write(root.join(&backup), b"before")?;
2004        let parent = held_parent(&root, relative)?;
2005
2006        recover_live_mount_replacement(&parent, relative, relative, &destination)?;
2007
2008        assert_eq!(std::fs::read(&destination)?, b"after");
2009        assert!(!root.join(backup).exists());
2010        Ok(())
2011    }
2012
2013    #[test]
2014    fn live_mount_recovery_preserves_an_unowned_backup_collision()
2015    -> Result<(), Box<dyn std::error::Error>> {
2016        let temporary = tempfile::tempdir()?;
2017        let root = temporary.path().join("root");
2018        std::fs::create_dir(&root)?;
2019        let relative = Path::new("entry");
2020        let destination = root.join(relative);
2021        std::fs::write(&destination, b"live")?;
2022        let backup = restore_artifact_name(".acyclic-restore-backup-", relative);
2023        std::fs::write(root.join(&backup), b"unrelated")?;
2024        let parent = held_parent(&root, relative)?;
2025
2026        assert!(recover_live_mount_replacement(&parent, relative, relative, &destination).is_err());
2027        assert_eq!(std::fs::read(&destination)?, b"live");
2028        assert_eq!(std::fs::read(root.join(backup))?, b"unrelated");
2029        Ok(())
2030    }
2031
2032    #[test]
2033    fn absent_restore_moves_then_reclaims_the_old_entry() -> Result<(), Box<dyn std::error::Error>>
2034    {
2035        let temporary = tempfile::tempdir()?;
2036        let root = temporary.path().join("root");
2037        std::fs::create_dir(&root)?;
2038        let relative = Path::new("entry");
2039        std::fs::write(root.join(relative), b"old")?;
2040        let parent = held_parent(&root, relative)?;
2041
2042        remove_restored_path(&parent, relative, &root, relative)?;
2043
2044        assert!(!root.join(relative).exists());
2045        assert!(
2046            !root
2047                .join(restore_artifact_name(".acyclic-restore-removed-", relative))
2048                .exists()
2049        );
2050        Ok(())
2051    }
2052}
2053
2054#[cfg(all(test, any(target_os = "macos", windows)))]
2055mod clone_tests {
2056    use super::*;
2057    use crate::kernel::{LogicalName, NameEncoding, NamespacePath};
2058    use crate::model::{
2059        AccessMode, CaseSensitivity, CheckoutMode, ConcurrencyMode, ConsistencyMode,
2060        FilesystemProfile, GenerationSelector, Lifecycle, MutationMode, UnicodePolicy,
2061        VolumeConfig, VolumeLimits,
2062    };
2063    use crate::{Fs, VolumeId};
2064
2065    #[tokio::test]
2066    async fn identical_regular_payloads_use_native_clone_without_aliasing()
2067    -> Result<(), Box<dyn std::error::Error>> {
2068        #[cfg(target_os = "macos")]
2069        let directory = tempfile::tempdir()?;
2070        #[cfg(windows)]
2071        let directory = {
2072            if let Some(parent) = std::env::var_os("ACYCLIC_TEST_BLOCK_CLONE_ROOT") {
2073                tempfile::Builder::new()
2074                    .prefix("acyclic-materialize-clone-")
2075                    .tempdir_in(parent)?
2076            } else {
2077                tempfile::tempdir()?
2078            }
2079        };
2080        let clone_available =
2081            crate::probe_native_storage_capabilities(directory.path())?.block_cloning;
2082        let limits = VolumeLimits::default();
2083        let fs = Fs::memory();
2084        let cancellation = CancellationToken::new();
2085        let volume = fs
2086            .create_volume_with_id(
2087                VolumeId::from_bytes([73; 16]),
2088                VolumeConfig {
2089                    profile: FilesystemProfile::Portable,
2090                    concurrency: ConcurrencyMode::Optimistic,
2091                    lifecycle: Lifecycle::Ephemeral,
2092                    case_sensitivity: CaseSensitivity::Sensitive,
2093                    unicode: UnicodePolicy::Preserve,
2094                    symbolic_links: true,
2095                    hard_links: true,
2096                    sparse_files: true,
2097                    limits,
2098                },
2099                WorkBudget::UNBOUNDED,
2100                &cancellation,
2101            )
2102            .await?
2103            .value;
2104        let mut checkout = volume
2105            .checkout(
2106                GenerationSelector::Head,
2107                CheckoutMode {
2108                    access: AccessMode::ReadWrite,
2109                    consistency: ConsistencyMode::Pinned,
2110                    mutations: MutationMode::PrivateOverlay,
2111                },
2112                WorkBudget::UNBOUNDED,
2113                &cancellation,
2114            )
2115            .await?
2116            .value;
2117        // The payload deliberately ends between cluster boundaries: Windows
2118        // must clone the aligned body and copy the short tail.
2119        let bytes = Bytes::from(vec![0xA5; 128 * 1024 + 17]);
2120        for name in ["first.bin", "second.bin"] {
2121            let path = NamespacePath::new(
2122                vec![LogicalName::new(
2123                    NameEncoding::Utf8,
2124                    name.as_bytes().to_vec(),
2125                    limits.maximum_component_bytes,
2126                )?],
2127                limits,
2128            )?;
2129            checkout
2130                .create_file(path, bytes.clone(), WorkBudget::UNBOUNDED, &cancellation)
2131                .await?;
2132        }
2133        let destination = directory.path().join("view");
2134        std::fs::create_dir(&destination)?;
2135        let receipt = materialize_checkout(
2136            &mut checkout,
2137            &MaterializeOptions {
2138                destination: destination.clone(),
2139                maximum_directory_entries: 16,
2140                maximum_extent_spans: 16,
2141                transfer_bytes: 64 * 1024,
2142            },
2143            WorkBudget::UNBOUNDED,
2144            &cancellation,
2145        )
2146        .await?;
2147        assert_eq!(receipt.value.files, 2);
2148        assert_eq!(
2149            receipt.value.written_bytes,
2150            bytes.len() as u64 * if clone_available { 1 } else { 2 }
2151        );
2152        let first = destination.join("first.bin");
2153        let second = destination.join("second.bin");
2154        assert_eq!(std::fs::read(&first)?.as_slice(), bytes.as_ref());
2155        assert_eq!(std::fs::read(&second)?.as_slice(), bytes.as_ref());
2156        std::fs::write(&first, b"changed")?;
2157        assert_eq!(std::fs::read(&second)?.as_slice(), bytes.as_ref());
2158        Ok(())
2159    }
2160}