use sha2::{Digest, Sha256};
use super::{alloc, error::AllocationSite};
pub(crate) mod compile;
pub(crate) mod naming;
pub(crate) mod stamp;
pub(crate) mod tree;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, derive_more::Display)]
#[display("{}", self.as_str())]
pub enum Part {
Text,
Audio,
Vision,
}
impl Part {
#[inline]
pub const fn as_str(&self) -> &'static str {
match self {
Self::Text => "text",
Self::Audio => "audio",
Self::Vision => "vision",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ArtifactLabel(Repr);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum Repr {
Revision(Revision),
Digest([u8; 12]),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct Revision {
publisher: Publisher,
rev: [u8; 7],
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum Publisher {
FluidInference,
FinditStudio,
}
impl Publisher {
const fn as_str(self) -> &'static str {
match self {
Self::FluidInference => "fluidinference",
Self::FinditStudio => "findit-studio",
}
}
}
impl ArtifactLabel {
pub const FLUIDINFERENCE: Self = Self(Repr::Revision(Revision {
publisher: Publisher::FluidInference,
rev: *b"3f978dd",
}));
pub fn from_digests<'p, I>(files: I) -> super::error::Result<Self>
where
I: IntoIterator<Item = (&'p str, [u8; 32])>,
{
let files = files.into_iter();
let mut lines: Vec<(&'p str, [u8; 32])> = Vec::new();
alloc::reserve(
&mut lines,
files.size_hint().0,
AllocationSite::StampDigests,
)?;
for line in files {
alloc::reserve(&mut lines, 1, AllocationSite::StampDigests)?;
lines.push(line);
}
lines.sort_unstable_by(|a, b| a.0.as_bytes().cmp(b.0.as_bytes()));
let mut manifest = Manifest::new();
for (path, digest) in &lines {
manifest.line(path, digest);
}
Ok(Self::from_manifest(&manifest.finish()))
}
pub(crate) fn from_manifest(digest: &[u8; 32]) -> Self {
let mut full = [0u8; 64];
hex_into(digest, &mut full);
let mut prefix = [0u8; 12];
prefix.copy_from_slice(&full[..12]);
Self(Repr::Digest(prefix))
}
#[inline]
pub const fn is_revision(&self) -> bool {
matches!(self.0, Repr::Revision(_))
}
}
pub(crate) fn manifest_sha256<'p>(lines: impl IntoIterator<Item = (&'p str, &'p str)>) -> [u8; 32] {
let mut hasher = Sha256::new();
let mut previous: Option<&str> = None;
for (path, sha) in lines {
debug_assert!(
previous.is_none_or(|p| p.as_bytes() <= path.as_bytes()),
"manifest lines arrive sorted bytewise by path"
);
previous = Some(path);
hasher.update(sha.as_bytes());
hasher.update(b" ");
hasher.update(path.as_bytes());
hasher.update(b"\n");
}
hasher.finalize().into()
}
pub(crate) struct Manifest(Sha256);
impl Manifest {
pub(crate) fn new() -> Self {
Self(Sha256::new())
}
pub(crate) fn line(&mut self, path: &str, digest: &[u8; 32]) {
let mut text = [0u8; 64];
hex_into(digest, &mut text);
self.0.update(text);
self.0.update(b" ");
self.0.update(path.as_bytes());
self.0.update(b"\n");
}
pub(crate) fn finish(self) -> [u8; 32] {
self.0.finalize().into()
}
}
pub(crate) fn hex_into(digest: &[u8; 32], out: &mut [u8; 64]) {
const DIGITS: &[u8; 16] = b"0123456789abcdef";
for (byte, pair) in digest.iter().zip(out.as_chunks_mut::<2>().0) {
*pair = [
DIGITS[usize::from(byte >> 4)],
DIGITS[usize::from(byte & 0x0f)],
];
}
}
pub(crate) fn hex(digest: &[u8; 32]) -> String {
let mut text = [0u8; 64];
hex_into(digest, &mut text);
text.iter().map(|&digit| char::from(digit)).collect()
}
impl core::fmt::Display for ArtifactLabel {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let (head, tail): (&str, &[u8]) = match &self.0 {
Repr::Revision(Revision { publisher, rev }) => (publisher.as_str(), rev),
Repr::Digest(prefix) => ("sha256", prefix),
};
let separator = if self.is_revision() { '@' } else { ':' };
write!(f, "{head}{separator}")?;
tail
.iter()
.try_for_each(|&digit| core::fmt::Write::write_char(f, char::from(digit)))
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error(
"`{text}` is not an artifact label: expected `fluidinference@<7 hex>`, `findit-studio@<7 hex>` or `sha256:<12 hex>`, in lowercase"
)]
pub struct ParseArtifactLabelError {
text: String,
}
impl ParseArtifactLabelError {
#[inline]
pub fn text(&self) -> &str {
&self.text
}
}
impl core::str::FromStr for ArtifactLabel {
type Err = ParseArtifactLabelError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let refuse = || ParseArtifactLabelError { text: s.to_owned() };
let lower_hex = |tail: &str| {
tail
.bytes()
.all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
};
let revision = |publisher: Publisher, tail: &str| -> Result<Self, ParseArtifactLabelError> {
let rev: [u8; 7] = tail.as_bytes().try_into().map_err(|_| refuse())?;
if lower_hex(tail) {
Ok(Self(Repr::Revision(Revision { publisher, rev })))
} else {
Err(refuse())
}
};
if let Some(tail) = s.strip_prefix("fluidinference@") {
revision(Publisher::FluidInference, tail)
} else if let Some(tail) = s.strip_prefix("findit-studio@") {
revision(Publisher::FinditStudio, tail)
} else if let Some(tail) = s.strip_prefix("sha256:") {
let prefix: [u8; 12] = tail.as_bytes().try_into().map_err(|_| refuse())?;
if lower_hex(tail) {
Ok(Self(Repr::Digest(prefix)))
} else {
Err(refuse())
}
} else {
Err(refuse())
}
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for ArtifactLabel {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.collect_str(self)
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for ArtifactLabel {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let text = <String as serde::Deserialize>::deserialize(deserializer)?;
text.parse().map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests;