1use std::fs;
19use std::io;
20use std::path::Path;
21
22use tempfile::NamedTempFile;
23
24use crate::{Error, buffer::Buffer};
25
26pub fn write_atomic(buffer: Buffer, target: &Path, append: bool) -> Result<(), Error> {
28 let parent = target
31 .parent()
32 .filter(|p| !p.as_os_str().is_empty())
33 .unwrap_or(Path::new("."));
34
35 let mut tempfile: NamedTempFile = tempfile::Builder::new()
36 .prefix(".rusty-sponge-")
37 .tempfile_in(parent)?;
38
39 if append {
43 if let Ok(mut existing) = fs::File::open(target) {
44 io::copy(&mut existing, tempfile.as_file_mut())?;
45 }
46 }
47
48 buffer.write_to(tempfile.as_file_mut())?;
51
52 tempfile.as_file_mut().sync_data().ok();
56
57 if let Ok(existing_meta) = fs::symlink_metadata(target) {
59 if existing_meta.file_type().is_file() {
60 preserve_perms_from(&existing_meta, tempfile.path())?;
61 }
62 }
63
64 tempfile.persist(target).map_err(|e| Error::Io(e.error))?;
67 Ok(())
68}
69
70#[cfg(unix)]
71fn preserve_perms_from(meta: &fs::Metadata, tempfile_path: &Path) -> Result<(), Error> {
72 use std::os::unix::fs::{MetadataExt, PermissionsExt};
73 let mode = meta.mode();
74 let mut perms = fs::metadata(tempfile_path)?.permissions();
75 perms.set_mode(mode);
76 fs::set_permissions(tempfile_path, perms)?;
77 Ok(())
78}
79
80#[cfg(windows)]
81fn preserve_perms_from(meta: &fs::Metadata, tempfile_path: &Path) -> Result<(), Error> {
82 let was_readonly = meta.permissions().readonly();
83 let mut perms = fs::metadata(tempfile_path)?.permissions();
84 #[allow(clippy::permissions_set_readonly_false)]
85 perms.set_readonly(was_readonly);
86 fs::set_permissions(tempfile_path, perms)?;
87 Ok(())
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93 use std::io::Cursor;
94 use std::path::PathBuf;
95
96 fn empty_buffer() -> Buffer {
97 Buffer::new()
98 }
99
100 fn buffer_from(bytes: &[u8]) -> Buffer {
101 let mut b = Buffer::new();
102 let tmpdir = tempfile::tempdir().unwrap();
103 b.drain_reader(Cursor::new(bytes), 1 << 30, tmpdir.path())
104 .unwrap();
105 b
107 }
108
109 fn target_in(tmpdir: &Path, name: &str) -> PathBuf {
110 tmpdir.join(name)
111 }
112
113 #[test]
114 fn writes_buffer_atomically_to_new_target() {
115 let tmpdir = tempfile::tempdir().unwrap();
116 let target = target_in(tmpdir.path(), "out.txt");
117 write_atomic(buffer_from(b"hello\n"), &target, false).unwrap();
118 assert_eq!(fs::read(&target).unwrap(), b"hello\n");
119 }
120
121 #[test]
122 fn empty_buffer_creates_zero_byte_target() {
123 let tmpdir = tempfile::tempdir().unwrap();
124 let target = target_in(tmpdir.path(), "empty.txt");
125 write_atomic(empty_buffer(), &target, false).unwrap();
126 assert!(target.exists());
127 assert_eq!(fs::metadata(&target).unwrap().len(), 0);
128 }
129
130 #[test]
131 fn binary_bytes_passthrough_unchanged() {
132 let tmpdir = tempfile::tempdir().unwrap();
133 let target = target_in(tmpdir.path(), "bin.dat");
134 let bytes: &[u8] = &[0x00, 0xFE, 0xFF, 0xC3, 0x28, 0xA0, 0xA1];
135 write_atomic(buffer_from(bytes), &target, false).unwrap();
136 assert_eq!(fs::read(&target).unwrap(), bytes);
137 }
138
139 #[test]
140 fn replaces_existing_target() {
141 let tmpdir = tempfile::tempdir().unwrap();
142 let target = target_in(tmpdir.path(), "replace.txt");
143 fs::write(&target, b"OLD\n").unwrap();
144 write_atomic(buffer_from(b"NEW\n"), &target, false).unwrap();
145 assert_eq!(fs::read(&target).unwrap(), b"NEW\n");
146 }
147
148 #[test]
149 fn append_mode_concatenates_existing_and_stdin() {
150 let tmpdir = tempfile::tempdir().unwrap();
151 let target = target_in(tmpdir.path(), "append.txt");
152 fs::write(&target, b"original\n").unwrap();
153 write_atomic(buffer_from(b"appended\n"), &target, true).unwrap();
154 assert_eq!(fs::read(&target).unwrap(), b"original\nappended\n");
155 }
156
157 #[test]
158 fn append_mode_missing_target_treats_as_empty() {
159 let tmpdir = tempfile::tempdir().unwrap();
160 let target = target_in(tmpdir.path(), "missing.txt");
161 write_atomic(buffer_from(b"first\n"), &target, true).unwrap();
163 assert_eq!(fs::read(&target).unwrap(), b"first\n");
164 }
165
166 #[test]
167 fn append_mode_empty_stdin_preserves_existing() {
168 let tmpdir = tempfile::tempdir().unwrap();
169 let target = target_in(tmpdir.path(), "preserve.txt");
170 fs::write(&target, b"keep me\n").unwrap();
171 write_atomic(empty_buffer(), &target, true).unwrap();
172 assert_eq!(fs::read(&target).unwrap(), b"keep me\n");
173 }
174
175 #[cfg(unix)]
176 #[test]
177 fn unix_mode_bits_preserved_on_replacement() {
178 use std::os::unix::fs::PermissionsExt;
179 let tmpdir = tempfile::tempdir().unwrap();
180 let target = target_in(tmpdir.path(), "perms.txt");
181 fs::write(&target, b"old\n").unwrap();
182 fs::set_permissions(&target, fs::Permissions::from_mode(0o640)).unwrap();
183 write_atomic(buffer_from(b"new\n"), &target, false).unwrap();
184 let mode = fs::metadata(&target).unwrap().permissions().mode() & 0o777;
185 assert_eq!(
186 mode, 0o640,
187 "prior mode must be preserved on atomic replace"
188 );
189 }
190}