prov_graph/graph/manifest.rs
1//! Reading manifests off the graph — the lookups every pass over a covered
2//! directory shares.
3//!
4//! The model and its serialization are [`crate::manifest`]; here is what needs a
5//! filesystem: loading the manifest a node declares, the reverse lookup from a
6//! directory to the node covering it, and the directory walk a manifest is
7//! compared against.
8//!
9//! The reverse lookup mirrors the attachment one exactly (`shadow.rs`): the
10//! `<dir>.<ext>` convention is the fast path, and the `manifest` → `root`
11//! chain is authoritative. A node under a non-conventional name still covers
12//! its directory; it just is not found by probing.
13
14use std::collections::BTreeSet;
15use std::future::Future;
16use std::path::{Path, PathBuf};
17use std::pin::Pin;
18
19use super::Graph;
20use crate::document::{is_opaque_payload, require_whole_file};
21use crate::error::{Error, Result};
22use crate::fs::ReadStorage;
23use crate::index::IdIndex;
24use crate::link;
25use crate::manifest::{Manifest, manifest_node_candidates};
26
27impl<FS: ReadStorage, Ix: IdIndex> Graph<FS, Ix> {
28 /// The manifest document `node` declares, loaded and parsed, with its
29 /// workspace-relative path. `None` when `node` declares no `manifest`.
30 ///
31 /// A manifest is a **record store** (spec §5): prov re-lays-out its rows, so
32 /// a markdown carrier has no stable home for them and is refused here, at
33 /// the one choke point every reader passes through.
34 pub async fn manifest_of(&self, node: &Path) -> Result<Option<(PathBuf, Manifest)>> {
35 let (_, doc) = self.load(node).await?;
36 let Some(raw) = doc.manifest_attr() else {
37 return Ok(None);
38 };
39 let path = link::resolve(node, raw);
40 let manifest = self.read_manifest(&path).await?;
41 Ok(Some((path, manifest)))
42 }
43
44 /// Read and parse the manifest document at `path` itself.
45 pub async fn read_manifest(&self, path: &Path) -> Result<Manifest> {
46 let (_, doc) = self.load(path).await?;
47 let carrier = doc
48 .carrier
49 .ok_or_else(|| Error::Structure(format!("{} carries no metadata", path.display())))?;
50 require_whole_file(path, carrier)?;
51 let manifest = Manifest::from_meta(&doc.meta)
52 .map_err(|e| Error::Structure(format!("{}: {e}", path.display())))?;
53 // Judged here, where the manifest's own location is known — see
54 // `Manifest::checked_root`.
55 manifest
56 .checked_root(path)
57 .map_err(|e| Error::Structure(format!("{}: {e}", path.display())))?;
58 Ok(manifest)
59 }
60
61 /// Whether the document at `candidate` is a manifest node whose manifest
62 /// covers the directory `dir` — the authoritative half of the reverse
63 /// lookup below.
64 ///
65 /// Unreadable, unparsable and non-manifest candidates simply do not claim:
66 /// this runs inside best-effort scans, where the question is "is this
67 /// directory already accounted for", and a damaged manifest is a finding
68 /// `check` raises rather than a reason to abort a walk.
69 pub async fn manifest_claims(&self, candidate: &Path, dir: &Path) -> bool {
70 match self.manifest_of(candidate).await {
71 Ok(Some((manifest_doc, manifest))) => {
72 manifest.covered_root(&manifest_doc) == link::normalize(dir)
73 }
74 _ => false,
75 }
76 }
77
78 /// The node covering the directory `dir`, or `None` when nothing does —
79 /// the counterpart of `attachment_for` for a whole directory. Probes the
80 /// `<dir>.<ext>` convention and confirms each hit through the node's own
81 /// `manifest` pointer.
82 pub async fn manifest_node_for(&self, dir: &Path) -> Result<Option<PathBuf>> {
83 let dir = link::normalize(dir);
84 for candidate in manifest_node_candidates(&dir) {
85 if self.exists(&candidate).await? && self.manifest_claims(&candidate, &dir).await {
86 return Ok(Some(candidate));
87 }
88 }
89 Ok(None)
90 }
91
92 /// Whether any directory on `path`'s way down from the workspace root is
93 /// covered by a manifest — the guard the loose-attachment sweeps use so a
94 /// covered directory is never offered up for ten thousand sidecars.
95 ///
96 /// Walks the ancestors rather than only the immediate parent, because a
97 /// manifest claims its root *recursively*: `photos/2019/a.jpg` is covered by
98 /// the node beside `photos/`.
99 ///
100 /// **Probe-only, and bounded on purpose.** A node renamed away from its
101 /// directory leaves no local evidence beside that directory (moving the
102 /// archive to keep the convention is the thing `rename` deliberately does
103 /// not do), so this can answer "no" where a census would answer "yes". The
104 /// caller is `attach`, which runs per file inside `--all`; making each one
105 /// authoritative would cost a census per file. The residue is a covered file
106 /// that also gains a sidecar — duplicated bookkeeping, not a contradiction,
107 /// since both records are derived from the same bytes. The operation where a
108 /// wrong "no" *would* matter — minting a second manifest over a whole
109 /// archive — asks `manifest_node_covering` instead and pays for the census.
110 pub async fn under_manifest(&self, path: &Path) -> Result<bool> {
111 let path = link::normalize(path);
112 let mut dir = path.parent().map(Path::to_path_buf);
113 while let Some(current) = dir {
114 if current.as_os_str().is_empty() {
115 break;
116 }
117 if self.manifest_node_for(¤t).await?.is_some() {
118 return Ok(true);
119 }
120 dir = current.parent().map(Path::to_path_buf);
121 }
122 Ok(false)
123 }
124
125 /// The opaque payloads under the covered directory `root`, as paths relative
126 /// to it, sorted — what a manifest is built from and compared against.
127 ///
128 /// Three exclusions, each deliberate. **Hidden entries** are skipped, as in
129 /// every other prov walk. **Files prov can read** (a `.md` note, a `.yaml`
130 /// store) are not payloads and stay ordinary documents — a manifest covers
131 /// bytes, never shadows a document. And a **nested manifest's** directory is
132 /// left to its own node, so two manifests never claim the same file.
133 pub async fn scan_covered(&self, root: &Path) -> Result<Vec<PathBuf>> {
134 let mut found = Vec::new();
135 self.scan_covered_into(root, PathBuf::new(), &mut found)
136 .await?;
137 found.sort_by(|a, b| {
138 crate::manifest::path_sort_key(a).cmp(&crate::manifest::path_sort_key(b))
139 });
140 Ok(found)
141 }
142
143 fn scan_covered_into<'a>(
144 &'a self,
145 root: &'a Path,
146 rel: PathBuf,
147 out: &'a mut Vec<PathBuf>,
148 ) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
149 Box::pin(async move {
150 let dir = link::normalize(root.join(&rel));
151 let Ok(entries) = self.listing(&dir).await else {
152 return Ok(());
153 };
154 let mut names: Vec<(String, bool)> = Vec::new();
155 for entry in entries {
156 let Some(name) = entry
157 .file_name()
158 .and_then(|n| n.to_str())
159 .map(str::to_owned)
160 else {
161 continue;
162 };
163 if name.starts_with('.') {
164 continue;
165 }
166 names.push((name, entry.file_type().is_dir()));
167 }
168 for (name, is_dir) in names {
169 let child = if rel.as_os_str().is_empty() {
170 PathBuf::from(&name)
171 } else {
172 rel.join(&name)
173 };
174 if is_dir {
175 // A nested manifest owns its own subtree.
176 if self
177 .manifest_node_for(&link::normalize(root.join(&child)))
178 .await?
179 .is_some()
180 {
181 continue;
182 }
183 self.scan_covered_into(root, child, out).await?;
184 } else if is_opaque_payload(&child) {
185 out.push(child);
186 }
187 }
188 Ok(())
189 })
190 }
191
192 /// The covered roots of every manifest reachable in `walk_docs` — the set a
193 /// scan consults to know which directories are already accounted for.
194 /// Damaged manifests contribute nothing (their damage is `check`'s to
195 /// report).
196 pub async fn manifest_roots(&self, walk_docs: &BTreeSet<PathBuf>) -> BTreeSet<PathBuf> {
197 let mut roots = BTreeSet::new();
198 for doc in walk_docs {
199 if let Ok(Some((manifest_doc, manifest))) = self.manifest_of(doc).await {
200 roots.insert(manifest.covered_root(&manifest_doc));
201 }
202 }
203 roots
204 }
205}