Skip to main content

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(&current).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}