tilewright-source 0.2.2

Loads and validates Tilewright world source trees into a typed, game-neutral model
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
// Copyright 2026 Seraphina Studios
// SPDX-License-Identifier: MIT

//! Confined access to the files of a source tree, for tools that edit it.
//!
//! This is the loader's own path confinement: paths are root-relative and
//! `/`-separated, with no empty, `.` or `..` component and no `\`, `:` or
//! NUL in any component, and nothing beneath the root is followed if it is
//! a symbolic link. On Unix every component is opened through its parent's
//! directory handle without following links, and writes, renames and
//! removals act on names inside a directory opened that way, so a link
//! swapped in at any moment is never followed. Elsewhere components are
//! checked by path before use, on Windows for any reparse point (symbolic
//! links, junctions and mount points among them), a best effort that a
//! local process changing the tree at the same time can race.

use crate::files::{Refused, components, platform};
use std::{
    fmt,
    io::Read as _,
    path::{Path, PathBuf},
};

/// Why a path beneath the root could not be used.
#[derive(Debug)]
pub enum AccessError {
    /// The path has an empty, `.` or `..` component, or a component with
    /// `\`, `:` or NUL; or a name has a `/`.
    Path(String),
    /// A symbolic link at this prefix of the path.
    Link(String),
    /// The entry is not a regular file or directory, as the access needs.
    Unexpected(String),
    /// The file is larger than the limit of the read.
    TooLarge(String),
    /// Another holder has the root's lock.
    Locked,
    Io(String, std::io::Error),
}

impl fmt::Display for AccessError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Path(path) => write!(formatter, "{path}: not a relative path beneath the root"),
            Self::Link(path) => write!(formatter, "{path}: symbolic links are not followed"),
            Self::Unexpected(path) => {
                write!(formatter, "{path}: not a regular file or directory")
            }
            Self::TooLarge(path) => write!(formatter, "{path}: file is too large"),
            Self::Locked => write!(formatter, "another edit holds the source tree's lock"),
            Self::Io(path, error) => write!(formatter, "{path}: {error}"),
        }
    }
}

impl std::error::Error for AccessError {}

impl AccessError {
    /// A removal of `path` that took effect but is not finished, as
    /// [`removal_pending`](Self::removal_pending) says, for tests of what
    /// is made of one; only Windows makes them otherwise.
    #[doc(hidden)]
    #[must_use]
    pub fn pending_removal(path: &str) -> Self {
        Self::Io(
            path.to_owned(),
            std::io::Error::new(std::io::ErrorKind::ResourceBusy, RemovalPending),
        )
    }

    /// Whether this is a removal that took effect but is not finished: on
    /// Windows a removed entry stays, on some file systems, until another
    /// program closes it, and goes then.
    #[must_use]
    pub fn removal_pending(&self) -> bool {
        type Inner = dyn std::error::Error + Send + Sync;
        matches!(self, Self::Io(_, error) if error.get_ref().is_some_and(Inner::is::<RemovalPending>))
    }

    /// Whether this is [`SourceDirectory::create`] refusing because
    /// something is already at the name: a file, a directory, a link
    /// (dangling or not, on Windows any reparse point), or any other entry,
    /// which it left as it was. It is an [`AccessError::Io`] whose error is
    /// of kind [`std::io::ErrorKind::AlreadyExists`].
    #[must_use]
    pub fn already_exists(&self) -> bool {
        type Inner = dyn std::error::Error + Send + Sync;
        matches!(self, Self::Io(_, error) if error.kind() == std::io::ErrorKind::AlreadyExists
            && error.get_ref().is_some_and(Inner::is::<AlreadyThere>))
    }

    /// Whether this is [`SourceDirectory::create`] failing after it made
    /// its file and not removing it, so that the file, partly written, may
    /// be left at the name: always on Windows, and on Unix where the entry
    /// at the name could not be told to be still that file. It is an
    /// [`AccessError::Io`] of the failure's own kind. When `create` fails
    /// otherwise, nothing it made is left.
    #[must_use]
    pub fn may_be_left(&self) -> bool {
        type Inner = dyn std::error::Error + Send + Sync;
        matches!(self, Self::Io(_, error) if error.get_ref().is_some_and(Inner::is::<LeftBehind>))
    }
}

/// Something already at the name a file is created as; see
/// [`AccessError::already_exists`].
#[derive(Debug)]
pub(crate) struct AlreadyThere;

impl fmt::Display for AlreadyThere {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str("something is already there, and it is left as it was")
    }
}

impl std::error::Error for AlreadyThere {}

/// The error for something already at the name a file is created as.
pub(crate) fn already_there() -> std::io::Error {
    std::io::Error::new(std::io::ErrorKind::AlreadyExists, AlreadyThere)
}

/// A failed creation whose file, partly written, was not removed, since it
/// could not be told to be still the one created at the name; see
/// [`AccessError::may_be_left`].
#[derive(Debug)]
pub(crate) struct LeftBehind(std::io::Error);

impl fmt::Display for LeftBehind {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            formatter,
            "{}; 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",
            self.0
        )
    }
}

impl std::error::Error for LeftBehind {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        Some(&self.0)
    }
}

/// `error`, of a creation whose file was not removed, saying so; of the
/// same kind.
pub(crate) fn left_behind(error: Refused) -> Refused {
    match error {
        Refused::Io(error) => Refused::Io(std::io::Error::new(error.kind(), LeftBehind(error))),
        other => other,
    }
}

/// A removal that took effect but stays until another program closes the
/// entry; see [`AccessError::removal_pending`].
#[derive(Debug)]
pub(crate) struct RemovalPending;

impl fmt::Display for RemovalPending {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str("it is removed, but stays until another program closes it")
    }
}

impl std::error::Error for RemovalPending {}

fn refused(path: &str, refused: Refused) -> AccessError {
    match refused {
        Refused::Link(prefix) => AccessError::Link(prefix),
        Refused::Missing | Refused::Unexpected => AccessError::Unexpected(path.to_owned()),
        Refused::Io(error) => AccessError::Io(path.to_owned(), error),
    }
}

/// The name of the file in the root that is its lock on Windows. The
/// command line writes no output there on any platform.
pub const LOCK_NAME: &str = ".tilewright.lock";

/// The checked components of a root-relative path; the empty path is the
/// root.
pub(crate) fn checked(path: &str) -> Result<Vec<&str>, AccessError> {
    if path.is_empty() {
        return Ok(Vec::new());
    }
    if path.len() > 4096 || path.split('/').any(|name| checked_name(name).is_err()) {
        return Err(AccessError::Path(path.to_owned()));
    }
    Ok(components(path))
}

/// Whether `name` can name an entry of a directory: not empty, `.` or `..`,
/// and without `/`, `\`, `:` or NUL, so that no platform reads it as more
/// than one component, a drive or a stream.
fn checked_name(name: &str) -> Result<&str, AccessError> {
    if name.is_empty()
        || name == "."
        || name == ".."
        || name.len() > 255
        || name.contains(['/', '\\', ':', '\0'])
    {
        Err(AccessError::Path(name.to_owned()))
    } else {
        Ok(name)
    }
}

/// Who may use a file or directory, as publishing keeps it: where there are
/// no modes, a read-only file has none of the write bits.
///
/// On Unix: the permission bits, the owner and the group, and on Linux the
/// mandatory access control labels (`SELinux`, Smack), which publishing
/// requires to be the same on every entry it compares. Access control that
/// publishing could not keep is refused when the access is read: on Linux
/// an access control list. On macOS and Windows access control lists are
/// neither read nor kept.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Access {
    /// The permission bits, with the set-user, set-group and sticky bits.
    pub mode: u32,
    /// The owning user.
    pub user: u32,
    /// The owning group.
    pub group: u32,
    /// A digest of the mandatory access control labels (Linux), all zero
    /// where there are none.
    pub control: [u8; 32],
}

/// Who may reach content through a file or directory: for a file who may
/// read it, for a directory who may search it. Publishing requires the
/// entries it compares to have one audience.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Audience {
    /// The classes that may reach, as read bits.
    reach: u32,
    /// The owner, where not everyone may reach.
    user: Option<u32>,
    /// The group, where only it may reach.
    group: Option<u32>,
    /// The labels.
    control: [u8; 32],
}

impl Access {
    /// Whether the group may read and others may not, so that the group
    /// says who may.
    #[must_use]
    pub const fn group_reads_alone(&self) -> bool {
        self.mode & 0o040 != 0 && self.mode & 0o004 == 0
    }

    /// Whether the classes that may reach content through an entry with
    /// this access, a directory if `directory`, nest: its owner may
    /// whenever its group may, and its group whenever others may. Where
    /// they do not, as for mode `0604`, which denies the group what it
    /// lets others read, who may reach depends on who the owner and group
    /// are, which [`audience`](Self::audience) does not say.
    #[must_use]
    pub const fn nested(&self, directory: bool) -> bool {
        let bits = if directory {
            self.mode & 0o111
        } else {
            (self.mode & 0o444) >> 2
        };
        let (owner, group, other) = (bits & 0o100 != 0, bits & 0o010 != 0, bits & 0o001 != 0);
        (owner || !group) && (group || !other)
    }

    /// Who may reach content through an entry with this access, a
    /// directory if `directory`.
    #[must_use]
    pub fn audience(&self, directory: bool) -> Audience {
        let reach = if directory {
            (self.mode & 0o111) << 2
        } else {
            self.mode & 0o444
        };
        Audience {
            reach,
            user: (reach != 0o444).then_some(self.user),
            group: (reach & 0o040 != 0 && reach & 0o004 == 0).then_some(self.group),
            control: self.control,
        }
    }

    /// The access a new file in a directory with this access is given: the
    /// directory's mode without the execute bits, its group and its labels,
    /// and as its owner the user that makes it, this process's effective
    /// user (0 where there are no users).
    #[must_use]
    pub fn new_file(&self) -> Self {
        #[cfg(unix)]
        let user = rustix::process::geteuid().as_raw();
        #[cfg(not(unix))]
        let user = 0;
        Self {
            mode: self.mode & 0o666,
            user,
            ..*self
        }
    }
}

/// A source tree root. The root itself may be a link; nothing beneath it is
/// followed.
pub struct SourceRoot {
    path: PathBuf,
    root: platform::Root,
}

impl SourceRoot {
    /// Opens the root. On Unix the directory is opened here, and every
    /// later access goes through that handle, so moving or replacing what
    /// the path names afterwards does not move the root; elsewhere the path
    /// is resolved to an absolute one here.
    #[must_use]
    pub fn new(root: &Path) -> Self {
        Self {
            path: root.to_path_buf(),
            root: platform::Root::new(root),
        }
    }

    /// A second handle on the same root directory.
    #[must_use]
    pub fn try_clone(&self) -> Self {
        Self {
            path: self.path.clone(),
            root: self.root.try_clone(),
        }
    }

    /// The path supplied when this root was opened, for display only.
    /// It may be relative, moved or replaced; using it to reopen the tree
    /// does not preserve this root's identity. Access files through this
    /// root instead.
    #[must_use]
    pub fn path(&self) -> &Path {
        &self.path
    }

    pub(crate) fn platform(&self) -> platform::Root {
        self.root.try_clone()
    }

    /// What is at `path`, not following it, if anything: as the file
    /// system finds it by that name, which on one that ignores case, or as
    /// on Windows drops trailing dots, can be an entry named otherwise.
    /// A path through a file names nothing; a link on the way is an error.
    pub fn exists(&self, path: &str) -> Result<bool, AccessError> {
        // The empty path is the root.
        if path.is_empty() {
            return self.directory(path, false).map(|root| root.is_some());
        }
        match self.root.kind(&checked(path)?) {
            Ok(crate::files::Found::Missing) | Err(Refused::Missing | Refused::Unexpected) => {
                Ok(false)
            }
            Ok(_) => Ok(true),
            // The entry itself is a link.
            Err(Refused::Link(prefix)) if prefix == path => Ok(true),
            Err(other) => Err(refused(path, other)),
        }
    }

    /// Reads a regular file of at most `limit` bytes. `Ok(None)` means the
    /// file, or a directory on its way, is missing.
    pub fn read(&self, path: &str, limit: u64) -> Result<Option<Vec<u8>>, AccessError> {
        let parts = checked(path)?;
        if parts.is_empty() {
            return Err(AccessError::Unexpected(path.to_owned()));
        }
        let (file, length) = match self.root.file(&parts) {
            Ok(opened) => opened,
            Err(Refused::Missing) => return Ok(None),
            Err(other) => return Err(refused(path, other)),
        };
        if length > limit {
            return Err(AccessError::TooLarge(path.to_owned()));
        }
        // One byte past the limit shows a file that grew after the check.
        let mut bytes = Vec::new();
        file.take(limit.saturating_add(1))
            .read_to_end(&mut bytes)
            .map_err(|error| AccessError::Io(path.to_owned(), error))?;
        if u64::try_from(bytes.len()).unwrap_or(u64::MAX) > limit {
            return Err(AccessError::TooLarge(path.to_owned()));
        }
        Ok(Some(bytes))
    }

    /// Opens the directory `path` (the empty path is the root), creating it
    /// and its missing parents when `create`. `Ok(None)` means it is missing
    /// and was not created.
    pub fn directory(
        &self,
        path: &str,
        create: bool,
    ) -> Result<Option<SourceDirectory>, AccessError> {
        match self.root.directory(&checked(path)?, create) {
            Ok(directory) => Ok(Some(SourceDirectory {
                directory,
                path: path.to_owned(),
            })),
            Err(Refused::Missing) => Ok(None),
            Err(other) => Err(refused(path, other)),
        }
    }

    /// Makes the directory `path` in its existing parent. Returns whether
    /// this call made it: `false` when something by that name was already
    /// there, which the caller then did not create.
    pub fn make_directory(&self, path: &str) -> Result<bool, AccessError> {
        self.root
            .make_directory(&checked(path)?)
            .map_err(|error| refused(path, error))
    }

    /// The UTF-8 names of the entries of the directory `path`, links
    /// included, and how many entries it has, those whose names are not
    /// UTF-8 included; `None` when it is missing. A directory with more
    /// than `limit` entries is an error, [`AccessError::TooLarge`].
    pub fn names(
        &self,
        path: &str,
        limit: usize,
    ) -> Result<Option<(std::collections::BTreeSet<String>, usize)>, AccessError> {
        let listing = match self.root.list(&checked(path)?) {
            Ok(listing) => listing,
            Err(Refused::Missing) => return Ok(None),
            Err(other) => return Err(refused(path, other)),
        };
        let mut names = std::collections::BTreeSet::new();
        let mut count = 0;
        for entry in listing {
            if count >= limit {
                return Err(AccessError::TooLarge(path.to_owned()));
            }
            count += 1;
            let entry = entry.map_err(|error| AccessError::Io(path.to_owned(), error))?;
            names.extend(entry.name);
        }
        Ok(Some((names, count)))
    }

    /// Takes the root's exclusive lock, held until the returned value is
    /// dropped. It fails at once with [`AccessError::Locked`] if another
    /// holder, in this process or another, has it. On Unix it is an
    /// advisory `flock` of the root directory; on Windows the file
    /// [`LOCK_NAME`] in the root, created afresh, shared with no one and
    /// deleted when the lock is released or its process stops, so a file by
    /// that name that no one holds is an error. Elsewhere there is no lock
    /// and this always succeeds.
    pub fn lock(&self) -> Result<SourceLock, AccessError> {
        match self.root.lock() {
            Ok(Some(lock)) => Ok(SourceLock(lock)),
            Ok(None) => Err(AccessError::Locked),
            Err(other) => Err(refused(".", other)),
        }
    }
}

/// The mounts of this process, read once to tell the file systems of
/// several directories ([`SourceDirectory::unchecked_file_system`]): on
/// Linux from `/proc/self/mountinfo`, bounded; elsewhere nothing.
pub struct Mounts(platform::Mounts);

impl Mounts {
    /// Reads the mounts. Where they cannot be read, every directory's file
    /// system is one that cannot be told.
    #[must_use]
    pub fn read() -> Self {
        Self(platform::Mounts::read())
    }
}

/// The root's lock; see [`SourceRoot::lock`].
pub struct SourceLock(#[allow(dead_code)] platform::Lock);

/// A directory beneath the root. Every name is a single entry of it.
pub struct SourceDirectory {
    directory: platform::Directory,
    path: String,
}

impl SourceDirectory {
    fn entry(&self, name: &str) -> String {
        crate::files::join(&self.path, name)
    }

    /// Writes a new file `name` with `bytes` and syncs it. Whatever was at
    /// `name` is removed first, so a link or hard link there is never
    /// written through. With `access`, the file is its owner's alone until
    /// written, then takes that mode and group, and on Linux no access
    /// control list it would get from the directory; it must have the
    /// labels of `access`, which is checked before anything is written.
    /// Where there are no modes, it is read-only if `access` has no write
    /// bits. Without `access`, it has the platform's default.
    pub fn write(
        &self,
        name: &str,
        access: Option<&Access>,
        bytes: &[u8],
    ) -> Result<(), AccessError> {
        self.directory
            .write(checked_name(name)?, access, bytes)
            .map_err(|error| refused(&self.entry(name), error))
    }

    /// Creates a new file `name` holding `bytes` and syncs it, only if
    /// nothing is at `name`: for callers that must never replace or remove
    /// a file that is not theirs, such as one writing output a user named.
    /// `name` is checked as for [`write`](Self::write), and `access` means
    /// what it does there.
    ///
    /// What it promises on every platform:
    ///
    /// - The file is made by an exclusive create (`O_CREAT | O_EXCL` on
    ///   Unix, `CREATE_NEW` on Windows), which fails if anything is at the
    ///   name, a link or a dangling link included, and follows none. Then
    ///   the error is [`AccessError::Io`] of kind
    ///   [`AlreadyExists`](std::io::ErrorKind::AlreadyExists), and
    ///   [`AccessError::already_exists`] says so.
    /// - Nothing that was at the name before the call is removed, replaced
    ///   or written through, on success or on any error. Something another
    ///   process puts at the name while a create is failing is removed
    ///   only in the Unix window below; on Windows nothing is removed.
    /// - All of `bytes` is written and the file is synced (`fsync`; on
    ///   macOS `F_FULLFSYNC`, as std syncs) before it succeeds. As with
    ///   [`write`](Self::write), the directory is not synced: call
    ///   [`sync`](Self::sync) to make the new name durable.
    ///
    /// When it fails after making the file, as when the disk is full or
    /// `access` cannot be given:
    ///
    /// - On Unix the file is removed only if the entry at `name` is still
    ///   the file it made: same device and inode as the open file, read
    ///   without following a link. However many links the file has, the
    ///   name is still that file's, so removing it removes no one else's
    ///   data. Between that check and the removal (`unlinkat`), a window
    ///   of the order of a microsecond, another process that renames a
    ///   file over the name, over the new file, has its file removed; that
    ///   is the only file at risk. POSIX has no call that removes a name
    ///   only if it is a given file, so this window cannot be closed. Where
    ///   the file is not removed, [`AccessError::may_be_left`] says so.
    /// - On Windows the file is never removed: std gives no way to tell the
    ///   entry at a path is the file it made, so a partly written file may
    ///   be left at the name, and [`AccessError::may_be_left`] says so.
    ///   Removing it is the caller's choice.
    ///
    /// What it cannot promise: on Unix the directory is the one this
    /// handle opened, but a process that can change an ancestor can move
    /// or replace it, and the file is then made where the handle is. On
    /// Windows every step is by path, after the directory's components were
    /// checked for reparse points when it was opened, so a local process
    /// changing the tree at the same time can redirect where the file is
    /// made (but the create still never replaces an entry).
    pub fn create(
        &self,
        name: &str,
        access: Option<&Access>,
        bytes: &[u8],
    ) -> Result<(), AccessError> {
        self.directory
            .create(checked_name(name)?, access, bytes)
            .map_err(|error| refused(&self.entry(name), error))
    }

    /// Who may use the regular file `name`; `None` when there is no regular
    /// file there. A file whose access publishing could not keep is
    /// refused: on Linux one with an access control list, and on Windows a
    /// read-only or encrypted one. macOS and
    /// Windows access control lists are neither read nor kept.
    pub fn access(&self, name: &str) -> Result<Option<Access>, AccessError> {
        self.directory
            .access(checked_name(name)?)
            .map_err(|error| refused(&self.entry(name), error))
    }

    /// Who may use this directory. On Linux, a directory with an access
    /// control list, its own or one it gives new files, is refused.
    pub fn own_access(&self) -> Result<Access, AccessError> {
        self.directory
            .own_access()
            .map_err(|error| refused(&self.path, error))
    }

    /// Gives this directory the mode and group of `access`, and checks that
    /// it has the labels of `access`.
    pub fn set_own_access(&self, access: &Access) -> Result<(), AccessError> {
        self.directory
            .set_own_access(access)
            .map_err(|error| refused(&self.path, error))
    }

    /// Checks, before either is made, that a file made here as `name` would
    /// get the access control beside the mode bits that one made as
    /// `staged` would: on Linux with `SELinux`, the label its policy gives a
    /// new file of that name, which may depend on the name and is not given
    /// again on renaming. Elsewhere there is nothing to check.
    pub fn check_final_name(&self, staged: &str, name: &str) -> Result<(), AccessError> {
        self.directory
            .check_final_name(checked_name(staged)?, checked_name(name)?)
            .map_err(|error| refused(&self.entry(name), error))
    }

    /// Renames `from` to `to`, replacing `to`, atomically.
    pub fn rename(&self, from: &str, to: &str) -> Result<(), AccessError> {
        self.directory
            .rename(checked_name(from)?, checked_name(to)?)
            .map_err(|error| refused(&self.entry(to), error))
    }

    /// Removes the file `name`; `false` when there was none.
    pub fn remove_file(&self, name: &str) -> Result<bool, AccessError> {
        self.directory
            .remove_file(checked_name(name)?)
            .map_err(|error| refused(&self.entry(name), error))
    }

    /// Removes the directory `name` if it is empty; `false` when it is
    /// missing or not empty.
    pub fn remove_directory(&self, name: &str) -> Result<bool, AccessError> {
        self.directory
            .remove_directory(checked_name(name)?)
            .map_err(|error| refused(&self.entry(name), error))
    }

    /// The file system this directory is on, in words, where publishing
    /// does not read the access control lists of what is in it: on Linux
    /// any but the local ones that keep them as the attributes publishing
    /// reads (ext2, ext3, ext4, XFS, Btrfs, tmpfs, F2FS, ZFS and bcachefs),
    /// so network, host-shared (such as WSL's Windows drives or a virtual
    /// machine's shared folders), FUSE, NTFS and stacked ones (overlayfs)
    /// among them. The directory's mount is found by its ID in `mounts`,
    /// and both the type the kernel names it with and the magic number
    /// `statfs` gives must be such a file system's; one that cannot be
    /// told, as where `/proc` cannot be read or the mount's line is
    /// malformed, is not checked
    /// either, and says why. `None` where it is checked, and on other
    /// platforms, which do not read access control lists at all.
    pub fn unchecked_file_system(&self, mounts: &Mounts) -> Result<Option<String>, AccessError> {
        self.directory
            .unchecked_file_system(&mounts.0)
            .map_err(|error| refused(&self.path, error))
    }

    /// Syncs the directory, making renames and removals in it durable.
    /// Returns `false` on platforms that cannot sync a directory, where
    /// they may not be durable yet.
    pub fn sync(&self) -> Result<bool, AccessError> {
        self.directory
            .sync()
            .map_err(|error| refused(&self.path, error))
    }
}

impl fmt::Debug for SourceRoot {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str("SourceRoot")
    }
}