feat(fs): sha256_file, write_etc_atomic, probe_config_files — closes Plan §6.4#9
Merged
Conversation
M1-W1-G — closes Plan §6.4. pearlite-fs is the third pure crate of M1 Week 1; primitives the engine and adapters share for hashing files, atomically writing /etc with mode/owner/group preserved, and probing declared ConfigEntry targets into a ConfigFileInventory. Public API: - pub fn sha256_file(p: &Path) -> Result<[u8; 32], FsError> - pub fn write_etc_atomic(target, content, mode, owner, group) -> Result<(), FsError> - pub fn probe_config_files(targets: &[ConfigEntry]) -> ConfigFileInventory ConfigEntry / ConfigFileInventory / ConfigFileMeta are re-exported read-only from pearlite-schema (PR #3). Internal modules: hash.rs, atomic.rs, chown.rs, inventory.rs, errors.rs. Implementation notes: - hash.rs: 64 KiB chunked read; constant memory regardless of file size. Heap-allocated buffer (clippy::large_stack_arrays denies the on-stack version). - atomic.rs: PRD §7.4 recipe — temp + chmod + chown BEFORE rename so the file appears with the right ownership atomically — followed by fsync of the file's data, atomic rename, fsync of the parent directory. - chown.rs: nix::unistd::User/Group for name<->id resolution; libc chown via nix; Permissions::from_mode for chmod. Plan §6.4 hard rule: no std::process::Command for ownership ops. - inventory.rs: stat each target, hash, encode hex, look up uid/gid -> name. Missing targets are silently absent — the diff engine treats absence as drift in its own pass. Workspace deps added: - nix = { version = "0.31", features = ["user", "fs"] } - hex = "0.4" Tests: - hash: known_file_known_digest (SHA-256("abc")), empty_file_known_digest, missing_file_yields_io_error. - chown: unknown_user_yields_typed_error, unknown_group_yields_typed_error. - atomic: write_then_read_round_trips, mode_preserved_through_temp, chown_to_self_succeeds (chowns to the running user since CI lacks root), unknown_owner_yields_typed_error. - inventory: missing_target_yields_none_in_inventory, present_target_records_sha256_and_mode, empty_target_list_yields_empty_inventory. - 2 fork-based tests stubbed as #[ignore]: crash_after_write_before_rename and rename_atomic_visible_to_concurrent_reader. These need rusty-fork machinery and land in M1 W2 with engine integration tests. Plan §6.4 "Done when" status: - sha256 implementation is constant-memory (chunked I/O). ✓ - Atomic write tested via simulated kill-9 in unit tests using forked processes. PARTIAL — ignored stubs in place; full test machinery arrives in M1 W2. - No std::process::Command — chown happens through libc directly via nix. ✓ - pearlite-audit reports no §13 violations. ✓ Verification: - cargo test -p pearlite-fs (12 passed; 0 failed; 2 ignored) - cargo clippy --workspace --all-targets -- -D warnings (zero warnings) - cargo fmt --all --check - scripts/ci/check-spdx.sh - pearlite-audit check . (1 check, 0 violations) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cargo-deny treats `pearlite-schema = { path = "..." }` as a wildcard
dep because no version constraint is present. The workspace lints have
`wildcards = "deny"`. Pinning to "0.1" matches workspace.package.version
(0.1.0) and the workspace SemVer baseline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
UnbreakableMJ
added a commit
that referenced
this pull request
Apr 27, 2026
… §6.5 (#10) * feat(nickel): land NickelEvaluator trait, LiveNickel, MockNickel, load_host Plan §6.5 — first M1 Week 2 adapter probe path. The thinnest possible shim around the `nickel` binary: argv-array subprocess invocation, stdout capture, delegation to pearlite_schema::from_resolved_toml. No Nickel parsing in Rust per Plan §6.5 hard rule. Public surface (re-exported from lib.rs): - pub trait NickelEvaluator { fn evaluate(&self, &Path) -> Result<String, NickelError>; } - pub struct LiveNickel { binary: PathBuf } - pub struct MockNickel — feature-gated behind test-mocks, in-memory canned-output map BTreeMap<PathBuf, String>. - pub fn load_host(host_file: &Path, eval: &dyn NickelEvaluator) -> Result<DeclaredState, NickelError>. - pub enum NickelError — NotInPath (hints `paru -S nickel-lang`), Io, EvaluationFailed { code, stderr }, NotUtf8, Schema (transparent to pearlite_schema::SchemaError), MockMissing. LiveNickel: - Default constructor resolves `nickel` from $PATH. - with_binary() lets tests inject a known-good path. - Spawn translates ErrorKind::NotFound to NickelError::NotInPath with a runnable hint. - Captures stderr verbatim; surfaces with exit code on non-zero. CI: - Add nickel-lang to taiki-e/install-action's tool list in T2 Unit so the live tests actually exercise nickel rather than silent-skipping. Tests (5 passing locally; live tests silent-skip when nickel isn't on PATH — CI installs it so they exercise there): - mock: canned_output_round_trips_to_declared_state. - mock: missing_path_yields_mock_missing_error. - live: nickel_not_in_path_error_class — pointed at a bogus path. - live: version_probe_succeeds — nickel --version contains "nickel". - live: minimal_host_evaluates — fixture host_minimal.ncl evaluates to TOML that parses as DeclaredState. Fixture: fixtures/nickel/host_minimal.ncl mirrors the schema fixture. Cargo.toml: pearlite-schema dep pinned to "0.1" per the cargo-deny wildcard rule (PR #9 precedent). New `test-mocks` feature. Verification: - cargo test -p pearlite-nickel (5 passed; 0 failed) - cargo clippy --workspace --all-targets -- -D warnings (zero warnings) - cargo fmt --all --check - scripts/ci/check-spdx.sh - pearlite-audit check . (1 check, 0 violations) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(ci): install nickel-lang-cli, not nickel-lang The library crate `nickel-lang` has no binaries; the CLI binary lives in the `nickel-lang-cli` crate. taiki-e/install-action's fallback to cargo-binstall correctly reported "no binaries specified" and failed the T2 Unit job. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(ci): replace extractions/setup-just with taiki-e/install-action extractions/setup-just@v3 hit "no release for just matching version specifier" on every recent PR run — the action's default version resolution is broken. taiki-e/install-action is already used in every job for cargo tooling; adding `just` to its tool list is the consistent fix and avoids the second action entirely. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
M1-W1-G — closes Plan §6.4. pearlite-fs is the third pure crate of M1 Week 1. Public API: sha256_file (chunked, constant memory), write_etc_atomic (PRD §7.4 recipe with chmod/chown before rename), probe_config_files. Workspace deps added: nix 0.31, hex 0.4. 12 tests passing; 2 fork-based crash tests stubbed as #[ignore] for M1 W2. clippy/fmt/SPDX/audit clean.
🤖 Generated with Claude Code