1use std::borrow::Cow;
39use std::collections::HashMap;
40use std::collections::VecDeque;
41use std::ffi::OsStr;
42use std::ffi::OsString;
43use std::io;
44use std::path::Component;
45use std::path::Path;
46use std::path::PathBuf;
47
48use crate::closed_enum::closed_enum;
49
50#[cfg(target_os = "macos")]
53pub const MAX_SYMLINK_HOPS: usize = 33;
54
55#[cfg(all(target_os = "linux", target_env = "musl"))]
58pub const MAX_SYMLINK_HOPS: usize = 39;
59
60#[cfg(all(target_os = "linux", not(target_env = "musl")))]
63pub const MAX_SYMLINK_HOPS: usize = 40;
64
65#[cfg(target_os = "windows")]
68pub const MAX_SYMLINK_HOPS: usize = 63;
69
70#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
73pub const MAX_SYMLINK_HOPS: usize = 32;
74
75pub const MAX_COMPONENT_LEN: usize = 255;
78
79#[cfg(target_os = "linux")]
82pub const MAX_PATH_LEN: usize = 4095;
83
84#[cfg(target_os = "windows")]
89pub const MAX_PATH_LEN: usize = 32767;
90
91#[cfg(not(any(target_os = "linux", target_os = "windows")))]
94pub const MAX_PATH_LEN: usize = 1023;
95
96#[cfg(target_os = "windows")]
104pub const MAX_SYMLINK_TARGET_LEN: usize = 4091;
105
106#[cfg(not(target_os = "windows"))]
109pub const MAX_SYMLINK_TARGET_LEN: usize = MAX_PATH_LEN;
110
111#[cfg(target_os = "windows")]
118pub const MAX_SYMLINK_TARGET_BYTES: usize = MAX_SYMLINK_TARGET_LEN * 3;
119
120#[cfg(not(target_os = "windows"))]
124pub const MAX_SYMLINK_TARGET_BYTES: usize = MAX_SYMLINK_TARGET_LEN;
125
126closed_enum! {
127 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
137 #[non_exhaustive]
138 pub enum LinkKind {
139 Symlink => "symlink",
141 Hardlink => "hardlink",
143 }
144}
145
146#[cfg(windows)]
147fn name_len(name: &OsStr) -> usize {
148 std::os::windows::ffi::OsStrExt::encode_wide(name).count()
149}
150
151#[cfg(not(windows))]
152fn name_len(name: &OsStr) -> usize {
153 name.as_encoded_bytes().len()
154}
155
156pub fn check_name_lengths(path: &Path) -> io::Result<()> {
162 let too_long = name_len(path.as_os_str()) > MAX_PATH_LEN
163 || path
164 .components()
165 .any(|c| name_len(c.as_os_str()) > MAX_COMPONENT_LEN);
166 if too_long {
167 return Err(name_too_long());
168 }
169 Ok(())
170}
171
172pub fn check_link_target_length(target: &Path, kind: LinkKind) -> io::Result<()> {
181 match kind {
182 LinkKind::Symlink if name_len(target.as_os_str()) > MAX_SYMLINK_TARGET_LEN => {
183 Err(name_too_long())
184 }
185 LinkKind::Symlink => Ok(()),
186 LinkKind::Hardlink => check_name_lengths(target),
187 }
188}
189
190#[must_use]
192pub fn name_too_long() -> io::Error {
193 io::Error::new(io::ErrorKind::InvalidFilename, "file name too long")
194}
195
196pub trait FsView {
198 fn exists(&self, path: &Path) -> bool;
200
201 fn is_dir(&self, path: &Path) -> bool;
203
204 fn is_symlink(&self, path: &Path) -> io::Result<bool>;
211
212 fn canonicalize(&self, path: &Path) -> io::Result<PathBuf>;
219}
220
221#[derive(Debug, Clone, Copy, Default)]
223pub struct HostFs;
224
225impl FsView for HostFs {
226 fn exists(&self, path: &Path) -> bool {
227 path.exists()
228 }
229
230 fn is_dir(&self, path: &Path) -> bool {
231 path.is_dir()
232 }
233
234 fn is_symlink(&self, path: &Path) -> io::Result<bool> {
235 std::fs::symlink_metadata(path).map(|m| m.file_type().is_symlink())
236 }
237
238 fn canonicalize(&self, path: &Path) -> io::Result<PathBuf> {
239 std::fs::canonicalize(path)
240 }
241}
242
243#[derive(Debug, Clone, Copy, PartialEq, Eq)]
245pub enum CollisionKind {
246 Directories,
249 DirectoriesWithContents,
252 Files,
254 Mixed,
257}
258
259#[derive(Debug, Clone, PartialEq, Eq)]
261pub struct NameCollision {
262 pub existing: PathBuf,
264 pub incoming: PathBuf,
266 pub kind: CollisionKind,
268}
269
270impl CollisionKind {
271 fn of(existing: &Node, incoming: &Node, existing_has_contents: bool) -> Self {
272 match (existing, incoming) {
273 (Node::Dir, Node::Dir) if existing_has_contents => Self::DirectoriesWithContents,
274 (Node::Dir, Node::Dir) => Self::Directories,
275 (Node::File, Node::File) => Self::Files,
276 _ => Self::Mixed,
277 }
278 }
279}
280
281#[derive(Debug, Clone, Copy, PartialEq, Eq)]
283pub enum Occupant {
284 File,
286 Directory,
288 Symlink,
290}
291
292impl Occupant {
293 pub(crate) const fn noun(self) -> &'static str {
295 match self {
296 Self::File => "file",
297 Self::Directory => "directory",
298 Self::Symlink => "symlink",
299 }
300 }
301}
302
303#[derive(Debug)]
305pub struct Conflict {
306 pub(crate) error: io::Error,
308 pub(crate) occupant: Option<Occupant>,
310}
311
312impl Conflict {
313 pub(crate) fn already_exists(occupant: Occupant) -> Self {
314 Self {
315 error: io::Error::from(io::ErrorKind::AlreadyExists),
316 occupant: Some(occupant),
317 }
318 }
319}
320
321impl From<io::Error> for Conflict {
322 fn from(error: io::Error) -> Self {
323 Self {
324 error,
325 occupant: None,
326 }
327 }
328}
329
330type FoldFn = for<'a> fn(&'a OsStr) -> Cow<'a, OsStr>;
331
332#[derive(Debug, Clone, PartialEq, Eq)]
334pub enum Node {
335 Dir,
337 File,
339 Symlink(PathBuf),
341 OpaqueSymlink,
345}
346
347#[derive(Debug)]
348struct TreeNode {
349 kind: Node,
350 name: OsString,
352 children: HashMap<OsString, usize>,
356 folded: HashMap<OsString, usize>,
358 empty_merges: Vec<(PathBuf, PathBuf)>,
361}
362
363impl TreeNode {
364 fn new(kind: Node, name: &OsStr) -> Self {
365 Self {
366 kind,
367 name: name.to_os_string(),
368 children: HashMap::new(),
369 folded: HashMap::new(),
370 empty_merges: Vec::new(),
371 }
372 }
373}
374
375const ROOT: usize = 0;
376
377enum Resolved {
378 Inside { rel: PathBuf, id: usize },
379 Outside(PathBuf),
380}
381
382#[derive(Clone, Copy)]
384struct Found {
385 id: usize,
386 alias: bool,
388}
389
390pub struct VirtualTree {
397 root: PathBuf,
398 nodes: Vec<TreeNode>,
399 fold: FoldFn,
400 collisions: Vec<NameCollision>,
401 empty_merge_nodes: Vec<usize>,
403}
404
405impl VirtualTree {
406 pub fn new(root: &Path) -> Self {
408 Self::with_fold(root, fold)
409 }
410
411 pub(crate) fn with_fold(root: &Path, fold: FoldFn) -> Self {
412 Self {
413 root: root.to_path_buf(),
414 nodes: vec![TreeNode::new(Node::Dir, OsStr::new(""))],
415 fold,
416 collisions: Vec::new(),
417 empty_merge_nodes: Vec::new(),
418 }
419 }
420
421 pub(crate) fn try_insert(&mut self, rel: &Path, node: Node) -> Result<(), Conflict> {
450 let Some(name) = rel.file_name() else {
451 return Ok(());
452 };
453 let parent = self.ensure_dir(rel.parent().unwrap_or_else(|| Path::new("")))?;
454 let existing = self.lookup(parent, name);
455 if let Some(found) = existing {
456 self.settle_alias(parent, found, name, &node, rel, false);
457 }
458 match (existing.map(|f| f.id), node) {
459 (None, node) => {
460 self.add_child(parent, name, node);
461 Ok(())
462 }
463 (Some(id), Node::Dir) => match self.node(id).kind {
464 Node::Dir => Ok(()),
465 Node::File => Err(Conflict::already_exists(Occupant::File)),
466 Node::OpaqueSymlink => Err(Conflict::already_exists(Occupant::Symlink)),
467 Node::Symlink(_) => {
468 if self.resolves_to_dir(&self.root.join(rel)) {
469 Ok(())
470 } else {
471 Err(Conflict::already_exists(Occupant::Symlink))
472 }
473 }
474 },
475 (Some(id), node @ (Node::Symlink(_) | Node::OpaqueSymlink)) => {
476 match self.node(id).kind {
477 Node::File => {
478 self.node_mut(id).kind = node;
479 Err(Conflict::already_exists(Occupant::File))
480 }
481 Node::Dir => Err(Conflict::already_exists(Occupant::Directory)),
482 Node::Symlink(ref kept) if matches!(&node, Node::Symlink(t) if t == kept) => {
483 Ok(())
484 }
485 Node::Symlink(_) | Node::OpaqueSymlink => {
486 Err(Conflict::already_exists(Occupant::Symlink))
487 }
488 }
489 }
490 (Some(id), Node::File) => match self.node(id).kind {
491 Node::File => Ok(()),
492 Node::Dir => Err(Conflict {
493 error: io::Error::from(io::ErrorKind::IsADirectory),
494 occupant: Some(Occupant::Directory),
495 }),
496 Node::Symlink(_) | Node::OpaqueSymlink => {
497 Err(Conflict::already_exists(Occupant::Symlink))
498 }
499 },
500 }
501 }
502
503 #[cfg(test)]
504 pub(crate) fn insert(&mut self, rel: &Path, node: Node) -> io::Result<()> {
505 self.try_insert(rel, node)
506 .map_err(|conflict| conflict.error)
507 }
508
509 pub(crate) fn occupant(&self, rel: &Path) -> Option<Occupant> {
512 let name = rel.file_name()?;
513 let parent = match self.resolve(&self.root.join(rel.parent()?)) {
514 Ok(Resolved::Inside { id, .. }) if self.node(id).kind == Node::Dir => id,
515 _ => return None,
516 };
517 let found = self.lookup(parent, name)?;
518 Some(match self.node(found.id).kind {
519 Node::Dir => Occupant::Directory,
520 Node::File => Occupant::File,
521 Node::Symlink(_) | Node::OpaqueSymlink => Occupant::Symlink,
522 })
523 }
524
525 pub fn resolves_to_dir(&self, path: &Path) -> bool {
527 matches!(
528 self.resolve(path),
529 Ok(Resolved::Inside { id, .. }) if self.node(id).kind == Node::Dir
530 )
531 }
532
533 pub(crate) fn take_collisions(&mut self) -> Vec<NameCollision> {
539 std::mem::take(&mut self.collisions)
540 }
541
542 pub(crate) fn take_empty_merges(&mut self) -> Vec<NameCollision> {
544 let nodes = std::mem::take(&mut self.empty_merge_nodes);
545 nodes
546 .into_iter()
547 .flat_map(|id| std::mem::take(&mut self.node_mut(id).empty_merges))
548 .map(|(existing, incoming)| NameCollision {
549 existing,
550 incoming,
551 kind: CollisionKind::Directories,
552 })
553 .collect()
554 }
555
556 fn settle_alias(
563 &mut self,
564 parent: usize,
565 found: Found,
566 name: &OsStr,
567 incoming_node: &Node,
568 incoming: &Path,
569 incoming_has_contents: bool,
570 ) {
571 if !found.alias {
572 return;
573 }
574 self.node_mut(parent)
575 .children
576 .insert(name.to_os_string(), found.id);
577 let existing = self.node(found.id);
578 let has_contents = incoming_has_contents || !existing.children.is_empty();
579 let kind = CollisionKind::of(&existing.kind, incoming_node, has_contents);
580 let existing_path = incoming.with_file_name(&existing.name);
581 if kind == CollisionKind::Directories {
582 let node = self.node_mut(found.id);
583 node.empty_merges
584 .push((existing_path, incoming.to_path_buf()));
585 self.empty_merge_nodes.push(found.id);
586 return;
587 }
588 self.collisions.push(NameCollision {
589 existing: existing_path,
590 incoming: incoming.to_path_buf(),
591 kind,
592 });
593 }
594
595 fn node(&self, id: usize) -> &TreeNode {
596 &self.nodes[id]
597 }
598
599 fn node_mut(&mut self, id: usize) -> &mut TreeNode {
600 &mut self.nodes[id]
601 }
602
603 fn lookup(&self, parent: usize, name: &OsStr) -> Option<Found> {
604 if name_len(name) > MAX_COMPONENT_LEN {
605 return None;
606 }
607 let node = self.node(parent);
608 if let Some(&id) = node.children.get(name) {
609 return Some(Found { id, alias: false });
610 }
611 match (self.fold)(name) {
612 Cow::Borrowed(_) => None,
613 Cow::Owned(key) => node.folded.get(&key).map(|&id| Found { id, alias: true }),
614 }
615 }
616
617 fn child(&self, parent: usize, name: &OsStr) -> Option<usize> {
618 self.lookup(parent, name).map(|found| found.id)
619 }
620
621 fn add_child(&mut self, parent: usize, name: &OsStr, kind: Node) -> usize {
623 let id = self.nodes.len();
624 self.nodes.push(TreeNode::new(kind, name));
625 let key = match (self.fold)(name) {
626 Cow::Borrowed(_) => None,
627 Cow::Owned(key) => Some(key),
628 };
629 let upgrade = {
630 let parent_node = self.node_mut(parent);
631 parent_node.children.insert(name.to_os_string(), id);
632 if let Some(key) = key {
633 parent_node.folded.insert(key, id);
634 }
635 std::mem::take(&mut parent_node.empty_merges)
636 };
637 for (existing, incoming) in upgrade {
638 self.collisions.push(NameCollision {
639 existing,
640 incoming,
641 kind: CollisionKind::DirectoriesWithContents,
642 });
643 }
644 id
645 }
646
647 fn ensure_dir(&mut self, rel: &Path) -> io::Result<usize> {
648 let mut current = ROOT;
649 let mut walked = PathBuf::new();
650 for component in rel.components() {
651 let name = match component {
652 Component::Normal(name) => name,
653 Component::CurDir => continue,
654 _ => return Err(io::Error::from(io::ErrorKind::InvalidInput)),
655 };
656 walked.push(name);
657 current = match self.lookup(current, name) {
658 None => self.add_child(current, name, Node::Dir),
659 Some(found) => {
660 self.settle_alias(current, found, name, &Node::Dir, &walked, true);
661 match self.node(found.id).kind {
662 Node::Dir => found.id,
663 Node::File => return Err(io::Error::from(io::ErrorKind::NotADirectory)),
664 Node::OpaqueSymlink => return Err(unread_target_error()),
665 Node::Symlink(_) => match self.resolve(&self.root.join(&walked))? {
666 Resolved::Inside { id, .. } if self.node(id).kind == Node::Dir => id,
667 Resolved::Inside { .. } => {
668 return Err(io::Error::from(io::ErrorKind::NotADirectory));
669 }
670 Resolved::Outside(_) => {
671 return Err(io::Error::other("path leaves the extraction root"));
672 }
673 },
674 }
675 }
676 };
677 }
678 Ok(current)
679 }
680
681 fn resolve(&self, path: &Path) -> io::Result<Resolved> {
682 let Ok(rel) = path.strip_prefix(&self.root) else {
683 return Ok(Resolved::Outside(lexical(path.components())));
684 };
685 let mut pending: VecDeque<Component<'_>> = rel.components().collect();
686 let mut stack = vec![ROOT];
687 let mut current = PathBuf::new();
688 let mut hops = 0;
689
690 while let Some(component) = pending.pop_front() {
691 match component {
692 Component::CurDir => {}
693 Component::ParentDir => {
694 if stack.len() == 1 {
695 return Ok(Resolved::Outside(self.leave_root(pending)));
696 }
697 stack.pop();
698 current.pop();
699 }
700 Component::Normal(name) => {
701 let top = stack.last().copied().unwrap_or(ROOT);
702 let Some(id) = self.child(top, name) else {
703 return Err(io::Error::from(io::ErrorKind::NotFound));
704 };
705 match &self.node(id).kind {
706 Node::Dir => {
707 stack.push(id);
708 current.push(name);
709 }
710 Node::File => {
711 if !pending.is_empty() {
712 return Err(io::Error::from(io::ErrorKind::NotADirectory));
713 }
714 stack.push(id);
715 current.push(name);
716 }
717 Node::OpaqueSymlink => {
718 if !pending.is_empty() {
719 return Err(unread_target_error());
720 }
721 stack.push(id);
722 current.push(name);
723 }
724 Node::Symlink(target) => {
725 hops += 1;
726 if hops > MAX_SYMLINK_HOPS {
727 return Err(io::Error::other("too many levels of symbolic links"));
728 }
729 if target.has_root()
730 || target.components().next().is_some_and(is_prefix)
731 {
732 return Ok(Resolved::Outside(lexical(
733 target.components().chain(pending),
734 )));
735 }
736 for part in target.components().rev() {
737 pending.push_front(part);
738 }
739 }
740 }
741 }
742 Component::RootDir | Component::Prefix(_) => {
743 return Ok(Resolved::Outside(lexical(
744 std::iter::once(component).chain(pending),
745 )));
746 }
747 }
748 }
749 Ok(Resolved::Inside {
750 rel: current,
751 id: stack.last().copied().unwrap_or(ROOT),
752 })
753 }
754
755 fn leave_root(&self, remaining: VecDeque<Component<'_>>) -> PathBuf {
756 let base = self.root.parent().unwrap_or(&self.root);
757 let mut out = base.to_path_buf();
758 for component in remaining {
759 match component {
760 Component::ParentDir => {
761 out.pop();
762 }
763 Component::Normal(name) => out.push(name),
764 _ => {}
765 }
766 }
767 out
768 }
769}
770
771impl FsView for VirtualTree {
772 fn exists(&self, path: &Path) -> bool {
773 matches!(self.resolve(path), Ok(Resolved::Inside { .. }))
774 }
775
776 fn is_dir(&self, path: &Path) -> bool {
777 self.resolves_to_dir(path)
778 }
779
780 fn is_symlink(&self, path: &Path) -> io::Result<bool> {
781 let not_found = || io::Error::from(io::ErrorKind::NotFound);
782 let Ok(rel) = path.strip_prefix(&self.root) else {
783 return Err(not_found());
784 };
785 let Some(name) = rel.file_name() else {
786 return Ok(false);
787 };
788 let parent = self
789 .root
790 .join(rel.parent().unwrap_or_else(|| Path::new("")));
791 let Resolved::Inside { id: parent, .. } = self.resolve(&parent)? else {
792 return Err(not_found());
793 };
794 let id = self.child(parent, name).ok_or_else(not_found)?;
795 Ok(matches!(
796 self.node(id).kind,
797 Node::Symlink(_) | Node::OpaqueSymlink
798 ))
799 }
800
801 fn canonicalize(&self, path: &Path) -> io::Result<PathBuf> {
802 match self.resolve(path)? {
803 Resolved::Inside { id, .. } if self.node(id).kind == Node::OpaqueSymlink => {
804 Err(unread_target_error())
805 }
806 Resolved::Inside { rel, .. } => Ok(self.root.join(rel)),
807 Resolved::Outside(path) => Ok(path),
808 }
809 }
810}
811
812fn unread_target_error() -> io::Error {
813 io::Error::other("path runs through a symlink whose target was not read")
814}
815
816fn is_prefix(component: Component<'_>) -> bool {
817 matches!(component, Component::Prefix(_))
818}
819
820fn lexical<'a>(components: impl Iterator<Item = Component<'a>>) -> PathBuf {
821 let mut out = PathBuf::new();
822 for component in components {
823 match component {
824 Component::ParentDir => {
825 out.pop();
826 }
827 Component::CurDir => {}
828 other => out.push(other),
829 }
830 }
831 out
832}
833
834#[cfg(any(target_os = "macos", target_os = "windows"))]
839fn fold(name: &OsStr) -> Cow<'_, OsStr> {
840 let text = name.to_string_lossy();
841 let folded = if text.is_ascii() {
842 text.to_ascii_uppercase()
843 } else {
844 fold_unicode(&text)
845 };
846 Cow::Owned(OsString::from(folded))
847}
848
849#[cfg(not(any(target_os = "macos", target_os = "windows")))]
850fn fold(name: &OsStr) -> Cow<'_, OsStr> {
851 Cow::Borrowed(name)
852}
853
854#[cfg(target_os = "macos")]
856fn fold_unicode(text: &str) -> String {
857 use unicode_normalization::UnicodeNormalization;
858 upper_lower_upper(text.nfd())
859}
860
861#[cfg(target_os = "windows")]
863fn fold_unicode(text: &str) -> String {
864 upper_lower_upper(text.chars())
865}
866
867#[cfg(any(target_os = "macos", target_os = "windows"))]
872fn upper_lower_upper(chars: impl Iterator<Item = char>) -> String {
873 chars
874 .flat_map(char::to_uppercase)
875 .flat_map(char::to_lowercase)
876 .flat_map(char::to_uppercase)
877 .collect()
878}
879
880#[cfg(test)]
881#[allow(clippy::unwrap_used, clippy::expect_used)]
882mod tests {
883 use super::*;
884
885 fn tree() -> VirtualTree {
886 VirtualTree::new(Path::new("/dest"))
887 }
888
889 fn put(t: &mut VirtualTree, path: &str, node: Node) {
890 t.insert(Path::new(path), node).unwrap();
891 }
892
893 fn sym(target: &str) -> Node {
894 Node::Symlink(PathBuf::from(target))
895 }
896
897 #[test]
898 fn insert_creates_implicit_parents() {
899 let mut t = tree();
900 put(&mut t, "a/b/c.txt", Node::File);
901 assert!(t.exists(Path::new("/dest/a/b")));
902 assert!(t.exists(Path::new("/dest/a/b/c.txt")));
903 assert!(!t.exists(Path::new("/dest/a/x")));
904 }
905
906 #[test]
907 fn missing_component_is_not_found() {
908 let t = tree();
909 let err = t.canonicalize(Path::new("/dest/nope")).unwrap_err();
910 assert_eq!(err.kind(), io::ErrorKind::NotFound);
911 }
912
913 #[test]
914 fn in_root_chain_resolves_to_final_location() {
915 let mut t = tree();
916 put(&mut t, "real", Node::Dir);
917 put(&mut t, "b", sym("real"));
918 put(&mut t, "a", sym("b"));
919 assert_eq!(
920 t.canonicalize(Path::new("/dest/a")).unwrap(),
921 PathBuf::from("/dest/real")
922 );
923 }
924
925 #[test]
926 fn chain_leaving_root_yields_outside_path() {
927 let mut t = tree();
928 put(&mut t, "y", sym("."));
929 put(&mut t, "d", sym("y/.."));
930 put(&mut t, "e", sym("d/.."));
931 let resolved = t.canonicalize(Path::new("/dest/e/secret")).unwrap();
932 assert!(!resolved.starts_with("/dest"), "{resolved:?}");
933 }
934
935 #[test]
936 fn symlink_loop_is_an_error() {
937 let mut t = tree();
938 put(&mut t, "a", sym("b"));
939 put(&mut t, "b", sym("a"));
940 assert!(t.canonicalize(Path::new("/dest/a")).is_err());
941 }
942
943 #[test]
944 fn long_chain_hits_hop_limit() {
945 let mut t = tree();
946 put(&mut t, "l0", Node::Dir);
947 for i in 1..=MAX_SYMLINK_HOPS + 1 {
948 put(&mut t, &format!("l{i}"), sym(&format!("l{}", i - 1)));
949 }
950 let over = format!("/dest/l{}", MAX_SYMLINK_HOPS + 1);
951 let at = format!("/dest/l{MAX_SYMLINK_HOPS}");
952 assert!(t.canonicalize(Path::new(&over)).is_err());
953 assert!(t.canonicalize(Path::new(&at)).is_ok());
954 }
955
956 #[cfg(any(unix, windows))]
957 fn make_symlink(target: &str, link: &Path) -> io::Result<()> {
958 #[cfg(unix)]
959 return std::os::unix::fs::symlink(target, link);
960 #[cfg(windows)]
961 return std::os::windows::fs::symlink_dir(target, link);
962 }
963
964 #[cfg(any(unix, windows))]
965 #[test]
966 fn hop_limit_matches_host_canonicalize() {
967 let temp = tempfile::TempDir::new().unwrap();
968 let root = temp.path().canonicalize().unwrap();
971 std::fs::create_dir(root.join("l0")).unwrap();
972
973 let mut resolvable = 0;
974 for i in 1..=MAX_SYMLINK_HOPS + 8 {
975 let linked = make_symlink(&format!("l{}", i - 1), &root.join(format!("l{i}")));
976 assert!(linked.is_ok(), "cannot create symlinks here: {linked:?}");
977 if std::fs::canonicalize(root.join(format!("l{i}"))).is_err() {
978 break;
979 }
980 resolvable = i;
981 }
982 assert_eq!(resolvable, MAX_SYMLINK_HOPS, "host and model limits differ");
983
984 let mut t = tree();
985 put(&mut t, "l0", Node::Dir);
986 for i in 1..=resolvable + 1 {
987 put(&mut t, &format!("l{i}"), sym(&format!("l{}", i - 1)));
988 }
989 assert!(
990 t.canonicalize(Path::new(&format!("/dest/l{resolvable}")))
991 .is_ok()
992 );
993 assert!(
994 t.canonicalize(Path::new(&format!("/dest/l{}", resolvable + 1)))
995 .is_err()
996 );
997 }
998
999 fn ascii_upper(name: &OsStr) -> Cow<'_, OsStr> {
1000 Cow::Owned(OsString::from(name.to_string_lossy().to_ascii_uppercase()))
1001 }
1002
1003 fn folding_tree() -> VirtualTree {
1004 VirtualTree::with_fold(Path::new("/dest"), ascii_upper)
1005 }
1006
1007 fn collisions_of(t: &mut VirtualTree) -> Vec<(String, String, CollisionKind)> {
1008 t.take_collisions()
1009 .into_iter()
1010 .map(|c| {
1011 (
1012 c.existing.display().to_string().replace('\\', "/"),
1013 c.incoming.display().to_string().replace('\\', "/"),
1014 c.kind,
1015 )
1016 })
1017 .collect()
1018 }
1019
1020 #[test]
1021 fn exact_repeat_is_not_a_collision() {
1022 let mut t = folding_tree();
1023 put(&mut t, "a/f", Node::File);
1024 put(&mut t, "a/f", Node::File);
1025 assert!(t.take_collisions().is_empty());
1026 }
1027
1028 #[test]
1029 fn differently_spelled_files_collide() {
1030 let mut t = folding_tree();
1031 put(&mut t, "d/a.txt", Node::File);
1032 put(&mut t, "d/A.txt", Node::File);
1033 assert_eq!(
1034 collisions_of(&mut t),
1035 [("d/a.txt".into(), "d/A.txt".into(), CollisionKind::Files)]
1036 );
1037 assert!(t.take_collisions().is_empty());
1038 }
1039
1040 #[test]
1041 fn differently_spelled_parent_directories_collide() {
1042 let mut t = folding_tree();
1043 put(&mut t, "Images/x", Node::File);
1044 put(&mut t, "images/y", Node::File);
1045 assert_eq!(
1046 collisions_of(&mut t),
1047 [(
1048 "Images".into(),
1049 "images".into(),
1050 CollisionKind::DirectoriesWithContents
1051 )]
1052 );
1053 }
1054
1055 #[test]
1056 fn a_spelling_is_reported_once_however_many_entries_use_it() {
1057 let mut t = folding_tree();
1058 put(&mut t, "Images/a", Node::File);
1059 for i in 0..100 {
1060 put(&mut t, &format!("images/f{i}"), Node::File);
1061 }
1062 assert_eq!(collisions_of(&mut t).len(), 1);
1063 }
1064
1065 #[test]
1066 fn empty_directories_merge_harmlessly_until_one_gains_an_entry() {
1067 let mut t = folding_tree();
1068 put(&mut t, "Dir", Node::Dir);
1069 put(&mut t, "dir", Node::Dir);
1070 assert!(collisions_of(&mut t).is_empty());
1071 put(&mut t, "Dir/f", Node::File);
1072 put(&mut t, "dir/g", Node::File);
1073 assert_eq!(
1074 collisions_of(&mut t),
1075 [(
1076 "Dir".into(),
1077 "dir".into(),
1078 CollisionKind::DirectoriesWithContents
1079 )]
1080 );
1081 }
1082
1083 #[test]
1084 fn empty_merges_are_reported_once_when_nothing_lands_in_them() {
1085 let mut t = folding_tree();
1086 put(&mut t, "Dir", Node::Dir);
1087 put(&mut t, "dir", Node::Dir);
1088 put(&mut t, "other", Node::File);
1089 let merges: Vec<_> = t
1090 .take_empty_merges()
1091 .into_iter()
1092 .map(|c| {
1093 (
1094 c.existing.display().to_string(),
1095 c.incoming.display().to_string(),
1096 c.kind,
1097 )
1098 })
1099 .collect();
1100 assert_eq!(
1101 merges,
1102 [("Dir".into(), "dir".into(), CollisionKind::Directories)]
1103 );
1104 assert!(t.take_empty_merges().is_empty());
1105 }
1106
1107 #[test]
1108 fn directory_entry_over_a_directory_with_contents_is_not_harmless() {
1109 let mut t = folding_tree();
1110 put(&mut t, "Dir/f", Node::File);
1111 put(&mut t, "dir", Node::Dir);
1112 assert_eq!(
1113 collisions_of(&mut t),
1114 [(
1115 "Dir".into(),
1116 "dir".into(),
1117 CollisionKind::DirectoriesWithContents
1118 )]
1119 );
1120 }
1121
1122 #[test]
1123 fn names_are_folded_once_however_often_they_repeat() {
1124 use std::sync::atomic::AtomicUsize;
1125 use std::sync::atomic::Ordering;
1126
1127 static CALLS: AtomicUsize = AtomicUsize::new(0);
1128 fn counting(name: &OsStr) -> Cow<'_, OsStr> {
1129 CALLS.fetch_add(1, Ordering::Relaxed);
1130 ascii_upper(name)
1131 }
1132
1133 let mut t = VirtualTree::with_fold(Path::new("/dest"), counting);
1134 for i in 0..500 {
1135 put(&mut t, &format!("a/b/c/d/file{i}"), Node::File);
1136 }
1137 assert!(CALLS.load(Ordering::Relaxed) <= 2 * 504 + 8);
1140 }
1141
1142 #[test]
1143 fn collision_across_kinds_or_with_a_symlink_is_mixed() {
1144 let mut t = folding_tree();
1145 put(&mut t, "a", Node::File);
1146 let _ = t.insert(Path::new("A"), Node::Dir);
1147 put(&mut t, "link", sym("a"));
1148 let _ = t.insert(Path::new("LINK"), Node::File);
1149 assert_eq!(
1150 collisions_of(&mut t),
1151 [
1152 ("a".into(), "A".into(), CollisionKind::Mixed),
1153 ("link".into(), "LINK".into(), CollisionKind::Mixed)
1154 ]
1155 );
1156 }
1157
1158 #[test]
1159 fn symlink_over_an_existing_dir_is_refused_and_keeps_the_dir() {
1160 let mut t = folding_tree();
1161 put(&mut t, "d/f", Node::File);
1162 let err = t.insert(Path::new("d"), sym("sub")).unwrap_err();
1163 assert_eq!(err.kind(), io::ErrorKind::AlreadyExists);
1164 let err = t.insert(Path::new("d"), Node::OpaqueSymlink).unwrap_err();
1165 assert_eq!(err.kind(), io::ErrorKind::AlreadyExists);
1166 assert!(t.resolves_to_dir(&t.root.join("d")));
1167 }
1168
1169 #[test]
1170 fn collision_is_recorded_when_insert_fails() {
1171 let mut t = folding_tree();
1172 put(&mut t, "y", Node::File);
1173 let err = t.insert(Path::new("Y"), sym(".")).unwrap_err();
1174 assert_eq!(err.kind(), io::ErrorKind::AlreadyExists);
1175 assert_eq!(
1176 collisions_of(&mut t),
1177 [("y".into(), "Y".into(), CollisionKind::Mixed)]
1178 );
1179 }
1180
1181 #[cfg(target_os = "macos")]
1182 mod macos {
1183 use super::super::fold;
1184 use super::*;
1185
1186 fn key(s: &str) -> OsString {
1187 fold(OsStr::new(s)).into_owned()
1188 }
1189
1190 #[test]
1191 fn special_case_pairs_fold_equal() {
1192 assert_eq!(key("\u{df}"), key("\u{1e9e}"));
1193 assert_eq!(key("\u{3b8}"), key("\u{3f4}"));
1194 assert_eq!(key("\u{3b8}"), key("\u{3d1}"));
1195 assert_eq!(key("\u{3c2}"), key("\u{3a3}"));
1196 }
1197
1198 #[test]
1199 fn canonically_equivalent_names_fold_equal() {
1200 assert_eq!(key("caf\u{e9}"), key("cafe\u{301}"));
1201 assert_eq!(key("\u{ac00}"), key("\u{1100}\u{1161}"));
1202 }
1203
1204 #[test]
1205 fn nfc_and_nfd_spellings_collide_in_the_tree() {
1206 let mut t = tree();
1207 put(&mut t, "caf\u{e9}", Node::File);
1208 put(&mut t, "cafe\u{301}", Node::File);
1209 assert_eq!(t.take_collisions().len(), 1);
1210 }
1211
1212 #[test]
1213 fn fold_never_under_merges_what_apfs_treats_as_equal() {
1214 let temp = tempfile::TempDir::new().unwrap();
1215 let root = temp.path().canonicalize().unwrap();
1216 let mut seen = std::collections::HashSet::new();
1217 let mut under = Vec::new();
1218 let candidates = (0x1..=0x3000)
1221 .chain((0xAC00..=0xD7A3).step_by(11))
1222 .chain(0xF900..=0xFFFF)
1223 .chain(0x1_0400..=0x1_044F)
1224 .chain(0x1_E900..=0x1_E943);
1225 for code in candidates {
1226 let Some(c) = char::from_u32(code).filter(|c| !matches!(c, '/' | '.')) else {
1227 continue;
1228 };
1229 let name = c.to_string();
1230 let path = root.join(&name);
1231 let host_equal = path.symlink_metadata().is_ok();
1232 if !host_equal && std::fs::File::create_new(&path).is_err() {
1233 continue;
1234 }
1235 let model_seen = !seen.insert(key(&name));
1236 if host_equal && !model_seen {
1237 under.push(format!("U+{code:04X}"));
1238 }
1239 }
1240 assert!(under.is_empty(), "under-merged: {under:?}");
1241 }
1242 }
1243
1244 #[cfg(target_os = "windows")]
1245 #[test]
1246 fn fold_covers_ntfs_simple_uppercase() {
1247 use std::collections::HashMap;
1248
1249 let mut by_upper: HashMap<char, OsString> = HashMap::new();
1250 for code in 0x1..=0x1_FFFFu32 {
1251 let Some(c) = char::from_u32(code) else {
1252 continue;
1253 };
1254 let mut upper = c.to_uppercase();
1255 let simple = match (upper.next(), upper.next()) {
1256 (Some(u), None) => u,
1257 _ => c,
1258 };
1259 let key = fold(OsStr::new(&c.to_string())).into_owned();
1260 let expected = by_upper.entry(simple).or_insert_with(|| key.clone());
1261 assert_eq!(*expected, key, "U+{code:04X}");
1262 }
1263 }
1264
1265 #[test]
1266 fn dangling_symlink_does_not_exist_but_is_a_symlink() {
1267 let mut t = tree();
1268 put(&mut t, "d", sym("missing"));
1269 assert!(!t.exists(Path::new("/dest/d")));
1270 assert!(t.is_symlink(Path::new("/dest/d")).unwrap());
1271 }
1272
1273 #[test]
1274 fn opaque_symlink_exists_and_is_a_symlink_but_not_a_directory() {
1275 let mut t = tree();
1276 put(&mut t, "d", Node::OpaqueSymlink);
1277 assert!(t.exists(Path::new("/dest/d")));
1278 assert!(t.is_symlink(Path::new("/dest/d")).unwrap());
1279 assert!(!t.resolves_to_dir(Path::new("/dest/d")));
1280 }
1281
1282 #[test]
1283 fn nothing_resolves_through_an_opaque_symlink() {
1284 let mut t = tree();
1285 put(&mut t, "d", Node::OpaqueSymlink);
1286 assert!(t.canonicalize(Path::new("/dest/d")).is_err());
1287 assert!(t.canonicalize(Path::new("/dest/d/x")).is_err());
1288 assert!(t.insert(Path::new("d/f"), Node::File).is_err());
1289 }
1290
1291 #[test]
1292 fn a_chain_ending_at_an_opaque_symlink_does_not_resolve() {
1293 let mut t = tree();
1294 put(&mut t, "d", Node::OpaqueSymlink);
1295 put(&mut t, "l", sym("d"));
1296 assert!(t.canonicalize(Path::new("/dest/l")).is_err());
1297 }
1298
1299 #[test]
1300 fn opaque_symlink_keeps_its_place_like_a_symlink() {
1301 let mut t = tree();
1302 put(&mut t, "d", Node::OpaqueSymlink);
1303 assert_eq!(
1304 t.insert(Path::new("d"), Node::Dir).unwrap_err().kind(),
1305 io::ErrorKind::AlreadyExists
1306 );
1307 assert_eq!(
1308 t.insert(Path::new("d"), Node::File).unwrap_err().kind(),
1309 io::ErrorKind::AlreadyExists
1310 );
1311 assert!(t.is_symlink(Path::new("/dest/d")).unwrap());
1312
1313 put(&mut t, "f", Node::File);
1314 assert!(t.insert(Path::new("f"), Node::OpaqueSymlink).is_err());
1315 assert!(t.is_symlink(Path::new("/dest/f")).unwrap());
1316 }
1317
1318 #[test]
1319 fn opaque_symlink_collision_is_mixed() {
1320 let mut t = folding_tree();
1321 put(&mut t, "link", Node::OpaqueSymlink);
1322 let _ = t.insert(Path::new("LINK"), Node::File);
1323 assert_eq!(
1324 collisions_of(&mut t),
1325 [("link".into(), "LINK".into(), CollisionKind::Mixed)]
1326 );
1327 }
1328
1329 #[test]
1330 fn is_symlink_follows_symlinked_parents() {
1331 let mut t = tree();
1332 put(&mut t, "real/inner", sym("x"));
1333 put(&mut t, "alias", sym("real"));
1334 assert!(t.is_symlink(Path::new("/dest/alias/inner")).unwrap());
1335 assert!(!t.is_symlink(Path::new("/dest/real")).unwrap());
1336 assert!(t.is_symlink(Path::new("/dest/none")).is_err());
1337 }
1338
1339 #[test]
1340 fn insert_through_symlinked_parent_lands_at_resolved_location() {
1341 let mut t = tree();
1342 put(&mut t, "real", Node::Dir);
1343 put(&mut t, "alias", sym("real"));
1344 put(&mut t, "alias/f", Node::File);
1345 assert!(t.exists(Path::new("/dest/real/f")));
1346 }
1347
1348 #[test]
1349 fn insert_dir_keeps_existing_symlink() {
1350 let mut t = tree();
1351 put(&mut t, "real", Node::Dir);
1352 put(&mut t, "alias", sym("real"));
1353 put(&mut t, "alias", Node::Dir);
1354 assert!(t.is_symlink(Path::new("/dest/alias")).unwrap());
1355 }
1356
1357 #[test]
1358 fn file_never_replaces_existing_symlink() {
1359 let mut t = tree();
1360 put(&mut t, "y", sym("."));
1361 let err = t.insert(Path::new("y"), Node::File).unwrap_err();
1362 assert_eq!(err.kind(), io::ErrorKind::AlreadyExists);
1363 assert!(t.is_symlink(Path::new("/dest/y")).unwrap());
1364 }
1365
1366 #[test]
1367 fn symlink_over_symlink_is_refused_and_keeps_the_first() {
1368 let mut t = tree();
1369 put(&mut t, "real", Node::Dir);
1370 put(&mut t, "d", sym("real"));
1371 for node in [sym("other"), Node::OpaqueSymlink] {
1372 let conflict = t.try_insert(Path::new("d"), node).unwrap_err();
1373 assert_eq!(conflict.error.kind(), io::ErrorKind::AlreadyExists);
1374 assert_eq!(conflict.occupant, Some(Occupant::Symlink));
1375 }
1376 assert_eq!(
1377 t.canonicalize(Path::new("/dest/d")).unwrap(),
1378 PathBuf::from("/dest/real")
1379 );
1380 }
1381
1382 #[test]
1383 fn symlink_repeating_the_same_target_is_an_unchanged_duplicate() {
1384 let mut t = tree();
1385 put(&mut t, "d", sym("real"));
1386 put(&mut t, "d", sym("real"));
1387 assert!(t.try_insert(Path::new("d"), Node::OpaqueSymlink).is_err());
1388 }
1389
1390 #[test]
1391 fn file_over_dir_is_refused_with_is_a_directory() {
1392 let mut t = tree();
1393 put(&mut t, "d/f", Node::File);
1394 let conflict = t.try_insert(Path::new("d"), Node::File).unwrap_err();
1395 assert_eq!(conflict.error.kind(), io::ErrorKind::IsADirectory);
1396 assert_eq!(conflict.occupant, Some(Occupant::Directory));
1397 }
1398
1399 #[test]
1400 fn file_over_file_is_kept_silently() {
1401 let mut t = tree();
1402 put(&mut t, "f", Node::File);
1403 put(&mut t, "f", Node::File);
1404 assert_eq!(t.occupant(Path::new("f")), Some(Occupant::File));
1405 }
1406
1407 #[test]
1408 fn occupant_names_what_sits_at_the_path_itself() {
1409 let mut t = tree();
1410 put(&mut t, "real/f", Node::File);
1411 put(&mut t, "alias", sym("real"));
1412 assert_eq!(t.occupant(Path::new("real")), Some(Occupant::Directory));
1413 assert_eq!(t.occupant(Path::new("alias")), Some(Occupant::Symlink));
1414 assert_eq!(t.occupant(Path::new("alias/f")), Some(Occupant::File));
1415 assert_eq!(t.occupant(Path::new("none")), None);
1416 }
1417
1418 #[test]
1419 fn symlink_replaces_earlier_file() {
1420 let mut t = tree();
1421 put(&mut t, "y", Node::File);
1422 let err = t.insert(Path::new("y"), sym(".")).unwrap_err();
1423 assert_eq!(err.kind(), io::ErrorKind::AlreadyExists);
1424 assert!(t.is_symlink(Path::new("/dest/y")).unwrap());
1425 }
1426
1427 #[test]
1428 fn dir_over_file_is_an_error() {
1429 let mut t = tree();
1430 put(&mut t, "f", Node::File);
1431 let err = t.insert(Path::new("f"), Node::Dir).unwrap_err();
1432 assert_eq!(err.kind(), io::ErrorKind::AlreadyExists);
1433 }
1434
1435 #[test]
1436 fn insert_under_file_is_an_error() {
1437 let mut t = tree();
1438 put(&mut t, "f", Node::File);
1439 let err = t.insert(Path::new("f/g"), Node::File).unwrap_err();
1440 assert_eq!(err.kind(), io::ErrorKind::NotADirectory);
1441 }
1442
1443 #[test]
1444 fn insert_under_chain_leaving_root_is_an_error() {
1445 let mut t = tree();
1446 put(&mut t, "out", sym(".."));
1447 assert!(t.insert(Path::new("out/f"), Node::File).is_err());
1448 assert!(!t.exists(Path::new("/dest/out/f")));
1449 }
1450
1451 #[test]
1452 fn resolves_to_dir_distinguishes_files_and_directories() {
1453 let mut t = tree();
1454 put(&mut t, "d/f", Node::File);
1455 put(&mut t, "alias", sym("d"));
1456 assert!(t.resolves_to_dir(Path::new("/dest/d")));
1457 assert!(t.resolves_to_dir(Path::new("/dest/alias")));
1458 assert!(!t.resolves_to_dir(Path::new("/dest/d/f")));
1459 }
1460
1461 #[test]
1462 fn overlong_component_is_never_found() {
1463 let mut t = tree();
1464 put(&mut t, "d", Node::Dir);
1465 let long = "a".repeat(1 << 20);
1466 let err = t
1467 .canonicalize(&Path::new("/dest/d").join(&long))
1468 .unwrap_err();
1469 assert_eq!(err.kind(), io::ErrorKind::NotFound);
1470 }
1471
1472 #[test]
1473 fn name_length_limits() {
1474 assert!(check_name_lengths(Path::new("a/b")).is_ok());
1475 let long_name = "a".repeat(255 + 1);
1476 assert!(check_name_lengths(Path::new(&long_name)).is_err());
1477 let long_path = path_of_len(HOST_PATH_LIMIT + 1);
1478 assert!(check_name_lengths(Path::new(&long_path)).is_err());
1479 assert!(check_name_lengths(Path::new(&path_of_len(HOST_PATH_LIMIT))).is_ok());
1480 }
1481
1482 use crate::test_utils::HOST_PATH_LIMIT;
1483 use crate::test_utils::path_of_len;
1484
1485 #[test]
1486 fn link_target_length_boundaries() {
1487 let symlink_limit = path_of_len(MAX_SYMLINK_TARGET_LEN);
1488 let symlink_over = path_of_len(MAX_SYMLINK_TARGET_LEN + 1);
1489 assert!(check_link_target_length(Path::new(&symlink_limit), LinkKind::Symlink).is_ok());
1490 assert!(check_link_target_length(Path::new(&symlink_over), LinkKind::Symlink).is_err());
1491
1492 let at_limit = path_of_len(HOST_PATH_LIMIT);
1493 assert_eq!(at_limit.len(), HOST_PATH_LIMIT);
1494 let over = path_of_len(HOST_PATH_LIMIT + 1);
1495 assert!(check_link_target_length(Path::new(&at_limit), LinkKind::Hardlink).is_ok());
1496 assert!(check_link_target_length(Path::new(&over), LinkKind::Hardlink).is_err());
1497 }
1498
1499 #[test]
1500 fn symlink_target_limits_match_the_host() {
1501 assert_eq!(
1502 MAX_SYMLINK_TARGET_LEN,
1503 if cfg!(windows) { 4091 } else { HOST_PATH_LIMIT }
1504 );
1505 assert_eq!(
1506 MAX_SYMLINK_TARGET_BYTES,
1507 if cfg!(windows) {
1508 4091 * 3
1509 } else {
1510 HOST_PATH_LIMIT
1511 }
1512 );
1513 }
1514
1515 #[cfg(windows)]
1518 #[test]
1519 fn symlink_target_limit_is_accepted_by_the_host() {
1520 use std::os::windows::fs::symlink_file;
1521
1522 let temp = tempfile::TempDir::new().unwrap();
1523 let mut counter = 0_u32;
1524 let mut creates = |len: usize| -> io::Result<bool> {
1525 counter += 1;
1526 let link = temp.path().join(format!("l{counter}"));
1527 let mut target = "abcdefg\\".repeat((len - 1) / 8);
1528 target.push_str(&"a".repeat(len - target.len()));
1529 match symlink_file(&target, &link) {
1530 Ok(()) => Ok(true),
1531 Err(e) if e.raw_os_error() == Some(1314) => Err(e),
1532 Err(_) => Ok(false),
1533 }
1534 };
1535
1536 match creates(1) {
1537 Ok(_) => {}
1538 Err(e) => {
1539 assert!(
1540 std::env::var_os("CI").is_none(),
1541 "symlink creation needs a privilege on CI: {e}"
1542 );
1543 eprintln!("skipped: symlink creation is not permitted: {e}");
1544 return;
1545 }
1546 }
1547
1548 let (mut ok, mut bad) = (1_usize, 32767_usize);
1549 while bad - ok > 1 {
1550 let mid = ok + (bad - ok) / 2;
1551 if creates(mid).unwrap() {
1552 ok = mid;
1553 } else {
1554 bad = mid;
1555 }
1556 }
1557 eprintln!("largest relative symlink target: {ok} UTF-16 units");
1558 assert!(MAX_SYMLINK_TARGET_LEN <= ok, "host limit is {ok}");
1559 }
1560
1561 #[test]
1562 fn symlink_target_ignores_component_limit() {
1563 let long_component = "a".repeat(255 + 45);
1564 let target = Path::new(&long_component);
1565 assert!(check_link_target_length(target, LinkKind::Symlink).is_ok());
1566 assert!(check_link_target_length(target, LinkKind::Hardlink).is_err());
1567 assert!(check_name_lengths(target).is_err());
1568 }
1569
1570 #[cfg(any(target_os = "macos", target_os = "windows"))]
1571 #[test]
1572 fn keys_are_case_insensitive_on_case_folding_hosts() {
1573 let mut t = tree();
1574 put(&mut t, "Dir/File", Node::File);
1575 assert!(t.exists(Path::new("/dest/dir/file")));
1576 }
1577
1578 #[test]
1579 fn host_fs_probes_the_real_filesystem() {
1580 let temp = tempfile::TempDir::new().unwrap();
1581 assert!(HostFs.exists(temp.path()));
1582 assert!(!HostFs.is_symlink(temp.path()).unwrap());
1583 assert!(HostFs.canonicalize(&temp.path().join("nope")).is_err());
1584 }
1585}