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(name, &texts, files)),
58 other => unreachable!("unvalidated parser \"{other}\""),
61 }
62 }
63
64 Ok(GraphSet::new(graphs))
65}
66
67fn auto_hash(graph: &mut Graph, files: &[SourceFile]) {
70 for file in files {
71 if let Some(bytes) = &file.bytes
72 && let Some(node) = graph.nodes.get_mut(&file.path)
73 {
74 node.metadata
75 .insert("hash".into(), Value::String(hash_bytes(bytes)));
76 }
77 }
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83 use std::fs;
84 use tempfile::TempDir;
85
86 #[test]
87 fn fs_graph_has_typed_hashed_nodes() {
88 let dir = TempDir::new().unwrap();
89 fs::write(dir.path().join("drft.toml"), "").unwrap();
90 fs::write(dir.path().join("index.md"), "# Index").unwrap();
91 let config = Config::defaults();
92
93 let set = build_set(dir.path(), &config).unwrap();
94 let fs_graph = &set.graphs[0];
96 assert_eq!(fs_graph.label.as_deref(), Some("fs"));
97
98 let node = &fs_graph.nodes["index.md"];
99 assert_eq!(node.metadata["type"], Value::String("file".into()));
100 assert!(
101 node.metadata["hash"].as_str().unwrap().starts_with("b3:"),
102 "node should be auto-hashed"
103 );
104 }
105
106 #[cfg(unix)]
107 #[test]
108 fn escaping_symlink_node_has_no_hash() {
109 let outer = TempDir::new().unwrap();
110 let root = outer.path().join("project");
111 fs::create_dir(&root).unwrap();
112 fs::write(root.join("drft.toml"), "").unwrap();
113 fs::write(outer.path().join("secret.md"), "secret").unwrap();
114 std::os::unix::fs::symlink(outer.path().join("secret.md"), root.join("trap.md")).unwrap();
115
116 let set = build_set(&root, &Config::defaults()).unwrap();
117 let trap = &set.graphs[0].nodes["trap.md"];
118 assert_eq!(trap.metadata["type"], Value::String("symlink".into()));
119 assert!(
120 trap.metadata.get("hash").is_none(),
121 "escaping symlink must not be hashed"
122 );
123 }
124
125 #[cfg(unix)]
126 #[test]
127 fn inroot_symlink_node_is_not_hashed() {
128 let dir = TempDir::new().unwrap();
131 fs::write(dir.path().join("drft.toml"), "").unwrap();
132 fs::write(dir.path().join("real.md"), "content").unwrap();
133 std::os::unix::fs::symlink(dir.path().join("real.md"), dir.path().join("alias.md"))
134 .unwrap();
135
136 let set = build_set(dir.path(), &Config::defaults()).unwrap();
137 let nodes = &set.graphs[0].nodes;
138 assert_eq!(
139 nodes["alias.md"].metadata["type"],
140 Value::String("symlink".into())
141 );
142 assert!(
143 nodes["alias.md"].metadata.get("hash").is_none(),
144 "in-root symlink must not be hashed"
145 );
146 assert!(
147 nodes["real.md"].metadata["hash"]
148 .as_str()
149 .unwrap()
150 .starts_with("b3:"),
151 "the real target is still hashed"
152 );
153 }
154}