Skip to main content

xmtp_attachments/store/
native.rs

1use std::{
2    collections::HashSet,
3    io,
4    path::{Path, PathBuf},
5    sync::Arc,
6};
7
8use cap_fs_ext::DirExt as _;
9use cap_fs_ext::{FollowSymlinks, OpenOptionsFollowExt};
10#[cfg(windows)]
11use cap_std::fs::MetadataExt;
12#[cfg(windows)]
13use cap_std::fs::OpenOptionsExt as _;
14use cap_std::fs::{Dir, OpenOptions};
15#[cfg(unix)]
16use cap_std::fs::{DirBuilderExt as _, OpenOptionsExt as _, PermissionsExt as _};
17
18use super::{
19    LocalStore, StagedFile, StoreFile, StoreMoveError, StoreRemoveError, StoreWriter,
20    is_reconcile_dir, validate_relative, validate_temp,
21};
22use crate::{AttachmentDecoder, AttachmentError, AttachmentFailureCause as Cause, DecodedMeta};
23
24/// Create missing directory components with private permissions.
25/// Existing directories keep their permissions.
26pub async fn create_private_directory(path: &Path) -> io::Result<()> {
27    #[cfg(unix)]
28    {
29        let path = path.to_path_buf();
30        xmtp_common::task::spawn_blocking(move || create_private_directories(&path))
31            .await
32            .map_err(io::Error::other)?
33    }
34    #[cfg(not(unix))]
35    {
36        tokio::fs::create_dir_all(path).await
37    }
38}
39
40/// Files below one native attachments directory.
41#[derive(Clone, Debug)]
42pub struct NativeStore {
43    root: PathBuf,
44    // Resolve every child from this handle, even if the root path changes.
45    root_dir: Arc<Dir>,
46    #[cfg(test)]
47    forced_hard_link_error: Option<std::io::ErrorKind>,
48    #[cfg(test)]
49    forced_source_unlink_error: Option<std::io::ErrorKind>,
50    #[cfg(test)]
51    fallback_race_bytes: Option<Vec<u8>>,
52    #[cfg(test)]
53    forced_chmod_error: bool,
54    #[cfg(test)]
55    forced_foreign_owner: bool,
56    #[cfg(test)]
57    forced_modified_error: bool,
58}
59
60impl NativeStore {
61    pub async fn new(root: impl AsRef<Path>) -> Result<Self, AttachmentError> {
62        let root = std::path::absolute(root.as_ref())
63            .map_err(|_| AttachmentError::new(Cause::LocalStorage))?;
64        #[cfg(unix)]
65        {
66            let path = root.clone();
67            xmtp_common::task::spawn_blocking(move || create_private_directories(&path))
68                .await
69                .map_err(storage_error)?
70                .map_err(storage_error)?;
71        }
72        #[cfg(not(unix))]
73        tokio::fs::create_dir_all(&root)
74            .await
75            .map_err(storage_error)?;
76        let root_dir =
77            Dir::open_ambient_dir(&root, cap_std::ambient_authority()).map_err(storage_error)?;
78        Ok(Self {
79            root,
80            root_dir: Arc::new(root_dir),
81            #[cfg(test)]
82            forced_hard_link_error: None,
83            #[cfg(test)]
84            forced_source_unlink_error: None,
85            #[cfg(test)]
86            fallback_race_bytes: None,
87            #[cfg(test)]
88            forced_chmod_error: false,
89            #[cfg(test)]
90            forced_foreign_owner: false,
91            #[cfg(test)]
92            forced_modified_error: false,
93        })
94    }
95
96    #[cfg(test)]
97    pub(crate) fn with_forced_hard_link_error(mut self, kind: std::io::ErrorKind) -> Self {
98        self.forced_hard_link_error = Some(kind);
99        self
100    }
101
102    #[cfg(test)]
103    pub(crate) fn with_forced_source_unlink_error(mut self, kind: std::io::ErrorKind) -> Self {
104        self.forced_source_unlink_error = Some(kind);
105        self
106    }
107
108    #[cfg(test)]
109    pub(crate) fn with_fallback_destination_race(mut self, bytes: Vec<u8>) -> Self {
110        self.fallback_race_bytes = Some(bytes);
111        self
112    }
113
114    #[cfg(test)]
115    pub(crate) fn with_forced_chmod_error(mut self) -> Self {
116        self.forced_chmod_error = true;
117        self
118    }
119
120    #[cfg(test)]
121    pub(crate) fn with_forced_foreign_owner(mut self) -> Self {
122        self.forced_foreign_owner = true;
123        self
124    }
125
126    #[cfg(test)]
127    pub(crate) fn with_forced_modified_error(mut self) -> Self {
128        self.forced_modified_error = true;
129        self
130    }
131
132    fn force_foreign_owner(&self) -> bool {
133        #[cfg(test)]
134        {
135            self.forced_foreign_owner
136        }
137        #[cfg(not(test))]
138        {
139            false
140        }
141    }
142
143    fn force_chmod_error(&self) -> bool {
144        #[cfg(test)]
145        {
146            self.forced_chmod_error
147        }
148        #[cfg(not(test))]
149        {
150            false
151        }
152    }
153
154    #[cfg(unix)]
155    fn open_managed_child(&self, parent: &Dir, name: &str, created: bool) -> io::Result<Dir> {
156        use cap_fs_ext::OsMetadataExt as _;
157        use std::os::unix::fs::{MetadataExt as _, PermissionsExt as _};
158
159        let before = parent.symlink_metadata(name)?;
160        if !before.is_dir()
161            || is_link(&before)
162            || self.force_foreign_owner()
163            || before.uid() != unsafe { libc::geteuid() }
164        {
165            return Err(io::Error::from(io::ErrorKind::PermissionDenied));
166        }
167        #[cfg(test)]
168        if created {
169            record_initial_mode(before.permissions().mode(), 0o700);
170        }
171        #[cfg(not(test))]
172        let _ = created;
173        let child = match open_child_mode_handle(parent, name) {
174            Ok(child) => child,
175            Err(error)
176                if error.kind() == io::ErrorKind::PermissionDenied
177                    && before.permissions().mode() & 0o7777 != 0o700
178                    && !self.force_chmod_error() =>
179            {
180                repair_unreadable_child_mode(parent, name, &before)?;
181                open_child_mode_handle(parent, name)?
182            }
183            Err(error) => return Err(error),
184        };
185        let metadata = child.metadata()?;
186        if !metadata.is_dir()
187            || metadata.uid() != before.uid()
188            || metadata.dev() != before.dev()
189            || metadata.ino() != before.ino()
190        {
191            return Err(io::Error::from(io::ErrorKind::PermissionDenied));
192        }
193        if metadata.permissions().mode() & 0o7777 != 0o700 {
194            if self.force_chmod_error() {
195                if created {
196                    tracing::warn!(
197                        name,
198                        "could not set private attachment directory permissions"
199                    );
200                } else {
201                    return Err(io::Error::from(io::ErrorKind::PermissionDenied));
202                }
203            } else {
204                set_private_child_mode(&child)?;
205            }
206        }
207        Ok(Dir::from_std_file(child))
208    }
209
210    fn remove_source(&self, parent: &Dir, name: &str) -> io::Result<()> {
211        #[cfg(test)]
212        if let Some(kind) = self.forced_source_unlink_error {
213            return Err(io::Error::from(kind));
214        }
215        parent.remove_file(name)
216    }
217
218    fn hard_link(
219        &self,
220        from_parent: &Dir,
221        from_name: &str,
222        to_parent: &Dir,
223        to_name: &str,
224    ) -> io::Result<()> {
225        #[cfg(test)]
226        if let Some(kind) = self.forced_hard_link_error {
227            return Err(io::Error::from(kind));
228        }
229        from_parent.hard_link(from_name, to_parent, to_name)
230    }
231
232    fn parent(&self, relative: &str, create: bool) -> io::Result<(Dir, String)> {
233        let (parent, name) = relative.rsplit_once('/').unwrap_or(("", relative));
234        let mut directory = self.root_dir.try_clone()?;
235        if !parent.is_empty() {
236            for part in parent.split('/') {
237                directory = match directory.symlink_metadata(part) {
238                    Ok(_) => {
239                        #[cfg(unix)]
240                        {
241                            self.open_managed_child(&directory, part, false)?
242                        }
243                        #[cfg(not(unix))]
244                        {
245                            directory.open_dir_nofollow(part)?
246                        }
247                    }
248                    Err(error) if create && error.kind() == io::ErrorKind::NotFound => {
249                        #[cfg(unix)]
250                        let mut builder = cap_std::fs::DirBuilder::new();
251                        #[cfg(unix)]
252                        builder.mode(0o700);
253                        #[cfg(unix)]
254                        let create_result = directory.create_dir_with(part, &builder);
255                        #[cfg(not(unix))]
256                        let create_result = directory.create_dir(part);
257                        let created = match create_result {
258                            Ok(()) => true,
259                            Err(error) if error.kind() == io::ErrorKind::AlreadyExists => false,
260                            Err(error) => return Err(error),
261                        };
262                        #[cfg(not(unix))]
263                        let _ = created;
264                        #[cfg(unix)]
265                        {
266                            self.open_managed_child(&directory, part, created)?
267                        }
268                        #[cfg(not(unix))]
269                        {
270                            directory.open_dir_nofollow(part)?
271                        }
272                    }
273                    Err(error) => return Err(error),
274                };
275            }
276        }
277        Ok((directory, name.to_owned()))
278    }
279}
280
281#[cfg(any(target_vendor = "apple", target_os = "linux", target_os = "android"))]
282fn open_child_mode_handle(parent: &Dir, name: &str) -> io::Result<std::fs::File> {
283    let fd = rustix::fs::openat(
284        parent,
285        name,
286        rustix::fs::OFlags::RDONLY
287            | rustix::fs::OFlags::DIRECTORY
288            | rustix::fs::OFlags::NOFOLLOW
289            | rustix::fs::OFlags::CLOEXEC,
290        rustix::fs::Mode::empty(),
291    )?;
292    Ok(fd.into())
293}
294
295#[cfg(all(
296    unix,
297    not(any(target_vendor = "apple", target_os = "linux", target_os = "android"))
298))]
299fn open_child_mode_handle(parent: &Dir, name: &str) -> io::Result<std::fs::File> {
300    let mut options = OpenOptions::new();
301    options.read(true).follow(FollowSymlinks::No);
302    let file = parent.open_with(name, &options)?;
303    if !file.metadata()?.is_dir() {
304        return Err(io::Error::from(io::ErrorKind::NotADirectory));
305    }
306    Ok(file.into_std())
307}
308
309#[cfg(any(target_os = "linux", target_os = "android"))]
310fn repair_unreadable_child_mode(
311    parent: &Dir,
312    name: &str,
313    before: &cap_std::fs::Metadata,
314) -> io::Result<()> {
315    use cap_fs_ext::OsMetadataExt as _;
316    use std::os::{
317        fd::AsRawFd as _,
318        unix::fs::{MetadataExt as _, PermissionsExt as _},
319    };
320
321    // O_PATH pins the child without requiring read or search permission.
322    let pinned = rustix::fs::openat(
323        parent,
324        name,
325        rustix::fs::OFlags::PATH
326            | rustix::fs::OFlags::DIRECTORY
327            | rustix::fs::OFlags::NOFOLLOW
328            | rustix::fs::OFlags::CLOEXEC,
329        rustix::fs::Mode::empty(),
330    )?;
331    let pinned: std::fs::File = pinned.into();
332    let metadata = pinned.metadata()?;
333    if !metadata.is_dir()
334        || metadata.uid() != before.uid()
335        || metadata.dev() != before.dev()
336        || metadata.ino() != before.ino()
337        || metadata.uid() != unsafe { libc::geteuid() }
338    {
339        return Err(io::Error::from(io::ErrorKind::PermissionDenied));
340    }
341    // Linux fchmod rejects O_PATH handles. The proc path refers to the pinned
342    // descriptor, so a replacement of the child name cannot redirect chmod.
343    let pinned_path = format!("/proc/self/fd/{}", pinned.as_raw_fd());
344    std::fs::set_permissions(pinned_path, std::fs::Permissions::from_mode(0o700))
345}
346
347#[cfg(all(unix, not(any(target_os = "linux", target_os = "android"))))]
348fn repair_unreadable_child_mode(
349    parent: &Dir,
350    name: &str,
351    before: &cap_std::fs::Metadata,
352) -> io::Result<()> {
353    use cap_fs_ext::OsMetadataExt as _;
354
355    let current = parent.symlink_metadata(name)?;
356    if !current.is_dir()
357        || is_link(&current)
358        || current.uid() != unsafe { libc::geteuid() }
359        || current.dev() != before.dev()
360        || current.ino() != before.ino()
361    {
362        return Err(io::Error::from(io::ErrorKind::PermissionDenied));
363    }
364    rustix::fs::chmodat(
365        parent,
366        name,
367        rustix::fs::Mode::from_raw_mode(0o700),
368        rustix::fs::AtFlags::SYMLINK_NOFOLLOW,
369    )?;
370    Ok(())
371}
372
373#[cfg(any(target_vendor = "apple", target_os = "linux", target_os = "android"))]
374fn set_private_child_mode(file: &std::fs::File) -> io::Result<()> {
375    rustix::fs::fchmod(file, rustix::fs::Mode::from_raw_mode(0o700))?;
376    Ok(())
377}
378
379#[cfg(all(
380    unix,
381    not(any(target_vendor = "apple", target_os = "linux", target_os = "android"))
382))]
383fn set_private_child_mode(file: &std::fs::File) -> io::Result<()> {
384    use std::os::unix::fs::PermissionsExt as _;
385
386    file.set_permissions(std::fs::Permissions::from_mode(0o700))
387}
388
389#[cfg(unix)]
390fn create_private_directories(path: &Path) -> io::Result<()> {
391    use std::os::unix::fs::PermissionsExt as _;
392
393    let mut current = PathBuf::new();
394    for component in path.components() {
395        current.push(component.as_os_str());
396        let mut builder = std::fs::DirBuilder::new();
397        std::os::unix::fs::DirBuilderExt::mode(&mut builder, 0o700);
398        match builder.create(&current) {
399            Ok(()) => {
400                let directory = open_created_directory(&current)?;
401                #[cfg(test)]
402                record_initial_mode(directory.metadata()?.permissions().mode(), 0o700);
403                directory.set_permissions(std::fs::Permissions::from_mode(0o700))?;
404            }
405            Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {
406                if !std::fs::metadata(&current)?.is_dir() {
407                    return Err(io::Error::from(io::ErrorKind::NotADirectory));
408                }
409            }
410            Err(error) => return Err(error),
411        }
412    }
413    Ok(())
414}
415
416#[cfg(any(target_vendor = "apple", target_os = "linux", target_os = "android"))]
417fn open_created_directory(path: &Path) -> io::Result<std::fs::File> {
418    let fd = rustix::fs::openat(
419        rustix::fs::CWD,
420        path,
421        rustix::fs::OFlags::RDONLY | rustix::fs::OFlags::DIRECTORY | rustix::fs::OFlags::NOFOLLOW,
422        rustix::fs::Mode::empty(),
423    )?;
424    Ok(fd.into())
425}
426
427#[cfg(all(
428    unix,
429    not(any(target_vendor = "apple", target_os = "linux", target_os = "android"))
430))]
431fn open_created_directory(path: &Path) -> io::Result<std::fs::File> {
432    std::fs::File::open(path)
433}
434
435#[cfg(all(test, unix))]
436static INITIAL_MODES: std::sync::Mutex<Vec<(u32, u32)>> = std::sync::Mutex::new(Vec::new());
437
438#[cfg(all(test, unix))]
439fn record_initial_mode(mode: u32, expected: u32) {
440    if std::env::var_os("XMTP_TEST_INITIAL_MODES").is_some() {
441        INITIAL_MODES.lock().unwrap().push((mode & 0o777, expected));
442    }
443}
444
445#[cfg(all(test, unix))]
446pub(super) fn take_initial_modes() -> Vec<(u32, u32)> {
447    std::mem::take(&mut INITIAL_MODES.lock().unwrap())
448}
449
450fn storage_error(_: impl Sized) -> AttachmentError {
451    AttachmentError::new(Cause::LocalStorage)
452}
453
454#[cfg(windows)]
455fn is_link(metadata: &cap_std::fs::Metadata) -> bool {
456    const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x400;
457    metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0
458}
459
460#[cfg(not(windows))]
461fn is_link(metadata: &cap_std::fs::Metadata) -> bool {
462    metadata.file_type().is_symlink()
463}
464
465/// Add the path of every entry below `dir` to `paths`. Do not follow links.
466fn entry_paths(dir: &Dir, prefix: &Path, paths: &mut HashSet<PathBuf>) -> io::Result<()> {
467    for entry in dir.entries()? {
468        let name = entry?.file_name();
469        let path = prefix.join(&name);
470        let metadata = dir.symlink_metadata(&name)?;
471        if metadata.is_dir() && !is_link(&metadata) {
472            entry_paths(&dir.open_dir_nofollow(&name)?, &path, paths)?;
473        }
474        paths.insert(path);
475    }
476    Ok(())
477}
478
479fn exists_nofollow(parent: &Dir, name: &str) -> io::Result<bool> {
480    match parent.symlink_metadata(name) {
481        Ok(metadata) if is_link(&metadata) => Err(io::Error::new(
482            io::ErrorKind::InvalidData,
483            "attachment path is a symlink",
484        )),
485        Ok(_) => Ok(true),
486        Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(false),
487        Err(error) => Err(error),
488    }
489}
490
491#[cfg(any(target_vendor = "apple", target_os = "linux", target_os = "android"))]
492fn rename_no_replace(
493    from_parent: &Dir,
494    from_name: &str,
495    to_parent: &Dir,
496    to_name: &str,
497) -> io::Result<()> {
498    rustix::fs::renameat_with(
499        from_parent,
500        from_name,
501        to_parent,
502        to_name,
503        rustix::fs::RenameFlags::NOREPLACE,
504    )
505    .map_err(Into::into)
506}
507
508#[cfg(windows)]
509fn rename_no_replace(
510    from_parent: &Dir,
511    from_name: &str,
512    to_parent: &Dir,
513    to_name: &str,
514) -> io::Result<()> {
515    use std::{
516        ffi::OsStr,
517        os::windows::{ffi::OsStrExt, io::AsRawHandle},
518    };
519    use windows_sys::Win32::Storage::FileSystem::{
520        DELETE, FILE_ADD_FILE, FILE_FLAG_BACKUP_SEMANTICS, FILE_READ_ATTRIBUTES, FILE_RENAME_INFO,
521        FileRenameInfo, SetFileInformationByHandle,
522    };
523
524    let mut options = OpenOptions::new();
525    options
526        .read(true)
527        .access_mode(DELETE | FILE_READ_ATTRIBUTES)
528        .follow(FollowSymlinks::No);
529    let source = from_parent.open_with(from_name, &options)?;
530    let mut directory_options = OpenOptions::new();
531    directory_options
532        .read(true)
533        .access_mode(FILE_ADD_FILE)
534        .custom_flags(FILE_FLAG_BACKUP_SEMANTICS);
535    let destination = to_parent.open_with(".", &directory_options)?;
536    let name: Vec<u16> = OsStr::new(to_name).encode_wide().collect();
537    let name_bytes = name
538        .len()
539        .checked_mul(2)
540        .ok_or(io::ErrorKind::InvalidInput)?;
541    let size = std::mem::offset_of!(FILE_RENAME_INFO, FileName)
542        .checked_add(name_bytes)
543        .ok_or(io::ErrorKind::InvalidInput)?
544        .max(std::mem::size_of::<FILE_RENAME_INFO>());
545    let size = u32::try_from(size).map_err(|_| io::ErrorKind::InvalidInput)?;
546    let name_bytes = u32::try_from(name_bytes).map_err(|_| io::ErrorKind::InvalidInput)?;
547    let words = (size as usize).div_ceil(std::mem::size_of::<usize>());
548    let mut storage = vec![0_usize; words];
549    let info = storage.as_mut_ptr().cast::<FILE_RENAME_INFO>();
550    // The buffer is aligned for FILE_RENAME_INFO and has room for the UTF-16 name.
551    unsafe {
552        (*info).Anonymous.ReplaceIfExists = false;
553        (*info).RootDirectory = destination.as_raw_handle();
554        (*info).FileNameLength = name_bytes;
555        std::ptr::copy_nonoverlapping(
556            name.as_ptr(),
557            std::ptr::addr_of_mut!((*info).FileName).cast::<u16>(),
558            name.len(),
559        );
560        if SetFileInformationByHandle(source.as_raw_handle(), FileRenameInfo, info.cast(), size)
561            == 0
562        {
563            return Err(io::Error::last_os_error());
564        }
565    }
566    Ok(())
567}
568
569#[cfg(not(any(
570    target_vendor = "apple",
571    target_os = "linux",
572    target_os = "android",
573    windows
574)))]
575fn rename_no_replace(
576    _from_parent: &Dir,
577    _from_name: &str,
578    _to_parent: &Dir,
579    _to_name: &str,
580) -> io::Result<()> {
581    // Other native targets, including BSD and Solaris, have no fallback here.
582    // Keep both files unchanged when hard links are unavailable.
583    Err(io::Error::new(
584        io::ErrorKind::Unsupported,
585        "atomic no-replace rename is unavailable",
586    ))
587}
588
589#[async_trait::async_trait]
590impl LocalStore for NativeStore {
591    async fn open_read(&self, path: &str) -> Result<StagedFile, AttachmentError> {
592        validate_relative(path)?;
593        let (parent, name) = self.parent(path, false).map_err(storage_error)?;
594        let mut options = OpenOptions::new();
595        options.read(true).follow(FollowSymlinks::No);
596        let file = parent.open_with(&name, &options).map_err(storage_error)?;
597        if !file.metadata().map_err(storage_error)?.is_file() {
598            return Err(storage_error(()));
599        }
600        Ok(StagedFile {
601            path: self.root.join(path),
602            opened: Some(Arc::new(file.into_std())),
603        })
604    }
605
606    async fn create_temp(&self, path: &str) -> Result<StoreWriter, AttachmentError> {
607        validate_temp(path)?;
608        let (parent, name) = self.parent(path, true).map_err(storage_error)?;
609        let mut options = OpenOptions::new();
610        options
611            .write(true)
612            .create_new(true)
613            .follow(FollowSymlinks::No);
614        #[cfg(unix)]
615        options.mode(0o600);
616        let file = parent.open_with(&name, &options).map_err(storage_error)?;
617        #[cfg(all(test, unix))]
618        record_initial_mode(
619            file.metadata().map_err(storage_error)?.permissions().mode(),
620            0o600,
621        );
622        #[cfg(unix)]
623        if let Err(error) = file.set_permissions(cap_std::fs::Permissions::from_mode(0o600)) {
624            let _ = parent.remove_file(&name);
625            return Err(storage_error(error));
626        }
627        Ok(StoreWriter {
628            file: tokio::fs::File::from_std(file.into_std()),
629        })
630    }
631
632    async fn create_dir_if_absent(&self, path: &str) -> Result<bool, AttachmentError> {
633        validate_relative(path)?;
634        let (parent, name) = self.parent(path, true).map_err(storage_error)?;
635        #[cfg(unix)]
636        let mut builder = cap_std::fs::DirBuilder::new();
637        #[cfg(unix)]
638        builder.mode(0o700);
639        #[cfg(unix)]
640        let result = parent.create_dir_with(&name, &builder);
641        #[cfg(not(unix))]
642        let result = parent.create_dir(&name);
643        let created = match result {
644            Ok(()) => true,
645            Err(error) if error.kind() == io::ErrorKind::AlreadyExists => false,
646            Err(error) => return Err(storage_error(error)),
647        };
648        #[cfg(unix)]
649        let _child = self
650            .open_managed_child(&parent, &name, created)
651            .map_err(storage_error)?;
652        #[cfg(not(unix))]
653        let _child = parent.open_dir_nofollow(&name).map_err(storage_error)?;
654        Ok(created)
655    }
656
657    async fn rename(&self, from: &str, to: &str) -> Result<(), StoreMoveError> {
658        validate_relative(from)?;
659        validate_relative(to)?;
660        let (from_parent, from_name) = self.parent(from, false).map_err(storage_error)?;
661        let source = from_parent
662            .symlink_metadata(&from_name)
663            .map_err(storage_error)?;
664        if !source.is_file() || is_link(&source) {
665            return Err(storage_error(()).into());
666        }
667        let (to_parent, to_name) = self.parent(to, true).map_err(storage_error)?;
668        if exists_nofollow(&to_parent, &to_name).map_err(storage_error)? {
669            return Err(StoreMoveError::DestinationExists);
670        }
671        match self.hard_link(&from_parent, &from_name, &to_parent, &to_name) {
672            Ok(()) => match self.remove_source(&from_parent, &from_name) {
673                Ok(()) => Ok(()),
674                Err(_) => {
675                    let _ = to_parent.remove_file(&to_name);
676                    Err(storage_error(()).into())
677                }
678            },
679            Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {
680                Err(StoreMoveError::DestinationExists)
681            }
682            Err(_) => {
683                // Some file systems cannot make hard links. The fallback must
684                // reject a destination created after this check.
685                if exists_nofollow(&to_parent, &to_name).map_err(storage_error)? {
686                    return Err(StoreMoveError::DestinationExists);
687                }
688                #[cfg(test)]
689                if let Some(bytes) = &self.fallback_race_bytes {
690                    use std::io::Write;
691                    let mut options = OpenOptions::new();
692                    options.write(true).create_new(true);
693                    to_parent
694                        .open_with(&to_name, &options)
695                        .and_then(|mut file| file.write_all(bytes))
696                        .map_err(storage_error)?;
697                }
698                rename_no_replace(&from_parent, &from_name, &to_parent, &to_name).map_err(|error| {
699                    if error.kind() == io::ErrorKind::AlreadyExists {
700                        StoreMoveError::DestinationExists
701                    } else {
702                        storage_error(error).into()
703                    }
704                })
705            }
706        }
707    }
708
709    async fn replace(&self, from: &str, to: &str) -> Result<(), AttachmentError> {
710        validate_relative(from)?;
711        validate_relative(to)?;
712        let (from_parent, from_name) = self.parent(from, false).map_err(storage_error)?;
713        let source = from_parent
714            .symlink_metadata(&from_name)
715            .map_err(storage_error)?;
716        if !source.is_file() || is_link(&source) {
717            return Err(storage_error(()));
718        }
719        let (to_parent, to_name) = self.parent(to, true).map_err(storage_error)?;
720        match to_parent.symlink_metadata(&to_name) {
721            Ok(metadata) if !metadata.is_file() || is_link(&metadata) => {
722                return Err(storage_error(()));
723            }
724            Ok(_) => {}
725            Err(error) if error.kind() == io::ErrorKind::NotFound => {}
726            Err(error) => return Err(storage_error(error)),
727        }
728        from_parent
729            .rename(&from_name, &to_parent, &to_name)
730            .map_err(storage_error)
731    }
732
733    async fn remove_dir_all(&self, path: &str) -> Result<(), StoreRemoveError> {
734        validate_relative(path)?;
735        let (parent, name) = self.parent(path, false).map_err(storage_error)?;
736        let open = || -> io::Result<Dir> {
737            #[cfg(unix)]
738            {
739                self.open_managed_child(&parent, &name, false)
740            }
741            #[cfg(not(unix))]
742            {
743                parent.open_dir_nofollow(&name)
744            }
745        };
746        let child = open().map_err(storage_error)?;
747        // A removal can fail after it deleted some entries. Compare the
748        // entries before and after a failure to report that.
749        let mut before = HashSet::new();
750        entry_paths(&child, Path::new(""), &mut before).map_err(storage_error)?;
751        let Err(error) = child.remove_open_dir_all() else {
752            return Ok(());
753        };
754        let mut after = HashSet::new();
755        let removed_any = open()
756            .and_then(|child| entry_paths(&child, Path::new(""), &mut after))
757            .map_or(true, |()| !before.is_subset(&after));
758        Err(StoreRemoveError {
759            error: storage_error(error),
760            removed_any,
761        })
762    }
763
764    async fn prepare_remove_dir(&self, path: &str) -> Result<bool, AttachmentError> {
765        validate_relative(path)?;
766        let (parent, name) = match self.parent(path, false) {
767            Ok(value) => value,
768            Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(false),
769            Err(error) => return Err(storage_error(error)),
770        };
771        if !exists_nofollow(&parent, &name).map_err(storage_error)? {
772            return Ok(false);
773        }
774        #[cfg(unix)]
775        self.open_managed_child(&parent, &name, false)
776            .map_err(storage_error)?;
777        #[cfg(not(unix))]
778        parent.open_dir_nofollow(&name).map_err(storage_error)?;
779        Ok(true)
780    }
781
782    async fn remove_empty_dir(&self, path: &str) -> Result<(), AttachmentError> {
783        validate_relative(path)?;
784        let (parent, name) = self.parent(path, false).map_err(storage_error)?;
785        #[cfg(unix)]
786        self.open_managed_child(&parent, &name, false)
787            .map_err(storage_error)?;
788        #[cfg(not(unix))]
789        parent.open_dir_nofollow(&name).map_err(storage_error)?;
790        parent.remove_dir(&name).map_err(storage_error)
791    }
792
793    async fn remove_file(&self, path: &str) -> Result<(), AttachmentError> {
794        validate_relative(path)?;
795        let (parent, name) = self.parent(path, false).map_err(storage_error)?;
796        let metadata = parent.symlink_metadata(&name).map_err(storage_error)?;
797        if !metadata.is_file() || is_link(&metadata) {
798            return Err(storage_error(()));
799        }
800        parent.remove_file(&name).map_err(storage_error)
801    }
802
803    async fn exists(&self, path: &str) -> Result<bool, AttachmentError> {
804        validate_relative(path)?;
805        let (parent, name) = match self.parent(path, false) {
806            Ok(value) => value,
807            Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(false),
808            Err(error) => return Err(storage_error(error)),
809        };
810        exists_nofollow(&parent, &name).map_err(storage_error)
811    }
812
813    async fn sync(&self, writer: &mut StoreWriter) -> Result<(), AttachmentError> {
814        writer
815            .file
816            .sync_all()
817            .await
818            .map_err(|_| AttachmentError::new(Cause::LocalStorage))
819    }
820
821    async fn finish_decode(
822        &self,
823        decoder: AttachmentDecoder,
824        source: &str,
825        output: &str,
826    ) -> Result<DecodedMeta, AttachmentError> {
827        validate_temp(source)?;
828        validate_temp(output)?;
829        let (source_parent, source_name) = self.parent(source, false).map_err(storage_error)?;
830        let mut read_options = OpenOptions::new();
831        read_options.read(true).follow(FollowSymlinks::No);
832        let input = source_parent
833            .open_with(&source_name, &read_options)
834            .map_err(storage_error)?;
835        let (output_parent, output_name) = self.parent(output, true).map_err(storage_error)?;
836        let mut write_options = OpenOptions::new();
837        write_options
838            .write(true)
839            .create_new(true)
840            .follow(FollowSymlinks::No);
841        #[cfg(unix)]
842        write_options.mode(0o600);
843        let decoded = output_parent
844            .open_with(&output_name, &write_options)
845            .map_err(storage_error)?;
846        #[cfg(unix)]
847        if let Err(error) = decoded.set_permissions(cap_std::fs::Permissions::from_mode(0o600)) {
848            let _ = output_parent.remove_file(&output_name);
849            return Err(storage_error(error));
850        }
851        xmtp_common::task::spawn_blocking(move || {
852            let mut input = input.into_std();
853            let mut decoded = decoded.into_std();
854            let meta = decoder.finish(&mut input, &mut decoded)?;
855            decoded
856                .sync_all()
857                .map_err(|_| AttachmentError::new(Cause::LocalStorage))?;
858            Ok(meta)
859        })
860        .await
861        .map_err(|_| AttachmentError::new(Cause::LocalStorage))?
862    }
863
864    async fn list_files(&self) -> Result<Vec<StoreFile>, AttachmentError> {
865        use cap_std::time::SystemClock;
866        let root = self.root_dir.clone();
867        #[cfg(any(unix, test))]
868        let store = self.clone();
869        xmtp_common::task::spawn_blocking(move || -> Result<Vec<StoreFile>, AttachmentError> {
870            let mut files = Vec::new();
871            let mut dirs = vec![(root.try_clone().map_err(storage_error)?, String::new())];
872            while let Some((dir, prefix)) = dirs.pop() {
873                for entry in dir.entries().map_err(storage_error)? {
874                    let entry = entry.map_err(storage_error)?;
875                    let Ok(name) = entry.file_name().into_string() else {
876                        continue;
877                    };
878                    let descend = prefix.is_empty() && is_reconcile_dir(&name);
879                    let path = if prefix.is_empty() {
880                        name.clone()
881                    } else {
882                        format!("{prefix}/{name}")
883                    };
884                    let metadata = dir.symlink_metadata(&name).map_err(storage_error)?;
885                    if metadata.is_dir() && !is_link(&metadata) {
886                        if descend {
887                            #[cfg(unix)]
888                            let child = store
889                                .open_managed_child(&dir, &name, false)
890                                .map_err(storage_error)?;
891                            #[cfg(not(unix))]
892                            let child = dir.open_dir_nofollow(&name).map_err(storage_error)?;
893                            dirs.push((child, path));
894                        }
895                    } else if metadata.is_file() && !is_link(&metadata) && !prefix.is_empty() {
896                        #[cfg(test)]
897                        let modified = if store.forced_modified_error {
898                            Err(io::Error::other("forced modification time error"))
899                        } else {
900                            metadata.modified()
901                        };
902                        #[cfg(not(test))]
903                        let modified = metadata.modified();
904                        let modified_at_ns = match modified
905                            .map_err(storage_error)?
906                            .duration_since(SystemClock::UNIX_EPOCH)
907                        {
908                            Ok(age) => age.as_nanos().min(i64::MAX as u128) as i64,
909                            Err(error) => {
910                                -(error.duration().as_nanos().min(i64::MAX as u128) as i64)
911                            }
912                        };
913                        files.push(StoreFile {
914                            path,
915                            modified_at_ns,
916                        });
917                    }
918                }
919            }
920            Ok(files)
921        })
922        .await
923        .map_err(storage_error)?
924    }
925}