From 0e113ef02ec392dc0a6d3ee95c723d8b98b3c580 Mon Sep 17 00:00:00 2001 From: Graham Christensen Date: Mon, 3 Aug 2026 22:52:48 -0400 Subject: [PATCH 01/15] Add editor and git config --- .editorconfig | 21 +++++++++++++++++++++ .gitignore | 5 +++++ 2 files changed, 26 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6308f0a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,21 @@ +# EditorConfig configuration for flakehub-cache-types + +# Top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file, utf-8 charset +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 + +# Rust +[*.rs] +indent_style = space +indent_size = 4 + +# Misc +[*.{yaml,yml,md,nix,toml}] +indent_style = space +indent_size = 2 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f027b8a --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.direnv + +/target +result +result-* From ce648827ffefeaf3b0785842e3fd594e123c2944 Mon Sep 17 00:00:00 2001 From: Graham Christensen Date: Mon, 3 Aug 2026 22:52:48 -0400 Subject: [PATCH 02/15] Import the store types from attic-priv --- Cargo.toml | 26 +++ src/error.rs | 65 ++++++++ src/hash/mod.rs | 141 +++++++++++++++++ src/hash/tests/.gitattributes | 1 + src/hash/tests/blob | 15 ++ src/hash/tests/mod.rs | 50 ++++++ src/lib.rs | 20 +++ src/nix_store/mod.rs | 288 ++++++++++++++++++++++++++++++++++ 8 files changed, 606 insertions(+) create mode 100644 Cargo.toml create mode 100644 src/error.rs create mode 100644 src/hash/mod.rs create mode 100644 src/hash/tests/.gitattributes create mode 100644 src/hash/tests/blob create mode 100644 src/hash/tests/mod.rs create mode 100644 src/lib.rs create mode 100644 src/nix_store/mod.rs diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..6fb2372 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "attic-store-types" +version = "0.2.0" +edition = "2021" +publish = false + +[lib] +name = "attic_store_types" +path = "src/lib.rs" + +[dependencies] +cxx = { version = "1.0", optional = true } +displaydoc = "0.2.4" +hex = "0.4.3" +lazy_static = "1.4.0" +nix-base32 = { git = "https://github.com/zhaofengli/nix-base32.git", rev = "b850c6e9273d1c39bd93abb704a53345f5be92eb" } +regex = "1.8.3" +serde = { version = "1.0.163", features = ["derive"] } +serde_json = "1.0.96" +sha2 = "0.10.6" + +[features] +default = [] + +# Enables `From` for `StoreError`. +cxx = ["dep:cxx"] diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..98a704a --- /dev/null +++ b/src/error.rs @@ -0,0 +1,65 @@ +use std::error::Error as StdError; +use std::io; +use std::path::PathBuf; + +use crate::nix_store::StorePath; + +pub type StoreResult = Result; + +#[derive(Debug, displaydoc::Display)] +pub enum StoreError { + /// Invalid store path {path:?}: {reason} + InvalidStorePath { path: PathBuf, reason: &'static str }, + + /// Invalid store path base name {base_name:?}: {reason} + InvalidStorePathName { + base_name: PathBuf, + reason: &'static str, + }, + + /// Invalid store path hash "{hash}": {reason} + InvalidStorePathHash { hash: String, reason: &'static str }, + + /// I/O error: {error}. + IoError { error: io::Error }, + + /// Unknown C++ exception: {exception}. + CxxError { exception: String }, + + /// Provenance for {path:?} was not valid JSON: {error_display}: {invalid_string} + InvalidProvenance { + path: StorePath, + error_display: String, + invalid_string: String, + }, +} + +impl StoreError { + pub fn name(&self) -> &'static str { + match self { + Self::InvalidStorePath { .. } => "InvalidStorePath", + Self::InvalidStorePathName { .. } => "InvalidStorePathName", + Self::InvalidStorePathHash { .. } => "InvalidStorePathHash", + Self::IoError { .. } => "IoError", + Self::CxxError { .. } => "CxxError", + Self::InvalidProvenance { .. } => "InvalidProvenance", + } + } +} + +impl StdError for StoreError {} + +impl From for StoreError { + fn from(error: io::Error) -> Self { + Self::IoError { error } + } +} + +#[cfg(feature = "cxx")] +impl From for StoreError { + fn from(exception: cxx::Exception) -> Self { + Self::CxxError { + exception: exception.what().to_string(), + } + } +} diff --git a/src/hash/mod.rs b/src/hash/mod.rs new file mode 100644 index 0000000..95cf0e4 --- /dev/null +++ b/src/hash/mod.rs @@ -0,0 +1,141 @@ +//! Hashing utilities. + +#[cfg(test)] +mod tests; + +use displaydoc::Display; +use serde::{de, ser, Deserialize, Serialize}; +use sha2::{Digest, Sha256}; + +/// A hash. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Hash { + /// An SHA-256 hash. + Sha256([u8; 32]), +} + +/// A hashing error. +#[derive(Debug, Display)] +pub enum Error { + /// The string lacks a colon separator. + NoColonSeparator, + + /// Hash algorithm {0} is not supported. + UnsupportedHashAlgorithm(String), + + /// Invalid base16 hash: {0} + InvalidBase16Hash(hex::FromHexError), + + /// Invalid base32 hash. + InvalidBase32Hash, + + /// Invalid length for {typ} string: Must be either {base16_len} (hexadecimal) or {base32_len} (base32), got {actual}. + InvalidHashStringLength { + typ: &'static str, + base16_len: usize, + base32_len: usize, + actual: usize, + }, +} + +impl std::error::Error for Error {} + +impl Hash { + /// Convenience function to generate a SHA-256 hash from a slice. + pub fn sha256_from_bytes(bytes: &[u8]) -> Self { + let mut hasher = Sha256::new(); + hasher.update(bytes); + Self::Sha256(hasher.finalize().into()) + } + + /// Parses a typed representation of a hash. + pub fn from_typed(s: &str) -> Result { + let colon = s.find(':').ok_or(Error::NoColonSeparator)?; + + let (typ, rest) = s.split_at(colon); + let hash = &rest[1..]; + + match typ { + "sha256" => { + let v = decode_hash(hash, "SHA-256", 32)?; + Ok(Self::Sha256(v.try_into().unwrap())) + } + _ => Err(Error::UnsupportedHashAlgorithm(typ.to_owned())), + } + } + + /// Returns the hash in Nix-specific Base32 format, with the hash type prepended. + pub fn to_typed_base32(&self) -> String { + format!("{}:{}", self.hash_type(), self.to_base32()) + } + + /// Returns the hash in hexadecimal format, with the hash type prepended. + /// + /// This is the canonical representation of hashes in the Attic database. + pub fn to_typed_base16(&self) -> String { + format!("{}:{}", self.hash_type(), hex::encode(self.data())) + } + + fn data(&self) -> &[u8] { + match self { + Self::Sha256(d) => d, + } + } + + fn hash_type(&self) -> &'static str { + match self { + Self::Sha256(_) => "sha256", + } + } + + /// Returns the hash in Nix-specific Base32 format. + pub fn to_base32(&self) -> String { + nix_base32::to_nix_base32(self.data()) + } +} + +impl<'de> Deserialize<'de> for Hash { + /// Deserializes a typed hash string. + fn deserialize(deserializer: D) -> Result + where + D: de::Deserializer<'de>, + { + use de::Error; + + String::deserialize(deserializer) + .and_then(|s| Self::from_typed(&s).map_err(|e| Error::custom(e.to_string()))) + } +} + +impl Serialize for Hash { + /// Serializes a hash into a hexadecimal hash string. + fn serialize(&self, serializer: S) -> Result + where + S: ser::Serializer, + { + serializer.serialize_str(&self.to_typed_base16()) + } +} + +/// Decodes a base16 or base32 encoded hash containing a specified number of bytes. +fn decode_hash<'s>(s: &'s str, typ: &'static str, expected_bytes: usize) -> Result, Error> { + let base16_len = expected_bytes * 2; + let base32_len = (expected_bytes * 8 - 1) / 5 + 1; + + let v = if s.len() == base16_len { + hex::decode(s).map_err(Error::InvalidBase16Hash)? + } else if s.len() == base32_len { + nix_base32::from_nix_base32(s).ok_or(Error::InvalidBase32Hash)? + } else { + return Err(Error::InvalidHashStringLength { + typ, + base16_len, + base32_len, + actual: s.len(), + }); + }; + + assert!(v.len() == expected_bytes); + + Ok(v) +} diff --git a/src/hash/tests/.gitattributes b/src/hash/tests/.gitattributes new file mode 100644 index 0000000..3a4be7f --- /dev/null +++ b/src/hash/tests/.gitattributes @@ -0,0 +1 @@ +blob -text diff --git a/src/hash/tests/blob b/src/hash/tests/blob new file mode 100644 index 0000000..afbfab0 --- /dev/null +++ b/src/hash/tests/blob @@ -0,0 +1,15 @@ +⊂_ヽ +  \\ _ +   \( •_•) F +    < ⌒ヽ A +   /   へ\ B +   /  / \\ U +   レ ノ   ヽ_つ L +  / / O +  / /| U + ( (ヽ S + | |、\ + | 丿 \ ⌒) + | |  ) / +`ノ )  Lノ +(_/ diff --git a/src/hash/tests/mod.rs b/src/hash/tests/mod.rs new file mode 100644 index 0000000..bcc7ef5 --- /dev/null +++ b/src/hash/tests/mod.rs @@ -0,0 +1,50 @@ +use super::*; + +const BLOB: &[u8] = include_bytes!("blob"); + +#[test] +fn test_basic() { + let hash = Hash::sha256_from_bytes(BLOB); + + let expected_base16 = "sha256:df3404eaf1481506db9ca155e0a871d5b4d22e62a96961e8bf4ad1a8ca525330"; + assert_eq!(expected_base16, hash.to_typed_base16()); + + let expected_base32 = "sha256:0c2kab5ailaapzl62sd9c8pd5d6mf6lf0md1kkdhc5a8y7m08d6z"; + assert_eq!(expected_base32, hash.to_typed_base32()); +} + +#[test] +fn test_from_typed() { + let base16 = "sha256:baeabdb75c223d171800c17b05c5e7e8e9980723a90eb6ffcc632a305afc5a42"; + let base32 = "sha256:0hjszid30ak3rkzvc3m94c3risg8wz2hayy100c1fg92bjvvvsms"; + + assert_eq!( + Hash::from_typed(base16).unwrap(), + Hash::from_typed(base32).unwrap() + ); + + assert!(matches!( + Hash::from_typed("sha256"), + Err(Error::NoColonSeparator) + )); + + assert!(matches!( + Hash::from_typed("sha256:"), + Err(Error::InvalidHashStringLength { .. }) + )); + + assert!(matches!( + Hash::from_typed("sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"), + Err(Error::InvalidBase32Hash) + )); + + assert!(matches!( + Hash::from_typed("sha256:gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg"), + Err(Error::InvalidBase16Hash(_)) + )); + + assert!(matches!( + Hash::from_typed("md5:invalid"), + Err(Error::UnsupportedHashAlgorithm(alg)) if alg == "md5" + )); +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..7aa4fe2 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,20 @@ +#![deny( + asm_sub_register, + deprecated, + missing_abi, + unsafe_code, + unused_macros, + unused_must_use, + unused_unsafe +)] +#![deny(clippy::from_over_into, clippy::needless_question_mark)] +#![cfg_attr( + not(debug_assertions), + deny(unused_imports, unused_mut, unused_variables) +)] + +pub mod error; +pub mod hash; +pub mod nix_store; + +pub use error::{StoreError, StoreResult}; diff --git a/src/nix_store/mod.rs b/src/nix_store/mod.rs new file mode 100644 index 0000000..1cebe27 --- /dev/null +++ b/src/nix_store/mod.rs @@ -0,0 +1,288 @@ +use std::ffi::OsStr; +#[cfg(target_family = "unix")] +use std::os::unix::ffi::OsStrExt; +use std::path::{Path, PathBuf}; +use std::str::FromStr; + +use lazy_static::lazy_static; +use regex::Regex; +use serde::{de, Deserialize, Serialize}; + +use crate::error::{StoreError, StoreResult}; +use crate::hash::Hash; + +/// Length of the hash in a store path. +pub const STORE_PATH_HASH_LEN: usize = 32; + +/// Regex that matches a store path hash, without anchors. +pub const STORE_PATH_HASH_REGEX_FRAGMENT: &str = "[0123456789abcdfghijklmnpqrsvwxyz]{32}"; + +lazy_static! { + /// Regex for a valid store path hash. + /// + /// This is the path portion of a base name. + static ref STORE_PATH_HASH_REGEX: Regex = { + Regex::new(&format!("^{}$", STORE_PATH_HASH_REGEX_FRAGMENT)).unwrap() + }; + + /// Regex for a valid store base name. + /// + /// A base name consists of two parts: A hash and a human-readable + /// label/name. The format of the hash is described in `StorePathHash`. + /// + /// The human-readable name can only contain the following characters: + /// + /// - A-Za-z0-9 + /// - `+-._?=` + /// + /// See the Nix implementation in `src/libstore/path.cc`. + static ref STORE_BASE_NAME_REGEX: Regex = { + Regex::new(r"^[0123456789abcdfghijklmnpqrsvwxyz]{32}-[A-Za-z0-9+-._?=]+$").unwrap() + }; +} + +/// Information on a valid store path. +#[derive(Debug)] +pub struct ValidPathInfo { + /// The store path. + pub path: StorePath, + + /// Hash of the NAR. + pub nar_hash: Hash, + + /// Size of the NAR. + pub nar_size: u64, + + /// References. + /// + /// This list only contains base names of the paths. + pub references: Vec, + + /// Signatures. + pub sigs: Vec, + + /// Content Address. + pub ca: Option, + + /// Provenance. + pub provenance: Option, +} + +/// A path in a Nix store. +/// +/// This must be a direct child of the store. This path may or +/// may not actually exist. +/// +/// This guarantees that the base name is of valid format. +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +pub struct StorePath { + /// Base name of the store path. + /// + /// For example, for `/nix/store/ia70ss13m22znbl8khrf2hq72qmh5drr-ruby-2.7.5`, + /// this would be `ia70ss13m22znbl8khrf2hq72qmh5drr-ruby-2.7.5`. + base_name: PathBuf, +} + +impl FromStr for StorePath { + type Err = StoreError; + + fn from_str(s: &str) -> Result { + Self::from_base_name(PathBuf::from(s)) + } +} + +impl std::fmt::Display for StorePath { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.base_name.display().fmt(f) + } +} + +impl Serialize for StorePath { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(self.to_string().as_str()) + } +} + +impl<'de> Deserialize<'de> for StorePath { + fn deserialize(deserializer: D) -> Result + where + D: de::Deserializer<'de>, + { + String::deserialize(deserializer).and_then(|base_name_str| { + StorePath::from_str(&base_name_str).map_err(|e| de::Error::custom(e.to_string())) + }) + } +} + +impl StorePath { + /// Creates a StorePath with a base name. + pub fn from_base_name(base_name: PathBuf) -> StoreResult { + let s = base_name + .as_os_str() + .to_str() + .ok_or_else(|| StoreError::InvalidStorePathName { + base_name: base_name.clone(), + reason: "Name contains non-UTF-8 characters", + })?; + + if !STORE_BASE_NAME_REGEX.is_match(s) { + return Err(StoreError::InvalidStorePathName { + base_name, + reason: "Name is of invalid format", + }); + } + + Ok(Self { base_name }) + } + + /// Creates a StorePath with a known valid base name. + /// + /// # Safety + /// + /// The caller must ensure that the name is of a valid format (refer + /// to the documentations for `STORE_BASE_NAME_REGEX`). Other operations + /// with this object will assume it's valid. + #[allow(unsafe_code)] + pub unsafe fn from_base_name_unchecked(base_name: PathBuf) -> Self { + Self { base_name } + } + + /// Gets the hash portion of the store path. + #[cfg(target_family = "unix")] + pub fn to_hash(&self) -> StorePathHash { + // Safety: We have already validated the format of the base name, + // including the hash part. The name is guaranteed valid UTF-8. + #[allow(unsafe_code)] + unsafe { + let s = std::str::from_utf8_unchecked(self.base_name.as_os_str().as_bytes()); + let hash = s[..STORE_PATH_HASH_LEN].to_string(); + StorePathHash::new_unchecked(hash) + } + } + + /// Returns the human-readable name. + #[cfg(target_family = "unix")] + pub fn name(&self) -> String { + // Safety: Already checked + #[allow(unsafe_code)] + unsafe { + let s = std::str::from_utf8_unchecked(self.base_name.as_os_str().as_bytes()); + s[STORE_PATH_HASH_LEN + 1..].to_string() + } + } + + pub fn as_os_str(&self) -> &OsStr { + self.base_name.as_os_str() + } + + /// Returns the bytes of the base name. + #[cfg(target_family = "unix")] + pub fn as_base_name_bytes(&self) -> &[u8] { + self.base_name.as_os_str().as_bytes() + } +} + +/// A fixed-length store path hash. +/// +/// For example, for `/nix/store/ia70ss13m22znbl8khrf2hq72qmh5drr-ruby-2.7.5`, +/// this would be `ia70ss13m22znbl8khrf2hq72qmh5drr`. +/// +/// It must contain exactly 32 "base-32 characters". Nix's special scheme +/// include the following valid characters: "0123456789abcdfghijklmnpqrsvwxyz" +/// ('e', 'o', 'u', 't' are banned). +/// +/// Examples of invalid store path hashes: +/// +/// - "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" +/// - "IA70SS13M22ZNBL8KHRF2HQ72QMH5DRR" +/// - "whatevenisthisthing" +#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize)] +pub struct StorePathHash(String); + +impl<'de> Deserialize<'de> for StorePathHash { + /// Deserializes a potentially-invalid store path hash. + fn deserialize(deserializer: D) -> Result + where + D: de::Deserializer<'de>, + { + use de::Error; + String::deserialize(deserializer) + .and_then(|s| Self::new(&s).map_err(|e| Error::custom(e.to_string()))) + } +} + +impl std::fmt::Display for StorePathHash { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl StorePathHash { + /// Creates a store path hash from a string. + pub fn new(hash: &str) -> StoreResult { + let hash = hash.to_owned(); + if hash.as_bytes().len() != STORE_PATH_HASH_LEN { + return Err(StoreError::InvalidStorePathHash { + hash, + reason: "Hash is of invalid length", + }); + } + + if !STORE_PATH_HASH_REGEX.is_match(&hash) { + return Err(StoreError::InvalidStorePathHash { + hash, + reason: "Hash is of invalid format", + }); + } + + Ok(Self(hash)) + } + + /// Creates a store path hash from a string, without checking its validity. + /// + /// # Safety + /// + /// The caller must make sure that it is of expected length and format. + #[allow(unsafe_code)] + pub unsafe fn new_unchecked(hash: String) -> Self { + Self(hash) + } + + pub fn as_str(&self) -> &str { + &self.0 + } + + pub fn to_string(&self) -> String { + self.0.clone() + } +} + +/// Returns the base store name of a path relative to a store root. +pub fn to_base_name(store_dir: &Path, path: &Path) -> StoreResult { + if let Ok(remaining) = path.strip_prefix(store_dir) { + let first = remaining + .iter() + .next() + .ok_or_else(|| StoreError::InvalidStorePath { + path: path.to_owned(), + reason: "Path is store directory itself", + })?; + + if first.len() < STORE_PATH_HASH_LEN { + Err(StoreError::InvalidStorePath { + path: path.to_owned(), + reason: "Path is too short", + }) + } else { + Ok(PathBuf::from(first)) + } + } else { + Err(StoreError::InvalidStorePath { + path: path.to_owned(), + reason: "Path is not in store directory", + }) + } +} From 6b505c5a934c0fa9fa3ddaad66a5c16bda5bbd87 Mon Sep 17 00:00:00 2001 From: Graham Christensen Date: Mon, 3 Aug 2026 22:53:26 -0400 Subject: [PATCH 03/15] Rename the crate to flakehub-cache-types --- Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6fb2372..f3c6184 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,11 @@ [package] -name = "attic-store-types" -version = "0.2.0" +name = "flakehub-cache-types" +version = "0.1.0" edition = "2021" publish = false [lib] -name = "attic_store_types" +name = "flakehub_cache_types" path = "src/lib.rs" [dependencies] From ae41be266485ad020d53d4dffaf6dea641f1def4 Mon Sep 17 00:00:00 2001 From: Graham Christensen Date: Tue, 4 Aug 2026 06:49:08 -0400 Subject: [PATCH 04/15] nix-base32: use the crates.io release --- Cargo.lock | 479 +++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 2 +- 2 files changed, 480 insertions(+), 1 deletion(-) create mode 100644 Cargo.lock diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..748203f --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,479 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[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 = "cc" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clap" +version = "4.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301b56658598e48f3648647ac6fc887be7e7108eddfa4e9b63fcf3ec58c0cadf" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94a65403d1a1bd28f7dc68eb8506e8874808ee5eecb59298de588e2e1407a078" +dependencies = [ + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "codespan-reporting" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" +dependencies = [ + "serde", + "termcolor", + "unicode-width", +] + +[[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 = "cxx" +version = "1.0.198" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe442a792c7c736eea18b32a7f8a3b63cf8aafabda6760042dc2fdeda456291" +dependencies = [ + "cc", + "cxx-build", + "cxxbridge-cmd", + "cxxbridge-flags", + "cxxbridge-macro", + "foldhash", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.198" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3184a94384c663718698311a78a51ac00c484c10b4eeac06fb0a068c5f64fa2" +dependencies = [ + "cc", + "codespan-reporting", + "indexmap", + "proc-macro2", + "quote", + "scratch", + "syn", +] + +[[package]] +name = "cxxbridge-cmd" +version = "1.0.198" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0148d8fd1199329ddf1d157a5e134e51ceff37c6a7ddd38615c399d81cb05d8d" +dependencies = [ + "clap", + "codespan-reporting", + "indexmap", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.198" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52850339faed2eaadd24e286dc1d8268cc6f8a7bd9524d713adc9099566b4c89" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.198" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c77c856545d886c9bd5215409ebb63b925e262135248b50c79e5a5f194ee47c" +dependencies = [ + "indexmap", + "proc-macro2", + "quote", + "syn", +] + +[[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 = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flakehub-cache-types" +version = "0.1.0" +dependencies = [ + "cxx", + "displaydoc", + "hex", + "lazy_static", + "nix-base32", + "regex", + "serde", + "serde_json", + "sha2", +] + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[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 = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "link-cplusplus" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f78c730aaa7d0b9336a299029ea49f9ee53b0ed06e9202e8cb7db9bae7b8c82" +dependencies = [ + "cc", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "nix-base32" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2628953ed836273ee4262e3708a8ef63ca38bd8a922070626eef7f9e5d8d536" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "scratch" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d68f2ec51b097e4c1a75b681a8bec621909b5e91f15bb7b840c4f2f7b01148b2" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[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 = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[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.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml index f3c6184..853b779 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ cxx = { version = "1.0", optional = true } displaydoc = "0.2.4" hex = "0.4.3" lazy_static = "1.4.0" -nix-base32 = { git = "https://github.com/zhaofengli/nix-base32.git", rev = "b850c6e9273d1c39bd93abb704a53345f5be92eb" } +nix-base32 = "0.2.0" regex = "1.8.3" serde = { version = "1.0.163", features = ["derive"] } serde_json = "1.0.96" From 9e66a7b983f0e4600cb2d3e25150d6ae48bf48b0 Mon Sep 17 00:00:00 2001 From: Graham Christensen Date: Tue, 4 Aug 2026 06:49:57 -0400 Subject: [PATCH 05/15] Add crates.io metadata --- Cargo.toml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 853b779..56d3d0f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,11 @@ name = "flakehub-cache-types" version = "0.1.0" edition = "2021" -publish = false +description = "Nix store path, hash, and error types shared by FlakeHub Cache services" +license = "Apache-2.0" +repository = "https://github.com/DeterminateSystems/flakehub-cache-types" +keywords = ["nix", "cache", "store-path"] +categories = ["data-structures"] [lib] name = "flakehub_cache_types" From 874fce9b2b527e19f09e4f91252e5c0212c82e01 Mon Sep 17 00:00:00 2001 From: Graham Christensen Date: Tue, 4 Aug 2026 06:51:41 -0400 Subject: [PATCH 06/15] Add a Nix flake --- .envrc | 1 + flake.lock | 78 ++++++++++++++++++++++++++++++++ flake.nix | 130 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 209 insertions(+) create mode 100644 .envrc create mode 100644 flake.lock create mode 100644 flake.nix diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..e751c08 --- /dev/null +++ b/flake.lock @@ -0,0 +1,78 @@ +{ + "nodes": { + "crane": { + "locked": { + "lastModified": 1779041105, + "narHash": "sha256-nnGD2f8OlAZT2i5OfwikJsw+ifWfiA4d6A8BWlgOXV0=", + "rev": "10e6e3cb966f7cfcc789fe5eee7a85f3188ce08b", + "revCount": 858, + "type": "tarball", + "url": "https://api.flakehub.com/f/pinned/ipetkov/crane/0.23.4/019e3726-d9ea-7820-aece-59009301cab1/source.tar.gz" + }, + "original": { + "type": "tarball", + "url": "https://flakehub.com/f/ipetkov/crane/0" + } + }, + "fenix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ], + "rust-analyzer-src": "rust-analyzer-src" + }, + "locked": { + "lastModified": 1782900513, + "narHash": "sha256-GHrsl1+ysDFgmDQtQSqWS7TiYD2Q58VlwLvnf1+crJM=", + "rev": "16810aa8f4ad89ca480b1513774e8b6f485fe368", + "revCount": 2708, + "type": "tarball", + "url": "https://api.flakehub.com/f/pinned/nix-community/fenix/0.1.2708%2Brev-16810aa8f4ad89ca480b1513774e8b6f485fe368/019f1d68-8b55-7783-bed1-d5926f21ae52/source.tar.gz" + }, + "original": { + "type": "tarball", + "url": "https://flakehub.com/f/nix-community/fenix/0.1" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1785692966, + "narHash": "sha256-vUfIeBEfpbAfZ5zjgIkYk7eHBeVfCYVjLbWnMkseYnk=", + "rev": "643809054d65fdd466a63e3155b8c498cb483c04", + "revCount": 1046609, + "type": "tarball", + "url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.1.1046609%2Brev-643809054d65fdd466a63e3155b8c498cb483c04/019fc6ca-7fa6-7ad3-a05c-e8e4d8fffab6/source.tar.gz" + }, + "original": { + "type": "tarball", + "url": "https://flakehub.com/f/NixOS/nixpkgs/0.1" + } + }, + "root": { + "inputs": { + "crane": "crane", + "fenix": "fenix", + "nixpkgs": "nixpkgs" + } + }, + "rust-analyzer-src": { + "flake": false, + "locked": { + "lastModified": 1782864348, + "narHash": "sha256-NVYhLbaefeIUftPlo3kS6qr0xd8eFJRodEiaHrvFKR4=", + "owner": "rust-lang", + "repo": "rust-analyzer", + "rev": "0d381ca097a8e0375a19387874d952c0a230ac4f", + "type": "github" + }, + "original": { + "owner": "rust-lang", + "ref": "nightly", + "repo": "rust-analyzer", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..adc647d --- /dev/null +++ b/flake.nix @@ -0,0 +1,130 @@ +{ + description = "Nix store path, hash, and error types shared by FlakeHub Cache services"; + + inputs = { + nixpkgs.url = "https://flakehub.com/f/NixOS/nixpkgs/0.1"; + + fenix = { + url = "https://flakehub.com/f/nix-community/fenix/0.1"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + crane.url = "https://flakehub.com/f/ipetkov/crane/0"; + }; + + outputs = + { + self, + nixpkgs, + fenix, + crane, + }: + let + supportedSystems = [ + "x86_64-linux" + "aarch64-linux" + "x86_64-darwin" + "aarch64-darwin" + ]; + + forAllSystems = + f: + nixpkgs.lib.genAttrs supportedSystems ( + system: + let + pkgs = import nixpkgs { inherit system; }; + + toolchain = fenix.packages.${system}.stable.withComponents [ + "cargo" + "clippy" + "rustc" + "rustfmt" + "rust-src" + ]; + + craneLib = (crane.mkLib pkgs).overrideToolchain (_: toolchain); + + src = pkgs.lib.fileset.toSource { + root = ./.; + fileset = pkgs.lib.fileset.unions [ + (craneLib.fileset.commonCargoSources ./.) + ./src/hash/tests + ]; + }; + + commonArgs = { + inherit src; + strictDeps = true; + }; + + cargoArtifacts = craneLib.buildDepsOnly commonArgs; + in + f { + inherit + pkgs + toolchain + craneLib + commonArgs + cargoArtifacts + ; + } + ); + in + { + packages = forAllSystems ( + { craneLib, commonArgs, cargoArtifacts, ... }: + { + default = craneLib.buildPackage ( + commonArgs + // { + inherit cargoArtifacts; + cargoExtraArgs = "--all-features"; + } + ); + } + ); + + checks = forAllSystems ( + { + craneLib, + commonArgs, + cargoArtifacts, + ... + }: + { + test = craneLib.cargoTest ( + commonArgs + // { + inherit cargoArtifacts; + cargoTestExtraArgs = "--all-features"; + } + ); + + clippy = craneLib.cargoClippy ( + commonArgs + // { + inherit cargoArtifacts; + cargoClippyExtraArgs = "--all-targets --all-features -- --deny warnings"; + } + ); + + fmt = craneLib.cargoFmt { inherit (commonArgs) src; }; + } + ); + + devShells = forAllSystems ( + { pkgs, toolchain, ... }: + { + default = pkgs.mkShell { + packages = [ + toolchain + pkgs.rust-analyzer + pkgs.cargo-watch + pkgs.cargo-deny + pkgs.editorconfig-checker + ]; + }; + } + ); + }; +} From 7107e9db0a39eac642cbff046c9d62abbfe8342a Mon Sep 17 00:00:00 2001 From: Graham Christensen Date: Tue, 4 Aug 2026 07:43:45 -0400 Subject: [PATCH 07/15] ci: tests, clippy, fmt --- .github/workflows/ci.yml | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1b82187 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,31 @@ +name: CI + +on: + pull_request: + merge_group: + push: + branches: [main] + +permissions: + contents: read + id-token: write + +jobs: + checks: + name: Nix checks (${{ matrix.os }}) + strategy: + matrix: + os: + - ubuntu-latest + - macos-latest + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: DeterminateSystems/nix-installer-action@main + with: + determinate: true + - uses: DeterminateSystems/flakehub-cache-action@main + - name: Build, test, clippy, rustfmt + run: nix flake check -L + - name: Check editorconfig conformance + run: nix develop --command editorconfig-checker From c356facde086af8d0218b6fd6e7a5bd117029584 Mon Sep 17 00:00:00 2001 From: Graham Christensen Date: Tue, 4 Aug 2026 08:09:09 -0400 Subject: [PATCH 08/15] ci: cargo-deny and flake-checker --- .github/workflows/ci.yml | 18 ++++++++++++++++++ deny.toml | 20 ++++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 deny.toml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1b82187..46b406b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,3 +29,21 @@ jobs: run: nix flake check -L - name: Check editorconfig conformance run: nix develop --command editorconfig-checker + + cargo-deny: + name: Advisories, licenses, sources + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: DeterminateSystems/nix-installer-action@main + with: + determinate: true + - uses: DeterminateSystems/flakehub-cache-action@main + - run: nix develop --command cargo deny check + + flake-checker: + name: Check flake.lock health + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: DeterminateSystems/flake-checker-action@main diff --git a/deny.toml b/deny.toml new file mode 100644 index 0000000..37f27ce --- /dev/null +++ b/deny.toml @@ -0,0 +1,20 @@ +[graph] +all-features = true + +[advisories] +yanked = "deny" + +[licenses] +allow = [ + "Apache-2.0", + "MIT", + "Unicode-3.0", + "Zlib", +] + +[bans] +multiple-versions = "warn" + +[sources] +unknown-registry = "deny" +unknown-git = "deny" From fd9d1ee644c6b32050ffc809d7ec587c2f53412e Mon Sep 17 00:00:00 2001 From: Graham Christensen Date: Tue, 4 Aug 2026 08:09:22 -0400 Subject: [PATCH 09/15] Publish to crates.io on tags --- .github/workflows/release.yml | 37 +++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..bf24eb9 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,37 @@ +name: Release + +on: + push: + tags: + - "v*" + +permissions: + contents: read + id-token: write + +jobs: + publish: + name: Publish to crates.io + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: DeterminateSystems/nix-installer-action@main + with: + determinate: true + - uses: DeterminateSystems/flakehub-cache-action@main + - name: Check that the tag matches the crate version + run: | + version="$(nix develop --command cargo metadata --format-version 1 --no-deps | jq -r '.packages[0].version')" + if [ "v${version}" != "${GITHUB_REF_NAME}" ]; then + echo "Tag ${GITHUB_REF_NAME} does not match crate version ${version}." >&2 + exit 1 + fi + - name: Build, test, clippy, rustfmt + run: nix flake check -L + - name: Authenticate with crates.io + id: auth + uses: rust-lang/crates-io-auth-action@v1 + - name: Publish + run: nix develop --command cargo publish --locked + env: + CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }} From 4234bf8c7b248d6dd6934a3bc6785916aeb1207b Mon Sep 17 00:00:00 2001 From: Graham Christensen Date: Tue, 4 Aug 2026 08:09:22 -0400 Subject: [PATCH 10/15] Add dependabot --- .github/dependabot.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..c7ecf5e --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: cargo + directory: / + schedule: + interval: weekly + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly From 8eebb67256e0c4c5e0c36c8cffd8bd44661f9929 Mon Sep 17 00:00:00 2001 From: Graham Christensen Date: Tue, 4 Aug 2026 08:09:58 -0400 Subject: [PATCH 11/15] README --- README.md | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..1c03ed4 --- /dev/null +++ b/README.md @@ -0,0 +1,52 @@ +# flakehub-cache-types + +Nix store path, hash, and error types shared by FlakeHub Cache services. + +The crate provides: + +- `nix_store::StorePath` and `nix_store::StorePathHash`: parsed, validated + store paths and their 32-character base name hashes. +- `hash::Hash`: SHA-256 hashes with Nix's base16 and base32 encodings. +- `StoreError` and `StoreResult`: the error type the above share. + +## Usage + +```console +cargo add flakehub-cache-types +``` + +```rust +use flakehub_cache_types::nix_store::StorePathHash; + +let hash = StorePathHash::new("ib3sh3pcz10wsmavxvkdbayhqivbghlq")?; +``` + +### Features + +- `cxx` (off by default): implements `From` for + `StoreError`, for use with C++ bindings to the Nix libraries. + +## Development + +`nix develop` provides a Rust toolchain, and `direnv` loads it +automatically. Run the tests with `cargo test`. CI enforces `cargo fmt`, +`cargo clippy`, `cargo deny`, and editorconfig conformance. + +## Releasing + +Bump the version in `Cargo.toml`, then push a matching tag: + +```console +git tag v0.1.1 +git push origin v0.1.1 +``` + +The release workflow checks that the tag matches the crate version, runs +the tests, and publishes to crates.io using [trusted +publishing](https://crates.io/docs/trusted-publishing). + +## Provenance and license + +These types derive from [Attic](https://github.com/zhaofengli/attic) by +Zhaofeng Li and the Attic contributors. Licensed under the Apache License, +Version 2.0; see [LICENSE](LICENSE). From 7ab605f566429094738252bc3d250fb4d0e17eb4 Mon Sep 17 00:00:00 2001 From: Graham Christensen Date: Tue, 4 Aug 2026 08:13:51 -0400 Subject: [PATCH 12/15] Fix clippy lints --- src/hash/mod.rs | 2 +- src/nix_store/mod.rs | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/hash/mod.rs b/src/hash/mod.rs index 95cf0e4..75fe4c8 100644 --- a/src/hash/mod.rs +++ b/src/hash/mod.rs @@ -118,7 +118,7 @@ impl Serialize for Hash { } /// Decodes a base16 or base32 encoded hash containing a specified number of bytes. -fn decode_hash<'s>(s: &'s str, typ: &'static str, expected_bytes: usize) -> Result, Error> { +fn decode_hash(s: &str, typ: &'static str, expected_bytes: usize) -> Result, Error> { let base16_len = expected_bytes * 2; let base32_len = (expected_bytes * 8 - 1) / 5 + 1; diff --git a/src/nix_store/mod.rs b/src/nix_store/mod.rs index 1cebe27..c55ca9c 100644 --- a/src/nix_store/mod.rs +++ b/src/nix_store/mod.rs @@ -224,7 +224,7 @@ impl StorePathHash { /// Creates a store path hash from a string. pub fn new(hash: &str) -> StoreResult { let hash = hash.to_owned(); - if hash.as_bytes().len() != STORE_PATH_HASH_LEN { + if hash.len() != STORE_PATH_HASH_LEN { return Err(StoreError::InvalidStorePathHash { hash, reason: "Hash is of invalid length", @@ -254,10 +254,6 @@ impl StorePathHash { pub fn as_str(&self) -> &str { &self.0 } - - pub fn to_string(&self) -> String { - self.0.clone() - } } /// Returns the base store name of a path relative to a store root. From 4cc9be3a52ff7faccc4c089a8524ff73217fd425 Mon Sep 17 00:00:00 2001 From: Graham Christensen Date: Tue, 4 Aug 2026 12:24:36 -0400 Subject: [PATCH 13/15] checks: give the cxx test binary libstdc++ at runtime --- flake.nix | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/flake.nix b/flake.nix index adc647d..961979e 100644 --- a/flake.nix +++ b/flake.nix @@ -86,6 +86,7 @@ checks = forAllSystems ( { + pkgs, craneLib, commonArgs, cargoArtifacts, @@ -98,6 +99,11 @@ inherit cargoArtifacts; cargoTestExtraArgs = "--all-features"; } + # The `cxx` feature links against the C++ standard library; + # the test binary needs it on its runtime path. + // pkgs.lib.optionalAttrs pkgs.stdenv.hostPlatform.isLinux { + LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [ pkgs.stdenv.cc.cc.lib ]; + } ); clippy = craneLib.cargoClippy ( From bc7604d403dc41761c930775345f0ced50dd471e Mon Sep 17 00:00:00 2001 From: Graham Christensen Date: Tue, 4 Aug 2026 12:34:02 -0400 Subject: [PATCH 14/15] A few nits and tests --- .github/workflows/ci.yml | 9 +++++++- README.md | 4 +++- src/nix_store/mod.rs | 18 +++++++++++---- src/nix_store/tests.rs | 49 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 74 insertions(+), 6 deletions(-) create mode 100644 src/nix_store/tests.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 46b406b..7e87ddc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,11 +8,14 @@ on: permissions: contents: read - id-token: write jobs: checks: name: Nix checks (${{ matrix.os }}) + permissions: + contents: read + # For flakehub-cache-action + id-token: write strategy: matrix: os: @@ -33,6 +36,10 @@ jobs: cargo-deny: name: Advisories, licenses, sources runs-on: ubuntu-latest + permissions: + contents: read + # For flakehub-cache-action + id-token: write steps: - uses: actions/checkout@v4 - uses: DeterminateSystems/nix-installer-action@main diff --git a/README.md b/README.md index 1c03ed4..602860d 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,9 @@ The crate provides: - `nix_store::StorePath` and `nix_store::StorePathHash`: parsed, validated store paths and their 32-character base name hashes. - `hash::Hash`: SHA-256 hashes with Nix's base16 and base32 encodings. -- `StoreError` and `StoreResult`: the error type the above share. +- `hash::Error`: the parsing error returned by `Hash::from_typed`. +- `StoreError` and `StoreResult`: the errors returned by the store path + operations above. ## Usage diff --git a/src/nix_store/mod.rs b/src/nix_store/mod.rs index c55ca9c..8383b62 100644 --- a/src/nix_store/mod.rs +++ b/src/nix_store/mod.rs @@ -1,3 +1,6 @@ +#[cfg(test)] +mod tests; + use std::ffi::OsStr; #[cfg(target_family = "unix")] use std::os::unix::ffi::OsStrExt; @@ -268,13 +271,20 @@ pub fn to_base_name(store_dir: &Path, path: &Path) -> StoreResult { })?; if first.len() < STORE_PATH_HASH_LEN { - Err(StoreError::InvalidStorePath { + return Err(StoreError::InvalidStorePath { path: path.to_owned(), reason: "Path is too short", - }) - } else { - Ok(PathBuf::from(first)) + }); } + + let store_path = StorePath::from_base_name(PathBuf::from(first)).map_err(|_| { + StoreError::InvalidStorePath { + path: path.to_owned(), + reason: "Base name is of invalid format", + } + })?; + + Ok(store_path.base_name) } else { Err(StoreError::InvalidStorePath { path: path.to_owned(), diff --git a/src/nix_store/tests.rs b/src/nix_store/tests.rs new file mode 100644 index 0000000..3633614 --- /dev/null +++ b/src/nix_store/tests.rs @@ -0,0 +1,49 @@ +use super::*; + +const STORE_DIR: &str = "/nix/store"; + +#[test] +fn test_to_base_name() { + let base_name = to_base_name( + Path::new(STORE_DIR), + Path::new("/nix/store/ia70ss13m22znbl8khrf2hq72qmh5drr-ruby-2.7.5"), + ) + .unwrap(); + + assert_eq!( + PathBuf::from("ia70ss13m22znbl8khrf2hq72qmh5drr-ruby-2.7.5"), + base_name + ); +} + +#[test] +fn test_to_base_name_invalid_base_name() { + // Long enough to pass the length check, but not a valid base name + let e = to_base_name( + Path::new(STORE_DIR), + Path::new("/nix/store/ia70ss13m22znbl8khrf2hq72qmh5drr-foo@"), + ) + .unwrap_err(); + + assert!(matches!(e, StoreError::InvalidStorePath { .. })); +} + +#[test] +fn test_to_base_name_too_short() { + let e = to_base_name(Path::new(STORE_DIR), Path::new("/nix/store/foo")).unwrap_err(); + + assert!(matches!( + e, + StoreError::InvalidStorePath { + reason: "Path is too short", + .. + } + )); +} + +#[test] +fn test_to_base_name_not_in_store() { + let e = to_base_name(Path::new(STORE_DIR), Path::new("/tmp/foo")).unwrap_err(); + + assert!(matches!(e, StoreError::InvalidStorePath { .. })); +} From fbebd181331b64253ec7897720a10fad81b4a889 Mon Sep 17 00:00:00 2001 From: Graham Christensen Date: Tue, 4 Aug 2026 12:36:32 -0400 Subject: [PATCH 15/15] Don't publish to crates.io --- .github/workflows/release.yml | 37 ----------------------------------- Cargo.toml | 3 +-- README.md | 15 +------------- 3 files changed, 2 insertions(+), 53 deletions(-) delete mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index bf24eb9..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: Release - -on: - push: - tags: - - "v*" - -permissions: - contents: read - id-token: write - -jobs: - publish: - name: Publish to crates.io - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: DeterminateSystems/nix-installer-action@main - with: - determinate: true - - uses: DeterminateSystems/flakehub-cache-action@main - - name: Check that the tag matches the crate version - run: | - version="$(nix develop --command cargo metadata --format-version 1 --no-deps | jq -r '.packages[0].version')" - if [ "v${version}" != "${GITHUB_REF_NAME}" ]; then - echo "Tag ${GITHUB_REF_NAME} does not match crate version ${version}." >&2 - exit 1 - fi - - name: Build, test, clippy, rustfmt - run: nix flake check -L - - name: Authenticate with crates.io - id: auth - uses: rust-lang/crates-io-auth-action@v1 - - name: Publish - run: nix develop --command cargo publish --locked - env: - CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }} diff --git a/Cargo.toml b/Cargo.toml index 56d3d0f..a648532 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,8 +5,7 @@ edition = "2021" description = "Nix store path, hash, and error types shared by FlakeHub Cache services" license = "Apache-2.0" repository = "https://github.com/DeterminateSystems/flakehub-cache-types" -keywords = ["nix", "cache", "store-path"] -categories = ["data-structures"] +publish = false [lib] name = "flakehub_cache_types" diff --git a/README.md b/README.md index 602860d..b4760da 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ The crate provides: ## Usage ```console -cargo add flakehub-cache-types +cargo add --git https://github.com/DeterminateSystems/flakehub-cache-types flakehub-cache-types ``` ```rust @@ -34,19 +34,6 @@ let hash = StorePathHash::new("ib3sh3pcz10wsmavxvkdbayhqivbghlq")?; automatically. Run the tests with `cargo test`. CI enforces `cargo fmt`, `cargo clippy`, `cargo deny`, and editorconfig conformance. -## Releasing - -Bump the version in `Cargo.toml`, then push a matching tag: - -```console -git tag v0.1.1 -git push origin v0.1.1 -``` - -The release workflow checks that the tag matches the crate version, runs -the tests, and publishes to crates.io using [trusted -publishing](https://crates.io/docs/trusted-publishing). - ## Provenance and license These types derive from [Attic](https://github.com/zhaofengli/attic) by