Skip to main content

artifactize_tools/platform/
files.rs

1//! Shared safe file access: pinned no-follow opens and directory listings.
2
3#[cfg(windows)]
4mod directory_scan;
5#[cfg(unix)]
6mod unix;
7#[cfg(windows)]
8mod windows;
9
10#[cfg(unix)]
11pub use unix::{
12    EntryName, canonicalize, case_sensitive, entry_kind, exact_name, is_link_refusal,
13    open_directory, open_entry, open_no_follow, open_nonblocking, read_dir,
14};
15#[cfg(windows)]
16pub use windows::{
17    EntryName, canonicalize, case_sensitive, entry_kind, exact_name, is_link_refusal,
18    open_directory, open_entry, open_no_follow, open_nonblocking, read_dir,
19};
20
21/// The filesystem root of an absolute path (`/`, or a Windows volume or share) and the names
22/// below it, in order. `None` when a component is `.` or `..`.
23/// Whether a native path is absolute on this system.
24pub fn is_absolute(path: &std::path::Path) -> bool {
25    path.is_absolute()
26}
27
28/// The logical form of `path` below `root`: an absolute native path is taken relative to
29/// `root` (`None` outside it), a relative one is already logical.
30pub fn logical_below(root: &std::path::Path, path: &str) -> Option<String> {
31    let path = std::path::Path::new(path);
32    if path.is_absolute() {
33        logical_from_native(path.strip_prefix(root).ok()?)
34    } else {
35        path.to_str().map(str::to_owned)
36    }
37}
38
39pub fn split_root(path: &std::path::Path) -> Option<(&std::path::Path, Vec<&std::ffi::OsStr>)> {
40    use std::path::Component;
41    let root = path.ancestors().last()?;
42    path.components()
43        .filter_map(|component| match component {
44            Component::Prefix(_) | Component::RootDir => None,
45            Component::Normal(name) => Some(Some(name)),
46            Component::CurDir | Component::ParentDir => Some(None),
47        })
48        .collect::<Option<Vec<_>>>()
49        .map(|names| (root, names))
50}
51
52/// The target a link names, read without following it: a symlink's text, or on Windows a
53/// junction's or symbolic link's substitute name.
54pub fn link_target(path: &std::path::Path) -> std::io::Result<std::path::PathBuf> {
55    std::fs::read_link(path)
56}
57
58/// Whether a name can reach an entry spelled differently, as case-insensitive Windows and
59/// macOS volumes allow; path-returning interfaces then also check the exact spelling.
60pub const ALIASED_NAMES: bool = cfg!(any(windows, target_os = "macos"));
61
62/// The type of a directory entry, read without following it.
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum FileKind {
65    File,
66    Directory,
67    Symlink,
68    /// FIFOs, sockets, devices, and on Windows reparse points that are not links.
69    Other,
70}
71
72/// Read the process's current directory and resolve its native filesystem aliases.
73pub fn current_directory() -> std::io::Result<std::path::PathBuf> {
74    canonicalize(&std::env::current_dir()?)
75}
76
77/// Render a native path for people and JSON clients with portable `/` separators.
78pub fn path_text(path: &std::path::Path) -> String {
79    let text = path.to_string_lossy();
80    #[cfg(windows)]
81    {
82        use std::path::{Component, Prefix};
83        let text = text.replace('\\', "/");
84        // Canonical Windows paths carry verbatim prefixes that are not useful in a
85        // breadcrumb. Only disk/UNC prefixes have equivalent ordinary path spellings.
86        const VERBATIM: &str = "//?/";
87        const VERBATIM_UNC: &str = "//?/UNC/";
88        match path.components().next() {
89            Some(Component::Prefix(prefix)) => match prefix.kind() {
90                Prefix::VerbatimDisk(_) => text[VERBATIM.len()..].to_owned(),
91                Prefix::VerbatimUNC(_, _) => format!("//{}", &text[VERBATIM_UNC.len()..]),
92                _ => text,
93            },
94            _ => text,
95        }
96    }
97    #[cfg(unix)]
98    {
99        text.into_owned()
100    }
101}
102
103/// Convert a native relative path into logical components; roots, traversal and non-UTF-8
104/// names are refused rather than guessing at the host's separator or prefix rules.
105pub fn logical_from_native(path: &std::path::Path) -> Option<String> {
106    use std::path::Component;
107    path.components()
108        .map(|component| match component {
109            Component::Normal(name) => name.to_str(),
110            _ => None,
111        })
112        .collect::<Option<Vec<_>>>()
113        .map(|names| names.join("/"))
114}
115
116/// Parse the physical relative input form used by declared Artifact roots. Preserve its
117/// existing rejection of backslashes and traversal while keeping native root checks here.
118pub fn scoped_relative(path: &std::path::Path) -> std::io::Result<String> {
119    let path = path.to_str().ok_or_else(|| {
120        std::io::Error::new(
121            std::io::ErrorKind::InvalidInput,
122            "Artifact paths must be UTF-8.",
123        )
124    })?;
125    if path.contains(['\0', '\\'])
126        || std::path::Path::new(path).is_absolute()
127        || path.split('/').any(|part| part == "." || part == "..")
128    {
129        return Err(std::io::Error::new(
130            std::io::ErrorKind::InvalidInput,
131            "Artifact path must be relative to its declared root.",
132        ));
133    }
134    Ok(path
135        .split('/')
136        .filter(|part| !part.is_empty())
137        .collect::<Vec<_>>()
138        .join("/"))
139}
140
141#[cfg(test)]
142mod tests {
143    use super::path_text;
144
145    // reason: only Unix allows '\' in file names.
146    #[cfg(unix)]
147    #[test]
148    fn literal_backslash_in_a_file_name_is_not_a_separator() {
149        let root = crate::test_os::tempdir();
150        let path = root.path().join(r"input\name");
151        std::fs::write(&path, "data").unwrap();
152        assert_eq!(
153            path_text(std::path::Path::new(path.file_name().unwrap())),
154            r"input\name"
155        );
156    }
157
158    // reason: Windows native paths use backslash separators.
159    #[cfg(windows)]
160    #[test]
161    fn native_separators_are_rendered_as_slashes() {
162        assert_eq!(
163            path_text(std::path::Path::new(r"C:\input\name")),
164            "C:/input/name"
165        );
166    }
167
168    // reason: Windows canonical paths can carry verbatim disk and UNC prefixes.
169    #[cfg(windows)]
170    #[test]
171    fn verbatim_paths_render_without_the_native_prefix() {
172        assert_eq!(
173            path_text(std::path::Path::new(r"\\?\C:\input\name")),
174            "C:/input/name"
175        );
176        assert_eq!(
177            path_text(std::path::Path::new(r"\\?\UNC\server\share\input")),
178            "//server/share/input"
179        );
180    }
181}