pristine/git.rs
1//! What git knows about a directory: where its work tree is, and whether anything under it is
2//! tracked.
3//!
4//! ## Why the index, and why by asking git for it
5//!
6//! Tier two's safety property is "contains no tracked file at any depth", which is exactly the
7//! guarantee `git clean` enforces and the only reason the tier can be on by default. The index
8//! is the authority for it, and getting the index wrong means deleting somebody's source — so
9//! this module asks `git ls-files` rather than parsing `.git/index` itself. Split indexes,
10//! version-4 path compression, sparse directory entries and linked work trees are all shapes a
11//! hand-rolled reader gets wrong quietly, and quietly is the one failure mode a cleaner cannot
12//! afford. One subprocess per work tree buys the exact answer.
13//!
14//! If git cannot be run, or the repository will not answer, tier two goes inert for that work
15//! tree and says so. It never falls back to a guess.
16
17use std::ffi::OsStr;
18use std::path::{Component, Path, PathBuf};
19use std::process::{Command, Stdio};
20use std::{fmt, io, str};
21
22use unicode_normalization::UnicodeNormalization;
23
24/// Environment variables that redirect git at a repository other than the one it was pointed
25/// at, cleared before every invocation.
26///
27/// This is not hygiene, it is the safety property again. Anything running inside a git hook,
28/// a `filter-branch` or a rebase has `GIT_DIR` and `GIT_INDEX_FILE` set, and they win over
29/// `-C`: `GIT_INDEX_FILE=<another repo's index> git -C here ls-files` lists the *other*
30/// repository's files and reports nothing about this one. Every path here would then look
31/// untracked, and looking untracked is what makes a directory deletable. It fails silently and
32/// in the dangerous direction, which is the combination that has to be designed out.
33const AMBIENT_GIT_ENV: [&str; 8] = [
34 "GIT_DIR",
35 "GIT_INDEX_FILE",
36 "GIT_WORK_TREE",
37 "GIT_COMMON_DIR",
38 "GIT_OBJECT_DIRECTORY",
39 "GIT_ALTERNATE_OBJECT_DIRECTORIES",
40 "GIT_CEILING_DIRECTORIES",
41 "GIT_NAMESPACE",
42];
43
44/// A `git` invocation pointed at `dir`, with everything ambient that could change its answer
45/// shut out.
46///
47/// Two families of variable, and they are dangerous for different reasons.
48///
49/// [`AMBIENT_GIT_ENV`] redirects git at another *repository*, which is the failure described
50/// above it.
51///
52/// The locale redirects git's *prose*, and repo mode reads git's prose because `git clean` has
53/// no `-z` and no porcelain format — it prints `Would remove <path>`, and that sentence is
54/// translated. Measured, not assumed: under `LANGUAGE=de` the same command prints
55/// `Würde … löschen`, and a parser looking for `Would remove` finds nothing at all. So a repo
56/// full of build output would report as already clean. `LC_ALL=C` wins over `LANGUAGE`
57/// (measured), and `LANGUAGE` is cleared as well because it is the one variable that otherwise
58/// wins over `LANG`.
59pub(crate) fn git(dir: &Path) -> Command {
60 let mut command = Command::new("git");
61 command.arg("-C").arg(dir).stdin(Stdio::null());
62 for variable in AMBIENT_GIT_ENV {
63 command.env_remove(variable);
64 }
65 command.env("LC_ALL", "C").env("LANGUAGE", "");
66 command
67}
68
69/// Whether `dir` is the root of a git work tree.
70///
71/// A `.git` that is a *file* rather than a directory is a linked work tree or a submodule, and
72/// is just as much a work tree root as the ordinary case.
73#[must_use]
74pub fn is_work_tree_root(dir: &Path) -> bool {
75 dir.join(".git").symlink_metadata().is_ok()
76}
77
78/// What the `.git` at a work tree root actually is.
79///
80/// [`is_work_tree_root`] deliberately collapses all three, which is right for the safety model's
81/// question — "is there a checkout here" — and wrong for the only question where the difference
82/// decides whether a directory is disposable.
83///
84/// **A linked work tree is the one kind that holds no history of its own.** Its commits go to the
85/// repository's object store and its branch is an ordinary ref there, so deleting the directory
86/// costs the checked-out files and nothing else — verified rather than assumed: a commit made in
87/// a linked work tree is still readable through its branch after the directory is removed
88/// outright. A [`Repository`](Self::Repository) *is* the object store, and a
89/// [`Submodule`](Self::Submodule) is a checkout the superproject's index points at, which is a
90/// different promise from "somewhere to do some work".
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub enum Checkout {
93 /// `.git` is a directory: the repository itself, holding every object and ref under it.
94 Repository,
95 /// `.git` is a file pointing into another repository's `worktrees/`, which is where the
96 /// objects and the branch actually live.
97 Linked,
98 /// `.git` is a file pointing into a superproject's `modules/`.
99 Submodule,
100}
101
102/// Which kind of checkout is rooted at `dir`, or `None` if there is not one.
103///
104/// **Asked of git rather than read out of the `.git` file**, for this module's founding reason:
105/// the file's `gitdir:` line has to be resolved relative to the work tree, can be absolute or
106/// relative, and points somewhere whose *shape* is what distinguishes a linked work tree from a
107/// submodule. A hand-rolled reader gets that wrong quietly, and quietly is the failure mode a
108/// cleaner cannot afford — here it would mean reading a submodule as disposable.
109///
110/// The discriminator is git's own: `--git-dir` and `--git-common-dir` are equal for a repository
111/// and for a submodule, and a linked work tree is the only case where the first sits at
112/// `<common>/worktrees/<name>`. Measured against all three, rather than inferred from the
113/// documentation.
114///
115/// Fails toward [`Repository`](Checkout::Repository) — never toward `Linked` — whenever git
116/// cannot be run or will not answer. Everything downstream reads `Linked` as permission.
117#[must_use]
118pub fn checkout_at(dir: &Path) -> Option<Checkout> {
119 if !is_work_tree_root(dir) {
120 return None;
121 }
122 let Ok(output) = git(dir)
123 .args(["rev-parse", "--path-format=absolute", "--git-dir"])
124 .arg("--git-common-dir")
125 .output()
126 else {
127 return Some(Checkout::Repository);
128 };
129 if !output.status.success() {
130 return Some(Checkout::Repository);
131 }
132 let text = String::from_utf8_lossy(&output.stdout);
133 let mut lines = text.lines();
134 let (Some(git_dir), Some(common)) = (lines.next(), lines.next()) else {
135 return Some(Checkout::Repository);
136 };
137 let git_dir = Path::new(git_dir.trim());
138 // `<common>/worktrees/<name>`, checked a component at a time rather than by string prefix, so
139 // a repository that happens to live under a directory called `worktrees` cannot match.
140 let linked = git_dir
141 .parent()
142 .is_some_and(|holder| holder.file_name() == Some(OsStr::new("worktrees")))
143 && git_dir.parent().and_then(Path::parent) == Some(Path::new(common.trim()));
144 Some(if linked {
145 Checkout::Linked
146 } else if git_dir
147 .components()
148 .any(|part| part.as_os_str() == OsStr::new("modules"))
149 {
150 Checkout::Submodule
151 } else {
152 Checkout::Repository
153 })
154}
155
156/// Whether the work tree at `dir` holds work that exists nowhere else.
157///
158/// `git status --porcelain` is empty exactly when there is nothing uncommitted and nothing
159/// untracked — and it says nothing about *ignored* files, which is what makes this usable here at
160/// all: a work tree carrying 4 GiB of `node_modules` reads clean, because that is precisely the
161/// content this program exists to regenerate rather than preserve.
162///
163/// Errs toward "not clean" on every failure. The answer is the gate on an irreversible removal.
164#[must_use]
165pub fn is_clean(dir: &Path) -> bool {
166 git(dir)
167 .args(["status", "--porcelain"])
168 .output()
169 .is_ok_and(|output| output.status.success() && output.stdout.is_empty())
170}
171
172/// Whether `HEAD` names a branch rather than sitting detached.
173///
174/// **The one way removing a linked work tree can lose a commit.** A commit made on a detached
175/// `HEAD` is reachable only through that work tree's own `HEAD`, so once the directory is gone
176/// and the administrative files are pruned nothing refers to it and `gc` will collect it —
177/// measured, not reasoned about: `git fsck --unreachable` lists it immediately after. A commit on
178/// a branch is reachable through an ordinary ref in the repository and survives.
179///
180/// Errs toward "detached" on every failure, which is the answer that keeps the directory.
181#[must_use]
182pub fn head_on_branch(dir: &Path) -> bool {
183 git(dir)
184 .args(["symbolic-ref", "--quiet", "HEAD"])
185 .output()
186 .is_ok_and(|output| output.status.success())
187}
188
189/// The nearest ancestor of `from`, `from` itself included, that is a git work tree root.
190///
191/// This is git's own rule, which is what makes a checkout inside another checkout behave: the
192/// inner repository is the authority for everything under it, and the outer one has no say.
193#[must_use]
194pub fn discover(from: &Path) -> Option<PathBuf> {
195 let mut cursor = Some(from);
196 while let Some(dir) = cursor {
197 if is_work_tree_root(dir) {
198 return Some(dir.to_path_buf());
199 }
200 cursor = dir.parent();
201 }
202 None
203}
204
205/// One git work tree, with the set of paths its index tracks.
206#[derive(Debug)]
207pub struct WorkTree {
208 root: PathBuf,
209 /// Every tracked path, relative to `root`, as raw bytes and sorted. Sorted so a "is
210 /// anything under this directory tracked" question is a binary search rather than a scan
211 /// of an index that can hold hundreds of thousands of entries.
212 tracked: Vec<Box<[u8]>>,
213}
214
215impl WorkTree {
216 /// Reads the index of the work tree rooted at `root`.
217 ///
218 /// # Errors
219 ///
220 /// If git cannot be run at all, or refuses to list the index.
221 pub fn open(root: &Path) -> Result<Self, GitError> {
222 let mut command = git(root);
223 // `--full-name` pins the paths to the work tree root rather than to a working
224 // directory, and `-z` gives them raw: git quotes and escapes any other way, and a
225 // path this module misreads is a path it wrongly believes is untracked.
226 command.args(["ls-files", "-z", "--full-name"]);
227
228 let output = command
229 .output()
230 .map_err(|err| GitError::Run(root.to_path_buf(), err))?;
231 if !output.status.success() {
232 return Err(GitError::Refused(
233 root.to_path_buf(),
234 String::from_utf8_lossy(&output.stderr).trim().to_owned(),
235 ));
236 }
237
238 // Sorted here rather than trusted to arrive sorted. The index is sorted and `ls-files`
239 // walks it in order, but the binary search below is a safety property and it should
240 // not rest on an implementation detail of another program.
241 let mut tracked: Vec<Box<[u8]>> = output
242 .stdout
243 .split(|byte| *byte == 0)
244 .filter(|path| !path.is_empty())
245 // Composed here as well as on the query side, so the two agree. See `comparable`.
246 .map(|path| Box::from(comparable(path.to_vec())))
247 .collect();
248 tracked.sort_unstable();
249 tracked.dedup();
250
251 Ok(Self {
252 root: root.to_path_buf(),
253 tracked,
254 })
255 }
256
257 /// The work tree's root directory.
258 #[must_use]
259 pub fn root(&self) -> &Path {
260 &self.root
261 }
262
263 /// How many paths the index tracks.
264 #[must_use]
265 pub fn tracked(&self) -> usize {
266 self.tracked.len()
267 }
268
269 /// Whether anything tracked lives at or below `dir`.
270 ///
271 /// Answers `true` for a path this work tree cannot express, because every caller is asking
272 /// in order to decide whether deleting `dir` is safe, and "I could not tell" has to read as
273 /// "do not touch it".
274 #[must_use]
275 pub fn holds_tracked_path(&self, dir: &Path) -> bool {
276 let Ok(relative) = dir.strip_prefix(&self.root) else {
277 return true;
278 };
279 let Some(mut prefix) = as_index_path(relative) else {
280 return true;
281 };
282 if prefix.is_empty() {
283 // The work tree root itself: anything at all is under it.
284 return !self.tracked.is_empty();
285 }
286 // A gitlink — a submodule — is a tracked entry for the directory itself rather than for
287 // anything beneath it, so the exact path has to be checked as well as the prefix.
288 if self.tracked.binary_search(&prefix.clone().into()).is_ok() {
289 return true;
290 }
291 prefix.push(b'/');
292 let at = self
293 .tracked
294 .partition_point(|path| path.as_ref() < prefix.as_slice());
295 self.tracked
296 .get(at)
297 .is_some_and(|path| path.starts_with(&prefix))
298 }
299}
300
301/// A work-tree-relative path in the form git's index uses: `/`-separated, raw bytes.
302///
303/// Returns `None` for anything that is not a plain sequence of normal components, which cannot
304/// be compared against index entries and which callers treat as "assume it is tracked".
305fn as_index_path(relative: &Path) -> Option<Vec<u8>> {
306 let mut out = Vec::new();
307 for component in relative.components() {
308 let Component::Normal(segment) = component else {
309 return None;
310 };
311 if !out.is_empty() {
312 out.push(b'/');
313 }
314 out.extend_from_slice(segment.as_encoded_bytes());
315 }
316 Some(comparable(out))
317}
318
319/// The form both sides of the tracked-path comparison have to be in.
320///
321/// The two sides disagree about Unicode normalization, and on macOS they disagree *by default*.
322/// `readdir` on APFS hands back the bytes a name was created with, which for anything touched by
323/// an HFS-era tool is decomposed; git sets `core.precomposeunicode` on macOS, so `git add`
324/// composes the name before storing it. A directory called `café` is then `cafe\xcc\x81` on disk
325/// and `caf\xc3\xa9` in the index — measured, not assumed — and a raw byte comparison misses.
326///
327/// That miss is the dangerous direction: a directory that *does* hold a tracked file looks
328/// untracked, and looking untracked is what makes it eligible for deletion. It is reachable
329/// without anyone doing anything exotic, since only one component of the path has to be
330/// non-ASCII: `docs/café/build` matched by an ordinary `build/` ignore rule is enough.
331///
332/// So both sides are composed before they are compared. Where two names on the same filesystem
333/// differ only by normalization — possible on Linux, which normalizes nothing — this conflates
334/// them, and conflating errs toward "tracked", which is the side to err on. Anything that is not
335/// UTF-8 cannot be normalized and is compared as it stands, which is correct: nothing converts
336/// it on either side either.
337fn comparable(path: Vec<u8>) -> Vec<u8> {
338 if path.is_ascii() {
339 return path;
340 }
341 match str::from_utf8(&path) {
342 Ok(text) => text.nfc().collect::<String>().into_bytes(),
343 Err(_) => path,
344 }
345}
346
347/// Why a work tree could not be consulted.
348#[derive(Debug)]
349#[non_exhaustive]
350pub enum GitError {
351 /// git could not be run — most often because it is not installed.
352 Run(PathBuf, io::Error),
353 /// git ran and refused, carrying whatever it said about why.
354 Refused(PathBuf, String),
355}
356
357impl fmt::Display for GitError {
358 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
359 match self {
360 Self::Run(root, err) => write!(
361 f,
362 "could not run git in {}, so nothing there can be judged safe to remove: {err}",
363 root.display()
364 ),
365 Self::Refused(root, message) => write!(
366 f,
367 "git would not list the index of {}, so nothing there can be judged safe to \
368 remove: {message}",
369 root.display()
370 ),
371 }
372 }
373}
374
375impl std::error::Error for GitError {
376 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
377 match self {
378 Self::Run(_, err) => Some(err),
379 Self::Refused(..) => None,
380 }
381 }
382}
383
384#[cfg(test)]
385mod tests {
386 use super::{
387 Checkout, WorkTree, as_index_path, checkout_at, comparable, git, head_on_branch, is_clean,
388 };
389 use std::path::{Path, PathBuf};
390
391 /// A repository with one commit, made with git rather than by writing `.git` by hand — the
392 /// shapes this module distinguishes are git's, and a fixture that spelled them itself would
393 /// be asserting that the fixture agrees with the code.
394 fn repo(at: &Path) {
395 std::fs::create_dir_all(at).unwrap();
396 run(at, &["init", "--quiet", "."]);
397 run(at, &["config", "user.email", "test@example.com"]);
398 run(at, &["config", "user.name", "test"]);
399 std::fs::write(at.join("tracked.txt"), "content").unwrap();
400 run(at, &["add", "."]);
401 run(at, &["commit", "--quiet", "-m", "first"]);
402 }
403
404 /// `git worktree add --quiet`, which every fixture below needs and which does not fit on
405 /// one line spelled out at each call.
406 fn worktree(main: &Path, args: &[&str]) {
407 let mut all = vec!["worktree", "add", "--quiet"];
408 all.extend_from_slice(args);
409 run(main, &all);
410 }
411
412 fn run(at: &Path, args: &[&str]) {
413 let output = git(at).args(args).output().unwrap();
414 assert!(
415 output.status.success(),
416 "git {args:?} in {}: {}",
417 at.display(),
418 String::from_utf8_lossy(&output.stderr)
419 );
420 }
421
422 #[test]
423 fn a_linked_work_tree_is_told_apart_from_the_repository_and_from_a_submodule() {
424 // The distinction the whole feature rests on, and none of the three can be told apart by
425 // whether `.git` is a file: a submodule has one too.
426 let tmp = tempfile::TempDir::new().unwrap();
427 let base = std::fs::canonicalize(tmp.path()).unwrap();
428 let main = base.join("main");
429 let inner = base.join("inner");
430 repo(&main);
431 repo(&inner);
432 worktree(&main, &["../linked", "-b", "feature"]);
433 run(
434 &main,
435 &[
436 "-c",
437 "protocol.file.allow=always",
438 "submodule",
439 "--quiet",
440 "add",
441 inner.to_str().unwrap(),
442 "vendored",
443 ],
444 );
445 run(&main, &["commit", "--quiet", "-m", "vendored"]);
446
447 assert_eq!(checkout_at(&main), Some(Checkout::Repository));
448 assert_eq!(checkout_at(&base.join("linked")), Some(Checkout::Linked));
449 assert_eq!(
450 checkout_at(&main.join("vendored")),
451 Some(Checkout::Submodule),
452 "a submodule was read as a disposable work tree"
453 );
454 // Not a checkout at all, which is every other directory on the disk.
455 assert_eq!(checkout_at(&base), None);
456 }
457
458 #[test]
459 fn a_work_tree_is_clean_despite_ignored_build_output_and_dirty_with_anything_else() {
460 // The property that makes this usable: the directories pristine exists to reclaim are
461 // exactly the ones that must not count as work.
462 let tmp = tempfile::TempDir::new().unwrap();
463 let base = std::fs::canonicalize(tmp.path()).unwrap();
464 let main = base.join("main");
465 repo(&main);
466 std::fs::write(main.join(".gitignore"), "node_modules/\n").unwrap();
467 run(&main, &["add", ".gitignore"]);
468 run(&main, &["commit", "--quiet", "-m", "ignore"]);
469
470 std::fs::create_dir_all(main.join("node_modules/dep")).unwrap();
471 std::fs::write(main.join("node_modules/dep/index.js"), "x").unwrap();
472 assert!(
473 is_clean(&main),
474 "4 GiB of node_modules must not read as work that exists nowhere else"
475 );
476
477 // An untracked file that nothing ignores is work, and so is an edit to a tracked one.
478 std::fs::write(main.join("notes.md"), "only copy").unwrap();
479 assert!(!is_clean(&main));
480 std::fs::remove_file(main.join("notes.md")).unwrap();
481 assert!(is_clean(&main));
482 std::fs::write(main.join("tracked.txt"), "edited").unwrap();
483 assert!(!is_clean(&main));
484 }
485
486 #[test]
487 fn a_detached_head_is_refused_because_its_commits_are_reachable_from_nothing_else() {
488 // Measured in #656's follow-up: a commit made on a detached HEAD in a linked work tree is
489 // listed by `git fsck --unreachable` the moment the directory is removed and pruned. On a
490 // branch it survives, because the branch is an ordinary ref in the repository.
491 let tmp = tempfile::TempDir::new().unwrap();
492 let base = std::fs::canonicalize(tmp.path()).unwrap();
493 let main = base.join("main");
494 repo(&main);
495 worktree(&main, &["../onbranch", "-b", "feature"]);
496 worktree(&main, &["--detach", "../loose"]);
497
498 assert!(head_on_branch(&base.join("onbranch")));
499 assert!(!head_on_branch(&base.join("loose")));
500 }
501
502 #[test]
503 fn every_answer_that_decides_a_deletion_fails_toward_keeping_the_directory() {
504 // A directory git will not speak for at all. `is_clean` and `head_on_branch` gate an
505 // irreversible removal, so silence has to read as "do not touch it" — the same discipline
506 // the tier-two fallback keeps when a work tree will not answer.
507 let tmp = tempfile::TempDir::new().unwrap();
508 let base = std::fs::canonicalize(tmp.path()).unwrap();
509 assert!(!is_clean(&base), "a directory git disowns read as clean");
510 assert!(!head_on_branch(&base));
511 // And a checkout it cannot classify is the kind nothing is allowed to remove.
512 assert_eq!(checkout_at(&base), None);
513 }
514
515 /// Mirrors what [`WorkTree::open`] does to `git ls-files` output, so a fixture and a real
516 /// index are the same shape.
517 fn work_tree(root: &str, tracked: &[&str]) -> WorkTree {
518 let mut tracked: Vec<Box<[u8]>> = tracked
519 .iter()
520 .map(|path| Box::from(comparable(path.as_bytes().to_vec())))
521 .collect();
522 tracked.sort_unstable();
523 WorkTree {
524 root: PathBuf::from(root),
525 tracked,
526 }
527 }
528
529 #[test]
530 fn a_tracked_file_at_any_depth_bars_the_directory_above_it() {
531 let tree = work_tree("/repo", &["out/deep/deeper/keep.txt", "src/main.rs"]);
532 assert!(tree.holds_tracked_path(Path::new("/repo/out")));
533 assert!(tree.holds_tracked_path(Path::new("/repo/out/deep")));
534 assert!(!tree.holds_tracked_path(Path::new("/repo/out/other")));
535 }
536
537 #[test]
538 fn a_name_that_merely_starts_the_same_is_not_a_match() {
539 // `-`, `.` and `/` are 0x2d, 0x2e and 0x2f, so these three sort either side of the
540 // `out/` prefix and a sloppy comparison picks up the wrong ones.
541 let tree = work_tree("/repo", &["out-takes/a.txt", "out.txt", "outer/b.txt"]);
542 assert!(!tree.holds_tracked_path(Path::new("/repo/out")));
543 assert!(tree.holds_tracked_path(Path::new("/repo/out-takes")));
544 assert!(tree.holds_tracked_path(Path::new("/repo/outer")));
545 }
546
547 #[test]
548 fn a_gitlink_bars_the_directory_it_names() {
549 // A submodule is one index entry for the directory itself, with nothing under it.
550 let tree = work_tree("/repo", &["vendor/sub"]);
551 assert!(tree.holds_tracked_path(Path::new("/repo/vendor/sub")));
552 assert!(tree.holds_tracked_path(Path::new("/repo/vendor")));
553 }
554
555 #[test]
556 fn a_path_outside_the_work_tree_is_assumed_tracked() {
557 let tree = work_tree("/repo", &["src/main.rs"]);
558 assert!(tree.holds_tracked_path(Path::new("/elsewhere/out")));
559 }
560
561 #[test]
562 fn an_empty_index_holds_nothing() {
563 let tree = work_tree("/repo", &[]);
564 assert!(!tree.holds_tracked_path(Path::new("/repo")));
565 assert!(!tree.holds_tracked_path(Path::new("/repo/out")));
566 }
567
568 #[test]
569 fn a_decomposed_path_matches_the_composed_one_git_stored() {
570 // `café` as git records it on macOS against `café` as `readdir` hands it back. Without
571 // composing both sides these are different byte strings, the search misses, and a
572 // directory holding a tracked file is reported as free to delete.
573 let tree = work_tree("/repo", &["caf\u{e9}/build/keep.txt"]);
574 assert!(tree.holds_tracked_path(Path::new("/repo/cafe\u{301}/build")));
575 assert!(tree.holds_tracked_path(Path::new("/repo/caf\u{e9}/build")));
576 assert!(!tree.holds_tracked_path(Path::new("/repo/cafe\u{301}/other")));
577 }
578
579 #[test]
580 fn index_paths_are_slash_separated_and_reject_anything_exotic() {
581 assert_eq!(
582 as_index_path(Path::new("a/b/c")).unwrap(),
583 b"a/b/c".to_vec()
584 );
585 assert_eq!(as_index_path(Path::new("")).unwrap(), Vec::<u8>::new());
586 assert!(as_index_path(Path::new("../a")).is_none());
587 }
588}