Skip to main content

tilewright_source/
access.rs

1// Copyright 2026 Seraphina Studios
2// SPDX-License-Identifier: MIT
3
4//! Confined access to the files of a source tree, for tools that edit it.
5//!
6//! This is the loader's own path confinement: paths are root-relative and
7//! `/`-separated, with no empty, `.` or `..` component and no `\`, `:` or
8//! NUL in any component, and nothing beneath the root is followed if it is
9//! a symbolic link. On Unix every component is opened through its parent's
10//! directory handle without following links, and writes, renames and
11//! removals act on names inside a directory opened that way, so a link
12//! swapped in at any moment is never followed. Elsewhere components are
13//! checked by path before use, on Windows for any reparse point (symbolic
14//! links, junctions and mount points among them), a best effort that a
15//! local process changing the tree at the same time can race.
16
17use crate::files::{Refused, components, platform};
18use std::{
19    fmt,
20    io::Read as _,
21    path::{Path, PathBuf},
22};
23
24/// Why a path beneath the root could not be used.
25#[derive(Debug)]
26pub enum AccessError {
27    /// The path has an empty, `.` or `..` component, or a component with
28    /// `\`, `:` or NUL; or a name has a `/`.
29    Path(String),
30    /// A symbolic link at this prefix of the path.
31    Link(String),
32    /// The entry is not a regular file or directory, as the access needs.
33    Unexpected(String),
34    /// The file is larger than the limit of the read.
35    TooLarge(String),
36    /// Another holder has the root's lock.
37    Locked,
38    Io(String, std::io::Error),
39}
40
41impl fmt::Display for AccessError {
42    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
43        match self {
44            Self::Path(path) => write!(formatter, "{path}: not a relative path beneath the root"),
45            Self::Link(path) => write!(formatter, "{path}: symbolic links are not followed"),
46            Self::Unexpected(path) => {
47                write!(formatter, "{path}: not a regular file or directory")
48            }
49            Self::TooLarge(path) => write!(formatter, "{path}: file is too large"),
50            Self::Locked => write!(formatter, "another edit holds the source tree's lock"),
51            Self::Io(path, error) => write!(formatter, "{path}: {error}"),
52        }
53    }
54}
55
56impl std::error::Error for AccessError {}
57
58impl AccessError {
59    /// A removal of `path` that took effect but is not finished, as
60    /// [`removal_pending`](Self::removal_pending) says, for tests of what
61    /// is made of one; only Windows makes them otherwise.
62    #[doc(hidden)]
63    #[must_use]
64    pub fn pending_removal(path: &str) -> Self {
65        Self::Io(
66            path.to_owned(),
67            std::io::Error::new(std::io::ErrorKind::ResourceBusy, RemovalPending),
68        )
69    }
70
71    /// Whether this is a removal that took effect but is not finished: on
72    /// Windows a removed entry stays, on some file systems, until another
73    /// program closes it, and goes then.
74    #[must_use]
75    pub fn removal_pending(&self) -> bool {
76        type Inner = dyn std::error::Error + Send + Sync;
77        matches!(self, Self::Io(_, error) if error.get_ref().is_some_and(Inner::is::<RemovalPending>))
78    }
79
80    /// Whether this is [`SourceDirectory::create`] refusing because
81    /// something is already at the name: a file, a directory, a link
82    /// (dangling or not, on Windows any reparse point), or any other entry,
83    /// which it left as it was. It is an [`AccessError::Io`] whose error is
84    /// of kind [`std::io::ErrorKind::AlreadyExists`].
85    #[must_use]
86    pub fn already_exists(&self) -> bool {
87        type Inner = dyn std::error::Error + Send + Sync;
88        matches!(self, Self::Io(_, error) if error.kind() == std::io::ErrorKind::AlreadyExists
89            && error.get_ref().is_some_and(Inner::is::<AlreadyThere>))
90    }
91
92    /// Whether this is [`SourceDirectory::create`] failing after it made
93    /// its file and not removing it, so that the file, partly written, may
94    /// be left at the name: always on Windows, and on Unix where the entry
95    /// at the name could not be told to be still that file. It is an
96    /// [`AccessError::Io`] of the failure's own kind. When `create` fails
97    /// otherwise, nothing it made is left.
98    #[must_use]
99    pub fn may_be_left(&self) -> bool {
100        type Inner = dyn std::error::Error + Send + Sync;
101        matches!(self, Self::Io(_, error) if error.get_ref().is_some_and(Inner::is::<LeftBehind>))
102    }
103}
104
105/// Something already at the name a file is created as; see
106/// [`AccessError::already_exists`].
107#[derive(Debug)]
108pub(crate) struct AlreadyThere;
109
110impl fmt::Display for AlreadyThere {
111    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
112        formatter.write_str("something is already there, and it is left as it was")
113    }
114}
115
116impl std::error::Error for AlreadyThere {}
117
118/// The error for something already at the name a file is created as.
119pub(crate) fn already_there() -> std::io::Error {
120    std::io::Error::new(std::io::ErrorKind::AlreadyExists, AlreadyThere)
121}
122
123/// A failed creation whose file, partly written, was not removed, since it
124/// could not be told to be still the one created at the name; see
125/// [`AccessError::may_be_left`].
126#[derive(Debug)]
127pub(crate) struct LeftBehind(std::io::Error);
128
129impl fmt::Display for LeftBehind {
130    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
131        write!(
132            formatter,
133            "{}; the file it made may be left at the name, partly written, since only a file known to be the one it made is removed",
134            self.0
135        )
136    }
137}
138
139impl std::error::Error for LeftBehind {
140    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
141        Some(&self.0)
142    }
143}
144
145/// `error`, of a creation whose file was not removed, saying so; of the
146/// same kind.
147pub(crate) fn left_behind(error: Refused) -> Refused {
148    match error {
149        Refused::Io(error) => Refused::Io(std::io::Error::new(error.kind(), LeftBehind(error))),
150        other => other,
151    }
152}
153
154/// A removal that took effect but stays until another program closes the
155/// entry; see [`AccessError::removal_pending`].
156#[derive(Debug)]
157pub(crate) struct RemovalPending;
158
159impl fmt::Display for RemovalPending {
160    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
161        formatter.write_str("it is removed, but stays until another program closes it")
162    }
163}
164
165impl std::error::Error for RemovalPending {}
166
167fn refused(path: &str, refused: Refused) -> AccessError {
168    match refused {
169        Refused::Link(prefix) => AccessError::Link(prefix),
170        Refused::Missing | Refused::Unexpected => AccessError::Unexpected(path.to_owned()),
171        Refused::Io(error) => AccessError::Io(path.to_owned(), error),
172    }
173}
174
175/// The name of the file in the root that is its lock on Windows. The
176/// command line writes no output there on any platform.
177pub const LOCK_NAME: &str = ".tilewright.lock";
178
179/// The checked components of a root-relative path; the empty path is the
180/// root.
181pub(crate) fn checked(path: &str) -> Result<Vec<&str>, AccessError> {
182    if path.is_empty() {
183        return Ok(Vec::new());
184    }
185    if path.len() > 4096 || path.split('/').any(|name| checked_name(name).is_err()) {
186        return Err(AccessError::Path(path.to_owned()));
187    }
188    Ok(components(path))
189}
190
191/// Whether `name` can name an entry of a directory: not empty, `.` or `..`,
192/// and without `/`, `\`, `:` or NUL, so that no platform reads it as more
193/// than one component, a drive or a stream.
194fn checked_name(name: &str) -> Result<&str, AccessError> {
195    if name.is_empty()
196        || name == "."
197        || name == ".."
198        || name.len() > 255
199        || name.contains(['/', '\\', ':', '\0'])
200    {
201        Err(AccessError::Path(name.to_owned()))
202    } else {
203        Ok(name)
204    }
205}
206
207/// Who may use a file or directory, as publishing keeps it: where there are
208/// no modes, a read-only file has none of the write bits.
209///
210/// On Unix: the permission bits, the owner and the group, and on Linux the
211/// mandatory access control labels (`SELinux`, Smack), which publishing
212/// requires to be the same on every entry it compares. Access control that
213/// publishing could not keep is refused when the access is read: on Linux
214/// an access control list. On macOS and Windows access control lists are
215/// neither read nor kept.
216#[derive(Clone, Copy, Debug, PartialEq, Eq)]
217pub struct Access {
218    /// The permission bits, with the set-user, set-group and sticky bits.
219    pub mode: u32,
220    /// The owning user.
221    pub user: u32,
222    /// The owning group.
223    pub group: u32,
224    /// A digest of the mandatory access control labels (Linux), all zero
225    /// where there are none.
226    pub control: [u8; 32],
227}
228
229/// Who may reach content through a file or directory: for a file who may
230/// read it, for a directory who may search it. Publishing requires the
231/// entries it compares to have one audience.
232#[derive(Clone, Copy, Debug, PartialEq, Eq)]
233pub struct Audience {
234    /// The classes that may reach, as read bits.
235    reach: u32,
236    /// The owner, where not everyone may reach.
237    user: Option<u32>,
238    /// The group, where only it may reach.
239    group: Option<u32>,
240    /// The labels.
241    control: [u8; 32],
242}
243
244impl Access {
245    /// Whether the group may read and others may not, so that the group
246    /// says who may.
247    #[must_use]
248    pub const fn group_reads_alone(&self) -> bool {
249        self.mode & 0o040 != 0 && self.mode & 0o004 == 0
250    }
251
252    /// Whether the classes that may reach content through an entry with
253    /// this access, a directory if `directory`, nest: its owner may
254    /// whenever its group may, and its group whenever others may. Where
255    /// they do not, as for mode `0604`, which denies the group what it
256    /// lets others read, who may reach depends on who the owner and group
257    /// are, which [`audience`](Self::audience) does not say.
258    #[must_use]
259    pub const fn nested(&self, directory: bool) -> bool {
260        let bits = if directory {
261            self.mode & 0o111
262        } else {
263            (self.mode & 0o444) >> 2
264        };
265        let (owner, group, other) = (bits & 0o100 != 0, bits & 0o010 != 0, bits & 0o001 != 0);
266        (owner || !group) && (group || !other)
267    }
268
269    /// Who may reach content through an entry with this access, a
270    /// directory if `directory`.
271    #[must_use]
272    pub fn audience(&self, directory: bool) -> Audience {
273        let reach = if directory {
274            (self.mode & 0o111) << 2
275        } else {
276            self.mode & 0o444
277        };
278        Audience {
279            reach,
280            user: (reach != 0o444).then_some(self.user),
281            group: (reach & 0o040 != 0 && reach & 0o004 == 0).then_some(self.group),
282            control: self.control,
283        }
284    }
285
286    /// The access a new file in a directory with this access is given: the
287    /// directory's mode without the execute bits, its group and its labels,
288    /// and as its owner the user that makes it, this process's effective
289    /// user (0 where there are no users).
290    #[must_use]
291    pub fn new_file(&self) -> Self {
292        #[cfg(unix)]
293        let user = rustix::process::geteuid().as_raw();
294        #[cfg(not(unix))]
295        let user = 0;
296        Self {
297            mode: self.mode & 0o666,
298            user,
299            ..*self
300        }
301    }
302}
303
304/// A source tree root. The root itself may be a link; nothing beneath it is
305/// followed.
306pub struct SourceRoot {
307    path: PathBuf,
308    root: platform::Root,
309}
310
311impl SourceRoot {
312    /// Opens the root. On Unix the directory is opened here, and every
313    /// later access goes through that handle, so moving or replacing what
314    /// the path names afterwards does not move the root; elsewhere the path
315    /// is resolved to an absolute one here.
316    #[must_use]
317    pub fn new(root: &Path) -> Self {
318        Self {
319            path: root.to_path_buf(),
320            root: platform::Root::new(root),
321        }
322    }
323
324    /// A second handle on the same root directory.
325    #[must_use]
326    pub fn try_clone(&self) -> Self {
327        Self {
328            path: self.path.clone(),
329            root: self.root.try_clone(),
330        }
331    }
332
333    /// The path supplied when this root was opened, for display only.
334    /// It may be relative, moved or replaced; using it to reopen the tree
335    /// does not preserve this root's identity. Access files through this
336    /// root instead.
337    #[must_use]
338    pub fn path(&self) -> &Path {
339        &self.path
340    }
341
342    pub(crate) fn platform(&self) -> platform::Root {
343        self.root.try_clone()
344    }
345
346    /// What is at `path`, not following it, if anything: as the file
347    /// system finds it by that name, which on one that ignores case, or as
348    /// on Windows drops trailing dots, can be an entry named otherwise.
349    /// A path through a file names nothing; a link on the way is an error.
350    pub fn exists(&self, path: &str) -> Result<bool, AccessError> {
351        // The empty path is the root.
352        if path.is_empty() {
353            return self.directory(path, false).map(|root| root.is_some());
354        }
355        match self.root.kind(&checked(path)?) {
356            Ok(crate::files::Found::Missing) | Err(Refused::Missing | Refused::Unexpected) => {
357                Ok(false)
358            }
359            Ok(_) => Ok(true),
360            // The entry itself is a link.
361            Err(Refused::Link(prefix)) if prefix == path => Ok(true),
362            Err(other) => Err(refused(path, other)),
363        }
364    }
365
366    /// Reads a regular file of at most `limit` bytes. `Ok(None)` means the
367    /// file, or a directory on its way, is missing.
368    pub fn read(&self, path: &str, limit: u64) -> Result<Option<Vec<u8>>, AccessError> {
369        let parts = checked(path)?;
370        if parts.is_empty() {
371            return Err(AccessError::Unexpected(path.to_owned()));
372        }
373        let (file, length) = match self.root.file(&parts) {
374            Ok(opened) => opened,
375            Err(Refused::Missing) => return Ok(None),
376            Err(other) => return Err(refused(path, other)),
377        };
378        if length > limit {
379            return Err(AccessError::TooLarge(path.to_owned()));
380        }
381        // One byte past the limit shows a file that grew after the check.
382        let mut bytes = Vec::new();
383        file.take(limit.saturating_add(1))
384            .read_to_end(&mut bytes)
385            .map_err(|error| AccessError::Io(path.to_owned(), error))?;
386        if u64::try_from(bytes.len()).unwrap_or(u64::MAX) > limit {
387            return Err(AccessError::TooLarge(path.to_owned()));
388        }
389        Ok(Some(bytes))
390    }
391
392    /// Opens the directory `path` (the empty path is the root), creating it
393    /// and its missing parents when `create`. `Ok(None)` means it is missing
394    /// and was not created.
395    pub fn directory(
396        &self,
397        path: &str,
398        create: bool,
399    ) -> Result<Option<SourceDirectory>, AccessError> {
400        match self.root.directory(&checked(path)?, create) {
401            Ok(directory) => Ok(Some(SourceDirectory {
402                directory,
403                path: path.to_owned(),
404            })),
405            Err(Refused::Missing) => Ok(None),
406            Err(other) => Err(refused(path, other)),
407        }
408    }
409
410    /// Makes the directory `path` in its existing parent. Returns whether
411    /// this call made it: `false` when something by that name was already
412    /// there, which the caller then did not create.
413    pub fn make_directory(&self, path: &str) -> Result<bool, AccessError> {
414        self.root
415            .make_directory(&checked(path)?)
416            .map_err(|error| refused(path, error))
417    }
418
419    /// The UTF-8 names of the entries of the directory `path`, links
420    /// included, and how many entries it has, those whose names are not
421    /// UTF-8 included; `None` when it is missing. A directory with more
422    /// than `limit` entries is an error, [`AccessError::TooLarge`].
423    pub fn names(
424        &self,
425        path: &str,
426        limit: usize,
427    ) -> Result<Option<(std::collections::BTreeSet<String>, usize)>, AccessError> {
428        let listing = match self.root.list(&checked(path)?) {
429            Ok(listing) => listing,
430            Err(Refused::Missing) => return Ok(None),
431            Err(other) => return Err(refused(path, other)),
432        };
433        let mut names = std::collections::BTreeSet::new();
434        let mut count = 0;
435        for entry in listing {
436            if count >= limit {
437                return Err(AccessError::TooLarge(path.to_owned()));
438            }
439            count += 1;
440            let entry = entry.map_err(|error| AccessError::Io(path.to_owned(), error))?;
441            names.extend(entry.name);
442        }
443        Ok(Some((names, count)))
444    }
445
446    /// Takes the root's exclusive lock, held until the returned value is
447    /// dropped. It fails at once with [`AccessError::Locked`] if another
448    /// holder, in this process or another, has it. On Unix it is an
449    /// advisory `flock` of the root directory; on Windows the file
450    /// [`LOCK_NAME`] in the root, created afresh, shared with no one and
451    /// deleted when the lock is released or its process stops, so a file by
452    /// that name that no one holds is an error. Elsewhere there is no lock
453    /// and this always succeeds.
454    pub fn lock(&self) -> Result<SourceLock, AccessError> {
455        match self.root.lock() {
456            Ok(Some(lock)) => Ok(SourceLock(lock)),
457            Ok(None) => Err(AccessError::Locked),
458            Err(other) => Err(refused(".", other)),
459        }
460    }
461}
462
463/// The mounts of this process, read once to tell the file systems of
464/// several directories ([`SourceDirectory::unchecked_file_system`]): on
465/// Linux from `/proc/self/mountinfo`, bounded; elsewhere nothing.
466pub struct Mounts(platform::Mounts);
467
468impl Mounts {
469    /// Reads the mounts. Where they cannot be read, every directory's file
470    /// system is one that cannot be told.
471    #[must_use]
472    pub fn read() -> Self {
473        Self(platform::Mounts::read())
474    }
475}
476
477/// The root's lock; see [`SourceRoot::lock`].
478pub struct SourceLock(#[allow(dead_code)] platform::Lock);
479
480/// A directory beneath the root. Every name is a single entry of it.
481pub struct SourceDirectory {
482    directory: platform::Directory,
483    path: String,
484}
485
486impl SourceDirectory {
487    fn entry(&self, name: &str) -> String {
488        crate::files::join(&self.path, name)
489    }
490
491    /// Writes a new file `name` with `bytes` and syncs it. Whatever was at
492    /// `name` is removed first, so a link or hard link there is never
493    /// written through. With `access`, the file is its owner's alone until
494    /// written, then takes that mode and group, and on Linux no access
495    /// control list it would get from the directory; it must have the
496    /// labels of `access`, which is checked before anything is written.
497    /// Where there are no modes, it is read-only if `access` has no write
498    /// bits. Without `access`, it has the platform's default.
499    pub fn write(
500        &self,
501        name: &str,
502        access: Option<&Access>,
503        bytes: &[u8],
504    ) -> Result<(), AccessError> {
505        self.directory
506            .write(checked_name(name)?, access, bytes)
507            .map_err(|error| refused(&self.entry(name), error))
508    }
509
510    /// Creates a new file `name` holding `bytes` and syncs it, only if
511    /// nothing is at `name`: for callers that must never replace or remove
512    /// a file that is not theirs, such as one writing output a user named.
513    /// `name` is checked as for [`write`](Self::write), and `access` means
514    /// what it does there.
515    ///
516    /// What it promises on every platform:
517    ///
518    /// - The file is made by an exclusive create (`O_CREAT | O_EXCL` on
519    ///   Unix, `CREATE_NEW` on Windows), which fails if anything is at the
520    ///   name, a link or a dangling link included, and follows none. Then
521    ///   the error is [`AccessError::Io`] of kind
522    ///   [`AlreadyExists`](std::io::ErrorKind::AlreadyExists), and
523    ///   [`AccessError::already_exists`] says so.
524    /// - Nothing that was at the name before the call is removed, replaced
525    ///   or written through, on success or on any error. Something another
526    ///   process puts at the name while a create is failing is removed
527    ///   only in the Unix window below; on Windows nothing is removed.
528    /// - All of `bytes` is written and the file is synced (`fsync`; on
529    ///   macOS `F_FULLFSYNC`, as std syncs) before it succeeds. As with
530    ///   [`write`](Self::write), the directory is not synced: call
531    ///   [`sync`](Self::sync) to make the new name durable.
532    ///
533    /// When it fails after making the file, as when the disk is full or
534    /// `access` cannot be given:
535    ///
536    /// - On Unix the file is removed only if the entry at `name` is still
537    ///   the file it made: same device and inode as the open file, read
538    ///   without following a link. However many links the file has, the
539    ///   name is still that file's, so removing it removes no one else's
540    ///   data. Between that check and the removal (`unlinkat`), a window
541    ///   of the order of a microsecond, another process that renames a
542    ///   file over the name, over the new file, has its file removed; that
543    ///   is the only file at risk. POSIX has no call that removes a name
544    ///   only if it is a given file, so this window cannot be closed. Where
545    ///   the file is not removed, [`AccessError::may_be_left`] says so.
546    /// - On Windows the file is never removed: std gives no way to tell the
547    ///   entry at a path is the file it made, so a partly written file may
548    ///   be left at the name, and [`AccessError::may_be_left`] says so.
549    ///   Removing it is the caller's choice.
550    ///
551    /// What it cannot promise: on Unix the directory is the one this
552    /// handle opened, but a process that can change an ancestor can move
553    /// or replace it, and the file is then made where the handle is. On
554    /// Windows every step is by path, after the directory's components were
555    /// checked for reparse points when it was opened, so a local process
556    /// changing the tree at the same time can redirect where the file is
557    /// made (but the create still never replaces an entry).
558    pub fn create(
559        &self,
560        name: &str,
561        access: Option<&Access>,
562        bytes: &[u8],
563    ) -> Result<(), AccessError> {
564        self.directory
565            .create(checked_name(name)?, access, bytes)
566            .map_err(|error| refused(&self.entry(name), error))
567    }
568
569    /// Who may use the regular file `name`; `None` when there is no regular
570    /// file there. A file whose access publishing could not keep is
571    /// refused: on Linux one with an access control list, and on Windows a
572    /// read-only or encrypted one. macOS and
573    /// Windows access control lists are neither read nor kept.
574    pub fn access(&self, name: &str) -> Result<Option<Access>, AccessError> {
575        self.directory
576            .access(checked_name(name)?)
577            .map_err(|error| refused(&self.entry(name), error))
578    }
579
580    /// Who may use this directory. On Linux, a directory with an access
581    /// control list, its own or one it gives new files, is refused.
582    pub fn own_access(&self) -> Result<Access, AccessError> {
583        self.directory
584            .own_access()
585            .map_err(|error| refused(&self.path, error))
586    }
587
588    /// Gives this directory the mode and group of `access`, and checks that
589    /// it has the labels of `access`.
590    pub fn set_own_access(&self, access: &Access) -> Result<(), AccessError> {
591        self.directory
592            .set_own_access(access)
593            .map_err(|error| refused(&self.path, error))
594    }
595
596    /// Checks, before either is made, that a file made here as `name` would
597    /// get the access control beside the mode bits that one made as
598    /// `staged` would: on Linux with `SELinux`, the label its policy gives a
599    /// new file of that name, which may depend on the name and is not given
600    /// again on renaming. Elsewhere there is nothing to check.
601    pub fn check_final_name(&self, staged: &str, name: &str) -> Result<(), AccessError> {
602        self.directory
603            .check_final_name(checked_name(staged)?, checked_name(name)?)
604            .map_err(|error| refused(&self.entry(name), error))
605    }
606
607    /// Renames `from` to `to`, replacing `to`, atomically.
608    pub fn rename(&self, from: &str, to: &str) -> Result<(), AccessError> {
609        self.directory
610            .rename(checked_name(from)?, checked_name(to)?)
611            .map_err(|error| refused(&self.entry(to), error))
612    }
613
614    /// Removes the file `name`; `false` when there was none.
615    pub fn remove_file(&self, name: &str) -> Result<bool, AccessError> {
616        self.directory
617            .remove_file(checked_name(name)?)
618            .map_err(|error| refused(&self.entry(name), error))
619    }
620
621    /// Removes the directory `name` if it is empty; `false` when it is
622    /// missing or not empty.
623    pub fn remove_directory(&self, name: &str) -> Result<bool, AccessError> {
624        self.directory
625            .remove_directory(checked_name(name)?)
626            .map_err(|error| refused(&self.entry(name), error))
627    }
628
629    /// The file system this directory is on, in words, where publishing
630    /// does not read the access control lists of what is in it: on Linux
631    /// any but the local ones that keep them as the attributes publishing
632    /// reads (ext2, ext3, ext4, XFS, Btrfs, tmpfs, F2FS, ZFS and bcachefs),
633    /// so network, host-shared (such as WSL's Windows drives or a virtual
634    /// machine's shared folders), FUSE, NTFS and stacked ones (overlayfs)
635    /// among them. The directory's mount is found by its ID in `mounts`,
636    /// and both the type the kernel names it with and the magic number
637    /// `statfs` gives must be such a file system's; one that cannot be
638    /// told, as where `/proc` cannot be read or the mount's line is
639    /// malformed, is not checked
640    /// either, and says why. `None` where it is checked, and on other
641    /// platforms, which do not read access control lists at all.
642    pub fn unchecked_file_system(&self, mounts: &Mounts) -> Result<Option<String>, AccessError> {
643        self.directory
644            .unchecked_file_system(&mounts.0)
645            .map_err(|error| refused(&self.path, error))
646    }
647
648    /// Syncs the directory, making renames and removals in it durable.
649    /// Returns `false` on platforms that cannot sync a directory, where
650    /// they may not be durable yet.
651    pub fn sync(&self) -> Result<bool, AccessError> {
652        self.directory
653            .sync()
654            .map_err(|error| refused(&self.path, error))
655    }
656}
657
658impl fmt::Debug for SourceRoot {
659    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
660        formatter.write_str("SourceRoot")
661    }
662}