use crate::{PackageId, PackageSource};
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct ContentHash(pub [u8; 32]);
impl ContentHash {
#[must_use]
pub fn of(bytes: &[u8]) -> Self {
Self(*blake3::hash(bytes).as_bytes())
}
#[must_use]
pub const fn genesis() -> Self {
Self([0u8; 32])
}
#[must_use]
pub fn hex(&self) -> String {
let mut s = String::with_capacity(64);
for byte in &self.0 {
s.push_str(&format!("{byte:02x}"));
}
s
}
#[must_use]
pub fn from_hex(s: &str) -> Option<Self> {
if s.len() != 64 {
return None;
}
let mut out = [0u8; 32];
for (i, chunk) in s.as_bytes().chunks(2).enumerate() {
let hex_str = std::str::from_utf8(chunk).ok()?;
out[i] = u8::from_str_radix(hex_str, 16).ok()?;
}
Some(Self(out))
}
#[must_use]
pub fn from_hex_padded(s: &str) -> Option<Self> {
if s.len() == 64 {
Self::from_hex(s)
} else {
Some(Self::of(s.as_bytes()))
}
}
}
impl fmt::Debug for ContentHash {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "ContentHash({}…)", &self.hex()[..16])
}
}
impl fmt::Display for ContentHash {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.hex())
}
}
impl Serialize for ContentHash {
fn serialize<S: serde::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
ser.serialize_str(&self.hex())
}
}
impl<'de> Deserialize<'de> for ContentHash {
fn deserialize<D: serde::Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
let s = String::deserialize(de)?;
if s.len() != 64 {
return Err(serde::de::Error::custom(format!(
"ContentHash expected 64 hex chars, got {}",
s.len()
)));
}
let mut out = [0u8; 32];
for (i, chunk) in s.as_bytes().chunks(2).enumerate() {
let hex_str = std::str::from_utf8(chunk).map_err(serde::de::Error::custom)?;
out[i] = u8::from_str_radix(hex_str, 16).map_err(serde::de::Error::custom)?;
}
Ok(Self(out))
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResolvedPackage {
pub id: PackageId,
pub source: PackageSource,
pub integrity: Option<String>,
#[serde(default)]
pub resolved_dependencies: Vec<PackageId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub links: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Lockfile {
pub resolved: IndexMap<String, ResolvedPackage>,
pub content_addressed_hash: ContentHash,
}
impl Lockfile {
#[must_use]
pub fn empty() -> Self {
Self {
resolved: IndexMap::new(),
content_addressed_hash: ContentHash::genesis(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Registry, Version};
#[test]
fn content_hash_round_trips_through_serde() {
let h = ContentHash::of(b"some bytes");
let j = serde_json::to_string(&h).unwrap();
let parsed: ContentHash = serde_json::from_str(&j).unwrap();
assert_eq!(h, parsed);
}
#[test]
fn content_hash_genesis_is_all_zero() {
assert_eq!(ContentHash::genesis().0, [0u8; 32]);
}
#[test]
fn content_hash_hex_is_64_chars() {
let h = ContentHash::of(b"x");
assert_eq!(h.hex().len(), 64);
}
#[test]
fn lockfile_round_trips_through_serde() {
let mut resolved = IndexMap::new();
resolved.insert(
"serde".to_string(),
ResolvedPackage {
id: PackageId {
name: "serde".into(),
version: Version::new(1, 0, 228),
registry: Registry::CratesIo,
},
source: PackageSource::Registry {
registry: Registry::CratesIo,
registry_name: "serde".into(),
integrity_hash: Some("sha256:abc".into()),
},
integrity: Some("sha256:abc".into()),
resolved_dependencies: vec![],
links: None,
},
);
let l = Lockfile {
resolved,
content_addressed_hash: ContentHash::of(b"snapshot"),
};
let j = serde_json::to_string(&l).unwrap();
let parsed: Lockfile = serde_json::from_str(&j).unwrap();
assert_eq!(l, parsed);
}
}