1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
//! Shared CLI helpers: color re-exports, parsing, state utilities.
use crate::core::{parser, types};
use std::path::Path;
// Re-export color system from colors.rs for backward compatibility.
// All callers that `use super::helpers::*` continue to work unchanged.
#[allow(unused_imports)]
pub(crate) use super::colors::{bold, color_enabled, dim, green, red, yellow, NO_COLOR};
/// Parse, validate, and expand recipes in a forjar config file.
pub(crate) fn parse_and_validate(file: &Path) -> Result<types::ForjarConfig, String> {
parser::parse_and_validate(file)
}
/// Refuse to report on a state directory that is not there.
///
/// [`discover_machines`] swallows the read error and answers "no machines", so
/// every command built on it used to treat a wrong `--state-dir`, a wiped
/// state, or a run before the first apply as an empty-but-healthy state and
/// exit 0 — `lock-validate` printing "All 0 lock files are valid" at a path
/// that does not exist. `lock-verify` and `lock-info` always rejected it; this
/// is that check, shared, so the rest can match them.
pub(crate) fn require_state_dir(state_dir: &Path) -> Result<(), String> {
if state_dir.exists() {
Ok(())
} else {
Err(format!(
"state directory not found: {}",
state_dir.display()
))
}
}
/// FJ-1270 / CB-2010: BLAKE3 sidecar verification failures for a state directory,
/// as `(machine, reason)` pairs. Empty means every lock file still hashes to the
/// `.b3` sidecar written beside it by `save_lock`.
///
/// This is the measurement the `lock-verify` / `lock-integrity` / `lock-validate` /
/// `lock-audit` commands exist to make. Every one of them used to skip it and print
/// a green result from the lock's own self-reported hash *fields* — which a tamperer
/// controls — so a rewritten body, a deleted sidecar and a deleted lock all passed.
///
/// Unlike [`crate::core::state::integrity::has_errors`] (used by `apply`, which
/// tolerates a missing sidecar so pre-FJ-1270 state dirs still converge), this
/// counts a missing sidecar as a failure: a command whose whole job is integrity has
/// no instrument without it, and an absent verifier is a NO-GO, never a pass.
pub(crate) fn sidecar_failures(state_dir: &Path) -> Vec<(String, String)> {
use crate::core::state::integrity;
integrity::verify_state_integrity(state_dir)
.iter()
.filter_map(|r| {
let reason = integrity::failure_reason(r)?;
Some((sidecar_machine_name(state_dir, r), reason))
})
.collect()
}
/// Machine a sidecar result belongs to — the lock's parent directory name, or
/// `<global>` for `state_dir/forjar.lock.yaml`.
fn sidecar_machine_name(
state_dir: &Path,
result: &crate::core::state::integrity::IntegrityResult,
) -> String {
crate::core::state::integrity::result_path(result)
.and_then(|p| p.parent())
.filter(|parent| *parent != state_dir)
.and_then(|p| p.file_name())
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| "<global>".to_string())
}
/// Discover machine names from a state directory by listing subdirectories that contain state.lock.yaml.
pub(crate) fn discover_machines(state_dir: &Path) -> Vec<String> {
let mut machines = Vec::new();
if let Ok(entries) = std::fs::read_dir(state_dir) {
for entry in entries.flatten() {
if entry.path().is_dir() {
let name = entry.file_name().to_string_lossy().to_string();
if entry.path().join("state.lock.yaml").exists() {
machines.push(name);
}
}
}
}
machines.sort();
machines
}