Skip to main content

prov_graph/graph/
load.rs

1//! The read primitive — root-escape clamp, read-scope memo, filesystem read,
2//! [`Document::parse`] — that every pass built on top of the graph shares.
3//! See the module doc at [`crate::graph`] for how this sits beside
4//! [`resolve`](super::resolve) and the census.
5
6use std::path::Path;
7
8use super::Graph;
9use crate::document::Document;
10use crate::error::{Error, Result};
11use crate::fs::ReadStorage;
12use crate::link;
13
14impl<FS: ReadStorage, Ix> Graph<FS, Ix> {
15    /// Read and parse the workspace-relative document at `path`, returning the
16    /// raw text alongside. The building block traversal, validation, and
17    /// mutation share.
18    pub async fn load(&self, path: &Path) -> Result<(String, Document)> {
19        // Clamp reads to the workspace root: `path` may originate in a document's
20        // own metadata (a `contents`/`part_of` target), so a hostile or careless
21        // `../../../etc/passwd` must be refused here rather than opened. The
22        // traversal turns this error into an `Unreadable` node; a direct caller
23        // sees the `Escape` error itself.
24        if link::escapes_root(path) {
25            return Err(Error::Escape(path.to_path_buf()));
26        }
27        // Inside a `read_scope`, a document already read this operation is
28        // answered from memory — the escape check above still runs first, so a
29        // memo can never be the thing that lets a hostile path through.
30        if let Some(hit) = self.memo_hit(path) {
31            return Ok(hit);
32        }
33        let text = self.fs().read_to_string(&self.root().join(path)).await?;
34        let doc = Document::parse(path, &text)?;
35        self.memo_remember(path, &text, &doc);
36        Ok((text, doc))
37    }
38
39    /// Read and parse the workspace-relative document at `path`, returning its
40    /// full [`Document`] — the public counterpart to [`load`](Self::load), for
41    /// a caller walking a [`Node`](crate::graph::Node) tree who needs more than
42    /// [`Node::title`](crate::graph::Node::title) (the rest of the frontmatter,
43    /// the body, the carrier) without re-reading and re-parsing the file by
44    /// hand.
45    ///
46    /// Unlike the traversal, which degrades a bad target to a
47    /// [`NodeKind::Unreadable`](crate::graph::NodeKind::Unreadable) node, this
48    /// surfaces the [`Error`] directly — a caller who names a path expects to
49    /// know why it failed, not to receive a placeholder.
50    pub async fn document(&self, path: impl AsRef<Path>) -> Result<Document> {
51        let path = link::normalize(path);
52        self.load(&path).await.map(|(_, doc)| doc)
53    }
54}
55
56// These tests use YAML frontmatter fixtures, so they run under the `yaml` feature.
57#[cfg(all(test, feature = "yaml"))]
58mod tests {
59    use std::path::PathBuf;
60
61    use super::*;
62    use crate::exec::block_on;
63    use crate::fs::StdFs;
64    use crate::graph::ReadSettings;
65    use crate::index::NoIndex;
66
67    fn write(dir: &Path, rel: &str, text: &str) {
68        let p = dir.join(rel);
69        std::fs::create_dir_all(p.parent().unwrap()).unwrap();
70        std::fs::write(p, text).unwrap();
71    }
72
73    fn tempdir(tag: &str) -> PathBuf {
74        let dir = std::env::temp_dir().join(format!("prov-load-{tag}-{}", std::process::id()));
75        let _ = std::fs::remove_dir_all(&dir);
76        std::fs::create_dir_all(&dir).unwrap();
77        dir
78    }
79
80    #[test]
81    fn document_reads_full_metadata_for_a_workspace_relative_path() {
82        let dir = tempdir("document");
83        write(
84            &dir,
85            "notes/a.md",
86            "---\ntitle: A\nauthor: Ada\n---\nbody text\n",
87        );
88
89        let ws = Graph::new(StdFs, &dir, NoIndex, ReadSettings::default());
90        let doc = block_on(ws.document("notes/a.md")).unwrap();
91        let meta = fig::Value::from(&doc.meta);
92        assert_eq!(meta.get("title").and_then(fig::Value::as_str), Some("A"));
93        assert_eq!(meta.get("author").and_then(fig::Value::as_str), Some("Ada"));
94        assert_eq!(doc.body, "body text\n");
95    }
96
97    #[test]
98    fn document_surfaces_the_error_for_an_unreadable_path() {
99        let dir = tempdir("document-missing");
100        let ws = Graph::new(StdFs, &dir, NoIndex, ReadSettings::default());
101        assert!(block_on(ws.document("nope.md")).is_err());
102    }
103}