1use crate::tree::TreeError;
7use filetime::FileTime;
8use std::collections::hash_map::Entry;
9use std::collections::HashMap;
10use std::fs;
11use std::io;
12use std::os::fd::AsRawFd;
13use std::os::unix::ffi::OsStrExt;
14use std::os::unix::fs::{MetadataExt, OpenOptionsExt, PermissionsExt};
15use std::path::{Component, Path, PathBuf};
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum DirReplace {
20 Any,
23 EmptyOnly,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30enum MissingParent {
31 Create,
32 Stop,
33}
34
35#[derive(Debug)]
36pub enum Source<'a> {
37 Bytes(&'a [u8]),
38 File(&'a Path),
39}
40
41enum Undo {
42 Created(PathBuf),
43 CreatedDir(PathBuf),
44 Overwritten {
45 path: PathBuf,
46 copy: PathBuf,
47 mode: u32,
48 mtime: FileTime,
49 },
50 MovedAside {
51 path: PathBuf,
52 aside: PathBuf,
53 },
54 DirMode {
57 path: PathBuf,
58 mode: u32,
59 },
60 DirAttrsSet {
63 path: PathBuf,
64 mode: u32,
65 mtime: FileTime,
66 },
67}
68
69struct SettleDir {
73 path: PathBuf,
74 mode: Option<u32>,
75 mtime: Option<FileTime>,
76 prior: Option<(u32, FileTime)>,
77}
78
79pub struct TreeTxn {
80 root: PathBuf,
81 root_canonical: PathBuf,
82 dirs: DirReplace,
83 undo: Vec<Undo>,
84 backups: HashMap<PathBuf, PathBuf>,
87 next_backup: u64,
88 settle: Vec<SettleDir>,
89 damaged: Vec<String>,
92}
93
94pub fn transact<T>(
99 root: &Path,
100 dirs: DirReplace,
101 f: impl FnOnce(&mut TreeTxn) -> Result<T, TreeError>,
102) -> Result<T, TreeError> {
103 let mut txn = TreeTxn {
104 root: root.to_path_buf(),
105 root_canonical: root.canonicalize()?,
106 dirs,
107 undo: Vec::new(),
108 backups: HashMap::new(),
109 next_backup: 0,
110 settle: Vec::new(),
111 damaged: Vec::new(),
112 };
113 match f(&mut txn).and_then(|value| txn.settle_dirs().map(|()| value)) {
114 Ok(value) => {
115 txn.commit();
116 Ok(value)
117 }
118 Err(cause) => Err(txn.roll_back(cause)),
119 }
120}
121
122fn holds(
125 path: &Path,
126 meta: &fs::Metadata,
127 source: &Source<'_>,
128 mode: u32,
129 mtime: FileTime,
130) -> Result<bool, TreeError> {
131 if meta.permissions().mode() & 0o7777 != mode & 0o7777
132 || FileTime::from_last_modification_time(meta) != mtime
133 {
134 return Ok(false);
135 }
136 let expected_len = match source {
137 Source::Bytes(bytes) => bytes.len() as u64,
138 Source::File(src) => fs::metadata(src)?.len(),
139 };
140 if meta.len() != expected_len {
141 return Ok(false);
142 }
143 let current = fs::read(path)?;
144 Ok(match source {
145 Source::Bytes(bytes) => current == *bytes,
146 Source::File(src) => current == fs::read(src)?,
147 })
148}
149
150impl TreeTxn {
151 pub fn write_file(
154 &mut self,
155 rel: &Path,
156 source: Source<'_>,
157 mode: u32,
158 mtime: FileTime,
159 ) -> Result<(), TreeError> {
160 check_relative(rel)?;
161 self.walk_parent(rel, MissingParent::Create)?;
162 let target = self.root.join(rel);
163 match fs::symlink_metadata(&target) {
164 Err(e) if e.kind() == io::ErrorKind::NotFound => {
165 self.undo.push(Undo::Created(target.clone()));
166 }
167 Err(e) => return Err(e.into()),
168 Ok(meta) if meta.file_type().is_symlink() => {
169 let resolved = self.resolve_in_root(&target, rel)?;
170 self.back_up_file(&resolved)?;
171 }
172 Ok(meta) if meta.is_file() => {
173 if holds(&target, &meta, &source, mode, mtime)? {
174 return Ok(());
175 }
176 self.back_up_file(&target)?
177 }
178 Ok(meta) => {
179 if meta.is_dir() {
180 self.check_replaceable_dir(&target, rel)?;
181 }
182 self.move_aside(&target)?;
183 self.undo.push(Undo::Created(target.clone()));
184 }
185 }
186 match source {
187 Source::Bytes(bytes) => fs::write(&target, bytes)?,
188 Source::File(path) => {
189 fs::copy(path, &target)?;
190 }
191 }
192 fs::set_permissions(&target, fs::Permissions::from_mode(mode))?;
194 filetime::set_file_mtime(&target, mtime)?;
195 Ok(())
196 }
197
198 pub fn create_dir(&mut self, rel: &Path, mode: u32, mtime: FileTime) -> Result<(), TreeError> {
204 check_relative(rel)?;
205 self.walk_parent(rel, MissingParent::Create)?;
206 let target = self.root.join(rel);
207 let occupied = || TreeError::NotADirectory {
208 path: rel.to_path_buf(),
209 };
210 match fs::symlink_metadata(&target) {
211 Err(e) if e.kind() == io::ErrorKind::NotFound => {
212 fs::create_dir(&target)?;
213 self.undo.push(Undo::CreatedDir(target.clone()));
214 self.settle.push(SettleDir {
215 path: target,
216 mode: Some(mode),
217 mtime: Some(mtime),
218 prior: None,
219 });
220 Ok(())
221 }
222 Err(e) => Err(e.into()),
223 Ok(meta) if meta.is_dir() => Ok(()),
224 Ok(meta) if meta.file_type().is_symlink() => {
225 if self.resolve_in_root(&target, rel)?.is_dir() {
226 Ok(())
227 } else {
228 Err(occupied())
229 }
230 }
231 Ok(_) => Err(occupied()),
232 }
233 }
234
235 pub fn create_symlink(
239 &mut self,
240 rel: &Path,
241 target: &Path,
242 mtime: FileTime,
243 ) -> Result<(), TreeError> {
244 check_relative(rel)?;
245 self.walk_parent(rel, MissingParent::Create)?;
246 let path = self.root.join(rel);
247 match fs::symlink_metadata(&path) {
248 Err(e) if e.kind() == io::ErrorKind::NotFound => {}
249 Err(e) => return Err(e.into()),
250 Ok(_) => {
251 return Err(TreeError::Occupied {
252 path: rel.to_path_buf(),
253 })
254 }
255 }
256 std::os::unix::fs::symlink(target, &path)?;
257 self.undo.push(Undo::Created(path.clone()));
258 let atime = FileTime::from_last_access_time(&fs::symlink_metadata(&path)?);
259 filetime::set_symlink_file_times(&path, atime, mtime)?;
260 Ok(())
261 }
262
263 pub fn set_dir_attrs(
269 &mut self,
270 rel: &Path,
271 mode: Option<u32>,
272 mtime: Option<FileTime>,
273 ) -> Result<(), TreeError> {
274 check_relative(rel)?;
275 let not_a_dir = || TreeError::NotADirectory {
276 path: rel.to_path_buf(),
277 };
278 if !self.walk_parent(rel, MissingParent::Stop)? {
279 return Err(not_a_dir());
280 }
281 let path = self.root.join(rel);
282 let meta = match fs::symlink_metadata(&path) {
283 Ok(meta) if meta.is_dir() => meta,
284 Ok(_) => return Err(not_a_dir()),
285 Err(e) if e.kind() == io::ErrorKind::NotFound => return Err(not_a_dir()),
286 Err(e) => return Err(e.into()),
287 };
288 let prior_mode = meta.permissions().mode() & 0o7777;
289 let euid = unsafe { libc::geteuid() };
295 if euid != 0 && meta.uid() != euid {
296 return Err(io::Error::from(io::ErrorKind::PermissionDenied).into());
297 }
298 refuse_unsettable(&path)?;
299 let prior_mtime = FileTime::from_last_modification_time(&meta);
300 self.undo.push(Undo::DirAttrsSet {
301 path: path.clone(),
302 mode: prior_mode,
303 mtime: prior_mtime,
304 });
305 self.settle.push(SettleDir {
306 path,
307 mode,
308 mtime,
309 prior: Some((prior_mode, prior_mtime)),
310 });
311 Ok(())
312 }
313
314 fn settle_dirs(&mut self) -> Result<(), TreeError> {
319 let mut dirs = std::mem::take(&mut self.settle);
320 dirs.sort_by_key(|dir| std::cmp::Reverse(dir.path.components().count()));
321 for dir in &dirs {
322 if let Some(backup) = self.backups.remove(&dir.path) {
323 if let Err(e) = remove_backup(&backup) {
324 self.damaged
325 .push(format!("partly removed backup {}: {e}", backup.display()));
326 self.backups.insert(dir.path.clone(), backup);
327 return Err(e.into());
328 }
329 }
330 }
331 for dir in dirs {
332 if let Some(mode) = dir.mode {
333 fs::set_permissions(&dir.path, fs::Permissions::from_mode(mode))?;
334 self.undo.push(Undo::DirMode {
335 path: dir.path.clone(),
336 mode: dir.prior.map_or(0o700, |(prior_mode, _)| prior_mode),
337 });
338 }
339 if let Some(mtime) = dir.mtime.or(dir.prior.map(|(_, prior_mtime)| prior_mtime)) {
340 set_dir_mtime(&dir.path, mtime)?;
341 }
342 }
343 Ok(())
344 }
345
346 pub fn remove(&mut self, rel: &Path) -> Result<(), TreeError> {
349 check_relative(rel)?;
350 if !self.walk_parent(rel, MissingParent::Stop)? {
351 return Ok(());
352 }
353 let target = self.root.join(rel);
354 match fs::symlink_metadata(&target) {
355 Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
356 Err(e) => Err(e.into()),
357 Ok(meta) => {
358 if meta.is_dir() {
359 self.check_replaceable_dir(&target, rel)?;
360 }
361 self.move_aside(&target)
362 }
363 }
364 }
365
366 fn walk_parent(&mut self, rel: &Path, missing: MissingParent) -> Result<bool, TreeError> {
370 let Some(parent) = rel.parent() else {
371 return Ok(true);
372 };
373 let mut current = self.root.clone();
374 for component in parent.components() {
375 current.push(component);
376 let not_a_dir = || TreeError::NotADirectory {
377 path: current
378 .strip_prefix(&self.root)
379 .unwrap_or(¤t)
380 .to_path_buf(),
381 };
382 let is_dir = match fs::symlink_metadata(¤t) {
383 Err(e) if e.kind() == io::ErrorKind::NotFound => {
384 if missing == MissingParent::Stop {
385 return Ok(false);
386 }
387 fs::create_dir(¤t)?;
388 self.undo.push(Undo::CreatedDir(current.clone()));
389 true
390 }
391 Err(e) => return Err(e.into()),
392 Ok(meta) if meta.file_type().is_symlink() => {
393 self.resolve_in_root(¤t, rel)?.is_dir()
394 }
395 Ok(meta) => meta.is_dir(),
396 };
397 if !is_dir {
398 return match missing {
399 MissingParent::Stop => Ok(false),
400 MissingParent::Create => Err(not_a_dir()),
401 };
402 }
403 }
404 Ok(true)
405 }
406
407 fn resolve_in_root(&self, link: &Path, rel: &Path) -> Result<PathBuf, TreeError> {
410 let escape = || TreeError::PathEscape {
411 path: rel.to_path_buf(),
412 };
413 let resolved = link.canonicalize().map_err(|_| escape())?;
414 if !resolved.starts_with(&self.root_canonical) {
415 return Err(escape());
416 }
417 Ok(resolved)
418 }
419
420 fn check_replaceable_dir(&self, dir: &Path, rel: &Path) -> Result<(), TreeError> {
421 if self.dirs == DirReplace::EmptyOnly && fs::read_dir(dir)?.next().is_some() {
422 return Err(TreeError::NonEmptyDirectory {
423 path: rel.to_path_buf(),
424 });
425 }
426 Ok(())
427 }
428
429 fn back_up_file(&mut self, path: &Path) -> Result<(), TreeError> {
430 fs::OpenOptions::new().write(true).open(path)?;
433 let meta = fs::metadata(path)?;
434 let copy = self.backup_slot_in(std::env::temp_dir())?;
435 fs::copy(path, ©)?;
436 self.undo.push(Undo::Overwritten {
437 path: path.to_path_buf(),
438 copy,
439 mode: meta.permissions().mode() & 0o7777,
440 mtime: FileTime::from_last_modification_time(&meta),
441 });
442 Ok(())
443 }
444
445 fn move_aside(&mut self, path: &Path) -> Result<(), TreeError> {
446 let aside = self.backup_slot(path)?;
447 fs::rename(path, &aside)?;
448 self.undo.push(Undo::MovedAside {
449 path: path.to_path_buf(),
450 aside,
451 });
452 Ok(())
453 }
454
455 fn backup_slot(&mut self, path: &Path) -> io::Result<PathBuf> {
458 let parent = path
459 .parent()
460 .ok_or_else(|| io::Error::other(format!("no parent for {}", path.display())))?
461 .to_path_buf();
462 self.backup_slot_in(parent)
463 }
464
465 fn backup_slot_in(&mut self, parent: PathBuf) -> io::Result<PathBuf> {
470 let dir = match self.backups.entry(parent) {
471 Entry::Occupied(e) => e.into_mut(),
472 Entry::Vacant(e) => {
473 let dir = tempfile::Builder::new()
474 .prefix(".verdant-undo-")
475 .tempdir_in(e.key())?
476 .keep();
477 e.insert(dir)
478 }
479 };
480 self.next_backup += 1;
481 Ok(dir.join(self.next_backup.to_string()))
482 }
483
484 fn commit(self) {
485 for dir in self.backups.into_values() {
486 if let Err(e) = remove_backup(&dir) {
487 eprintln!(
488 "verdant: could not remove replay backup {}: {e}",
489 dir.display()
490 );
491 }
492 }
493 }
494
495 fn drop_emptied_backup(&mut self, dir: &Path, failures: &mut Vec<String>) {
500 if let Some(backup) = self.backups.remove(dir) {
501 if let Err(e) = fs::remove_dir(&backup) {
502 failures.push(format!("{}: {e}", backup.display()));
503 self.backups.insert(dir.to_path_buf(), backup);
504 }
505 }
506 }
507
508 fn roll_back(mut self, cause: TreeError) -> TreeError {
509 let mut failures = std::mem::take(&mut self.damaged);
510 while let Some(step) = self.undo.pop() {
511 if let Undo::DirAttrsSet { path, .. } = &step {
512 if failures.is_empty() {
513 self.drop_emptied_backup(path, &mut failures);
514 }
515 }
516 if let Err((path, e)) = undo(step) {
517 failures.push(format!("{}: {e}", path.display()));
518 }
519 }
520 if failures.is_empty() {
521 self.commit();
522 return cause;
523 }
524 for dir in self.backups.into_values() {
525 failures.push(format!("backups kept in {}", dir.display()));
526 }
527 TreeError::RollbackIncomplete {
528 cause: Box::new(cause),
529 failures,
530 }
531 }
532}
533
534fn undo(step: Undo) -> Result<(), (PathBuf, io::Error)> {
535 match step {
536 Undo::Created(path) => match fs::remove_file(&path) {
537 Err(e) if e.kind() != io::ErrorKind::NotFound => Err((path, e)),
538 _ => Ok(()),
539 },
540 Undo::CreatedDir(path) => fs::remove_dir(&path).map_err(|e| (path, e)),
541 Undo::Overwritten {
542 path,
543 copy,
544 mode,
545 mtime,
546 } => fs::set_permissions(&path, fs::Permissions::from_mode(mode | 0o200))
547 .and_then(|()| fs::copy(©, &path))
548 .and_then(|_| set_mode_checked(&path, mode))
549 .and_then(|()| filetime::set_file_mtime(&path, mtime))
550 .map_err(|e| (path, e)),
551 Undo::MovedAside { path, aside } => fs::rename(&aside, &path).map_err(|e| (path, e)),
552 Undo::DirMode { path, mode } => restore_dir_mode(&path, mode).map_err(|e| (path, e)),
553 Undo::DirAttrsSet { path, mode, mtime } => restore_dir_mode(&path, mode)
554 .and_then(|()| {
555 let now = fs::symlink_metadata(&path)?;
556 if FileTime::from_last_modification_time(&now) == mtime {
557 Ok(())
558 } else {
559 set_dir_mtime(&path, mtime)
560 }
561 })
562 .map_err(|e| (path, e)),
563 }
564}
565
566fn set_dir_mtime(path: &Path, mtime: FileTime) -> io::Result<()> {
569 let c = std::ffi::CString::new(path.as_os_str().as_bytes())
570 .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
571 let times = [
572 libc::timespec {
573 tv_sec: 0,
574 tv_nsec: libc::UTIME_OMIT,
575 },
576 libc::timespec {
577 tv_sec: mtime.unix_seconds() as libc::time_t,
578 tv_nsec: mtime.nanoseconds() as libc::c_long,
579 },
580 ];
581 if unsafe { libc::utimensat(libc::AT_FDCWD, c.as_ptr(), times.as_ptr(), 0) } != 0 {
583 return Err(io::Error::last_os_error());
584 }
585 Ok(())
586}
587
588fn restore_dir_mode(path: &Path, mode: u32) -> io::Result<()> {
591 if fs::symlink_metadata(path)?.permissions().mode() & 0o7777 == mode {
592 Ok(())
593 } else {
594 set_mode_checked(path, mode)
595 }
596}
597
598fn set_mode_checked(path: &Path, mode: u32) -> io::Result<()> {
602 fs::set_permissions(path, fs::Permissions::from_mode(mode))?;
603 #[cfg(test)]
604 if CHMOD_CLEARS_SETGID.get() {
605 fs::set_permissions(path, fs::Permissions::from_mode(mode & !0o2000))?;
606 }
607 let now = fs::symlink_metadata(path)?.permissions().mode() & 0o7777;
608 if now == mode & 0o7777 {
609 Ok(())
610 } else {
611 Err(io::Error::other(format!(
612 "mode {now:o} where {:o} was restored",
613 mode & 0o7777
614 )))
615 }
616}
617
618#[cfg(test)]
619thread_local! {
620 static BACKUP_REMOVAL_FAILS: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
622 static CHMOD_CLEARS_SETGID: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
626 static INJECTED_DIR_FLAGS: std::cell::Cell<Option<(libc::c_int, libc::c_ulong)>> =
627 const { std::cell::Cell::new(None) };
628}
629
630const FS_IMMUTABLE_FL: libc::c_int = 0x10;
631const FS_APPEND_FL: libc::c_int = 0x20;
632
633fn refuse_unsettable(dir: &Path) -> io::Result<()> {
637 let (inode, mount) = dir_flags(dir)?;
638 if inode & (FS_APPEND_FL | FS_IMMUTABLE_FL) != 0 {
639 return Err(io::Error::from_raw_os_error(libc::EPERM));
640 }
641 if mount & libc::ST_RDONLY != 0 {
642 return Err(io::Error::from_raw_os_error(libc::EROFS));
643 }
644 Ok(())
645}
646
647fn dir_flags(dir: &Path) -> io::Result<(libc::c_int, libc::c_ulong)> {
651 #[cfg(test)]
652 if let Some(flags) = INJECTED_DIR_FLAGS.get() {
653 return Ok(flags);
654 }
655 let c = std::ffi::CString::new(dir.as_os_str().as_bytes())
656 .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
657 let mut stx: libc::statx = unsafe { std::mem::zeroed() };
659 let rc = unsafe {
661 libc::statx(
662 libc::AT_FDCWD,
663 c.as_ptr(),
664 libc::AT_SYMLINK_NOFOLLOW,
665 libc::STATX_BASIC_STATS,
666 &mut stx,
667 )
668 };
669 if rc != 0 {
670 return Err(io::Error::last_os_error());
671 }
672 let wanted = (libc::STATX_ATTR_APPEND | libc::STATX_ATTR_IMMUTABLE) as u64;
673 let inode = if stx.stx_attributes_mask & wanted == wanted {
674 (stx.stx_attributes & wanted) as libc::c_int
675 } else {
676 ioctl_flags(dir)?
677 };
678 let mut mount: libc::statvfs = unsafe { std::mem::zeroed() };
680 if unsafe { libc::statvfs(c.as_ptr(), &mut mount) } != 0 {
682 return Err(io::Error::last_os_error());
683 }
684 Ok((inode, mount.f_flag))
685}
686
687fn ioctl_flags(dir: &Path) -> io::Result<libc::c_int> {
691 let file = fs::OpenOptions::new()
692 .read(true)
693 .custom_flags(libc::O_DIRECTORY | libc::O_NOFOLLOW)
694 .open(dir)?;
695 let mut inode: libc::c_int = 0;
696 if unsafe { libc::ioctl(file.as_raw_fd(), libc::FS_IOC_GETFLAGS, &mut inode) } != 0 {
698 let e = io::Error::last_os_error();
699 match e.raw_os_error() {
700 Some(libc::ENOTTY | libc::EOPNOTSUPP | libc::EINVAL | libc::ENOSYS) => return Ok(0),
701 _ => return Err(e),
702 }
703 }
704 Ok(inode)
705}
706
707fn remove_backup(backup: &Path) -> io::Result<()> {
712 give_owner_rwx(backup)?;
713 #[cfg(test)]
714 if BACKUP_REMOVAL_FAILS.get() {
715 return Err(io::Error::other("injected"));
716 }
717 fs::remove_dir_all(backup)
718}
719
720fn give_owner_rwx(dir: &Path) -> io::Result<()> {
723 let mode = fs::symlink_metadata(dir)?.permissions().mode() & 0o7777;
724 if mode & 0o700 != 0o700 {
725 fs::set_permissions(dir, fs::Permissions::from_mode(mode | 0o700))?;
726 }
727 for entry in fs::read_dir(dir)? {
728 let entry = entry?;
729 if entry.file_type()?.is_dir() {
730 give_owner_rwx(&entry.path())?;
731 }
732 }
733 Ok(())
734}
735
736fn check_relative(rel: &Path) -> Result<(), TreeError> {
737 let normal = rel.components().next().is_some()
738 && rel.components().all(|c| matches!(c, Component::Normal(_)));
739 if normal {
740 Ok(())
741 } else {
742 Err(TreeError::PathEscape {
743 path: rel.to_path_buf(),
744 })
745 }
746}
747
748#[cfg(test)]
749mod tests {
750 use super::*;
751
752 type Attempt<'a> = &'a dyn Fn(&mut TreeTxn) -> Result<(), TreeError>;
753
754 fn at(secs: i64) -> FileTime {
755 FileTime::from_unix_time(secs, 0)
756 }
757
758 #[test]
759 fn overwriting_a_file_in_place_leaves_its_directory_unchanged() {
760 let root = tempfile::TempDir::new().unwrap();
761 fs::create_dir(root.path().join("dir")).unwrap();
762 fs::write(root.path().join("dir/out.txt"), b"old").unwrap();
763 filetime::set_file_mtime(root.path().join("dir"), at(1)).unwrap();
764
765 transact(root.path(), DirReplace::Any, |txn| {
766 txn.write_file(
767 Path::new("dir/out.txt"),
768 Source::Bytes(b"new"),
769 0o644,
770 at(2),
771 )
772 })
773 .unwrap();
774
775 assert_eq!(fs::read(root.path().join("dir/out.txt")).unwrap(), b"new");
776 let dir_mtime =
777 FileTime::from_last_modification_time(&fs::metadata(root.path().join("dir")).unwrap());
778 assert_eq!(dir_mtime, at(1), "the backup must not touch the directory");
779 }
780
781 #[test]
782 fn an_undo_that_fails_reports_it_and_keeps_the_backup() {
783 let root = tempfile::TempDir::new().unwrap();
784 fs::create_dir(root.path().join("locked")).unwrap();
785 fs::write(root.path().join("locked/kept.txt"), b"kept").unwrap();
786
787 let err = transact(root.path(), DirReplace::Any, |txn| {
788 txn.remove(Path::new("locked/kept.txt"))?;
789 fs::set_permissions(
790 root.path().join("locked"),
791 fs::Permissions::from_mode(0o555),
792 )?;
793 Err::<(), _>(TreeError::BlobMissing {
794 path: PathBuf::from("later.txt"),
795 })
796 })
797 .expect_err("the closure failed");
798 fs::set_permissions(
799 root.path().join("locked"),
800 fs::Permissions::from_mode(0o755),
801 )
802 .unwrap();
803
804 let TreeError::RollbackIncomplete { cause, failures } = err else {
805 panic!("unexpected error variant: {err:?}");
806 };
807 assert!(matches!(*cause, TreeError::BlobMissing { .. }));
808 assert!(
809 failures.iter().any(|f| f.starts_with("backups kept in ")),
810 "{failures:?}"
811 );
812 let backup = fs::read_dir(root.path().join("locked"))
813 .unwrap()
814 .map(|e| e.unwrap().path())
815 .find(|p| {
816 p.file_name()
817 .unwrap()
818 .to_string_lossy()
819 .starts_with(".verdant-undo-")
820 })
821 .expect("the backup directory must survive");
822 assert_eq!(fs::read(backup.join("1")).unwrap(), b"kept");
823 }
824
825 #[test]
826 fn a_panic_mid_apply_keeps_the_backups() {
827 let root = tempfile::TempDir::new().unwrap();
828 fs::create_dir(root.path().join("dist")).unwrap();
829 fs::write(root.path().join("dist/app.js"), b"built").unwrap();
830
831 let panicked = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
832 transact::<()>(root.path(), DirReplace::Any, |txn| {
833 txn.remove(Path::new("dist"))?;
834 panic!("a bug inside the closure");
835 })
836 }));
837
838 assert!(panicked.is_err());
839 let backup = fs::read_dir(root.path())
840 .unwrap()
841 .map(|e| e.unwrap().path())
842 .find(|p| {
843 p.file_name()
844 .unwrap()
845 .to_string_lossy()
846 .starts_with(".verdant-undo-")
847 })
848 .expect("the moved-aside directory must survive a panic");
849 assert_eq!(fs::read(backup.join("1/app.js")).unwrap(), b"built");
850 }
851
852 #[test]
853 fn a_parent_that_is_a_file_or_a_link_to_one_is_refused() {
854 let root = tempfile::TempDir::new().unwrap();
855 fs::write(root.path().join("out"), b"was a file").unwrap();
856 std::os::unix::fs::symlink("out", root.path().join("lnk")).unwrap();
857
858 for parent in ["out", "lnk"] {
859 let file = Path::new(parent).join("inner.txt");
860 let dir = Path::new(parent).join("d");
861 let link = Path::new(parent).join("l");
862 let attempts: [Attempt; 3] = [
863 &|txn| txn.write_file(&file, Source::Bytes(b"x"), 0o644, at(1)),
864 &|txn| txn.create_dir(&dir, 0o755, at(1)),
865 &|txn| txn.create_symlink(&link, Path::new("t"), at(1)),
866 ];
867 for attempt in attempts {
868 let err = transact(root.path(), DirReplace::Any, |txn| {
869 txn.create_dir(Path::new("made"), 0o755, at(1))?;
870 attempt(txn)
871 })
872 .expect_err("a fresh run would get ENOTDIR");
873 assert!(matches!(err, TreeError::NotADirectory { .. }), "{err:?}");
874 assert_eq!(fs::read(root.path().join("out")).unwrap(), b"was a file");
875 assert_eq!(fs::read_dir(root.path()).unwrap().count(), 2);
876 }
877 }
878 }
879
880 #[test]
881 fn a_symlink_is_created_with_its_text_and_mtime() {
882 let root = tempfile::TempDir::new().unwrap();
883
884 transact(root.path(), DirReplace::EmptyOnly, |txn| {
885 txn.create_symlink(Path::new("a/dangling"), Path::new("../nowhere"), at(5))?;
886 txn.create_symlink(Path::new("abs"), Path::new("/etc/hostname"), at(6))
887 })
888 .unwrap();
889
890 let link = root.path().join("a/dangling");
891 assert_eq!(fs::read_link(&link).unwrap(), Path::new("../nowhere"));
892 let meta = fs::symlink_metadata(&link).unwrap();
893 assert_eq!(FileTime::from_last_modification_time(&meta), at(5));
894 assert_eq!(
895 fs::read_link(root.path().join("abs")).unwrap(),
896 Path::new("/etc/hostname")
897 );
898 }
899
900 #[test]
901 fn a_symlink_over_an_entry_is_refused_unless_it_was_removed_first() {
902 let root = tempfile::TempDir::new().unwrap();
903 fs::write(root.path().join("f"), b"kept").unwrap();
904 fs::create_dir(root.path().join("d")).unwrap();
905
906 for name in ["f", "d"] {
907 let err = transact(root.path(), DirReplace::EmptyOnly, |txn| {
908 txn.create_symlink(Path::new("new"), Path::new("t"), at(1))?;
909 txn.create_symlink(Path::new(name), Path::new("t"), at(1))
910 })
911 .expect_err("a fresh ln -s would fail with EEXIST");
912 assert!(matches!(err, TreeError::Occupied { .. }), "{err:?}");
913 assert!(fs::symlink_metadata(root.path().join("new")).is_err());
914 }
915 assert_eq!(fs::read(root.path().join("f")).unwrap(), b"kept");
916
917 transact(root.path(), DirReplace::EmptyOnly, |txn| {
918 txn.remove(Path::new("f"))?;
919 txn.create_symlink(Path::new("f"), Path::new("t"), at(1))
920 })
921 .unwrap();
922 assert_eq!(
923 fs::read_link(root.path().join("f")).unwrap(),
924 Path::new("t")
925 );
926 assert_eq!(fs::read_dir(root.path()).unwrap().count(), 2);
927 }
928
929 #[test]
930 fn an_existing_directory_takes_the_changed_attributes_after_its_contents() {
931 let root = tempfile::TempDir::new().unwrap();
932 let dir = root.path().join("d");
933 fs::create_dir(&dir).unwrap();
934 fs::write(dir.join("old"), b"x").unwrap();
935 fs::set_permissions(&dir, fs::Permissions::from_mode(0o751)).unwrap();
936 filetime::set_file_mtime(&dir, at(3)).unwrap();
937 fs::create_dir(root.path().join("touched")).unwrap();
938 fs::set_permissions(
939 root.path().join("touched"),
940 fs::Permissions::from_mode(0o751),
941 )
942 .unwrap();
943
944 transact(root.path(), DirReplace::EmptyOnly, |txn| {
945 txn.set_dir_attrs(Path::new("d"), Some(0o555), Some(at(9)))?;
946 txn.set_dir_attrs(Path::new("touched"), None, Some(at(8)))?;
947 txn.remove(Path::new("d/old"))?;
948 txn.write_file(Path::new("d/new"), Source::Bytes(b"y"), 0o644, at(1))
949 })
950 .unwrap();
951
952 let meta = fs::metadata(&dir).unwrap();
953 assert_eq!(meta.permissions().mode() & 0o7777, 0o555);
954 assert_eq!(FileTime::from_last_modification_time(&meta), at(9));
955 let names: Vec<_> = fs::read_dir(&dir)
956 .unwrap()
957 .map(|e| e.unwrap().file_name())
958 .collect();
959 assert_eq!(names, ["new"]);
960 let touched = fs::metadata(root.path().join("touched")).unwrap();
961 assert_eq!(touched.permissions().mode() & 0o7777, 0o751);
962 assert_eq!(FileTime::from_last_modification_time(&touched), at(8));
963 fs::set_permissions(&dir, fs::Permissions::from_mode(0o700)).unwrap();
964 }
965
966 #[test]
967 fn changed_attributes_are_undone_when_a_later_step_fails() {
968 let root = tempfile::TempDir::new().unwrap();
969 let dir = root.path().join("d");
970 fs::create_dir(&dir).unwrap();
971 fs::set_permissions(&dir, fs::Permissions::from_mode(0o751)).unwrap();
972 filetime::set_file_mtime(&dir, at(3)).unwrap();
973
974 let err = transact(root.path(), DirReplace::Any, |txn| {
975 txn.set_dir_attrs(Path::new("d"), Some(0o700), Some(at(9)))?;
976 txn.create_dir(Path::new("z"), 0o755, at(1))?;
977 txn.remove(Path::new("z"))
978 })
979 .expect_err("settling the removed z fails");
980
981 assert!(matches!(err, TreeError::Io(_)), "{err:?}");
982 let meta = fs::metadata(&dir).unwrap();
983 assert_eq!(meta.permissions().mode() & 0o7777, 0o751);
984 assert_eq!(FileTime::from_last_modification_time(&meta), at(3));
985 assert_eq!(fs::read_dir(root.path()).unwrap().count(), 1);
986 }
987
988 fn undo_dirs(root: &Path) -> Vec<PathBuf> {
989 let mut found = Vec::new();
990 for entry in fs::read_dir(root).unwrap() {
991 let path = entry.unwrap().path();
992 if path
993 .file_name()
994 .unwrap()
995 .to_string_lossy()
996 .starts_with(".verdant-undo-")
997 {
998 found.push(path.clone());
999 }
1000 if fs::symlink_metadata(&path).unwrap().is_dir() {
1001 found.extend(undo_dirs(&path));
1002 }
1003 }
1004 found
1005 }
1006
1007 #[test]
1008 fn a_removed_directory_holding_a_read_only_subdir_leaves_no_backup() {
1009 let root = tempfile::TempDir::new().unwrap();
1010 let dir = root.path().join("d");
1011 for removed in [dir.join("x"), root.path().join("top")] {
1012 fs::create_dir_all(removed.join("ro")).unwrap();
1013 fs::write(removed.join("ro/f"), b"x").unwrap();
1014 fs::set_permissions(removed.join("ro"), fs::Permissions::from_mode(0o555)).unwrap();
1015 }
1016
1017 let settled = transact(root.path(), DirReplace::Any, |txn| {
1018 txn.set_dir_attrs(Path::new("d"), Some(0o750), Some(at(9)))?;
1019 txn.remove(Path::new("d/x"))?;
1020 txn.remove(Path::new("top"))
1021 });
1022 let leftover = undo_dirs(root.path());
1023 for backup in &leftover {
1024 fs::set_permissions(backup, fs::Permissions::from_mode(0o700)).unwrap();
1025 }
1026
1027 settled.unwrap();
1028 assert_eq!(leftover, Vec::<PathBuf>::new());
1029 assert_eq!(fs::read_dir(&dir).unwrap().count(), 0);
1030 let meta = fs::metadata(&dir).unwrap();
1031 assert_eq!(meta.permissions().mode() & 0o7777, 0o750);
1032 assert_eq!(FileTime::from_last_modification_time(&meta), at(9));
1033 }
1034
1035 #[test]
1036 fn a_backup_removal_that_fails_while_settling_is_never_a_clean_rollback() {
1037 let root = tempfile::TempDir::new().unwrap();
1038 let dir = root.path().join("d");
1039 fs::create_dir_all(dir.join("x")).unwrap();
1040 fs::write(dir.join("x/f"), b"kept").unwrap();
1041
1042 BACKUP_REMOVAL_FAILS.set(true);
1043 let err = transact(root.path(), DirReplace::Any, |txn| {
1044 txn.set_dir_attrs(Path::new("d"), Some(0o750), Some(at(9)))?;
1045 txn.remove(Path::new("d/x"))
1046 })
1047 .expect_err("removing the backup failed");
1048 BACKUP_REMOVAL_FAILS.set(false);
1049
1050 let TreeError::RollbackIncomplete { failures, .. } = err else {
1051 panic!("a partly removed backup must not look clean: {err:?}");
1052 };
1053 let backup = undo_dirs(root.path()).pop().expect("the backup is kept");
1054 let named = format!("partly removed backup {}: injected", backup.display());
1055 assert!(failures.contains(&named), "{failures:?}");
1056 assert!(
1057 failures.contains(&format!("backups kept in {}", backup.display())),
1058 "{failures:?}"
1059 );
1060 }
1061
1062 #[test]
1063 fn an_append_only_immutable_or_read_only_directory_is_refused_before_any_write() {
1064 for flags in [
1065 (FS_APPEND_FL, 0),
1066 (FS_IMMUTABLE_FL, 0),
1067 (0, libc::ST_RDONLY),
1068 ] {
1069 let root = tempfile::TempDir::new().unwrap();
1070 let dir = root.path().join("d");
1071 fs::create_dir(&dir).unwrap();
1072 fs::set_permissions(&dir, fs::Permissions::from_mode(0o751)).unwrap();
1073 filetime::set_file_mtime(&dir, at(3)).unwrap();
1074 let ctime = fs::metadata(&dir).unwrap().ctime_nsec();
1075
1076 INJECTED_DIR_FLAGS.set(Some(flags));
1077 let result = transact(root.path(), DirReplace::Any, |txn| {
1078 txn.set_dir_attrs(Path::new("d"), Some(0o700), Some(at(9)))?;
1079 txn.write_file(Path::new("d/new"), Source::Bytes(b"x"), 0o644, at(1))
1080 });
1081 INJECTED_DIR_FLAGS.set(None);
1082
1083 let err = result.expect_err("the directory's attributes could not be put back");
1084 assert!(matches!(err, TreeError::Io(_)), "{flags:?}: {err:?}");
1085 let meta = fs::metadata(&dir).unwrap();
1086 assert_eq!(meta.permissions().mode() & 0o7777, 0o751);
1087 assert_eq!(FileTime::from_last_modification_time(&meta), at(3));
1088 assert_eq!(meta.ctime_nsec(), ctime);
1089 assert!(!dir.join("new").exists());
1090 }
1091 }
1092
1093 #[test]
1094 fn an_undo_that_loses_the_setgid_bit_is_not_a_clean_rollback() {
1095 let root = tempfile::TempDir::new().unwrap();
1096 let dir = root.path().join("d");
1097 fs::create_dir(&dir).unwrap();
1098 fs::write(root.path().join("f"), b"old").unwrap();
1099 for path in [&dir, &root.path().join("f")] {
1100 fs::set_permissions(path, fs::Permissions::from_mode(0o2750)).unwrap();
1101 }
1102
1103 let err = transact(root.path(), DirReplace::Any, |txn| {
1104 txn.set_dir_attrs(Path::new("d"), Some(0o700), None)?;
1105 txn.write_file(Path::new("f"), Source::Bytes(b"new"), 0o644, at(1))?;
1106 CHMOD_CLEARS_SETGID.set(true);
1107 txn.create_dir(Path::new("z"), 0o755, at(1))?;
1108 txn.remove(Path::new("z"))
1109 })
1110 .expect_err("settling the removed z fails");
1111 CHMOD_CLEARS_SETGID.set(false);
1112
1113 let TreeError::RollbackIncomplete { failures, .. } = err else {
1114 panic!("a restored mode without S_ISGID must not look clean: {err:?}");
1115 };
1116 for name in ["d", "f"] {
1117 let path = root.path().join(name).display().to_string();
1118 assert!(
1119 failures.iter().any(|f| f.starts_with(&format!("{path}: "))),
1120 "{name}: {failures:?}"
1121 );
1122 }
1123 }
1124
1125 #[test]
1126 fn a_directory_its_owner_cannot_list_still_takes_recorded_attributes() {
1127 let root = tempfile::TempDir::new().unwrap();
1128 let dir = root.path().join("d");
1129 fs::create_dir(&dir).unwrap();
1130 fs::write(dir.join("x"), b"x").unwrap();
1131 fs::set_permissions(&dir, fs::Permissions::from_mode(0o311)).unwrap();
1132
1133 let result = transact(root.path(), DirReplace::Any, |txn| {
1134 txn.set_dir_attrs(Path::new("d"), None, Some(at(9)))?;
1135 txn.remove(Path::new("d/x"))
1136 });
1137
1138 fs::set_permissions(&dir, fs::Permissions::from_mode(0o755)).unwrap();
1139 result.unwrap();
1140 let meta = fs::metadata(&dir).unwrap();
1141 assert_eq!(FileTime::from_last_modification_time(&meta), at(9));
1142 assert!(!dir.join("x").exists());
1143 }
1144
1145 #[test]
1146 fn a_directory_without_inode_flags_reports_none() {
1147 let root = tempfile::TempDir::new().unwrap();
1148 let (inode, mount) = dir_flags(root.path()).unwrap();
1149 assert_eq!(inode & (FS_APPEND_FL | FS_IMMUTABLE_FL), 0);
1150 assert_eq!(mount & libc::ST_RDONLY, 0);
1151 }
1152
1153 #[test]
1154 fn recorded_attributes_survive_a_settle_failure_after_a_write_inside() {
1155 let root = tempfile::TempDir::new().unwrap();
1156 let dir = root.path().join("d");
1157 fs::create_dir(&dir).unwrap();
1158 fs::write(dir.join("old"), b"x").unwrap();
1159 fs::set_permissions(&dir, fs::Permissions::from_mode(0o751)).unwrap();
1160 filetime::set_file_mtime(&dir, at(3)).unwrap();
1161
1162 let err = transact(root.path(), DirReplace::Any, |txn| {
1163 txn.set_dir_attrs(Path::new("d"), Some(0o555), Some(at(9)))?;
1164 txn.write_file(Path::new("d/new"), Source::Bytes(b"y"), 0o644, at(1))?;
1165 txn.create_dir(Path::new("z"), 0o755, at(1))?;
1166 txn.remove(Path::new("z"))
1167 })
1168 .expect_err("settling the removed z fails");
1169
1170 assert!(matches!(err, TreeError::Io(_)), "{err:?}");
1171 let meta = fs::metadata(&dir).unwrap();
1172 assert_eq!(meta.permissions().mode() & 0o7777, 0o751);
1173 assert_eq!(FileTime::from_last_modification_time(&meta), at(3));
1174 let names: Vec<_> = fs::read_dir(&dir)
1175 .unwrap()
1176 .map(|e| e.unwrap().file_name())
1177 .collect();
1178 assert_eq!(names, ["old"]);
1179 }
1180
1181 #[test]
1182 fn a_mode_only_change_keeps_the_mtime_the_directory_had() {
1183 let root = tempfile::TempDir::new().unwrap();
1184 let dir = root.path().join("d");
1185 fs::create_dir(&dir).unwrap();
1186 fs::write(dir.join("x"), b"x").unwrap();
1187 fs::set_permissions(&dir, fs::Permissions::from_mode(0o751)).unwrap();
1188 filetime::set_file_mtime(&dir, at(3)).unwrap();
1189
1190 transact(root.path(), DirReplace::Any, |txn| {
1191 txn.set_dir_attrs(Path::new("d"), Some(0o700), None)?;
1192 txn.remove(Path::new("d/x"))
1193 })
1194 .unwrap();
1195
1196 let meta = fs::metadata(&dir).unwrap();
1197 assert_eq!(meta.permissions().mode() & 0o7777, 0o700);
1198 assert_eq!(FileTime::from_last_modification_time(&meta), at(3));
1199 assert_eq!(fs::read_dir(&dir).unwrap().count(), 0);
1200 }
1201
1202 #[test]
1203 fn a_refused_replay_that_never_touched_a_recorded_directory_leaves_its_ctime() {
1204 let root = tempfile::TempDir::new().unwrap();
1205 let dir = root.path().join("d");
1206 fs::create_dir(&dir).unwrap();
1207 fs::write(root.path().join("taken"), b"x").unwrap();
1208 std::thread::sleep(std::time::Duration::from_millis(20));
1210 let before = fs::metadata(&dir).unwrap();
1211
1212 let err = transact(root.path(), DirReplace::Any, |txn| {
1213 txn.set_dir_attrs(Path::new("d"), Some(0o700), Some(at(4)))?;
1214 txn.create_symlink(Path::new("taken"), Path::new("t"), at(1))
1215 })
1216 .expect_err("the link name is taken");
1217
1218 assert!(matches!(err, TreeError::Occupied { .. }), "{err:?}");
1219 let after = fs::metadata(&dir).unwrap();
1220 assert_eq!(
1221 (after.ctime(), after.ctime_nsec()),
1222 (before.ctime(), before.ctime_nsec())
1223 );
1224 assert_eq!(after.mode(), before.mode());
1225 }
1226
1227 #[test]
1228 fn attributes_for_a_directory_that_is_no_longer_one_are_refused() {
1229 let root = tempfile::TempDir::new().unwrap();
1230 fs::write(root.path().join("f"), b"x").unwrap();
1231 fs::create_dir(root.path().join("real")).unwrap();
1232 std::os::unix::fs::symlink("real", root.path().join("lnk")).unwrap();
1233
1234 for name in ["f", "lnk", "absent", "f/below"] {
1235 let err = transact(root.path(), DirReplace::EmptyOnly, |txn| {
1236 txn.set_dir_attrs(Path::new(name), Some(0o700), None)
1237 })
1238 .expect_err("the run saw a directory there");
1239 assert!(
1240 matches!(err, TreeError::NotADirectory { .. }),
1241 "{name}: {err:?}"
1242 );
1243 }
1244 assert!(!root.path().join("absent").exists());
1245 }
1246
1247 fn read_only(path: &Path) -> bool {
1248 fs::set_permissions(path, fs::Permissions::from_mode(0o444)).unwrap();
1249 fs::OpenOptions::new().write(true).open(path).is_err()
1250 }
1251
1252 #[test]
1253 fn a_read_only_target_fails_without_touching_the_tree() {
1254 let root = tempfile::TempDir::new().unwrap();
1255 fs::write(root.path().join("ro"), b"old").unwrap();
1256 if !read_only(&root.path().join("ro")) {
1257 eprintln!("skipping: running as a user that writes read-only files");
1258 return;
1259 }
1260
1261 let err = transact(root.path(), DirReplace::Any, |txn| {
1262 txn.write_file(Path::new("ro"), Source::Bytes(b"new"), 0o644, at(1))
1263 })
1264 .expect_err("a read-only file cannot be written");
1265
1266 assert!(matches!(err, TreeError::Io(_)), "{err:?}");
1267 assert_eq!(fs::read(root.path().join("ro")).unwrap(), b"old");
1268 assert_eq!(
1269 fs::read_dir(root.path()).unwrap().count(),
1270 1,
1271 "no backups left"
1272 );
1273 }
1274
1275 #[test]
1276 fn a_write_that_leaves_a_file_read_only_is_undone() {
1277 let root = tempfile::TempDir::new().unwrap();
1278 fs::write(root.path().join("a"), b"old").unwrap();
1279 fs::set_permissions(root.path().join("a"), fs::Permissions::from_mode(0o640)).unwrap();
1280
1281 let err = transact(root.path(), DirReplace::Any, |txn| {
1282 txn.write_file(Path::new("a"), Source::Bytes(b"new"), 0o444, at(1))?;
1283 Err::<(), _>(TreeError::BlobMissing {
1284 path: PathBuf::from("later.txt"),
1285 })
1286 })
1287 .expect_err("the closure failed");
1288
1289 assert!(matches!(err, TreeError::BlobMissing { .. }), "{err:?}");
1290 assert_eq!(fs::read(root.path().join("a")).unwrap(), b"old");
1291 let mode = fs::metadata(root.path().join("a"))
1292 .unwrap()
1293 .permissions()
1294 .mode();
1295 assert_eq!(mode & 0o7777, 0o640);
1296 }
1297
1298 #[test]
1299 fn a_path_leaving_the_root_is_refused() {
1300 let root = tempfile::TempDir::new().unwrap();
1301 for rel in ["../x", "/etc/x", "a/../../x", ""] {
1302 let err = transact(root.path(), DirReplace::Any, |txn| {
1303 txn.write_file(Path::new(rel), Source::Bytes(b"x"), 0o644, at(1))
1304 })
1305 .expect_err(rel);
1306 assert!(
1307 matches!(err, TreeError::PathEscape { .. }),
1308 "{rel}: {err:?}"
1309 );
1310 }
1311 }
1312
1313 #[test]
1314 fn an_overwrite_keeps_the_inode_so_hard_links_see_it() {
1315 let root = tempfile::TempDir::new().unwrap();
1316 fs::write(root.path().join("a"), b"old").unwrap();
1317 fs::hard_link(root.path().join("a"), root.path().join("b")).unwrap();
1318
1319 transact(root.path(), DirReplace::Any, |txn| {
1320 txn.write_file(Path::new("a"), Source::Bytes(b"new"), 0o644, at(1))
1321 })
1322 .unwrap();
1323
1324 assert_eq!(fs::read(root.path().join("b")).unwrap(), b"new");
1325 assert_eq!(
1326 fs::read_dir(root.path()).unwrap().count(),
1327 2,
1328 "no backups left"
1329 );
1330 }
1331
1332 #[test]
1333 fn a_write_that_changes_nothing_leaves_the_file_untouched() {
1334 let root = tempfile::TempDir::new().unwrap();
1335 let path = root.path().join("a");
1336 let write = |bytes: &'static [u8], mode: u32, mtime: i64| {
1337 transact(root.path(), DirReplace::Any, |txn| {
1338 txn.write_file(Path::new("a"), Source::Bytes(bytes), mode, at(mtime))
1339 })
1340 .unwrap();
1341 let m = fs::metadata(&path).unwrap();
1342 (m.ctime(), m.ctime_nsec())
1343 };
1344 let first = write(b"same", 0o644, 1);
1345 std::thread::sleep(std::time::Duration::from_millis(20));
1346 assert_eq!(write(b"same", 0o644, 1), first);
1347 for changed in [
1348 write(b"diff", 0o644, 1),
1349 write(b"diff", 0o600, 1),
1350 write(b"diff", 0o600, 2),
1351 ] {
1352 assert_ne!(changed, first);
1353 }
1354 assert_eq!(fs::read(&path).unwrap(), b"diff");
1355 }
1356
1357 #[test]
1358 fn a_created_directory_gets_its_mode_and_mtime_after_its_contents() {
1359 let root = tempfile::TempDir::new().unwrap();
1360
1361 transact(root.path(), DirReplace::EmptyOnly, |txn| {
1362 txn.create_dir(Path::new("ro"), 0o555, at(7))?;
1363 txn.create_dir(Path::new("ro/inner"), 0o500, at(8))?;
1364 txn.write_file(Path::new("ro/inner/f"), Source::Bytes(b"x"), 0o644, at(9))
1365 })
1366 .unwrap();
1367
1368 let ro = fs::metadata(root.path().join("ro")).unwrap();
1369 let inner = fs::metadata(root.path().join("ro/inner")).unwrap();
1370 assert_eq!(ro.permissions().mode() & 0o7777, 0o555);
1371 assert_eq!(FileTime::from_last_modification_time(&ro), at(7));
1372 assert_eq!(inner.permissions().mode() & 0o7777, 0o500);
1373 assert_eq!(FileTime::from_last_modification_time(&inner), at(8));
1374 assert_eq!(fs::read(root.path().join("ro/inner/f")).unwrap(), b"x");
1375 fs::set_permissions(
1376 root.path().join("ro/inner"),
1377 fs::Permissions::from_mode(0o700),
1378 )
1379 .unwrap();
1380 fs::set_permissions(root.path().join("ro"), fs::Permissions::from_mode(0o700)).unwrap();
1381 }
1382
1383 #[test]
1384 fn an_existing_directory_is_left_as_it_is() {
1385 let root = tempfile::TempDir::new().unwrap();
1386 let dir = root.path().join("d");
1387 fs::create_dir(&dir).unwrap();
1388 fs::set_permissions(&dir, fs::Permissions::from_mode(0o751)).unwrap();
1389 filetime::set_file_mtime(&dir, at(3)).unwrap();
1390 fs::create_dir(root.path().join("target")).unwrap();
1391 std::os::unix::fs::symlink("target", root.path().join("link")).unwrap();
1392
1393 transact(root.path(), DirReplace::EmptyOnly, |txn| {
1394 txn.create_dir(Path::new("d"), 0o700, at(5))?;
1395 txn.create_dir(Path::new("link"), 0o700, at(5))
1396 })
1397 .unwrap();
1398
1399 let meta = fs::metadata(&dir).unwrap();
1400 assert_eq!(meta.permissions().mode() & 0o7777, 0o751);
1401 assert_eq!(FileTime::from_last_modification_time(&meta), at(3));
1402 assert!(fs::symlink_metadata(root.path().join("link"))
1403 .unwrap()
1404 .file_type()
1405 .is_symlink());
1406 }
1407
1408 #[test]
1409 fn a_directory_over_a_file_or_a_dangling_link_is_refused() {
1410 let root = tempfile::TempDir::new().unwrap();
1411 fs::write(root.path().join("file"), b"kept").unwrap();
1412 std::os::unix::fs::symlink("nowhere", root.path().join("dangling")).unwrap();
1413
1414 for name in ["file", "dangling"] {
1415 let err = transact(root.path(), DirReplace::Any, |txn| {
1416 txn.create_dir(Path::new("made"), 0o755, at(1))?;
1417 txn.create_dir(Path::new(name), 0o755, at(1))
1418 })
1419 .expect_err("an occupied name must be refused");
1420 assert!(
1421 matches!(
1422 err,
1423 TreeError::NotADirectory { .. } | TreeError::PathEscape { .. }
1424 ),
1425 "{name}: {err:?}"
1426 );
1427 assert!(!root.path().join("made").exists(), "{name}");
1428 }
1429 assert_eq!(fs::read(root.path().join("file")).unwrap(), b"kept");
1430 assert_eq!(
1431 fs::read_link(root.path().join("dangling")).unwrap(),
1432 Path::new("nowhere")
1433 );
1434 }
1435
1436 #[test]
1437 fn a_failure_after_a_mode_was_set_still_removes_the_created_directories() {
1438 let root = tempfile::TempDir::new().unwrap();
1439
1440 let err = transact(root.path(), DirReplace::Any, |txn| {
1441 txn.create_dir(Path::new("x/y"), 0o500, at(1))?;
1442 txn.write_file(Path::new("x/y/f"), Source::Bytes(b"x"), 0o644, at(1))?;
1443 txn.create_dir(Path::new("z"), 0o755, at(1))?;
1444 txn.remove(Path::new("z"))
1445 })
1446 .expect_err("setting the mode of the removed z fails");
1447
1448 assert!(matches!(err, TreeError::Io(_)), "{err:?}");
1449 assert_eq!(fs::read_dir(root.path()).unwrap().count(), 0);
1450 }
1451}