rusty_sponge/
writethrough.rs1use std::fs::OpenOptions;
21use std::path::Path;
22
23use crate::{Error, buffer::Buffer};
24
25pub fn write_through(buffer: Buffer, target: &Path, append: bool) -> Result<(), Error> {
28 let mut file = if append {
29 OpenOptions::new().create(true).append(true).open(target)?
35 } else {
36 OpenOptions::new()
38 .write(true)
39 .create(true)
40 .truncate(true)
41 .open(target)?
42 };
43
44 buffer.write_to(&mut file)?;
45 file.sync_data().ok();
47 Ok(())
48}
49
50pub fn requires_write_through(target: &Path) -> bool {
55 let Ok(meta) = std::fs::symlink_metadata(target) else {
56 return false;
57 };
58 let ft = meta.file_type();
59 if ft.is_symlink() {
60 return true;
61 }
62 if !ft.is_file() && !ft.is_dir() {
66 return true;
67 }
68 false
69}
70
71#[cfg(test)]
72mod tests {
73 use super::*;
74 use std::io::Cursor;
75
76 fn buffer_from(bytes: &[u8]) -> Buffer {
77 let mut b = Buffer::new();
78 let tmpdir = tempfile::tempdir().unwrap();
79 b.drain_reader(Cursor::new(bytes), 1 << 30, tmpdir.path())
80 .unwrap();
81 b
82 }
83
84 #[test]
85 fn write_through_to_new_file_creates_with_content() {
86 let tmpdir = tempfile::tempdir().unwrap();
87 let target = tmpdir.path().join("new.txt");
88 write_through(buffer_from(b"hello\n"), &target, false).unwrap();
89 assert_eq!(std::fs::read(&target).unwrap(), b"hello\n");
90 }
91
92 #[test]
93 fn write_through_truncates_existing_file() {
94 let tmpdir = tempfile::tempdir().unwrap();
95 let target = tmpdir.path().join("preexisting.txt");
96 std::fs::write(&target, b"OLD_CONTENT\n").unwrap();
97 write_through(buffer_from(b"NEW\n"), &target, false).unwrap();
98 assert_eq!(std::fs::read(&target).unwrap(), b"NEW\n");
99 }
100
101 #[test]
102 fn write_through_append_mode_concatenates() {
103 let tmpdir = tempfile::tempdir().unwrap();
104 let target = tmpdir.path().join("append.txt");
105 std::fs::write(&target, b"first\n").unwrap();
106 write_through(buffer_from(b"second\n"), &target, true).unwrap();
107 assert_eq!(std::fs::read(&target).unwrap(), b"first\nsecond\n");
108 }
109
110 #[test]
111 fn requires_write_through_false_for_missing_target() {
112 let tmpdir = tempfile::tempdir().unwrap();
113 let missing = tmpdir.path().join("does-not-exist");
114 assert!(!requires_write_through(&missing));
115 }
116
117 #[test]
118 fn requires_write_through_false_for_regular_file() {
119 let tmpdir = tempfile::tempdir().unwrap();
120 let f = tmpdir.path().join("regular.txt");
121 std::fs::write(&f, b"x").unwrap();
122 assert!(!requires_write_through(&f));
123 }
124
125 #[cfg(unix)]
126 #[test]
127 fn requires_write_through_true_for_unix_symlink() {
128 let tmpdir = tempfile::tempdir().unwrap();
129 let realfile = tmpdir.path().join("real.txt");
130 std::fs::write(&realfile, b"linked\n").unwrap();
131 let link = tmpdir.path().join("via.link");
132 std::os::unix::fs::symlink(&realfile, &link).unwrap();
133 assert!(requires_write_through(&link));
134 }
135
136 #[cfg(unix)]
137 #[test]
138 fn write_through_unix_symlink_updates_linked_file_keeps_link() {
139 let tmpdir = tempfile::tempdir().unwrap();
140 let realfile = tmpdir.path().join("real.txt");
141 std::fs::write(&realfile, b"original\n").unwrap();
142 let link = tmpdir.path().join("via.link");
143 std::os::unix::fs::symlink(&realfile, &link).unwrap();
144
145 write_through(buffer_from(b"via the link\n"), &link, false).unwrap();
146
147 assert_eq!(std::fs::read(&realfile).unwrap(), b"via the link\n");
149 let link_meta = std::fs::symlink_metadata(&link).unwrap();
151 assert!(
152 link_meta.file_type().is_symlink(),
153 "FR-010: the symlink itself MUST be preserved (not replaced)"
154 );
155 }
156}