metrale_bench/artifacts/
home.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-26: Which directory is the Metrale Engine home, where that answer came
4//! from, and whether this process can write there.
5//!
6//! Owner: bench (artifacts).
7//! Invariants:
8//! - `resolve_from` reads only its two arguments: it neither reads nor writes
9//!   the filesystem.
10
11use std::path::{Path, PathBuf};
12
13use anyhow::{Context, Result, bail};
14
15/// 2026-09-26: Where the Metrale Engine home came from. It matters because the
16/// signing identity lives under the home (`<root>/identity/ed25519.pk8`,
17/// `gate::signing`), so two homes sign as two signers.
18#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub enum HomeSource {
20    /// 2026-09-26: `METRALE_HOME` was set.
21    Env,
22    /// 2026-09-26: `$HOME/.metrale`.
23    HomeDefault,
24}
25
26impl HomeSource {
27    /// 2026-09-26: The provenance as operator-facing text.
28    pub fn describe(self) -> &'static str {
29        match self {
30            Self::Env => "from METRALE_HOME",
31            Self::HomeDefault => "default, $HOME/.metrale",
32        }
33    }
34}
35
36/// 2026-09-26: A resolved Metrale Engine home and its provenance.
37#[derive(Clone, Debug)]
38pub struct MetraleHome {
39    pub root: PathBuf,
40    pub source: HomeSource,
41}
42
43/// 2026-09-26: Why a Metrale Engine home cannot be used.
44#[derive(Clone, Debug, PartialEq, Eq)]
45pub enum HomeFault {
46    /// 2026-09-26: The directory exists but the write probe failed.
47    NotWritable {
48        /// 2026-09-26: The directory's owner uid, when it could be read.
49        owner_uid: Option<u32>,
50        /// 2026-09-26: This process's uid, when it could be read.
51        process_uid: Option<u32>,
52    },
53    /// 2026-09-26: The path exists and is not a directory.
54    NotADirectory,
55    /// 2026-09-26: The directory did not exist and `create_dir_all` failed.
56    Uncreatable(String),
57}
58
59impl std::fmt::Display for HomeFault {
60    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61        match self {
62            Self::NotWritable {
63                owner_uid: Some(o),
64                process_uid: Some(p),
65            } if o != p => write!(
66                f,
67                "not writable: owned by uid {o}, this process is uid {p}. A `sudo` \
68                 or container run most likely created it. Fix with \
69                 `sudo chown -R {p} <path>`, or point METRALE_HOME somewhere this \
70                 user owns."
71            ),
72            Self::NotWritable { .. } => write!(
73                f,
74                "not writable by this process. Fix the permissions, or point \
75                 METRALE_HOME somewhere this user owns."
76            ),
77            Self::NotADirectory => write!(f, "exists but is not a directory"),
78            Self::Uncreatable(e) => write!(f, "does not exist and could not be created: {e}"),
79        }
80    }
81}
82
83impl MetraleHome {
84    /// 2026-09-26: Resolve from the environment: a non-empty `METRALE_HOME`
85    /// wins, then `$HOME/.metrale`. An empty `METRALE_HOME` is an error, and so
86    /// is having neither variable. Touches no file.
87    pub fn resolve() -> Result<Self> {
88        resolve_from(std::env::var_os("METRALE_HOME"), std::env::var_os("HOME"))
89    }
90
91    /// 2026-09-26: Can this process use the home? `None` means yes.
92    ///
93    /// Creates the directory if it is missing, then probes by writing and
94    /// removing `.metrale-write-probe`, so ownership, ACLs, read-only mounts
95    /// and a full disk all show up as the one failure that matters.
96    pub fn fault(&self) -> Option<HomeFault> {
97        check_usable(&self.root)
98    }
99
100    /// 2026-09-26: One line naming the root and where it came from.
101    pub fn describe(&self) -> String {
102        format!("{} ({})", self.root.display(), self.source.describe())
103    }
104}
105
106/// 2026-09-26: [`MetraleHome::resolve`] over explicit values of `METRALE_HOME`
107/// and `HOME`, so tests need not call `set_var`, which is unsafe and
108/// process-global.
109pub(super) fn resolve_from(
110    metrale_home: Option<std::ffi::OsString>,
111    home: Option<std::ffi::OsString>,
112) -> Result<MetraleHome> {
113    if let Some(explicit) = metrale_home {
114        let root = PathBuf::from(explicit);
115        if root.as_os_str().is_empty() {
116            bail!("METRALE_HOME is set but empty");
117        }
118        return Ok(MetraleHome {
119            root,
120            source: HomeSource::Env,
121        });
122    }
123    let home = home
124        .map(PathBuf::from)
125        .filter(|h| !h.as_os_str().is_empty())
126        .context("neither METRALE_HOME nor HOME is set — cannot place ~/.metrale")?;
127    Ok(MetraleHome {
128        root: home.join(".metrale"),
129        source: HomeSource::HomeDefault,
130    })
131}
132
133pub(super) fn check_usable(root: &Path) -> Option<HomeFault> {
134    if root.exists() && !root.is_dir() {
135        return Some(HomeFault::NotADirectory);
136    }
137    if !root.exists()
138        && let Err(e) = std::fs::create_dir_all(root)
139    {
140        return Some(HomeFault::Uncreatable(e.to_string()));
141    }
142    let probe = root.join(".metrale-write-probe");
143    match std::fs::write(&probe, b"") {
144        Ok(()) => {
145            let _ = std::fs::remove_file(&probe);
146            None
147        }
148        Err(_) => Some(HomeFault::NotWritable {
149            owner_uid: owner_uid_of(root),
150            process_uid: current_uid(),
151        }),
152    }
153}
154
155#[cfg(unix)]
156fn owner_uid_of(p: &Path) -> Option<u32> {
157    use std::os::unix::fs::MetadataExt;
158    std::fs::metadata(p).ok().map(|m| m.uid())
159}
160
161#[cfg(not(unix))]
162fn owner_uid_of(_p: &Path) -> Option<u32> {
163    None
164}
165
166/// 2026-09-26: This process's uid, read as the owner of `/proc/self` because
167/// `libc` is not a dependency of this crate. Checked 2026-09-26 on Linux:
168/// `stat` of `/proc/self`, following the link, gives the effective uid.
169#[cfg(unix)]
170fn current_uid() -> Option<u32> {
171    use std::os::unix::fs::MetadataExt;
172    std::fs::metadata("/proc/self").ok().map(|m| m.uid())
173}
174
175#[cfg(not(unix))]
176fn current_uid() -> Option<u32> {
177    None
178}