use std::collections::HashMap;
use std::error::Error as StdError;
use std::path::{Path, PathBuf};
use quillmark_core::{Diagnostic, FileTreeNode, Quill, QuillIgnore, RenderError, Severity};
pub fn quill_from_path<P: AsRef<Path>>(path: P) -> Result<Quill, RenderError> {
let tree = load_tree_from_path(path.as_ref()).map_err(|e| {
RenderError::from_diag(
Diagnostic::new(Severity::Error, format!("Failed to load quill: {}", e))
.with_code("quill::load_failed".to_string()),
)
})?;
Quill::from_tree(tree).map_err(RenderError::new)
}
fn load_tree_from_path(path: &Path) -> Result<FileTreeNode, Box<dyn StdError + Send + Sync>> {
use std::fs;
let quillignore_path = path.join(".quillignore");
let ignore = if quillignore_path.exists() {
let content = fs::read_to_string(&quillignore_path)
.map_err(|e| format!("Failed to read .quillignore: {}", e))?;
QuillIgnore::from_content(&content)
} else {
QuillIgnore::default()
};
load_dir(path, path, &ignore)
}
const MAX_QUILL_FILE_SIZE: u64 = 50 * 1024 * 1024;
fn load_dir(
current_dir: &Path,
base_dir: &Path,
ignore: &QuillIgnore,
) -> Result<FileTreeNode, Box<dyn StdError + Send + Sync>> {
use std::fs;
if !current_dir.exists() {
return Ok(FileTreeNode::Directory {
files: HashMap::new(),
});
}
let mut files = HashMap::new();
for entry in fs::read_dir(current_dir)? {
let entry = entry?;
let path = entry.path();
let relative_path: PathBuf = path
.strip_prefix(base_dir)
.map_err(|e| format!("Failed to get relative path: {}", e))?
.to_path_buf();
if ignore.is_ignored(&relative_path) {
continue;
}
let filename = path
.file_name()
.and_then(|n| n.to_str())
.ok_or_else(|| format!("Invalid filename: {}", path.display()))?
.to_string();
let meta = std::fs::symlink_metadata(&path)
.map_err(|e| format!("Failed to stat '{}': {}", path.display(), e))?;
let file_type = meta.file_type();
if file_type.is_symlink() {
continue;
} else if file_type.is_file() {
if meta.len() > MAX_QUILL_FILE_SIZE {
return Err(format!(
"File '{}' exceeds the {} MiB quill file-size limit",
path.display(),
MAX_QUILL_FILE_SIZE / (1024 * 1024)
)
.into());
}
let contents = fs::read(&path)
.map_err(|e| format!("Failed to read file '{}': {}", path.display(), e))?;
files.insert(filename, FileTreeNode::File { contents });
} else if file_type.is_dir() {
let subdir_tree = load_dir(&path, base_dir, ignore)?;
files.insert(filename, subdir_tree);
}
}
Ok(FileTreeNode::Directory { files })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[cfg(unix)]
fn load_dir_skips_symlinks() {
use std::io::Write;
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
let mut f = std::fs::File::create(root.join("real.txt")).unwrap();
f.write_all(b"ok").unwrap();
let secret = dir.path().join("secret.txt");
std::fs::write(&secret, b"TOPSECRET").unwrap();
std::os::unix::fs::symlink(&secret, root.join("leak.txt")).unwrap();
let tree = load_tree_from_path(root).unwrap();
assert!(tree.get_file("leak.txt").is_none());
assert_eq!(tree.get_file("real.txt"), Some(&b"ok"[..]));
}
}