use std::fmt;
use sha2::{Digest as _, Sha256};
use url::Url;
use crate::policy::{Digest, Ecosystem};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ArtifactReference {
pub ecosystem: Ecosystem,
pub name: String,
pub version: String,
pub filename: String,
pub upstream_url: Url,
pub expected: Vec<Digest>,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)]
pub struct ReferenceId([u8; 32]);
impl ReferenceId {
pub fn compute(reference: &ArtifactReference) -> ReferenceId {
fn part(hasher: &mut Sha256, bytes: &[u8]) {
hasher.update((bytes.len() as u32).to_be_bytes());
hasher.update(bytes);
}
let mut hasher = Sha256::new();
part(&mut hasher, reference.ecosystem.as_tag().as_bytes());
part(&mut hasher, reference.name.as_bytes());
part(&mut hasher, reference.version.as_bytes());
part(&mut hasher, reference.filename.as_bytes());
part(&mut hasher, reference.upstream_url.as_str().as_bytes());
let mut digests: Vec<&Digest> = reference.expected.iter().collect();
digests.sort_by(|left, right| {
left.algorithm
.name()
.cmp(right.algorithm.name())
.then_with(|| left.bytes.cmp(&right.bytes))
});
hasher.update((digests.len() as u32).to_be_bytes());
for digest in digests {
part(&mut hasher, digest.algorithm.name().as_bytes());
part(&mut hasher, &digest.bytes);
}
ReferenceId(hasher.finalize().into())
}
pub fn to_hex(&self) -> String {
hex::encode(self.0)
}
pub fn parse_hex(text: &str) -> Result<ReferenceId, InvalidReferenceId> {
let mut bytes = [0u8; 32];
hex::decode_to_slice(text, &mut bytes).map_err(|_| InvalidReferenceId)?;
Ok(ReferenceId(bytes))
}
pub fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
pub(crate) fn from_bytes(bytes: [u8; 32]) -> ReferenceId {
ReferenceId(bytes)
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct InvalidReferenceId;
impl fmt::Display for InvalidReferenceId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a reference id is 64 hexadecimal characters")
}
}
impl std::error::Error for InvalidReferenceId {}
#[cfg(test)]
mod tests {
use super::*;
use crate::policy::HashAlgorithm;
fn reference() -> ArtifactReference {
ArtifactReference {
ecosystem: Ecosystem::Npm,
name: "left-pad".to_owned(),
version: "1.3.0".to_owned(),
filename: "left-pad-1.3.0.tgz".to_owned(),
upstream_url: Url::parse("https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz")
.expect("a test URL"),
expected: vec![
Digest::parse_hex(HashAlgorithm::Sha512, &"ab".repeat(64)).expect("a digest"),
],
}
}
#[test]
fn a_field_boundary_shift_is_a_different_reference() {
let original = ReferenceId::compute(&reference());
let mut shifted = reference();
shifted.name = "left-pa".to_owned();
shifted.version = "d1.3.0".to_owned();
assert_ne!(
original,
ReferenceId::compute(&shifted),
"the same concatenated bytes split differently must not collide"
);
}
#[test]
fn digest_order_does_not_change_the_id_but_digest_content_does() {
let sha512 = Digest::parse_hex(HashAlgorithm::Sha512, &"ab".repeat(64)).expect("a digest");
let sha1 = Digest::parse_hex(HashAlgorithm::Sha1, &"cd".repeat(20)).expect("a digest");
let mut forwards = reference();
forwards.expected = vec![sha512.clone(), sha1.clone()];
let mut backwards = reference();
backwards.expected = vec![sha1, sha512];
assert_eq!(
ReferenceId::compute(&forwards),
ReferenceId::compute(&backwards)
);
let mut changed = reference();
changed.expected =
vec![Digest::parse_hex(HashAlgorithm::Sha512, &"ac".repeat(64)).expect("a digest")];
assert_ne!(
ReferenceId::compute(&reference()),
ReferenceId::compute(&changed),
"new expected digests are a new reference, never the same one with new bytes"
);
}
}