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}