1use std::path::{Component, Path, PathBuf};
4use std::{fs, process};
5
6use crate::error::{StoreError, io_error};
7
8pub const SPEC: &str = "spec";
10
11#[must_use]
16pub fn is_under_spec(root: &Path, path: &Path) -> bool {
17 resolved(&root.join(path)).starts_with(resolved(&root.join(SPEC)))
18}
19
20fn resolved(path: &Path) -> PathBuf {
22 let absolute = std::path::absolute(path).unwrap_or_else(|_| path.to_path_buf());
23 let mut normal = PathBuf::new();
24 for part in absolute.components() {
25 match part {
26 Component::CurDir => {}
27 Component::ParentDir => {
28 normal.pop();
29 }
30 other => normal.push(other),
31 }
32 }
33 let mut missing = Vec::new();
34 let mut existing = normal.as_path();
35 while !existing.exists() {
36 let (Some(name), Some(parent)) = (existing.file_name(), existing.parent()) else {
37 break;
38 };
39 missing.push(name);
40 existing = parent;
41 }
42 let mut resolved = existing
43 .canonicalize()
44 .unwrap_or_else(|_| existing.to_path_buf());
45 resolved.extend(missing.iter().rev());
46 resolved
47}
48
49#[derive(Debug, Default, PartialEq, Eq)]
52pub struct WrittenOutputs {
53 pub written: Vec<PathBuf>,
54 pub unchanged: Vec<PathBuf>,
55}
56
57pub fn write_outputs(
64 root: &Path,
65 outputs: &[(PathBuf, Vec<u8>)],
66) -> Result<WrittenOutputs, StoreError> {
67 let mut done = WrittenOutputs::default();
68 for (path, bytes) in outputs {
69 let target = root.join(path);
70 if fs::read(&target).is_ok_and(|current| current == *bytes) {
71 done.unchanged.push(path.clone());
72 continue;
73 }
74 if let Some(parent) = target.parent() {
75 fs::create_dir_all(parent).map_err(io_error(parent))?;
76 }
77 let name = target
78 .file_name()
79 .map_or_else(Default::default, |name| name.to_string_lossy());
80 let temporary = target.with_file_name(format!(".{name}.loft-{}", process::id()));
81 fs::write(&temporary, bytes).map_err(io_error(&temporary))?;
82 fs::rename(&temporary, &target).map_err(io_error(&target))?;
83 done.written.push(path.clone());
84 }
85 Ok(done)
86}
87
88pub(crate) fn holds_entries(dir: &Path) -> Result<bool, StoreError> {
94 for entry in fs::read_dir(dir).map_err(io_error(dir))? {
95 let entry = entry.map_err(io_error(dir))?;
96 if !entry.file_name().to_str().is_some_and(hidden) {
97 return Ok(true);
98 }
99 }
100 Ok(false)
101}
102
103pub(crate) fn hidden(name: &str) -> bool {
106 name.starts_with('.')
107}