use std::cell::RefCell;
use std::io;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use harn_vm::bytecode_cache;
use crate::package::CheckConfig;
use super::check_cmd::{CheckDiagnostic, CheckFileReport, CheckFileStatus, CheckSpan};
use super::driver::CheckedFile;
const RESULT_CACHE_SCHEMA: u32 = 1;
pub(crate) const RESULT_CACHE_ENV: &str = "HARN_CHECK_RESULT_CACHE";
const CHECK_FINGERPRINT: &str = env!("HARN_CHECK_FINGERPRINT");
pub(super) fn enabled() -> bool {
if !bytecode_cache::cache_enabled() {
return false;
}
match std::env::var(RESULT_CACHE_ENV).ok().as_deref() {
Some(value) => !matches!(
value.to_ascii_lowercase().as_str(),
"0" | "false" | "no" | "off"
),
None => true,
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(super) enum Probe {
Exists { path: PathBuf, existed: bool },
IsDir { path: PathBuf, was_dir: bool },
ReadFile {
path: PathBuf,
digest: Option<String>,
},
ResolveTarget {
anchor: PathBuf,
target: String,
candidates: Vec<PathBuf>,
},
WalkUnique {
root: PathBuf,
basename: String,
result: Option<PathBuf>,
},
}
thread_local! {
static PROBE_LOG: RefCell<Option<Vec<Probe>>> = const { RefCell::new(None) };
}
pub(super) fn with_probe_recording<T>(active: bool, f: impl FnOnce() -> T) -> (T, Vec<Probe>) {
if !active {
return (f(), Vec::new());
}
PROBE_LOG.with(|log| *log.borrow_mut() = Some(Vec::new()));
let value = f();
let probes = PROBE_LOG.with(|log| log.borrow_mut().take().unwrap_or_default());
(value, probes)
}
fn record(probe: Probe) {
PROBE_LOG.with(|log| {
if let Some(probes) = log.borrow_mut().as_mut() {
probes.push(probe);
}
});
}
pub(super) fn probe_exists(path: &Path) -> bool {
let existed = path.exists();
record(Probe::Exists {
path: path.to_path_buf(),
existed,
});
existed
}
pub(super) fn probe_is_dir(path: &Path) -> bool {
let was_dir = path.is_dir();
record(Probe::IsDir {
path: path.to_path_buf(),
was_dir,
});
was_dir
}
pub(super) fn probe_read_to_string(path: &Path) -> io::Result<String> {
let result = std::fs::read_to_string(path);
record(Probe::ReadFile {
path: path.to_path_buf(),
digest: result.as_deref().ok().map(sha256_hex),
});
result
}
pub(super) fn record_resolve_target(anchor: &Path, target: &str, candidates: &[PathBuf]) {
record(Probe::ResolveTarget {
anchor: anchor.to_path_buf(),
target: target.to_string(),
candidates: candidates.to_vec(),
});
}
pub(super) fn record_walk_unique(root: &Path, basename: &str, result: Option<&Path>) {
record(Probe::WalkUnique {
root: root.to_path_buf(),
basename: basename.to_string(),
result: result.map(Path::to_path_buf),
});
}
fn probe_still_valid(probe: &Probe, config: &CheckConfig) -> bool {
match probe {
Probe::Exists { path, existed } => path.exists() == *existed,
Probe::IsDir { path, was_dir } => path.is_dir() == *was_dir,
Probe::ReadFile { path, digest } => {
std::fs::read_to_string(path).ok().map(|c| sha256_hex(&c)) == *digest
}
Probe::ResolveTarget {
anchor,
target,
candidates,
} => super::preflight::resolve_preflight_target(anchor, target, config) == *candidates,
Probe::WalkUnique {
root,
basename,
result,
} => super::preflight::find_unique_basename(root, basename) == *result,
}
}
pub(super) fn result_cache_key(
file: &Path,
path_str: &str,
source: &str,
config: &CheckConfig,
check_invariants: bool,
lint_exemptions: &[String],
) -> [u8; 32] {
let base = bytecode_cache::CacheKey::from_source(file, source);
let mut hasher = Sha256::new();
let mut fold = |label: &str, value: &[u8]| {
hasher.update(label.as_bytes());
hasher.update(b"\0");
hasher.update(value);
hasher.update(b"\0");
};
fold("schema", &RESULT_CACHE_SCHEMA.to_le_bytes());
fold(
"check-schema",
&super::check_cmd::CHECK_SCHEMA_VERSION.to_le_bytes(),
);
fold("source-hash", &base.source_hash);
fold("import-graph-hash", &base.import_graph_hash);
fold("harn-version", base.harn_version.as_bytes());
fold("compiler-tag", &[base.compiler_tag]);
fold("check-fingerprint", CHECK_FINGERPRINT.as_bytes());
fold("path", path_str.as_bytes());
fold("invariants", &[u8::from(check_invariants)]);
for name in lint_exemptions {
fold("lint-exemption", name.as_bytes());
}
fold_config(&mut fold, config);
hasher.finalize().into()
}
fn fold_config(fold: &mut impl FnMut(&str, &[u8]), config: &CheckConfig) {
let CheckConfig {
strict,
strict_types,
disable_rules,
host_capabilities,
host_capabilities_path,
bundle_root,
preflight_severity,
preflight_allow,
} = config;
fold("strict", &[u8::from(*strict)]);
fold("strict-types", &[u8::from(*strict_types)]);
let mut disable_rules: Vec<&String> = disable_rules.iter().collect();
disable_rules.sort();
for rule in disable_rules {
fold("disable-rule", rule.as_bytes());
}
let mut caps: Vec<(&String, &Vec<String>)> = host_capabilities.iter().collect();
caps.sort_by_key(|(name, _)| *name);
for (name, ops) in caps {
fold("host-capability", name.as_bytes());
let mut ops: Vec<&String> = ops.iter().collect();
ops.sort();
for op in ops {
fold("host-capability-op", op.as_bytes());
}
}
if let Some(path) = host_capabilities_path {
fold("host-capabilities-path", path.as_bytes());
let content = std::fs::read_to_string(path).unwrap_or_default();
fold("host-capabilities-content", content.as_bytes());
}
if let Some(root) = bundle_root {
fold("bundle-root", root.as_bytes());
}
if let Some(severity) = preflight_severity {
fold("preflight-severity", severity.as_bytes());
}
let mut allow: Vec<&String> = preflight_allow.iter().collect();
allow.sort();
for tag in allow {
fold("preflight-allow", tag.as_bytes());
}
}
#[derive(Debug, Serialize, Deserialize)]
struct CachedCheckResult {
schema: u32,
status: CachedStatus,
diagnostics: Vec<CachedDiagnostic>,
stdout_text: String,
stderr_text: String,
probes: Vec<Probe>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
enum CachedStatus {
Ok,
Warning,
Error,
}
#[derive(Debug, Serialize, Deserialize)]
struct CachedDiagnostic {
source: String,
severity: String,
code: Option<String>,
message: String,
span: Option<(usize, usize)>,
help: Option<String>,
}
fn intern_diag_str(value: &str) -> Option<&'static str> {
Some(match value {
"type" => "type",
"compile" => "compile",
"lint" => "lint",
"preflight" => "preflight",
"invariant" => "invariant",
"io" => "io",
"analysis" => "analysis",
"lexer" => "lexer",
"parser" => "parser",
"error" => "error",
"warning" => "warning",
"info" => "info",
_ => return None,
})
}
fn cache_path(key: &[u8; 32]) -> PathBuf {
bytecode_cache::cache_dir()
.join("check")
.join(format!("{}.harncheck", hex(key)))
}
pub(super) fn load(
key: &[u8; 32],
path_str: &str,
config: &CheckConfig,
want_text: bool,
) -> Option<CheckedFile> {
if !enabled() {
return None;
}
let bytes = std::fs::read(cache_path(key)).ok()?;
let cached: CachedCheckResult = serde_json::from_slice(&bytes).ok()?;
if cached.schema != RESULT_CACHE_SCHEMA {
return None;
}
for probe in &cached.probes {
if !probe_still_valid(probe, config) {
return None;
}
}
let mut diagnostics = Vec::with_capacity(cached.diagnostics.len());
for diag in cached.diagnostics {
diagnostics.push(CheckDiagnostic {
source: intern_diag_str(&diag.source)?,
severity: intern_diag_str(&diag.severity)?,
code: diag.code,
message: diag.message,
span: diag.span.map(|(start, end)| CheckSpan { start, end }),
help: diag.help,
});
}
let status = match cached.status {
CachedStatus::Ok => CheckFileStatus::Ok,
CachedStatus::Warning => CheckFileStatus::Warning,
CachedStatus::Error => CheckFileStatus::Error,
};
let mut text = super::check_cmd::CheckTextOutput::default();
if want_text {
text.stdout = cached.stdout_text;
text.stderr = cached.stderr_text;
}
Some(CheckedFile {
report: CheckFileReport {
path: path_str.to_string(),
status,
diagnostics,
},
strict: config.strict,
text,
})
}
pub(super) fn store(key: &[u8; 32], checked: &CheckedFile, probes: Vec<Probe>) {
if !enabled() {
return;
}
let cached = CachedCheckResult {
schema: RESULT_CACHE_SCHEMA,
status: match checked.report.status {
CheckFileStatus::Ok => CachedStatus::Ok,
CheckFileStatus::Warning => CachedStatus::Warning,
CheckFileStatus::Error => CachedStatus::Error,
},
diagnostics: checked
.report
.diagnostics
.iter()
.map(|diag| CachedDiagnostic {
source: diag.source.to_string(),
severity: diag.severity.to_string(),
code: diag.code.clone(),
message: diag.message.clone(),
span: diag.span.map(|span| (span.start, span.end)),
help: diag.help.clone(),
})
.collect(),
stdout_text: checked.text.stdout.clone(),
stderr_text: checked.text.stderr.clone(),
probes,
};
let path = cache_path(key);
let Some(parent) = path.parent() else {
return;
};
if std::fs::create_dir_all(parent).is_err() {
return;
}
let Ok(bytes) = serde_json::to_vec(&cached) else {
return;
};
let tmp = path.with_extension(format!("tmp{}", std::process::id()));
if std::fs::write(&tmp, bytes).is_ok() {
let _ = std::fs::rename(&tmp, &path);
}
}
fn sha256_hex(content: &str) -> String {
hex(&Sha256::digest(content.as_bytes()).into())
}
fn hex(bytes: &[u8; 32]) -> String {
let mut out = String::with_capacity(64);
for byte in bytes {
out.push_str(&format!("{byte:02x}"));
}
out
}