From 50ce2860c942d83a375cccfcd220c9a2b1baf86f Mon Sep 17 00:00:00 2001 From: LGUG2Z Date: Sat, 31 Oct 2026 08:40:11 -0700 Subject: [PATCH] feat(nixgen): write the hash of the schema as the first line of the output Consumers that check whether generated options are older than their schema (komorebi-for-kde's `schema-fresh`) compare a `# schema-sha256: ` first line with the hash of schema.json. That line was added by hand in the consumer's justfile with `printf` and `nix hash file`, so the tool's own output could never pass the check. Write the line from the tool. `schema_hash_header` returns it for the raw bytes of the schema file, in lowercase hex, which is the digest `nix hash file --type sha256 --base16` prints. The binary puts it before the generated code; the library functions that return generated code are unchanged, so library users get no new line unless they ask for it. Adds the sha2 dependency. Verified: cargo test passes (49, including two known SHA-256 vectors). On the komorebi-for-kde schema.json the first line equals `nix hash file --type sha256 --base16 schema.json`. --- Cargo.lock | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/lib.rs | 29 +++++++++++++++++++++ src/main.rs | 10 ++++++-- 4 files changed, 110 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c99caf58..442a5731 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -88,6 +88,15 @@ version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -168,6 +177,35 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "dyn-clone" version = "1.0.20" @@ -194,6 +232,16 @@ dependencies = [ "once_cell", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "gimli" version = "0.32.3" @@ -375,6 +423,7 @@ dependencies = [ "schemars", "serde", "serde_json_lenient", + "sha2", "thiserror", ] @@ -456,6 +505,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -562,6 +622,12 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + [[package]] name = "unicode-ident" version = "1.0.22" @@ -580,6 +646,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "windows-link" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index 01f031cb..88409183 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,4 +18,5 @@ color-eyre = "0.6" schemars = "1.1" serde = { version = "1", features = ["derive"] } serde_json = { package = "serde_json_lenient", version = "0.2" } +sha2 = "0.10" thiserror = "2" diff --git a/src/lib.rs b/src/lib.rs index 65c5a1b6..064bfb76 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -32,6 +32,22 @@ pub trait SchemaExt: JsonSchema { // Implement SchemaExt for all types that implement JsonSchema impl SchemaExt for T {} +/// The first line of a generated file: the SHA-256 of the schema file's bytes, in lowercase hex +/// +/// This is the same digest as `nix hash file --type sha256 --base16 `, so a check can compare +/// the header of a generated file with the hash of the schema it says it was made from. +pub fn schema_hash_header(schema_bytes: &[u8]) -> String { + use sha2::Digest; + use std::fmt::Write; + + let mut header = String::from("# schema-sha256: "); + for byte in sha2::Sha256::digest(schema_bytes) { + write!(header, "{byte:02x}").expect("writing to a String cannot fail"); + } + header.push('\n'); + header +} + /// Generate Nix options from a JSON schema value pub fn generate_from_schema( schema: &Value, @@ -56,6 +72,19 @@ mod tests { use super::*; use serde_json::json; + #[test] + fn test_schema_hash_header() { + // the SHA-256 of the empty input and of "abc" + assert_eq!( + schema_hash_header(b""), + "# schema-sha256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\n" + ); + assert_eq!( + schema_hash_header(b"abc"), + "# schema-sha256: ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad\n" + ); + } + #[test] fn test_generate_from_schema() { let schema = json!({ diff --git a/src/main.rs b/src/main.rs index d4fda572..dd024719 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ use clap::Parser; use schemars_nixgen::GeneratorOptions; use schemars_nixgen::generate_from_schema; +use schemars_nixgen::schema_hash_header; use std::fs; use std::io::Write; use std::io::{self}; @@ -42,8 +43,13 @@ fn main() -> color_eyre::Result<()> { root_description: cli.description, }; - // Generate Nix code - let nix_code = generate_from_schema(&schema, &options)?; + // Generate Nix code, with the hash of the schema file first so a check can tell when the options + // are older than the schema + let nix_code = format!( + "{}{}", + schema_hash_header(schema_str.as_bytes()), + generate_from_schema(&schema, &options)? + ); // Write output if let Some(output_path) = cli.output {