1use crate::diagnostic_log::debug;
25use crate::fs_beneath::{ObjectId, entry_metadata_at, identity_at, identity_of, open_directory_at, open_file_at, read_link_at};
26use std::ffi::{OsStr, OsString};
27use std::fs::File;
28use std::io::{Read, Write};
29use std::os::unix::ffi::OsStrExt;
30use std::os::unix::fs::MetadataExt;
31use std::path::{Path, PathBuf};
32
33const TAG: &str = "conflictSideSource";
35
36#[derive(Clone, Copy, Debug, Eq, PartialEq)]
38pub enum FailureKind {
39 Io,
43 Ancestor,
46 Changed,
49}
50
51#[derive(Clone, Debug, Eq, PartialEq)]
53pub struct SaveFailure {
54 pub subject: String,
59 pub reason: String,
62 pub kind: FailureKind,
64}
65
66impl SaveFailure {
67 pub fn io(subject: String, reason: String) -> SaveFailure {
70 debug(TAG, &format!("fail closed: {subject}: {reason}"));
71 return SaveFailure { subject, reason, kind: FailureKind::Io };
72 }
73
74 fn of_kind(kind: FailureKind, subject: String, reason: String) -> SaveFailure {
77 debug(TAG, &format!("fail closed ({kind:?}): {subject}: {reason}"));
78 return SaveFailure { subject, reason, kind };
79 }
80}
81
82fn io_failure(subject: &str, error: &std::io::Error) -> SaveFailure {
85 return SaveFailure::io(String::from(subject), error.to_string());
86}
87
88fn joined(names: &[OsString]) -> String {
91 let mut text: String = String::new();
92 for (position, name) in names.iter().enumerate() {
93 if position > 0 {
94 text.push('/');
95 }
96 text.push_str(&name.to_string_lossy());
97 }
98 return text;
99}
100
101#[derive(Debug)]
103pub struct Located {
104 root: PathBuf,
108 relative: Vec<u8>,
111 parent: File,
113 name: OsString,
115}
116
117fn open_parent(root: &Path, relative: &[u8]) -> Result<Option<(File, OsString)>, SaveFailure> {
135 let shown: String = String::from_utf8_lossy(relative).into_owned();
136 let names: Vec<OsString> = match crate::fs_beneath::plain_components(Path::new(OsStr::from_bytes(relative))) {
137 Ok(found) => found,
138 Err(error) => return Err(io_failure(&shown, &error)),
139 };
140 let Some((last, ancestors)) = names.split_last() else {
141 return Err(SaveFailure::io(shown, String::from("it names no file")));
142 };
143 let mut current: File = match File::open(root) {
146 Ok(opened) => opened,
147 Err(error) => return Err(io_failure(&root.display().to_string(), &error)),
148 };
149 for (depth, name) in ancestors.iter().enumerate() {
150 match open_directory_at(¤t, name.as_os_str()) {
151 Ok(next) => current = next,
152 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
153 Err(error) => {
154 let subject: String = joined(&names[..=depth]);
155 let reason: String = match error.raw_os_error() {
156 Some(libc::ELOOP) => String::from("is a symbolic link"),
157 Some(libc::ENOTDIR) => String::from("is a file, not a directory"),
158 Some(libc::EXDEV) => {
159 return Err(SaveFailure::io(subject, String::from("is on another mount, which the wrapper does not read across")));
161 }
162 _ => format!("cannot be opened without following a link ({error})"),
163 };
164 return Err(SaveFailure::of_kind(FailureKind::Ancestor, subject, reason));
165 }
166 }
167 }
168 return Ok(Some((current, last.clone())));
169}
170
171pub fn locate(root: &Path, relative: &[u8]) -> Result<Option<Located>, SaveFailure> {
186 let Some((parent, name)) = open_parent(root, relative)? else {
187 return Ok(None);
188 };
189 return match entry_metadata_at(&parent, name.as_os_str()) {
190 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
191 Err(error) => Err(io_failure(&String::from_utf8_lossy(relative), &error)),
192 Ok(_) => Ok(Some(Located { root: root.to_path_buf(), relative: relative.to_vec(), parent, name })),
193 };
194}
195
196#[derive(Clone, Copy, Debug, Eq, PartialEq)]
203struct Stamp {
204 object: (u64, u64),
206 size: u64,
208 modified: (i64, i64),
210 changed: (i64, i64),
212 mode: u32,
214}
215
216impl Stamp {
217 fn of(metadata: &std::fs::Metadata) -> Stamp {
219 return Stamp {
220 object: (metadata.dev(), metadata.ino()),
221 size: metadata.size(),
222 modified: (metadata.mtime(), metadata.mtime_nsec()),
223 changed: (metadata.ctime(), metadata.ctime_nsec()),
224 mode: metadata.mode(),
225 };
226 }
227}
228
229fn changed(located: &Located, what: &str) -> SaveFailure {
232 return SaveFailure::of_kind(FailureKind::Changed, String::from_utf8_lossy(located.relative.as_slice()).into_owned(), what.to_string());
233}
234
235fn copy_link(located: &Located, before: &std::fs::Metadata, destination: &Path) -> Result<(), SaveFailure> {
246 let target: PathBuf = match read_link_at(&located.parent, located.name.as_os_str()) {
247 Ok(read) => read,
248 Err(error) => return Err(changed(located, format!("could not be read as a link ({error})").as_str())),
249 };
250 if let Err(error) = std::os::unix::fs::symlink(&target, destination) {
251 return Err(io_failure(&String::from_utf8_lossy(located.relative.as_slice()), &error));
252 }
253 if !link_unchanged(located, before, &target) {
254 let _removed = std::fs::remove_file(destination);
255 return Err(changed(located, "was replaced while its link target was copied"));
256 }
257 return Ok(());
258}
259
260fn link_unchanged(located: &Located, before: &std::fs::Metadata, target: &Path) -> bool {
262 let Ok(now) = entry_metadata_at(&located.parent, located.name.as_os_str()) else {
263 return false;
264 };
265 if Stamp::of(&now) != Stamp::of(before) {
266 return false;
267 }
268 let Ok(read) = read_link_at(&located.parent, located.name.as_os_str()) else {
269 return false;
270 };
271 return read == target;
272}
273
274enum CopyError {
278 System(std::io::Error),
281 Failed(SaveFailure),
284}
285
286impl From<std::io::Error> for CopyError {
287 fn from(error: std::io::Error) -> CopyError {
288 return CopyError::System(error);
289 }
290}
291
292impl From<SaveFailure> for CopyError {
293 fn from(failure: SaveFailure) -> CopyError {
294 return CopyError::Failed(failure);
295 }
296}
297
298fn copy_stream(located: &Located, from: &mut File, to: &mut File, before: Stamp, between: &mut dyn FnMut()) -> Result<(), CopyError> {
315 let mut buffer: Vec<u8> = vec![0; 1 << 16];
316 let mut copied: u64 = 0;
317 loop {
318 let count: usize = from.read(buffer.as_mut_slice())?;
319 if count == 0 {
320 break;
321 }
322 to.write_all(&buffer[..count])?;
323 copied += u64::try_from(count).unwrap_or(u64::MAX);
324 between();
325 }
326 let after: Stamp = Stamp::of(&from.metadata()?);
327 if after != before {
328 debug(TAG, &format!("{}: stamp before the copy {before:?}, after {after:?}", String::from_utf8_lossy(located.relative.as_slice())));
329 return Err(changed(located, "changed while it was copied").into());
330 }
331 if copied != before.size {
332 return Err(changed(located, format!("gave {copied} bytes where it had {}", before.size).as_str()).into());
333 }
334 let named_now: Option<ObjectId> = match open_parent(located.root.as_path(), located.relative.as_slice())? {
336 Some((parent, name)) => identity_at(&parent, name.as_os_str()).ok(),
337 None => None,
338 };
339 if named_now != identity_of(from).ok() {
340 return Err(changed(located, "no longer holds the file that was copied").into());
341 }
342 to.set_permissions(<std::fs::Permissions as std::os::unix::fs::PermissionsExt>::from_mode(before.mode & 0o777))?;
343 to.sync_all()?;
344 return Ok(());
345}
346
347fn copy_entry(located: &Located, metadata: &std::fs::Metadata, destination: &Path, between: &mut dyn FnMut()) -> Result<(), CopyError> {
361 let shown: String = String::from_utf8_lossy(located.relative.as_slice()).into_owned();
362 let mut from: File = open_file_at(&located.parent, located.name.as_os_str())?;
363 let opened: std::fs::Metadata = from.metadata()?;
364 if !opened.file_type().is_file() {
365 return Err(SaveFailure::io(shown, String::from("it is neither a file nor a symbolic link")).into());
366 }
367 let before: Stamp = Stamp::of(&opened);
368 if before != Stamp::of(metadata) {
369 return Err(changed(located, "changed between the look at its name and the open").into());
370 }
371 let mut to: File = std::fs::OpenOptions::new().write(true).create_new(true).mode_600().open(destination)?;
372 let streamed: Result<(), CopyError> = copy_stream(located, &mut from, &mut to, before, between);
373 if streamed.is_err() {
374 let _removed = std::fs::remove_file(destination);
375 }
376 return streamed;
377}
378
379pub fn copy_located(located: &Located, destination: &Path, between: &mut dyn FnMut()) -> Result<(), SaveFailure> {
404 let shown: String = String::from_utf8_lossy(located.relative.as_slice()).into_owned();
405 let metadata: std::fs::Metadata = match entry_metadata_at(&located.parent, located.name.as_os_str()) {
406 Ok(found) => found,
407 Err(error) => return Err(io_failure(&shown, &error)),
408 };
409 if metadata.file_type().is_symlink() {
410 return copy_link(located, &metadata, destination);
411 }
412 if !metadata.file_type().is_file() {
413 return Err(SaveFailure::io(shown, String::from("it is neither a file nor a symbolic link")));
414 }
415 return match copy_entry(located, &metadata, destination, between) {
416 Ok(()) => Ok(()),
417 Err(CopyError::System(error)) => Err(io_failure(&shown, &error)),
418 Err(CopyError::Failed(failure)) => Err(failure),
419 };
420}
421
422trait PrivateCreation {
425 fn mode_600(&mut self) -> &mut Self;
427}
428
429impl PrivateCreation for std::fs::OpenOptions {
430 fn mode_600(&mut self) -> &mut Self {
431 return std::os::unix::fs::OpenOptionsExt::mode(self, 0o600);
432 }
433}
434
435#[cfg(test)]
438#[path = "conflict_side_source_tests.rs"]
439mod tests;