Skip to main content

artifactize_core/platform/
interaction.rs

1//! Portable boundaries for path identity and interactive editing.
2use std::{io, path::Path, sync::OnceLock};
3
4/// Compare opened identities, not spelling: case aliases and system directory aliases
5/// on case-insensitive volumes must name the same repository.
6pub(crate) fn paths_equal(left: &Path, right: &Path) -> bool {
7    let identity = |path: &Path| {
8        super::open_directory(path)
9            .or_else(|_| super::open_regular(path))
10            .and_then(|file| super::file_identity(&file))
11    };
12    match (identity(left), identity(right)) {
13        (Ok(left), Ok(right)) => left == right,
14        _ => left == right,
15    }
16}
17
18/// Include the root itself; compare ancestor identities to honor volume semantics.
19pub(crate) fn is_within(path: &Path, root: &Path) -> bool {
20    path.ancestors().any(|ancestor| paths_equal(ancestor, root))
21}
22
23/// The host label used in provider-facing diagnostics and user agents.
24pub(crate) fn label() -> &'static str {
25    static LABEL: OnceLock<String> = OnceLock::new();
26    LABEL.get_or_init(|| format!("{} {}", std::env::consts::OS, std::env::consts::ARCH))
27}
28
29pub fn selected_editor() -> String {
30    ["VISUAL", "EDITOR"]
31        .into_iter()
32        .find_map(|name| {
33            super::environment::var_text(name).filter(|value| !value.trim().is_empty())
34        })
35        .unwrap_or_else(|| super::DEFAULT_EDITOR.into())
36}
37
38/// An editor draft is owner-only even on platforms where tempfile's default ACL inherits.
39pub fn private_temp_file(prefix: &str) -> io::Result<PrivateTempFile> {
40    // Build the protected ACL/mode at creation, not after creating a public draft.
41    let root = super::canonicalize(&std::env::temp_dir())?;
42    let directory = super::private_tempdir_in(prefix, &root)?;
43    let file = tempfile::Builder::new()
44        .prefix(prefix)
45        .suffix(".json")
46        .tempfile_in(directory.path())?;
47    super::restrict_file(file.as_file())?;
48    Ok(PrivateTempFile {
49        file,
50        _directory: directory,
51    })
52}
53
54/// An unnamed owner-only scratch file in the system temporary directory, removed when
55/// closed.
56pub(crate) fn private_anonymous_file() -> io::Result<std::fs::File> {
57    let file = tempfile::tempfile()?;
58    super::restrict_file(&file)?;
59    Ok(file)
60}
61
62/// Do not expose signal-specific process statuses outside the platform boundary.
63pub async fn run_editor(command: &str, file: &Path) -> io::Result<()> {
64    let status = super::editor(command, file).status().await?;
65    if status.success() {
66        Ok(())
67    } else {
68        Err(io::Error::other(format!(
69            "The editor exited unsuccessfully ({status}); nothing was submitted."
70        )))
71    }
72}
73
74/// Keep the protected parent alive until its editor draft has been removed.
75pub struct PrivateTempFile {
76    file: tempfile::NamedTempFile,
77    _directory: super::PrivateTempDir,
78}
79impl PrivateTempFile {
80    pub fn path(&self) -> &Path {
81        self.file.path()
82    }
83}