1#[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#[derive(Clone, Debug, Eq, PartialEq)]
29pub struct MaterializeOptions {
30 pub destination: PathBuf,
32 pub maximum_directory_entries: u32,
34 pub maximum_extent_spans: u32,
36 pub transfer_bytes: u64,
38}
39
40#[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 #[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#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
79pub struct MaterializationReceipt {
80 pub files: u64,
82 pub directories: u64,
84 pub symbolic_links: u64,
86 pub special_files: u64,
88 pub logical_file_bytes: u64,
90 pub written_bytes: u64,
92 pub work: WorkCounters,
94}
95
96#[derive(Clone, Copy, Debug, Eq, PartialEq)]
98pub enum HostPathReplacement {
99 Atomic,
101 LiveMount,
103}
104
105#[derive(Clone, Copy, Debug, Eq, PartialEq)]
107pub enum HostPathRestore {
108 Restored,
110 Removed,
112}
113
114#[derive(Debug, Error)]
116pub enum MaterializeError {
117 #[error("materialization destination must be an existing empty real directory")]
119 InvalidDestination,
120 #[error("canonical name cannot be represented exactly on this host")]
122 UnrepresentableName,
123 #[error("requested path is absent from the checkout")]
125 MissingPath,
126 #[error("requested host path is not a valid relative path")]
128 InvalidPath,
129 #[error("file kind {0:?} cannot be materialized exactly on this host")]
131 UnsupportedKind(FileKind),
132 #[error("metadata field {0} cannot be materialized exactly on this host")]
134 UnsupportedMetadata(&'static str),
135 #[error("materialization options are invalid")]
137 InvalidOptions,
138 #[error("materialization paths must be unique and non-overlapping")]
140 OverlappingPaths,
141 #[error("filesystem engine failed: {0}")]
143 Engine(String),
144 #[error("host filesystem operation failed: {0}")]
146 Io(#[from] std::io::Error),
147 #[error(transparent)]
149 Work(#[from] WorkError),
150}
151
152pub 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#[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#[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
451pub 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
483pub 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 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 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 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 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 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 #[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 #[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 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}