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}