use std::path::Path;
use anyhow::Result;
use serde_json::Value;
use crate::builders;
use crate::config::{Config, compile_globs};
use crate::model::{Graph, GraphSet};
use crate::sources::{self, fs::SourceFile};
use crate::util::hash_bytes;
const LOCKFILE_IGNORE: &str = "drft.lock";
pub fn build_set(root: &Path, config: &Config) -> Result<GraphSet> {
let mut ignore = config.ignore_patterns().to_vec();
ignore.push(LOCKFILE_IGNORE.to_string());
let files = sources::fs::walk(root, &ignore)?;
let mut fs_graph = builders::fs::build(root, &files);
auto_hash(&mut fs_graph, &files);
let mut graphs = vec![fs_graph];
let texts: Vec<(String, String)> = files
.iter()
.filter_map(|f| {
f.bytes
.as_ref()
.and_then(|b| std::str::from_utf8(b).ok())
.map(|text| (f.path.clone(), text.to_string()))
})
.collect();
for (name, graph) in &config.graphs {
let files = compile_globs(&graph.files)?;
match graph.parser.as_str() {
"markdown" => graphs.push(builders::markdown::build(name, &texts, files)),
"frontmatter" => graphs.push(builders::frontmatter::build(
name,
&texts,
files,
graph.keys.clone(),
)),
other => unreachable!("unvalidated parser \"{other}\""),
}
}
Ok(GraphSet::new(graphs))
}
fn auto_hash(graph: &mut Graph, files: &[SourceFile]) {
for file in files {
if let Some(bytes) = &file.bytes
&& let Some(node) = graph.nodes.get_mut(&file.path)
{
node.metadata
.insert("hash".into(), Value::String(hash_bytes(bytes)));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn fs_graph_has_typed_hashed_nodes() {
let dir = TempDir::new().unwrap();
fs::write(dir.path().join("drft.toml"), "").unwrap();
fs::write(dir.path().join("index.md"), "# Index").unwrap();
let config = Config::defaults();
let set = build_set(dir.path(), &config).unwrap();
let fs_graph = &set.graphs[0];
assert_eq!(fs_graph.label.as_deref(), Some("fs"));
let node = &fs_graph.nodes["index.md"];
assert_eq!(node.metadata["type"], Value::String("file".into()));
assert!(
node.metadata["hash"].as_str().unwrap().starts_with("b3:"),
"node should be auto-hashed"
);
}
#[cfg(unix)]
#[test]
fn escaping_symlink_node_has_no_hash() {
let outer = TempDir::new().unwrap();
let root = outer.path().join("project");
fs::create_dir(&root).unwrap();
fs::write(root.join("drft.toml"), "").unwrap();
fs::write(outer.path().join("secret.md"), "secret").unwrap();
std::os::unix::fs::symlink(outer.path().join("secret.md"), root.join("trap.md")).unwrap();
let set = build_set(&root, &Config::defaults()).unwrap();
let trap = &set.graphs[0].nodes["trap.md"];
assert_eq!(trap.metadata["type"], Value::String("symlink".into()));
assert!(
trap.metadata.get("hash").is_none(),
"escaping symlink must not be hashed"
);
}
#[cfg(unix)]
#[test]
fn inroot_symlink_node_is_not_hashed() {
let dir = TempDir::new().unwrap();
fs::write(dir.path().join("drft.toml"), "").unwrap();
fs::write(dir.path().join("real.md"), "content").unwrap();
std::os::unix::fs::symlink(dir.path().join("real.md"), dir.path().join("alias.md"))
.unwrap();
let set = build_set(dir.path(), &Config::defaults()).unwrap();
let nodes = &set.graphs[0].nodes;
assert_eq!(
nodes["alias.md"].metadata["type"],
Value::String("symlink".into())
);
assert!(
nodes["alias.md"].metadata.get("hash").is_none(),
"in-root symlink must not be hashed"
);
assert!(
nodes["real.md"].metadata["hash"]
.as_str()
.unwrap()
.starts_with("b3:"),
"the real target is still hashed"
);
}
}