1use crate::{Diagnostic, Document, Position, Report, TokenCounter, lint, parse};
2use std::{
3 fs,
4 path::{Path, PathBuf},
5};
6
7#[derive(Debug)]
9#[cfg_attr(feature = "json", derive(serde::Serialize))]
10pub struct FileReport {
11 pub file: PathBuf,
12 pub report: Report,
13}
14
15pub fn parse_file(path: impl AsRef<Path>) -> Result<Document, Diagnostic> {
17 parse(&read_source(path.as_ref())?)
18}
19
20fn read_source(path: &Path) -> Result<String, Diagnostic> {
21 use std::io::Read;
22 let failure = |e: std::io::Error| {
23 Diagnostic::new(
24 "io",
25 format!("{}: {e}", path.display()),
26 Position::default(),
27 )
28 };
29 let file = fs::File::open(path).map_err(failure)?;
30 let mut source = String::new();
31 file.take(4 * 1024 * 1024 + 1)
32 .read_to_string(&mut source)
33 .map_err(failure)?;
34 Ok(source)
35}
36
37pub fn check_path(
41 path: impl AsRef<Path>,
42 counter: &impl TokenCounter,
43) -> Result<Vec<FileReport>, String> {
44 let paths = source_paths(path.as_ref())?;
45 Ok(paths
46 .into_iter()
47 .map(|file| {
48 let report = match parse_file(&file) {
49 Ok(doc) => lint(&doc, counter),
50 Err(error) => Report {
51 diagnostics: vec![error],
52 measurements: Vec::new(),
53 },
54 };
55 FileReport { file, report }
56 })
57 .collect())
58}
59
60fn source_paths(path: &Path) -> Result<Vec<PathBuf>, String> {
61 fn visit(path: &Path, files: &mut Vec<PathBuf>, depth: usize) -> Result<(), String> {
62 if depth > 128 {
63 return Err("Directory nesting exceeds 128 levels".into());
64 }
65 let metadata =
66 fs::symlink_metadata(path).map_err(|e| format!("{}: {e}", path.display()))?;
67 if metadata.file_type().is_symlink() {
68 return Ok(());
69 }
70 if metadata.is_file() {
71 if path.extension().is_some_and(|e| e == "htmlp") {
72 files.push(path.to_path_buf());
73 }
74 } else if metadata.is_dir() {
75 let entries = fs::read_dir(path).map_err(|e| e.to_string())?;
76 for entry in entries {
77 let entry = entry.map_err(|e| e.to_string())?;
78 if matches!(
79 entry.file_name().to_str(),
80 Some(".git" | "node_modules" | "target" | "dist" | "vendor")
81 ) {
82 continue;
83 }
84 visit(&entry.path(), files, depth + 1)?;
85 }
86 }
87 Ok(())
88 }
89 let mut paths = Vec::new();
90 visit(path, &mut paths, 0)?;
91 paths.sort();
92 if paths.is_empty() {
93 return Err(format!("No .htmlp files found at {}", path.display()));
94 }
95 Ok(paths)
96}
97
98pub fn sign_path(path: impl AsRef<Path>) -> Result<Vec<PathBuf>, String> {
103 let mut changes = Vec::new();
104 for file in source_paths(path.as_ref())? {
105 let before = read_source(&file).map_err(|e| format!("{}: {e}", file.display()))?;
106 let after = crate::sign_source(&before).map_err(|e| format!("{}: {e}", file.display()))?;
107 if before != after {
108 changes.push((file, before, after));
109 }
110 }
111 let mut changed = Vec::new();
112 for (file, before, after) in changes {
113 replace_source(&file, &before, &after).map_err(|e| format!("{}: {e}", file.display()))?;
114 changed.push(file);
115 }
116 Ok(changed)
117}
118
119fn replace_source(path: &Path, before: &str, after: &str) -> std::io::Result<()> {
120 use std::io::{Error, Write};
121 use std::sync::atomic::{AtomicU64, Ordering};
122 static NEXT: AtomicU64 = AtomicU64::new(0);
123 let metadata = fs::symlink_metadata(path)?;
124 if !metadata.is_file() || metadata.file_type().is_symlink() {
125 return Err(Error::other("Source is no longer a regular file"));
126 }
127 let temporary = path.with_file_name(format!(
128 ".htmlp-sign-{}-{}.tmp",
129 std::process::id(),
130 NEXT.fetch_add(1, Ordering::Relaxed)
131 ));
132 let mut output = fs::OpenOptions::new()
133 .write(true)
134 .create_new(true)
135 .open(&temporary)?;
136 let result = (|| {
137 output.write_all(after.as_bytes())?;
138 output.set_permissions(metadata.permissions())?;
139 output.sync_all()?;
140 drop(output);
141 if read_source(path).map_err(Error::other)? != before {
142 return Err(Error::other(
143 "Source changed while signing; rerun htmlp sign",
144 ));
145 }
146 fs::rename(&temporary, path)
147 })();
148 if result.is_err() {
149 let _ = fs::remove_file(&temporary);
150 }
151 result
152}