Skip to main content

ic_host_fs/read/beneath/
mod.rs

1//! No-follow traversal relative to an explicitly supplied directory capability.
2
3use super::{
4    ArtifactError, admit_file,
5    entry::{entry_type, open_child, opened_type},
6};
7use rustix::fs::{FileType, Mode, OFlags};
8use std::{
9    fs::File,
10    io,
11    os::fd::{AsFd, BorrowedFd, OwnedFd},
12    path::{Component, Path},
13};
14
15/// Open an existing directory without following its final symlink.
16///
17/// DIRECTORY and NONBLOCK refuse non-directories without waiting on a FIFO.
18/// Ancestor custody and root selection remain caller-owned; use the returned
19/// capability with [`open_regular_file_beneath`] for descendant traversal.
20/// No entry is created or modified.
21///
22/// # Errors
23/// Returns native open failures, including final symlink/non-directory refusal.
24pub fn open_directory_no_follow(path: &Path) -> io::Result<File> {
25    rustix::fs::open(
26        path,
27        OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::NONBLOCK | OFlags::CLOEXEC,
28        Mode::empty(),
29    )
30    .map(File::from)
31    .map_err(io::Error::from)
32}
33
34/// One admitted root-relative entry with its original descriptor retained.
35#[derive(Debug)]
36pub enum OpenedEntry {
37    /// A regular file, opened without following any selected component.
38    RegularFile(File),
39    /// A directory, opened without following any selected component.
40    Directory(File),
41}
42
43/// Refusal or native failure when selecting a root-relative entry.
44#[derive(Debug, thiserror::Error)]
45pub enum OpenEntryError {
46    /// A native open or metadata operation failed.
47    #[error("root-relative opening failed: {0}")]
48    Io(#[from] io::Error),
49    /// The selected entry is neither a regular file nor a directory.
50    #[error("unsupported entry: {kind:?}")]
51    UnsupportedEntry {
52        /// Observed descriptor kind.
53        kind: FileType,
54    },
55}
56
57/// Open one regular file or directory beneath a caller-held root descriptor.
58///
59/// Every component must be normal and is selected through its held parent.
60/// Symlinks and special final entries refuse. A directory result composes with
61/// [`super::tree::walk_tree`]; a file result still needs a caller-selected byte
62/// allowance before reading. The caller owns root custody, stable content and
63/// protection against concurrent namespace mutation. An open descriptor does
64/// not prove that its original pathname still names the same entry.
65///
66/// # Errors
67/// Rejects invalid paths, observed symlinks and unsupported final entry types,
68/// or returns native traversal/open/metadata errors.
69pub fn open_entry_beneath(
70    root: BorrowedFd<'_>,
71    relative: &Path,
72) -> Result<OpenedEntry, OpenEntryError> {
73    let mut parts = relative.components().peekable();
74    if parts.peek().is_none() || parts.clone().any(|p| !matches!(p, Component::Normal(_))) {
75        return Err(io::Error::new(
76            io::ErrorKind::InvalidInput,
77            "relative normal components required",
78        )
79        .into());
80    }
81    let mut parent: Option<OwnedFd> = None;
82    while let Some(Component::Normal(name)) = parts.next() {
83        let directory = parent.as_ref().map_or(root, AsFd::as_fd);
84        let observed = entry_type(directory, name)?;
85        if observed == FileType::Symlink {
86            return Err(
87                io::Error::new(io::ErrorKind::InvalidInput, "symlink component refused").into(),
88            );
89        }
90        let final_component = parts.peek().is_none();
91        if final_component && !matches!(observed, FileType::RegularFile | FileType::Directory) {
92            return Err(OpenEntryError::UnsupportedEntry { kind: observed });
93        }
94        let opened = open_child(
95            directory,
96            name,
97            !final_component || observed == FileType::Directory,
98        )?;
99        if final_component {
100            let actual = opened_type(&opened)?;
101            let file = File::from(opened);
102            return match (observed, actual) {
103                (FileType::RegularFile, FileType::RegularFile) => {
104                    Ok(OpenedEntry::RegularFile(file))
105                }
106                (FileType::Directory, FileType::Directory) => Ok(OpenedEntry::Directory(file)),
107                _ => Err(OpenEntryError::UnsupportedEntry { kind: actual }),
108            };
109        }
110        parent = Some(opened);
111    }
112    unreachable!("validated nonempty normal path");
113}
114
115/// Open one bounded regular file through a caller-owned directory descriptor.
116///
117/// Rejects empty/absolute paths, parent traversal and symlinks at every selected
118/// component. Each admitted parent descriptor is retained until its child opens;
119/// path replacement cannot redirect later traversal through an ancestor alias.
120/// Final opening uses NONBLOCK before type/size admission, so a FIFO without a
121/// writer is refused without waiting. Reads can use [`super::read_opened_file`].
122///
123/// The caller owns the root's admission/custody, relative-name bounds, credentials,
124/// deadlines and content validation. An opened inode is not immutable content;
125/// concurrent writers and hard links are not excluded. Directory renames retain
126/// the selected capability; this does not prove current pathname containment.
127/// No file or directory is created, modified, synced or removed.
128///
129/// # Errors
130/// Syntax and observed symlink refusals return an `InvalidInput` I/O cause.
131/// Other traversal/open errors retain their native cause. Opened type/size and
132/// metadata failures use the existing artifact admission errors.
133pub fn open_regular_file_beneath(
134    root: BorrowedFd<'_>,
135    relative: &Path,
136    max_bytes: u64,
137) -> Result<File, ArtifactError> {
138    let opened = open_entry_beneath(root, relative).map_err(|error| match error {
139        OpenEntryError::Io(error) => ArtifactError::Io(error),
140        OpenEntryError::UnsupportedEntry { .. } => ArtifactError::NotRegularFile,
141    })?;
142    match opened {
143        OpenedEntry::RegularFile(file) => {
144            admit_file(&file, max_bytes)?;
145            Ok(file)
146        }
147        OpenedEntry::Directory(_) => Err(ArtifactError::NotRegularFile),
148    }
149}
150
151#[cfg(test)]
152mod tests;