Skip to main content

runx_runtime/receipts/
paths.rs

1use std::collections::BTreeMap;
2use std::fmt;
3use std::path::{Component, Path, PathBuf};
4
5use crate::path_util::lexical_normalize;
6
7pub const RUNTIME_RECEIPTS_DIR_CONFIG_KEY: &str = "runtime.receipts.dir";
8pub const RUNX_RECEIPT_DIR_ENV: &str = "RUNX_RECEIPT_DIR";
9pub const RUNX_PROJECT_DIR_ENV: &str = "RUNX_PROJECT_DIR";
10pub const RUNX_CWD_ENV: &str = "RUNX_CWD";
11pub const INIT_CWD_ENV: &str = "INIT_CWD";
12
13#[derive(Clone, Debug, Default, PartialEq, Eq)]
14pub struct RuntimeReceiptConfig {
15    pub dir: Option<PathBuf>,
16}
17
18#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub enum ReceiptPathSource {
20    ExplicitInput,
21    RuntimeConfig,
22    Environment,
23    ProjectDefault,
24}
25
26#[derive(Clone, Debug, PartialEq, Eq)]
27pub struct ReceiptStoreLabel(String);
28
29impl ReceiptStoreLabel {
30    #[must_use]
31    pub fn as_str(&self) -> &str {
32        &self.0
33    }
34}
35
36impl fmt::Display for ReceiptStoreLabel {
37    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
38        formatter.write_str(&self.0)
39    }
40}
41
42#[derive(Clone, Debug, PartialEq, Eq)]
43pub struct ReceiptStorePublicProjection {
44    label: ReceiptStoreLabel,
45}
46
47impl ReceiptStorePublicProjection {
48    #[must_use]
49    pub fn label(&self) -> &ReceiptStoreLabel {
50        &self.label
51    }
52
53    #[must_use]
54    pub fn summary(&self) -> String {
55        format!("receipt store: {}", self.label)
56    }
57}
58
59impl fmt::Display for ReceiptStorePublicProjection {
60    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
61        write!(formatter, "receipt store: {}", self.label)
62    }
63}
64
65#[derive(Clone, Debug, PartialEq, Eq)]
66pub struct ResolvedReceiptPath {
67    pub path: PathBuf,
68    pub source: ReceiptPathSource,
69    pub label: ReceiptStoreLabel,
70    pub project_runx_dir: PathBuf,
71    pub workspace_base: PathBuf,
72}
73
74impl ResolvedReceiptPath {
75    #[must_use]
76    pub fn public_projection(&self) -> ReceiptStorePublicProjection {
77        ReceiptStorePublicProjection {
78            label: self.label.clone(),
79        }
80    }
81}
82
83#[derive(Clone, Copy, Debug)]
84pub struct ReceiptPathInputs<'a> {
85    pub explicit_dir: Option<&'a Path>,
86    pub runtime_config: Option<&'a RuntimeReceiptConfig>,
87    pub env: &'a BTreeMap<String, String>,
88    pub cwd: &'a Path,
89}
90
91#[must_use]
92pub fn resolve_receipt_path(inputs: ReceiptPathInputs<'_>) -> ResolvedReceiptPath {
93    let workspace_base = resolve_workspace_base(inputs.env, inputs.cwd);
94    let project_runx_dir = resolve_project_runx_dir(inputs.env, &workspace_base);
95    let (path, source) = match inputs.explicit_dir {
96        Some(path) => (
97            resolve_from_workspace_base(path, &workspace_base),
98            ReceiptPathSource::ExplicitInput,
99        ),
100        None => match inputs
101            .runtime_config
102            .and_then(|config| config.dir.as_deref())
103        {
104            Some(path) => (
105                resolve_from_workspace_base(path, &workspace_base),
106                ReceiptPathSource::RuntimeConfig,
107            ),
108            None => match env_path(inputs.env, RUNX_RECEIPT_DIR_ENV) {
109                Some(path) => (
110                    resolve_from_workspace_base(path, &workspace_base),
111                    ReceiptPathSource::Environment,
112                ),
113                None => (
114                    project_runx_dir.join("receipts"),
115                    ReceiptPathSource::ProjectDefault,
116                ),
117            },
118        },
119    };
120    let path = lexical_normalize(&path);
121    let label = safe_receipt_store_label(&path, &workspace_base, &project_runx_dir);
122    ResolvedReceiptPath {
123        path,
124        source,
125        label,
126        project_runx_dir,
127        workspace_base,
128    }
129}
130
131#[must_use]
132pub fn resolve_workspace_base(env: &BTreeMap<String, String>, cwd: &Path) -> PathBuf {
133    env_path(env, RUNX_CWD_ENV)
134        .or_else(|| env_path(env, INIT_CWD_ENV))
135        .map_or_else(|| absolute_cwd(cwd), |path| resolve_from_cwd(path, cwd))
136}
137
138#[must_use]
139pub fn resolve_project_runx_dir(env: &BTreeMap<String, String>, workspace_base: &Path) -> PathBuf {
140    env_path(env, RUNX_PROJECT_DIR_ENV).map_or_else(
141        || lexical_normalize(&workspace_base.join(".runx")),
142        |path| resolve_from_workspace_base(path, workspace_base),
143    )
144}
145
146#[must_use]
147pub fn safe_receipt_store_label(
148    receipt_dir: &Path,
149    workspace_base: &Path,
150    project_runx_dir: &Path,
151) -> ReceiptStoreLabel {
152    let receipt_dir = lexical_normalize(receipt_dir);
153    let workspace_base = lexical_normalize(workspace_base);
154    let project_runx_dir = lexical_normalize(project_runx_dir);
155
156    if let Ok(relative_to_project) = receipt_dir.strip_prefix(&project_runx_dir) {
157        if let Ok(relative_to_workspace) = receipt_dir.strip_prefix(&workspace_base) {
158            return ReceiptStoreLabel(path_label(relative_to_workspace));
159        }
160        return ReceiptStoreLabel(format!("runx-project:{}", path_label(relative_to_project)));
161    }
162
163    ReceiptStoreLabel(format!(
164        "external-receipt-store:{}",
165        stable_path_hash(&receipt_dir)
166    ))
167}
168
169#[must_use]
170pub fn safe_receipt_store_projection(
171    receipt_dir: &Path,
172    workspace_base: &Path,
173    project_runx_dir: &Path,
174) -> ReceiptStorePublicProjection {
175    ReceiptStorePublicProjection {
176        label: safe_receipt_store_label(receipt_dir, workspace_base, project_runx_dir),
177    }
178}
179
180fn env_path<'a>(env: &'a BTreeMap<String, String>, key: &str) -> Option<&'a Path> {
181    env.get(key)
182        .filter(|value| !value.trim().is_empty())
183        .map(Path::new)
184}
185
186fn resolve_from_workspace_base(path: &Path, workspace_base: &Path) -> PathBuf {
187    if path.is_absolute() {
188        lexical_normalize(path)
189    } else {
190        lexical_normalize(&workspace_base.join(path))
191    }
192}
193
194fn resolve_from_cwd(path: &Path, cwd: &Path) -> PathBuf {
195    if path.is_absolute() {
196        lexical_normalize(path)
197    } else {
198        lexical_normalize(&absolute_cwd(cwd).join(path))
199    }
200}
201
202fn absolute_cwd(cwd: &Path) -> PathBuf {
203    if cwd.is_absolute() {
204        lexical_normalize(cwd)
205    } else {
206        let base = match std::env::current_dir() {
207            Ok(path) => path,
208            Err(_) => PathBuf::from("."),
209        };
210        lexical_normalize(&base.join(cwd))
211    }
212}
213
214fn path_label(path: &Path) -> String {
215    let label = path
216        .components()
217        .filter_map(|component| match component {
218            Component::Normal(segment) => Some(segment.to_string_lossy().into_owned()),
219            Component::CurDir => Some(".".to_owned()),
220            Component::ParentDir => Some("..".to_owned()),
221            Component::Prefix(_) | Component::RootDir => None,
222        })
223        .collect::<Vec<_>>()
224        .join("/");
225    if label.is_empty() {
226        ".".to_owned()
227    } else {
228        label
229    }
230}
231
232fn stable_path_hash(path: &Path) -> String {
233    let mut hash = 0xcbf29ce484222325u64;
234    for byte in path.to_string_lossy().as_bytes() {
235        hash ^= u64::from(*byte);
236        hash = hash.wrapping_mul(0x100000001b3);
237    }
238    format!("{hash:016x}")
239}