Skip to main content

artifactize_core/
platform.rs

1//! Operating-system specifics behind one interface: owner-only files and directories, scoped
2//! opens and listings that never follow a link, process trees held until admitted, stop
3//! signals, hidden terminal input, and host details.
4//!
5//! Unix uses modes, `openat` with `O_NOFOLLOW`, `/proc`, and process groups. Windows uses
6//! protected owner-only DACLs, handle-relative opens that refuse every reparse point, and
7//! kill-on-close Job Objects.
8//!
9//! Kept as is (#201): process-wrap 10.0.1 is used for Unix process groups, but its Windows
10//! JobObject assigns a job after spawning and waits for the whole job, not just the leader.
11//! It exposes neither creation-time job assignment nor our separately owned admission gate;
12//! keep the current suspended-spawn/assignment and detached-launch policy on Windows.
13//! rpassword 7.5.4 exposes blocking password reads, not a caller-owned mode guard that can
14//! restore the terminal when a pending read is cancelled. Keep the echo-only guard and the
15//! bounded stdin reader; its restoration contract is tested in `hidden_input`.
16//! Program lookup and desktop-opener crate choices are documented in artifactize-tools'
17//! platform module, where those implementations live.
18
19pub mod desktop_commands;
20pub mod environment;
21mod hidden_input;
22pub(crate) mod net;
23pub(crate) mod path_serde;
24pub(crate) mod paths;
25pub(crate) mod program;
26#[cfg(unix)]
27mod unix;
28#[cfg(windows)]
29mod windows;
30
31#[cfg(unix)]
32use unix as os;
33#[cfg(windows)]
34use windows as os;
35
36pub(crate) use os::{
37    CRLF_LINE_ENDINGS, Child, ENV_NAMES_IGNORE_CASE, HOME_VARIABLE, PrivateTempDir,
38    STATE_VARIABLES, TEMP_VARIABLES, USER_VARIABLES, create_private_dir, create_private_dir_all,
39    exit_signal, file_identity, home_directory, host_name, ipc, is_owner_only, is_private_dir,
40    is_private_file, kill_saved_group, kill_saved_session, path_from_bytes, private_options,
41    private_tempdir_in, process_start_time, remove_abandoned_directory, resolve_system_aliases,
42    restrict_file, running_process_start_time, spawn_detached, spawn_gated, spawn_run,
43    spawn_watched, sync_dir, transient_file_access,
44};
45pub use os::{HiddenInput, VISIBLE_INPUT_FALLBACK, stop_requested};
46pub(crate) use os::{PRIVATE_DIRECTORY, PRIVATE_FILE};
47
48/// A file's identity on its volume. Two open files with equal identities are the same file,
49/// whatever paths reached them; a replaced file gets a new identity.
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub(crate) struct FileIdentity {
52    volume: u64,
53    index: u64,
54}
55
56/// How a finished process ended.
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub(crate) enum ProcessEnd {
59    /// It exited with this code.
60    Exited(i32),
61    /// It was ended without an exit code: on Unix by a signal, when known.
62    Signaled(Option<i32>),
63}
64
65/// How the process whose `status` this is ended.
66pub(crate) fn process_end(status: &std::process::ExitStatus) -> ProcessEnd {
67    match status.code() {
68        Some(code) => ProcessEnd::Exited(code),
69        None => ProcessEnd::Signaled(exit_signal(status)),
70    }
71}
72
73/// How a finished process ended, for a message: its exit code, or on Unix the signal that
74/// ended it, in the system's own wording.
75pub(crate) fn exit_description(status: &std::process::ExitStatus) -> String {
76    status.to_string()
77}
78
79/// Poll contended process-shared file locks without blocking the async runtime;
80/// 25 ms keeps session sends and credential refreshes responsive without busy-waiting.
81pub(crate) const FILE_LOCK_RETRY_INTERVAL: std::time::Duration =
82    std::time::Duration::from_millis(25);
83
84pub(crate) use artifactize_tools::files::{
85    EntryName, FileKind, canonicalize, entry_kind, link_target, open_directory, open_entry,
86    open_no_follow, read_dir,
87};
88pub use artifactize_tools::files::{open_nonblocking, path_text};
89
90/// Open an existing regular file for reading without following a link in its last
91/// component (on Windows, any reparse point), and check what was opened.
92pub fn open_regular(path: &std::path::Path) -> std::io::Result<std::fs::File> {
93    let file = open_no_follow(std::fs::OpenOptions::new().read(true), path)?;
94    if file.metadata()?.is_file() {
95        Ok(file)
96    } else {
97        Err(std::io::Error::new(
98            std::io::ErrorKind::InvalidInput,
99            "not a regular file",
100        ))
101    }
102}
103
104/// Decode one native tool path line. Unix paths may end in a literal carriage return;
105/// only Windows accepts CRLF as the line terminator rather than part of the path.
106pub(crate) fn path_from_line(bytes: &[u8]) -> std::path::PathBuf {
107    let path = if CRLF_LINE_ENDINGS {
108        bytes
109            .strip_suffix(b"\r\n")
110            .or_else(|| bytes.strip_suffix(b"\n"))
111            .unwrap_or(bytes)
112    } else {
113        bytes.strip_suffix(b"\n").unwrap_or(bytes)
114    };
115    path_from_bytes(path)
116}
117
118/// Default state directory policy belongs with the platform's environment names.
119pub(crate) fn state_directory() -> Option<std::path::PathBuf> {
120    resolve_state_directory(
121        environment::var("ARTIFACTIZE_STATE_HOME").map(Into::into),
122        STATE_VARIABLES
123            .iter()
124            .find_map(|name| environment::var(name).filter(|v| !v.is_empty()))
125            .map(Into::into),
126        home_directory(),
127    )
128}
129
130fn resolve_state_directory(
131    artifactize: Option<std::path::PathBuf>,
132    state: Option<std::path::PathBuf>,
133    home: Option<std::path::PathBuf>,
134) -> Option<std::path::PathBuf> {
135    artifactize
136        .filter(|p| !p.as_os_str().is_empty())
137        .or_else(|| {
138            state
139                .filter(|p| !p.as_os_str().is_empty())
140                .map(|p| p.join("artifactize"))
141        })
142        .or_else(|| {
143            home.filter(|p| !p.as_os_str().is_empty())
144                .map(|p| p.join(".local/state/artifactize"))
145        })
146}
147
148/// Printable user metadata, not an authenticated principal.
149pub(crate) fn user_name() -> Option<String> {
150    USER_VARIABLES
151        .iter()
152        .find_map(|name| environment::var_text(name).filter(|value| !value.is_empty()))
153}
154
155/// Strip one platform-supported script line ending and name the accepted endings.
156pub(crate) fn fingerprint_line_ending(stdout: &[u8]) -> (&[u8], &'static str) {
157    if CRLF_LINE_ENDINGS {
158        (
159            stdout
160                .strip_suffix(b"\r\n")
161                .or_else(|| stdout.strip_suffix(b"\n"))
162                .unwrap_or(stdout),
163            "LF or CRLF",
164        )
165    } else {
166        (stdout.strip_suffix(b"\n").unwrap_or(stdout), "LF")
167    }
168}
169
170/// Classify the final component without following links or Windows reparse points.
171pub(crate) fn path_kind(path: &std::path::Path) -> std::io::Result<FileKind> {
172    match (path.parent(), path.file_name()) {
173        (Some(parent), Some(name)) => entry_kind(&open_directory(parent)?, name),
174        _ => open_directory(path).map(|_| FileKind::Directory),
175    }
176}
177
178/// Render serialized path lists with portable separators, just like single saved paths.
179pub(crate) fn serialize_paths<S: serde::Serializer>(
180    paths: &[std::path::PathBuf],
181    serializer: S,
182) -> Result<S::Ok, S::Error> {
183    use serde::Serialize;
184    paths
185        .iter()
186        .map(|path| path_text(path))
187        .collect::<Vec<_>>()
188        .serialize(serializer)
189}
190
191#[cfg(test)]
192mod state_directory_tests {
193    use super::*;
194    use std::path::PathBuf;
195
196    #[test]
197    fn state_home_precedence_and_empty_values() {
198        let artifactize = PathBuf::from("custom/state");
199        let state = PathBuf::from("state");
200        let home = PathBuf::from("reviewer");
201        assert_eq!(
202            resolve_state_directory(
203                Some(artifactize.clone()),
204                Some(state.clone()),
205                Some(home.clone())
206            ),
207            Some(artifactize)
208        );
209        assert_eq!(
210            resolve_state_directory(None, Some(state.clone()), Some(home.clone())),
211            Some(state.join("artifactize"))
212        );
213        assert_eq!(
214            resolve_state_directory(None, None, Some(home.clone())),
215            Some(home.join(".local/state/artifactize"))
216        );
217        let empty = Some(PathBuf::new());
218        assert_eq!(
219            resolve_state_directory(empty.clone(), empty.clone(), Some(home.clone())),
220            Some(home.join(".local/state/artifactize"))
221        );
222        assert_eq!(
223            resolve_state_directory(empty.clone(), empty.clone(), empty),
224            None
225        );
226        assert_eq!(resolve_state_directory(None, None, None), None);
227    }
228}
229
230mod console;
231pub use console::pause_after_help;
232
233mod interaction;
234pub(crate) use interaction::{is_within, label, paths_equal, private_anonymous_file};
235
236/// Whether an entry exists without following links (including dangling links).
237pub(crate) fn entry_exists(path: &std::path::Path) -> std::io::Result<bool> {
238    let Some(parent) = path.parent() else {
239        return Ok(false);
240    };
241    let directory = match open_directory(parent) {
242        Ok(directory) => directory,
243        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(false),
244        Err(error) => return Err(error),
245    };
246    let Some(name) = path.file_name() else {
247        return Ok(false);
248    };
249    match entry_kind(&directory, name) {
250        Ok(_) => Ok(true),
251        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(false),
252        Err(error) => Err(error),
253    }
254}
255
256/// A no-follow stat of `path`. Unlike [`entry_exists`], this never opens `path`'s parent, so
257/// it still works on a search-only ancestor (executable but unreadable): opening a directory
258/// needs read permission on it, while naming one of its entries by an exact path needs only
259/// search (execute) permission on every ancestor up to it.
260pub(crate) fn marker_metadata(path: &std::path::Path) -> std::io::Result<std::fs::Metadata> {
261    path.symlink_metadata()
262}