1use std::path::Path;
11
12use anyhow::Result;
13use serde_json::Value;
14
15use crate::builders;
16use crate::config::{Config, compile_globs};
17use crate::model::{Graph, GraphSet};
18use crate::sources::{self, fs::SourceFile};
19use crate::util::hash_bytes;
20
21const LOCKFILE_IGNORE: &str = "drft.lock";
25
26pub fn build_set(root: &Path, config: &Config) -> Result<GraphSet> {
32 let mut ignore = config.ignore_patterns().to_vec();
33 ignore.push(LOCKFILE_IGNORE.to_string());
34 let files = sources::fs::walk(root, &ignore)?;
35
36 let mut fs_graph = builders::fs::build(root, &files);
37 auto_hash(&mut fs_graph, &files);
38
39 let mut graphs = vec![fs_graph];
40
41 let texts: Vec<(String, String)> = files
44 .iter()
45 .filter_map(|f| {
46 f.bytes
47 .as_ref()
48 .and_then(|b| std::str::from_utf8(b).ok())
49 .map(|text| (f.path.clone(), text.to_string()))
50 })
51 .collect();
52
53 for (name, graph) in &config.graphs {
54 let files = compile_globs(&graph.files)?;
55 match graph.parser.as_str() {
56 "markdown" => graphs.push(builders::markdown::build(name, &texts, files)),
57 "frontmatter" => graphs.push(builders::frontmatter::build(
58 name,
59 &texts,
60 files,
61 graph.keys.clone(),
62 )),
63 other => unreachable!("unvalidated parser \"{other}\""),
66 }
67 }
68
69 Ok(GraphSet::new(graphs))
70}
71
72fn auto_hash(graph: &mut Graph, files: &[SourceFile]) {
75 for file in files {
76 if let Some(bytes) = &file.bytes
77 && let Some(node) = graph.nodes.get_mut(&file.path)
78 {
79 node.metadata
80 .insert("hash".into(), Value::String(hash_bytes(bytes)));
81 }
82 }
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88 use std::fs;
89 use tempfile::TempDir;
90
91 #[test]
92 fn fs_graph_has_typed_hashed_nodes() {
93 let dir = TempDir::new().unwrap();
94 fs::write(dir.path().join("drft.toml"), "").unwrap();
95 fs::write(dir.path().join("index.md"), "# Index").unwrap();
96 let config = Config::defaults();
97
98 let set = build_set(dir.path(), &config).unwrap();
99 let fs_graph = &set.graphs[0];
101 assert_eq!(fs_graph.label.as_deref(), Some("fs"));
102
103 let node = &fs_graph.nodes["index.md"];
104 assert_eq!(node.metadata["type"], Value::String("file".into()));
105 assert!(
106 node.metadata["hash"].as_str().unwrap().starts_with("b3:"),
107 "node should be auto-hashed"
108 );
109 }
110
111 #[cfg(unix)]
112 #[test]
113 fn escaping_symlink_node_has_no_hash() {
114 let outer = TempDir::new().unwrap();
115 let root = outer.path().join("project");
116 fs::create_dir(&root).unwrap();
117 fs::write(root.join("drft.toml"), "").unwrap();
118 fs::write(outer.path().join("secret.md"), "secret").unwrap();
119 std::os::unix::fs::symlink(outer.path().join("secret.md"), root.join("trap.md")).unwrap();
120
121 let set = build_set(&root, &Config::defaults()).unwrap();
122 let trap = &set.graphs[0].nodes["trap.md"];
123 assert_eq!(trap.metadata["type"], Value::String("symlink".into()));
124 assert!(
125 trap.metadata.get("hash").is_none(),
126 "escaping symlink must not be hashed"
127 );
128 }
129
130 #[cfg(unix)]
131 #[test]
132 fn inroot_symlink_node_is_not_hashed() {
133 let dir = TempDir::new().unwrap();
136 fs::write(dir.path().join("drft.toml"), "").unwrap();
137 fs::write(dir.path().join("real.md"), "content").unwrap();
138 std::os::unix::fs::symlink(dir.path().join("real.md"), dir.path().join("alias.md"))
139 .unwrap();
140
141 let set = build_set(dir.path(), &Config::defaults()).unwrap();
142 let nodes = &set.graphs[0].nodes;
143 assert_eq!(
144 nodes["alias.md"].metadata["type"],
145 Value::String("symlink".into())
146 );
147 assert!(
148 nodes["alias.md"].metadata.get("hash").is_none(),
149 "in-root symlink must not be hashed"
150 );
151 assert!(
152 nodes["real.md"].metadata["hash"]
153 .as_str()
154 .unwrap()
155 .starts_with("b3:"),
156 "the real target is still hashed"
157 );
158 }
159}