1use crate::cas::{CasError, ManifestEntry, TreeBlobStore, TreeManifest};
2use crate::tree_txn::{transact, DirReplace, Source};
3use std::os::unix::fs::MetadataExt;
4use std::path::{Path, PathBuf};
5use std::{fs, io};
6
7#[derive(Debug, thiserror::Error)]
8pub enum TreeError {
9 #[error("io error: {0}")]
10 Io(#[from] io::Error),
11 #[error("cas error: {0}")]
12 Cas(#[from] CasError),
13 #[error("blob missing for {path}")]
14 BlobMissing { path: PathBuf },
15 #[error("restore path escapes destination root: {path}")]
16 PathEscape { path: PathBuf },
17 #[error("replay would remove the non-empty directory {path}")]
18 NonEmptyDirectory { path: PathBuf },
19 #[error("replay expects a directory at {path}, where the tree holds none")]
20 NotADirectory { path: PathBuf },
21 #[error("replay would create {path} where another entry exists")]
22 Occupied { path: PathBuf },
23 #[error("{cause}; undoing the part already applied failed: {}", failures.join("; "))]
24 RollbackIncomplete {
25 cause: Box<TreeError>,
26 failures: Vec<String>,
27 },
28}
29
30pub fn capture_tree(blobs: &TreeBlobStore, root: &Path) -> Result<TreeManifest, TreeError> {
31 let mut entries: Vec<ManifestEntry> = Vec::new();
32 collect_entries(blobs, root, root, &mut entries)?;
33 entries.sort_by(|a, b| a.path.cmp(&b.path));
34 Ok(TreeManifest::new(entries, vec![]))
35}
36
37fn collect_entries(
38 blobs: &TreeBlobStore,
39 root: &Path,
40 dir: &Path,
41 entries: &mut Vec<ManifestEntry>,
42) -> Result<(), TreeError> {
43 for entry in fs::read_dir(dir)? {
44 let entry = entry?;
45 let path = entry.path();
46 let file_type = entry.file_type()?;
47
48 if file_type.is_symlink() {
49 continue;
51 }
52
53 if file_type.is_dir() {
54 collect_entries(blobs, root, &path, entries)?;
55 } else if file_type.is_file() {
56 let metadata = fs::metadata(&path)?;
57 let content = fs::read(&path)?;
58 let key = blobs.put_file_blob(&content)?;
59 let mode = (metadata.mode() & 0o7777) as u32;
60 let mtime_secs = metadata.mtime();
61 let mtime_nanos = metadata.mtime_nsec() as u32;
62 let rel = path
63 .strip_prefix(root)
64 .expect("path must be under root")
65 .to_path_buf();
66 entries.push(ManifestEntry::entry_for(
67 rel,
68 key,
69 mode,
70 (mtime_secs, mtime_nanos),
71 ));
72 }
73 }
74 Ok(())
75}
76
77pub fn restore_tree(
81 blobs: &TreeBlobStore,
82 manifest: &TreeManifest,
83 dest: &Path,
84) -> Result<(), TreeError> {
85 fs::create_dir_all(dest)?;
89 transact(dest, DirReplace::EmptyOnly, |txn| {
90 for dir in &manifest.dir_attrs {
91 let mtime = dir
92 .mtime
93 .map(|(secs, nanos)| filetime::FileTime::from_unix_time(secs, nanos));
94 txn.set_dir_attrs(&dir.path, dir.mode, mtime)?;
95 }
96 for deleted in &manifest.deleted {
97 txn.remove(deleted)?;
98 }
99 for dir in &manifest.dirs {
100 let mtime = filetime::FileTime::from_unix_time(dir.mtime_secs, dir.mtime_nanos);
101 txn.create_dir(&dir.path, dir.mode, mtime)?;
102 }
103 for link in &manifest.links {
104 let mtime = filetime::FileTime::from_unix_time(link.mtime_secs, link.mtime_nanos);
105 txn.create_symlink(&link.path, &link.target, mtime)?;
106 }
107 for entry in &manifest.entries {
108 let bytes = blobs.get_file_blob(&entry.content_hash)?.ok_or_else(|| {
109 TreeError::BlobMissing {
110 path: entry.path.clone(),
111 }
112 })?;
113 let mtime = filetime::FileTime::from_unix_time(entry.mtime_secs, entry.mtime_nanos);
114 txn.write_file(&entry.path, Source::Bytes(&bytes), entry.mode, mtime)?;
115 }
116 Ok(())
117 })
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123 use crate::cas::{ManifestDir, ManifestDirAttrs, ManifestLink, TreeBlobStore};
124 use crate::store::FileStore;
125 use filetime::FileTime;
126 use std::os::unix::fs::PermissionsExt;
127 use std::path::PathBuf;
128 use tempfile::TempDir;
129
130 fn setup_store() -> (TempDir, FileStore) {
131 let dir = TempDir::new().unwrap();
132 let store = FileStore::open(dir.path()).unwrap();
133 (dir, store)
134 }
135
136 #[test]
137 fn round_trip_fidelity() {
138 let (_store_dir, store) = setup_store();
139 let blobs = TreeBlobStore::new(&store);
140
141 let src_dir = TempDir::new().unwrap();
142 let nested = src_dir.path().join("sub/dir");
143 fs::create_dir_all(&nested).unwrap();
144
145 let file_a = src_dir.path().join("top.txt");
146 fs::write(&file_a, b"hello top").unwrap();
147
148 let file_b = nested.join("deep.bin");
149 fs::write(&file_b, b"\x00\x01\x02\x03").unwrap();
150
151 let known_mtime = FileTime::from_unix_time(1_700_000_000, 123_456_789);
153 filetime::set_file_mtime(&file_a, known_mtime).unwrap();
154
155 use std::os::unix::fs::PermissionsExt;
157 let mode_a = fs::metadata(&file_a).unwrap().permissions().mode() & 0o7777;
158 let mode_b = fs::metadata(&file_b).unwrap().permissions().mode() & 0o7777;
159 let mtime_b = {
160 use std::os::unix::fs::MetadataExt;
161 let md = fs::metadata(&file_b).unwrap();
162 FileTime::from_unix_time(md.mtime(), md.mtime_nsec() as u32)
163 };
164
165 let manifest = capture_tree(&blobs, src_dir.path()).unwrap();
166 assert_eq!(manifest.entries.len(), 2);
167 assert!(manifest.deleted.is_empty());
168
169 let dest_dir = TempDir::new().unwrap();
170 restore_tree(&blobs, &manifest, dest_dir.path()).unwrap();
171
172 let restored_a = fs::read(dest_dir.path().join("top.txt")).unwrap();
174 assert_eq!(restored_a, b"hello top");
175 let restored_b = fs::read(dest_dir.path().join("sub/dir/deep.bin")).unwrap();
176 assert_eq!(restored_b, b"\x00\x01\x02\x03");
177
178 let restored_mode_a = fs::metadata(dest_dir.path().join("top.txt"))
180 .unwrap()
181 .permissions()
182 .mode()
183 & 0o7777;
184 let restored_mode_b = fs::metadata(dest_dir.path().join("sub/dir/deep.bin"))
185 .unwrap()
186 .permissions()
187 .mode()
188 & 0o7777;
189 assert_eq!(restored_mode_a, mode_a);
190 assert_eq!(restored_mode_b, mode_b);
191
192 use std::os::unix::fs::MetadataExt;
194 let md_a = fs::metadata(dest_dir.path().join("top.txt")).unwrap();
195 assert_eq!(md_a.mtime(), 1_700_000_000);
196 assert_eq!(md_a.mtime_nsec() as u32, 123_456_789);
197
198 let md_b = fs::metadata(dest_dir.path().join("sub/dir/deep.bin")).unwrap();
199 assert_eq!(
200 FileTime::from_unix_time(md_b.mtime(), md_b.mtime_nsec() as u32),
201 mtime_b
202 );
203 }
204
205 #[test]
206 fn deletion_removes_file_from_dest() {
207 let (_store_dir, store) = setup_store();
208 let blobs = TreeBlobStore::new(&store);
209
210 let dest_dir = TempDir::new().unwrap();
211 let target = dest_dir.path().join("to_delete.txt");
212 fs::write(&target, b"present").unwrap();
213 assert!(target.exists());
214
215 let manifest = TreeManifest::new(vec![], vec![PathBuf::from("to_delete.txt")]);
216 restore_tree(&blobs, &manifest, dest_dir.path()).unwrap();
217
218 assert!(!target.exists(), "deleted path must be removed from dest");
219 }
220
221 #[test]
222 fn restore_refuses_symlink_escaping_root() {
223 let (_store_dir, store) = setup_store();
224 let blobs = TreeBlobStore::new(&store);
225
226 let outside = TempDir::new().unwrap();
227 let victim = outside.path().join("victim.txt");
228 fs::write(&victim, b"original").unwrap();
229
230 let dest_dir = TempDir::new().unwrap();
231 let link = dest_dir.path().join("link");
232 std::os::unix::fs::symlink(&victim, &link).unwrap();
233
234 let key = blobs.put_file_blob(b"pwned").unwrap();
235 let entry = ManifestEntry::entry_for(PathBuf::from("link"), key, 0o644, (0, 0));
236 let manifest = TreeManifest::new(vec![entry], vec![]);
237
238 let err = restore_tree(&blobs, &manifest, dest_dir.path())
239 .expect_err("must refuse a symlink escaping root");
240 assert!(
241 matches!(err, TreeError::PathEscape { .. }),
242 "unexpected error variant: {err:?}"
243 );
244 assert_eq!(fs::read(&victim).unwrap(), b"original", "victim untouched");
245 }
246
247 #[test]
248 fn restore_allows_symlink_inside_root() {
249 let (_store_dir, store) = setup_store();
250 let blobs = TreeBlobStore::new(&store);
251
252 let dest_dir = TempDir::new().unwrap();
253 let real = dest_dir.path().join("real.txt");
254 fs::write(&real, b"original").unwrap();
255 let link = dest_dir.path().join("link");
256 std::os::unix::fs::symlink(&real, &link).unwrap();
257
258 let key = blobs.put_file_blob(b"updated").unwrap();
259 let entry = ManifestEntry::entry_for(PathBuf::from("link"), key, 0o644, (0, 0));
260 let manifest = TreeManifest::new(vec![entry], vec![]);
261
262 restore_tree(&blobs, &manifest, dest_dir.path()).unwrap();
263 assert_eq!(fs::read(&real).unwrap(), b"updated");
264 }
265
266 #[test]
267 fn missing_blob_returns_error() {
268 let (_store_dir, store) = setup_store();
269 let blobs = TreeBlobStore::new(&store);
270 let dest_dir = TempDir::new().unwrap();
271
272 let phantom_key = crate::store::Key::from_bytes(b"never-stored");
273 let entry =
274 ManifestEntry::entry_for(PathBuf::from("ghost.txt"), phantom_key, 0o644, (0, 0));
275 let manifest = TreeManifest::new(vec![entry], vec![]);
276 let err = restore_tree(&blobs, &manifest, dest_dir.path())
277 .expect_err("must error on missing blob");
278 assert!(
279 matches!(err, TreeError::BlobMissing { .. }),
280 "unexpected error variant: {err:?}"
281 );
282 }
283 fn mtime_of(path: &Path) -> (i64, i64) {
284 let meta = fs::symlink_metadata(path).unwrap();
285 (meta.mtime(), meta.mtime_nsec())
286 }
287
288 #[test]
289 fn deletion_removes_an_empty_directory() {
290 let (_store_dir, store) = setup_store();
291 let blobs = TreeBlobStore::new(&store);
292 let dest_dir = TempDir::new().unwrap();
293 fs::create_dir(dest_dir.path().join("dist")).unwrap();
294 fs::write(dest_dir.path().join("log"), b"a\n").unwrap();
295
296 let key = blobs.put_file_blob(b"a\nx\n").unwrap();
297 let entry = ManifestEntry::entry_for(PathBuf::from("log"), key, 0o644, (0, 0));
298 let manifest = TreeManifest::new(vec![entry], vec![PathBuf::from("dist")]);
299 restore_tree(&blobs, &manifest, dest_dir.path()).unwrap();
300
301 assert!(!dest_dir.path().join("dist").exists());
302 assert_eq!(fs::read(dest_dir.path().join("log")).unwrap(), b"a\nx\n");
303 }
304
305 #[test]
306 fn a_file_replaces_an_empty_directory() {
307 let (_store_dir, store) = setup_store();
308 let blobs = TreeBlobStore::new(&store);
309 let dest_dir = TempDir::new().unwrap();
310 fs::create_dir(dest_dir.path().join("dist")).unwrap();
311
312 let key = blobs.put_file_blob(b"now a file").unwrap();
313 let entry = ManifestEntry::entry_for(PathBuf::from("dist"), key, 0o644, (0, 0));
314 restore_tree(
315 &blobs,
316 &TreeManifest::new(vec![entry], vec![]),
317 dest_dir.path(),
318 )
319 .unwrap();
320
321 assert_eq!(
322 fs::read(dest_dir.path().join("dist")).unwrap(),
323 b"now a file"
324 );
325 }
326
327 #[test]
328 fn a_replay_never_removes_a_non_empty_directory_and_writes_nothing() {
329 let (_store_dir, store) = setup_store();
330 let blobs = TreeBlobStore::new(&store);
331 let dest_dir = TempDir::new().unwrap();
332 let dest = dest_dir.path();
333 fs::create_dir(dest.join("dist")).unwrap();
334 fs::write(dest.join("dist/kept"), b"kept").unwrap();
335 fs::write(dest.join("log"), b"a\n").unwrap();
336
337 let key = blobs.put_file_blob(b"a\nx\n").unwrap();
338 let entry = ManifestEntry::entry_for(PathBuf::from("log"), key, 0o644, (0, 0));
339 let manifest = TreeManifest::new(vec![entry], vec![PathBuf::from("dist")]);
340 let err = restore_tree(&blobs, &manifest, dest).expect_err("must refuse");
341
342 assert!(
343 matches!(err, TreeError::NonEmptyDirectory { .. }),
344 "unexpected error variant: {err:?}"
345 );
346 assert_eq!(fs::read(dest.join("dist/kept")).unwrap(), b"kept");
347 assert_eq!(fs::read(dest.join("log")).unwrap(), b"a\n");
348 }
349
350 #[test]
351 fn a_failed_restore_leaves_the_tree_as_it_was() {
352 let (_store_dir, store) = setup_store();
353 let blobs = TreeBlobStore::new(&store);
354 let outside = TempDir::new().unwrap();
355 let victim = outside.path().join("victim.txt");
356 fs::write(&victim, b"original").unwrap();
357
358 let dest_dir = TempDir::new().unwrap();
359 let dest = dest_dir.path();
360 fs::write(dest.join("a.txt"), b"old").unwrap();
361 fs::set_permissions(dest.join("a.txt"), fs::Permissions::from_mode(0o600)).unwrap();
362 filetime::set_file_mtime(
363 dest.join("a.txt"),
364 FileTime::from_unix_time(1_600_000_000, 5),
365 )
366 .unwrap();
367 fs::create_dir(dest.join("gone")).unwrap();
368 fs::write(dest.join("gone.txt"), b"still here").unwrap();
369 std::os::unix::fs::symlink(&victim, dest.join("z_link")).unwrap();
370 let before = mtime_of(&dest.join("a.txt"));
371
372 let entries = vec![
373 ManifestEntry::entry_for(
374 PathBuf::from("a.txt"),
375 blobs.put_file_blob(b"new").unwrap(),
376 0o644,
377 (0, 0),
378 ),
379 ManifestEntry::entry_for(
380 PathBuf::from("fresh/dir/b.txt"),
381 blobs.put_file_blob(b"b").unwrap(),
382 0o644,
383 (0, 0),
384 ),
385 ManifestEntry::entry_for(
386 PathBuf::from("z_link"),
387 blobs.put_file_blob(b"pwned").unwrap(),
388 0o644,
389 (0, 0),
390 ),
391 ];
392 let deleted = vec![PathBuf::from("gone"), PathBuf::from("gone.txt")];
393 let err = restore_tree(&blobs, &TreeManifest::new(entries, deleted), dest)
394 .expect_err("the escaping symlink must fail the restore");
395
396 assert!(
397 matches!(err, TreeError::PathEscape { .. }),
398 "unexpected error variant: {err:?}"
399 );
400 assert_eq!(fs::read(dest.join("a.txt")).unwrap(), b"old");
401 let meta = fs::metadata(dest.join("a.txt")).unwrap();
402 assert_eq!(meta.permissions().mode() & 0o7777, 0o600);
403 assert_eq!(mtime_of(&dest.join("a.txt")), before);
404 assert!(!dest.join("fresh").exists(), "created dirs must be removed");
405 assert!(dest.join("gone").is_dir(), "removed dir must come back");
406 assert_eq!(fs::read(dest.join("gone.txt")).unwrap(), b"still here");
407 assert_eq!(fs::read(&victim).unwrap(), b"original");
408 let mut names: Vec<_> = fs::read_dir(dest)
409 .unwrap()
410 .map(|e| e.unwrap().file_name())
411 .collect();
412 names.sort();
413 assert_eq!(
414 names,
415 ["a.txt", "gone", "gone.txt", "z_link"],
416 "no backups left"
417 );
418 }
419
420 #[test]
421 fn restore_refuses_a_write_through_a_parent_symlink_escaping_root() {
422 let (_store_dir, store) = setup_store();
423 let blobs = TreeBlobStore::new(&store);
424 let outside = TempDir::new().unwrap();
425 let dest_dir = TempDir::new().unwrap();
426 std::os::unix::fs::symlink(outside.path(), dest_dir.path().join("sub")).unwrap();
427
428 let key = blobs.put_file_blob(b"pwned").unwrap();
429 let entry = ManifestEntry::entry_for(PathBuf::from("sub/new/x.txt"), key, 0o644, (0, 0));
430 let err = restore_tree(
431 &blobs,
432 &TreeManifest::new(vec![entry], vec![]),
433 dest_dir.path(),
434 )
435 .expect_err("must refuse");
436
437 assert!(
438 matches!(err, TreeError::PathEscape { .. }),
439 "unexpected error variant: {err:?}"
440 );
441 assert!(!outside.path().join("new").exists());
442 }
443
444 #[test]
445 fn a_deletion_through_a_parent_symlink_escaping_root_is_refused() {
446 let (_store_dir, store) = setup_store();
447 let blobs = TreeBlobStore::new(&store);
448 let outside = TempDir::new().unwrap();
449 fs::write(outside.path().join("victim.txt"), b"original").unwrap();
450 let dest_dir = TempDir::new().unwrap();
451 std::os::unix::fs::symlink(outside.path(), dest_dir.path().join("sub")).unwrap();
452
453 let manifest = TreeManifest::new(vec![], vec![PathBuf::from("sub/victim.txt")]);
454 let err = restore_tree(&blobs, &manifest, dest_dir.path()).expect_err("must refuse");
455
456 assert!(
457 matches!(err, TreeError::PathEscape { .. }),
458 "unexpected error variant: {err:?}"
459 );
460 assert!(outside.path().join("victim.txt").exists());
461 }
462
463 #[test]
464 fn restore_replaces_a_deleted_link_with_a_created_directory_before_its_files() {
465 let (_store_dir, store) = setup_store();
466 let blobs = TreeBlobStore::new(&store);
467 let dest = TempDir::new().unwrap();
468 fs::create_dir(dest.path().join("kept")).unwrap();
469 std::os::unix::fs::symlink("kept", dest.path().join("current")).unwrap();
470 let key = blobs.put_file_blob(b"x").unwrap();
471 let mut manifest = TreeManifest::new(
472 vec![ManifestEntry::entry_for(
473 PathBuf::from("current/f"),
474 key,
475 0o644,
476 (1, 0),
477 )],
478 vec![PathBuf::from("current")],
479 );
480 manifest.dirs.push(ManifestDir {
481 path: PathBuf::from("current"),
482 mode: 0o555,
483 mtime_secs: 1_700_000_000,
484 mtime_nanos: 9,
485 });
486
487 restore_tree(&blobs, &manifest, dest.path()).unwrap();
488
489 let current = fs::symlink_metadata(dest.path().join("current")).unwrap();
490 assert!(current.is_dir());
491 assert_eq!(current.permissions().mode() & 0o7777, 0o555);
492 assert_eq!(
493 FileTime::from_last_modification_time(¤t),
494 FileTime::from_unix_time(1_700_000_000, 9)
495 );
496 assert_eq!(fs::read(dest.path().join("current/f")).unwrap(), b"x");
497 assert_eq!(fs::read_dir(dest.path().join("kept")).unwrap().count(), 0);
498 fs::set_permissions(
499 dest.path().join("current"),
500 fs::Permissions::from_mode(0o700),
501 )
502 .unwrap();
503 }
504
505 #[test]
506 fn restore_replays_links_and_the_attributes_of_kept_directories() {
507 let (_store_dir, store) = setup_store();
508 let blobs = TreeBlobStore::new(&store);
509 let dest = TempDir::new().unwrap();
510 let d = dest.path().join("d");
511 fs::create_dir(&d).unwrap();
512 fs::write(d.join("old"), b"x").unwrap();
513 fs::write(dest.path().join("f"), b"x").unwrap();
514 let mut manifest =
515 TreeManifest::new(vec![], vec![PathBuf::from("d/old"), PathBuf::from("f")]);
516 for (path, target) in [("f", "t"), ("d/l", "../nowhere")] {
517 manifest.links.push(ManifestLink {
518 path: PathBuf::from(path),
519 target: PathBuf::from(target),
520 mtime_secs: 1_600_000_000,
521 mtime_nanos: 3,
522 });
523 }
524 manifest.dir_attrs.push(ManifestDirAttrs {
525 path: PathBuf::from("d"),
526 mode: Some(0o500),
527 mtime: Some((1_700_000_000, 9)),
528 });
529
530 restore_tree(&blobs, &manifest, dest.path()).unwrap();
531
532 assert_eq!(
533 fs::read_link(dest.path().join("f")).unwrap(),
534 Path::new("t")
535 );
536 let link = fs::symlink_metadata(d.join("l")).unwrap();
537 assert_eq!(fs::read_link(d.join("l")).unwrap(), Path::new("../nowhere"));
538 assert_eq!(
539 FileTime::from_last_modification_time(&link),
540 FileTime::from_unix_time(1_600_000_000, 3)
541 );
542 let meta = fs::metadata(&d).unwrap();
543 assert_eq!(meta.permissions().mode() & 0o7777, 0o500);
544 assert_eq!(
545 FileTime::from_last_modification_time(&meta),
546 FileTime::from_unix_time(1_700_000_000, 9)
547 );
548 fs::set_permissions(&d, fs::Permissions::from_mode(0o700)).unwrap();
549 let names: Vec<_> = fs::read_dir(&d)
550 .unwrap()
551 .map(|e| e.unwrap().file_name())
552 .collect();
553 assert_eq!(names, ["l"]);
554 }
555
556 #[test]
557 fn a_failed_restore_leaves_a_recorded_directory_as_it_was() {
558 let (_store_dir, store) = setup_store();
559 let blobs = TreeBlobStore::new(&store);
560 let new_blob = blobs.put_file_blob(b"new").unwrap();
561 let gone_blob = blobs.put_file_blob(b"gone").unwrap();
562 store.remove(&gone_blob).unwrap();
563 let before = FileTime::from_unix_time(1_600_000_000, 7);
564
565 let occupied_link = {
566 let mut m = TreeManifest::new(vec![], vec![PathBuf::from("d/old")]);
567 m.links.push(ManifestLink {
568 path: PathBuf::from("d/taken"),
569 target: PathBuf::from("t"),
570 mtime_secs: 1,
571 mtime_nanos: 0,
572 });
573 m
574 };
575 let missing_blob = TreeManifest::new(
576 vec![
577 ManifestEntry::entry_for(PathBuf::from("d/new"), new_blob, 0o644, (1, 0)),
578 ManifestEntry::entry_for(PathBuf::from("e"), gone_blob, 0o644, (1, 0)),
579 ],
580 vec![],
581 );
582 for mut manifest in [occupied_link, missing_blob] {
583 let dest = TempDir::new().unwrap();
584 let d = dest.path().join("d");
585 fs::create_dir(&d).unwrap();
586 fs::write(d.join("old"), b"x").unwrap();
587 fs::write(d.join("taken"), b"x").unwrap();
588 fs::set_permissions(&d, fs::Permissions::from_mode(0o751)).unwrap();
589 filetime::set_file_mtime(&d, before).unwrap();
590 manifest.dir_attrs.push(ManifestDirAttrs {
591 path: PathBuf::from("d"),
592 mode: Some(0o500),
593 mtime: Some((1_700_000_000, 9)),
594 });
595
596 let err = restore_tree(&blobs, &manifest, dest.path())
597 .expect_err("a later entry cannot be replayed");
598
599 assert!(
600 matches!(
601 err,
602 TreeError::Occupied { .. } | TreeError::BlobMissing { .. }
603 ),
604 "{err:?}"
605 );
606 let meta = fs::metadata(&d).unwrap();
607 assert_eq!(meta.permissions().mode() & 0o7777, 0o751);
608 assert_eq!(FileTime::from_last_modification_time(&meta), before);
609 let mut names: Vec<_> = fs::read_dir(&d)
610 .unwrap()
611 .map(|e| e.unwrap().file_name())
612 .collect();
613 names.sort();
614 assert_eq!(names, ["old", "taken"]);
615 }
616 }
617}