1use std::fs::{File, OpenOptions};
7use std::io::{self, copy};
8use std::path::Path;
9
10#[cfg(unix)]
11use std::os::unix::fs::OpenOptionsExt;
12
13pub fn copy_file_cifs_safe(source: &Path, destination: &Path) -> io::Result<u64> {
23 let mut src = File::open(source)?;
24 let mut dst = File::create(destination)?;
25 copy(&mut src, &mut dst)
26}
27
28pub fn atomic_create_file(path: &Path) -> io::Result<File> {
38 let mut opts = OpenOptions::new();
39 opts.write(true).create_new(true);
40 #[cfg(unix)]
41 {
42 opts.mode(0o644);
43 }
44 opts.open(path)
45}
46
47pub fn validate_write_target(target: &Path, expected_parent: &Path) -> io::Result<()> {
60 let target_parent = target.parent().ok_or_else(|| {
61 io::Error::new(
62 io::ErrorKind::InvalidInput,
63 "target has no parent directory",
64 )
65 })?;
66
67 let canon_parent = target_parent.canonicalize()?;
68 let canon_expected = expected_parent.canonicalize()?;
69
70 if !canon_parent.starts_with(&canon_expected) {
71 return Err(io::Error::new(
72 io::ErrorKind::PermissionDenied,
73 format!(
74 "target parent {} escapes expected parent {}",
75 canon_parent.display(),
76 canon_expected.display()
77 ),
78 ));
79 }
80
81 match std::fs::symlink_metadata(target) {
82 Ok(meta) => {
83 if meta.file_type().is_symlink() {
84 return Err(io::Error::new(
85 io::ErrorKind::PermissionDenied,
86 format!(
87 "refusing to operate on symlink target: {}",
88 target.display()
89 ),
90 ));
91 }
92 }
93 Err(e) if e.kind() == io::ErrorKind::NotFound => {}
94 Err(e) => return Err(e),
95 }
96
97 Ok(())
98}
99
100pub fn check_file_size(path: &Path, max_bytes: u64, label: &str) -> io::Result<()> {
109 let metadata = std::fs::metadata(path)?;
110 let size = metadata.len();
111 if size > max_bytes {
112 return Err(io::Error::new(
113 io::ErrorKind::InvalidInput,
114 format!(
115 "{} file too large: {} bytes (limit: {} bytes): {}",
116 label,
117 size,
118 max_bytes,
119 path.display()
120 ),
121 ));
122 }
123 Ok(())
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129 use std::fs;
130 use std::io::Write;
131 use tempfile::TempDir;
132
133 #[test]
134 fn test_copy_file_cifs_safe() -> io::Result<()> {
135 let temp = TempDir::new()?;
136 let src_path = temp.path().join("src.txt");
137 let dst_path = temp.path().join("dst.txt");
138 let content = b"hello cifs safe copy";
139 fs::write(&src_path, content)?;
140 let bytes = copy_file_cifs_safe(&src_path, &dst_path)?;
141 assert_eq!(bytes as usize, content.len());
142 let copied = fs::read(&dst_path)?;
143 assert_eq!(copied, content);
144 Ok(())
145 }
146
147 #[test]
148 fn test_atomic_create_file_new() -> io::Result<()> {
149 let temp = TempDir::new()?;
150 let path = temp.path().join("new.txt");
151 let mut f = atomic_create_file(&path)?;
152 f.write_all(b"data")?;
153 drop(f);
154 assert_eq!(fs::read(&path)?, b"data");
155 Ok(())
156 }
157
158 #[test]
159 fn test_atomic_create_file_existing_fails() -> io::Result<()> {
160 let temp = TempDir::new()?;
161 let path = temp.path().join("exists.txt");
162 fs::write(&path, b"x")?;
163 let err = atomic_create_file(&path).unwrap_err();
164 assert_eq!(err.kind(), io::ErrorKind::AlreadyExists);
165 Ok(())
166 }
167
168 #[cfg(unix)]
169 #[test]
170 fn test_atomic_create_file_mode() -> io::Result<()> {
171 use std::os::unix::fs::PermissionsExt;
172 let temp = TempDir::new()?;
173 let path = temp.path().join("mode.txt");
174 atomic_create_file(&path)?;
175 let meta = fs::metadata(&path)?;
176 let mode = meta.permissions().mode() & 0o777;
177 assert!(mode & !0o644 == 0, "unexpected mode: {:o}", mode);
179 Ok(())
180 }
181
182 #[test]
183 fn test_validate_write_target_ok() -> io::Result<()> {
184 let temp = TempDir::new()?;
185 let target = temp.path().join("file.txt");
186 validate_write_target(&target, temp.path())?;
187 Ok(())
188 }
189
190 #[cfg(unix)]
191 #[test]
192 fn test_validate_write_target_rejects_symlink_target() -> io::Result<()> {
193 let temp = TempDir::new()?;
194 let real = temp.path().join("real.txt");
195 fs::write(&real, b"x")?;
196 let link = temp.path().join("link.txt");
197 std::os::unix::fs::symlink(&real, &link)?;
198 let err = validate_write_target(&link, temp.path()).unwrap_err();
199 assert_eq!(err.kind(), io::ErrorKind::PermissionDenied);
200 Ok(())
201 }
202
203 #[test]
204 fn test_check_file_size_under_limit() -> io::Result<()> {
205 let temp = TempDir::new()?;
206 let path = temp.path().join("small.txt");
207 fs::write(&path, b"hello")?;
208 check_file_size(&path, 1024, "Test")?;
209 Ok(())
210 }
211
212 #[test]
213 fn test_check_file_size_over_limit() -> io::Result<()> {
214 let temp = TempDir::new()?;
215 let path = temp.path().join("big.txt");
216 fs::write(&path, vec![0u8; 2048])?;
217 let err = check_file_size(&path, 1024, "Test").unwrap_err();
218 assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
219 assert!(err.to_string().contains("Test file too large"));
220 Ok(())
221 }
222
223 #[test]
224 fn test_check_file_size_at_limit() -> io::Result<()> {
225 let temp = TempDir::new()?;
226 let path = temp.path().join("exact.txt");
227 fs::write(&path, vec![0u8; 1024])?;
228 check_file_size(&path, 1024, "Test")?;
229 Ok(())
230 }
231}