// what the store is for: a path a sandbox should not read living somewhere it // cannot. every case here is one that was once wrong. mod support; use std::fs; use support::Case; #[test] fn a_path_moves_into_the_store_and_reads_back_through_the_link() { let case = Case::new("roundtrip"); case.write(".env", b"SECRET=1\n"); case.write("secrets/token", b"tok\n"); case.gitignore(".env\nsecrets/\n"); case.ahab(&["link", "add", ".env", "secrets"]).ok(); assert!(case.is_symlink(".env")); assert_eq!(fs::read(case.path(".env")).unwrap(), b"SECRET=1\n"); assert_eq!(fs::read(case.path("secrets/token")).unwrap(), b"tok\n"); case.ahab(&["link", "check", "--exit-code"]).ok(); case.ahab(&["link", "restore", "--all"]).ok(); assert!(!case.is_symlink(".env")); assert_eq!(fs::read(case.path(".env")).unwrap(), b"SECRET=1\n"); } #[test] fn add_refuses_a_symlink_that_already_leads_out_of_the_repository() { let case = Case::new("launder"); fs::write(case.outside.join("key"), b"KEY\n").unwrap(); case.link(&case.path("cache"), &case.outside); // moving the link would move the pointer and leave the contents there, // and check would then see a link into the store and call it clean case.ahab(&["link", "add", "cache"]) .failed() .says("outside the repository"); case.ahab(&["link", "check", "--exit-code"]).code_is(4); } #[test] fn check_reports_a_store_entry_that_leads_back_out() { let case = Case::new("poisoned"); fs::create_dir_all(&case.store).unwrap(); fs::write(case.outside.join("key"), b"KEY\n").unwrap(); // the state an older ahab left: the store holds a way back out case.link(&case.store.join("cache"), &case.outside); case.link(&case.path("cache"), &case.store.join("cache")); case.ahab(&["link", "check", "--exit-code"]).code_is(4); } #[test] fn check_reports_a_tracked_symlink_leading_out() { let case = Case::new("tracked-link"); fs::create_dir_all(case.outside.join("aws")).unwrap(); case.link(&case.path("awsdir"), &case.outside.join("aws")); case.git(&["add", "-f", "awsdir"]); case.git(&["commit", "-qm", "commit a symlink out"]); // git tracks symlinks, so this is in no untracked or ignored listing case.ahab(&["link", "check", "--exit-code"]).code_is(4); case.ahab(&["link", "check", "--porcelain"]).says("T>"); // and add cannot fix it, so saying so is all check can do case.ahab(&["link", "add", "awsdir"]) .failed() .says("tracked by git"); } #[test] fn a_stored_symlink_is_one_entry_rather_than_a_tree_to_walk() { let case = Case::new("walk-out"); fs::create_dir_all(case.outside.join("private")).unwrap(); fs::write(case.outside.join("private/diary"), b"x\n").unwrap(); fs::write(case.outside.join(".netrc"), b"x\n").unwrap(); fs::create_dir_all(&case.store).unwrap(); case.link(&case.store.join("cache"), &case.outside); let run = case.ahab(&["link", "list"]); run.ok().says("cache"); // is_dir() would follow the link and enumerate what is behind it run.silent_about(".netrc"); run.silent_about("diary"); } #[test] fn a_store_inside_the_repository_is_refused_however_it_is_spelled() { let case = Case::new("store-inside"); case.write(".env", b"SECRET=1\n"); case.gitignore(".env\n"); // named directly case.ahab(&["link", "add", "--store", "./within", ".env"]) .failed() .says("inside the repository"); // and reached through a symlink, which a prefix test does not catch case.mkdir("within"); let sneaky = case.repo.parent().unwrap().join("sneaky"); case.link(&sneaky, &case.path("within")); let run = case.ahab(&[ "link".as_ref(), "add".as_ref(), "--store".as_ref(), sneaky.as_os_str(), ".env".as_ref(), ]); run.failed().says("inside the repository"); assert!(!case.is_symlink(".env")); } #[test] fn a_relative_store_root_still_resolves_from_anywhere() { let case = Case::new("relative-store"); case.write("sub/.env", b"SECRET=1\n"); case.gitignore("sub/.env\n"); case.ahab(&["link", "add", "--store", "../store", "sub/.env"]) .ok(); // the target is written into the symlink, so a relative one would resolve // from the link's own directory rather than the working one assert!(case.is_symlink("sub/.env")); assert_eq!(fs::read(case.path("sub/.env")).unwrap(), b"SECRET=1\n"); } #[test] fn the_store_directories_are_the_owners_alone() { let case = Case::new("modes"); case.write("deep/nested/.env", b"SECRET=1\n"); case.gitignore("deep\n"); case.ahab(&["link", "add", "deep/nested/.env"]).ok(); for dir in [ case.store.as_path(), &case.store.join("deep"), &case.store.join("deep/nested"), ] { assert_eq!(case.mode(dir), 0o700, "{}", dir.display()); } } #[test] fn a_failed_link_puts_the_payload_back() { let case = Case::new("rollback"); case.write("x.env", b"SECRET=1\n"); // ahab's own temp name, but a real file: it must not be removed, and the // payload must not be left in the store with nothing pointing at it case.write("x.env.ahab-tmp", b"THE PROJECT OWNS THIS\n"); case.gitignore("x.env\n"); case.ahab(&["link", "add", "x.env"]).failed(); assert!(!case.is_symlink("x.env")); assert_eq!(fs::read(case.path("x.env")).unwrap(), b"SECRET=1\n"); assert_eq!( fs::read(case.path("x.env.ahab-tmp")).unwrap(), b"THE PROJECT OWNS THIS\n" ); assert!(!case.store.join("x.env").exists()); } #[test] fn a_directory_of_symlinks_out_keeps_its_destinations() { let case = Case::new("collapse"); fs::write(case.outside.join("a"), b"A\n").unwrap(); case.link(&case.path("bundle/one"), &case.outside.join("a")); // collapsing to `bundle/` would drop both the destination and the code let run = case.ahab(&["link", "check", "--porcelain"]); run.code_is(0).says("?>").says("bundle/one"); } #[test] fn a_filename_that_is_not_utf_8_is_reported_and_moved_as_itself() { use std::ffi::OsStr; use std::os::unix::ffi::OsStrExt; let case = Case::new("latin1"); let name = OsStr::from_bytes(b"caf\xe9.env"); fs::write(case.repo.join(name), b"SECRET=1\n").unwrap(); // the whole listing used to fail on the one entry let run = case.ahab(&["link", "check", "-z"]); run.code_is(0); assert!( run.stdout.windows(4).any(|w| w == b"caf\xe9"), "-z must write the bytes the name actually has" ); case.gitignore("caf\u{e9}.env\n"); case.ahab(&["link".as_ref(), "add".as_ref(), name]).ok(); assert!( fs::symlink_metadata(case.repo.join(name)) .unwrap() .is_symlink() ); } #[test] fn a_tracked_filename_that_is_not_utf_8_is_still_refused() { use std::ffi::OsStr; use std::os::unix::ffi::OsStrExt; let case = Case::new("latin1-tracked"); let name = OsStr::from_bytes(b"caf\xe9.env"); fs::write(case.repo.join(name), b"TRACKED\n").unwrap(); case.git(&["add".as_ref(), "-f".as_ref(), name]); case.git(&["commit", "-qm", "track a latin-1 name"]); // asked about lossily, git answers about a path nothing has and says // "not tracked", and a tracked file leaves the working tree case.ahab(&["link".as_ref(), "add".as_ref(), name]) .failed() .says("tracked by git"); assert!( !fs::symlink_metadata(case.repo.join(name)) .unwrap() .is_symlink() ); } #[test] fn a_pathspec_is_asked_about_as_a_name_not_a_pattern() { let case = Case::new("pathspec"); // git reads pathspec magic after `--` too, so this used to have git list // every tracked file *except* the named one, which read as "it is tracked" case.write(":!untracked.env", b"SECRET=1\n"); case.gitignore(":!untracked.env\n"); case.ahab(&["link", "add", ":!untracked.env"]).ok(); assert!(case.is_symlink(":!untracked.env")); } #[test] fn distinct_remotes_do_not_share_one_store_directory() { let case = Case::new("remotes"); case.git(&[ "remote", "set-url", "origin", "https://git.example.org/a/my_api", ]); let plain = case.ahab(&["link", "list"]).out(); case.git(&[ "remote", "set-url", "origin", "https://git.example.org/a/my~api", ]); let awkward = case.ahab(&["link", "list"]).out(); assert_ne!(plain, awkward); // the ordinary remote keeps the path it already had assert!(plain.contains("/a/my_api"), "{plain}"); }