winbrew_core/
paths.rs

1//! Managed-root path helpers and derived directory contracts.
2//!
3//! The path layer owns the active WinBrew root and the directories derived from
4//! it. That includes the current runtime database layout, per-package journal
5//! paths, package-scoped evidence directories, and reserved shim locations.
6
7use std::path::{Path, PathBuf};
8/// Return the journal file for the given package key.
9#[cfg(windows)]
10use winbrew_windows::host::search_path_file;
11
12/// Fully resolved path set for the active WinBrew root.
13#[derive(Debug, Clone)]
14pub struct ResolvedPaths {
15    /// Managed root directory.
16    pub root: PathBuf,
17    /// Install root for packages.
18    pub packages: PathBuf,
19    /// Root data directory.
20    pub data: PathBuf,
21    /// Process log directory.
22    pub logs: PathBuf,
23    /// Package-scoped log parent directory.
24    pub package_logs: PathBuf,
25    /// Download and staging cache directory.
26    pub cache: PathBuf,
27    /// Recovery journal parent directory.
28    pub pkgdb: PathBuf,
29    /// Reserved package shim root.
30    pub shims: PathBuf,
31    /// Primary SQLite database directory.
32    pub db: PathBuf,
33    /// Catalog database path.
34    pub catalog_db: PathBuf,
35    /// Persisted configuration file.
36    pub config: PathBuf,
37    /// Process log file.
38    pub log: PathBuf,
39}
40
41#[derive(Debug, Clone)]
42struct ManagedRootLayout {
43    root: PathBuf,
44    packages: PathBuf,
45    data: PathBuf,
46    logs: PathBuf,
47    package_logs: PathBuf,
48    cache: PathBuf,
49    pkgdb: PathBuf,
50    shims: PathBuf,
51    db: PathBuf,
52    catalog_db: PathBuf,
53    config: PathBuf,
54    log: PathBuf,
55}
56
57impl ManagedRootLayout {
58    fn resolve(root: &Path, packages: &str, data: &str, logs: &str, cache: &str) -> Self {
59        let root = PathBuf::from(root);
60        let packages = resolve_template(&root, packages);
61        let data = resolve_template(&root, data);
62        let logs = resolve_template(&root, logs);
63        let cache = resolve_template(&root, cache);
64        let pkgdb = data.join("pkgdb");
65        let package_logs = logs.join("packages");
66        let shims = root.join("shims");
67        let db = data.join("db");
68
69        Self {
70            catalog_db: db.join("catalog.db"),
71            config: data.join("winbrew.toml"),
72            db: db.join("winbrew.db"),
73            log: logs.join("winbrew.log"),
74            package_logs,
75            packages,
76            pkgdb,
77            root,
78            cache,
79            data,
80            logs,
81            shims,
82        }
83    }
84
85    fn into_resolved_paths(self) -> ResolvedPaths {
86        ResolvedPaths {
87            root: self.root,
88            packages: self.packages,
89            data: self.data,
90            logs: self.logs,
91            package_logs: self.package_logs,
92            cache: self.cache,
93            pkgdb: self.pkgdb,
94            shims: self.shims,
95            db: self.db,
96            catalog_db: self.catalog_db,
97            config: self.config,
98            log: self.log,
99        }
100    }
101}
102
103/// Return the persisted configuration file for a root.
104pub fn config_file_at(root: &Path) -> PathBuf {
105    root.join("data").join("winbrew.toml")
106}
107
108/// Return the install root directory for a root.
109pub fn packages_dir_at(root: &Path) -> PathBuf {
110    root.join("packages")
111}
112
113/// Return the data directory for a root.
114pub fn data_dir_at(root: &Path) -> PathBuf {
115    root.join("data")
116}
117
118/// Return the package journal directory for a root.
119pub fn pkgdb_dir_at(root: &Path) -> PathBuf {
120    data_dir_at(root).join("pkgdb")
121}
122
123/// Return the SQLite database directory for a root.
124pub fn db_dir_at(root: &Path) -> PathBuf {
125    data_dir_at(root).join("db")
126}
127
128/// Return the primary SQLite database path for a root.
129pub fn db_path_at(root: &Path) -> PathBuf {
130    db_dir_at(root).join("winbrew.db")
131}
132
133/// Return the catalog database path for a root.
134pub fn catalog_db_at(root: &Path) -> PathBuf {
135    db_dir_at(root).join("catalog.db")
136}
137
138/// Return the process log directory for a root.
139pub fn log_dir_at(root: &Path) -> PathBuf {
140    root.join("data").join("logs")
141}
142
143/// Return the process log file for a root.
144pub fn log_file_at(root: &Path) -> PathBuf {
145    log_dir_at(root).join("winbrew.log")
146}
147
148/// Return the installer cache directory for a root.
149pub fn cache_dir_at(root: &Path) -> PathBuf {
150    root.join("data").join("cache")
151}
152
153/// Return a cache file path for the given package name and version.
154pub fn cache_file_at(root: &Path, name: &str, version: &str, ext: &str) -> PathBuf {
155    cache_dir_at(root).join(cache_filename(name, version, ext))
156}
157
158/// Return the 7-Zip runtime directory for a managed root.
159pub fn sevenz_runtime_dir_from_runtime_root(runtime_root: &Path) -> PathBuf {
160    runtime_root.join("bin/7zip")
161}
162
163/// Return the 7-Zip binary path for a managed root.
164pub fn sevenz_bin_path_from_runtime_root(runtime_root: &Path) -> PathBuf {
165    sevenz_runtime_dir_from_runtime_root(runtime_root).join("7z.exe")
166}
167
168/// Return the 7-Zip DLL path for a managed root.
169pub fn sevenz_dll_path_from_runtime_root(runtime_root: &Path) -> PathBuf {
170    sevenz_runtime_dir_from_runtime_root(runtime_root).join("7z.dll")
171}
172
173/// Return the first usable 7-Zip binary found on the current PATH.
174#[cfg(windows)]
175pub fn system_sevenz_binary_path() -> Option<PathBuf> {
176    search_path_file("7z.exe").and_then(|binary_path| {
177        let runtime_root = binary_path.parent()?;
178
179        if runtime_root.join("7z.dll").exists() {
180            Some(binary_path)
181        } else {
182            None
183        }
184    })
185}
186
187/// Return the first usable 7-Zip binary found on the current PATH.
188#[cfg(not(windows))]
189pub fn system_sevenz_binary_path() -> Option<PathBuf> {
190    None
191}
192
193/// Return the journal file for the given package key.
194pub fn package_journal_file_at(root: &Path, package_key: &str) -> PathBuf {
195    pkgdb_dir_at(root).join(package_key).join("journal.jsonl")
196}
197
198fn cache_filename(name: &str, version: &str, ext: &str) -> String {
199    let mut filename = String::with_capacity(name.len() + version.len() + ext.len() + 2);
200    filename.push_str(name);
201    filename.push('-');
202    filename.push_str(version);
203    filename.push('.');
204    filename.push_str(ext);
205    filename
206}
207
208/// Expand `${root}` placeholders inside a path template.
209pub fn resolve_template(root: &Path, template: &str) -> PathBuf {
210    let root_text = root.to_string_lossy();
211
212    if template.contains("${root}") {
213        PathBuf::from(template.replace("${root}", &root_text))
214    } else {
215        PathBuf::from(template)
216    }
217}
218
219/// Build the resolved path set for the active root layout.
220pub fn resolved_paths(
221    root: &Path,
222    packages: &str,
223    data: &str,
224    logs: &str,
225    cache: &str,
226) -> ResolvedPaths {
227    ManagedRootLayout::resolve(root, packages, data, logs, cache).into_resolved_paths()
228}
229
230impl ResolvedPaths {
231    /// Return the install directory for a package name.
232    pub fn package_install_dir(&self, package_name: &str) -> PathBuf {
233        self.packages.join(package_name)
234    }
235
236    /// Return the journal directory for a package key.
237    pub fn package_journal_dir(&self, package_key: &str) -> PathBuf {
238        self.pkgdb.join(package_key)
239    }
240
241    /// Return the journal file for a package key.
242    pub fn package_journal_file(&self, package_key: &str) -> PathBuf {
243        self.package_journal_dir(package_key).join("journal.jsonl")
244    }
245
246    /// Return the package-scoped log directory for a package key.
247    pub fn package_log_dir(&self, package_key: &str) -> PathBuf {
248        self.package_logs.join(package_key)
249    }
250
251    /// Return the reserved shim directory for a package key.
252    pub fn package_shim_dir(&self, package_key: &str) -> PathBuf {
253        self.shims.join(package_key)
254    }
255}
256
257/// Recover the managed root from a package install directory.
258pub fn install_root_from_package_dir(install_dir: &Path) -> PathBuf {
259    install_dir
260        .parent()
261        .and_then(|path| path.parent())
262        .map(PathBuf::from)
263        .unwrap_or_default()
264}
265
266#[cfg(test)]
267mod tests {
268    use super::{
269        cache_dir_at, catalog_db_at, config_file_at, data_dir_at, db_path_at, log_dir_at,
270        log_file_at, package_journal_file_at, packages_dir_at, pkgdb_dir_at, resolved_paths,
271        sevenz_bin_path_from_runtime_root, sevenz_dll_path_from_runtime_root,
272        sevenz_runtime_dir_from_runtime_root,
273    };
274    use std::path::PathBuf;
275    use tempfile::tempdir;
276
277    #[test]
278    fn package_journal_file_lives_under_pkgdb() {
279        let root = tempdir().expect("temp dir");
280        let package_key = "winget_Contoso.App-c47f5b18b8a430e6";
281
282        let journal_file = package_journal_file_at(root.path(), package_key);
283
284        assert_eq!(
285            journal_file,
286            pkgdb_dir_at(root.path())
287                .join(package_key)
288                .join("journal.jsonl")
289        );
290    }
291
292    #[test]
293    fn sevenz_runtime_layout_uses_expected_relative_paths() {
294        let runtime_root = PathBuf::from("C:/winbrew");
295
296        assert_eq!(
297            sevenz_runtime_dir_from_runtime_root(&runtime_root),
298            PathBuf::from("C:/winbrew/bin/7zip")
299        );
300        assert_eq!(
301            sevenz_bin_path_from_runtime_root(&runtime_root),
302            PathBuf::from("C:/winbrew/bin/7zip/7z.exe")
303        );
304        assert_eq!(
305            sevenz_dll_path_from_runtime_root(&runtime_root),
306            PathBuf::from("C:/winbrew/bin/7zip/7z.dll")
307        );
308    }
309
310    #[test]
311    fn resolved_paths_derive_managed_layout_and_package_scopes() {
312        let root = tempdir().expect("temp dir");
313        let package_key = "winget_Contoso.App-c47f5b18b8a430e6";
314        let paths = resolved_paths(
315            root.path(),
316            "${root}\\packages",
317            "${root}\\data",
318            "${root}\\data\\logs",
319            "${root}\\data\\cache",
320        );
321
322        assert_eq!(paths.root, root.path());
323        assert_eq!(paths.packages, packages_dir_at(root.path()));
324        assert_eq!(
325            paths.package_install_dir("Contoso.App"),
326            paths.packages.join("Contoso.App")
327        );
328        assert_eq!(paths.data, data_dir_at(root.path()));
329        assert_eq!(paths.logs, log_dir_at(root.path()));
330        assert_eq!(paths.package_logs, paths.logs.join("packages"));
331        assert_eq!(paths.cache, cache_dir_at(root.path()));
332        assert_eq!(paths.pkgdb, pkgdb_dir_at(root.path()));
333        assert_eq!(paths.shims, root.path().join("shims"));
334        assert_eq!(paths.db, db_path_at(root.path()));
335        assert_eq!(paths.catalog_db, catalog_db_at(root.path()));
336        assert_eq!(paths.config, config_file_at(root.path()));
337        assert_eq!(paths.log, log_file_at(root.path()));
338        assert_eq!(
339            paths.package_journal_dir(package_key),
340            paths.pkgdb.join(package_key)
341        );
342        assert_eq!(
343            paths.package_journal_file(package_key),
344            package_journal_file_at(root.path(), package_key)
345        );
346        assert_eq!(
347            paths.package_log_dir(package_key),
348            paths.package_logs.join(package_key)
349        );
350        assert_eq!(
351            paths.package_shim_dir(package_key),
352            paths.shims.join(package_key)
353        );
354    }
355}