use crate::{ArtifactId, Error, Intent, KeyFingerprint, Result};
use chrono::{DateTime, TimeZone, Utc};
use std::io::{Read, Write};
pub const MAGIC: &[u8; 4] = b"VWH\0";
pub const VERSION: u16 = 1;
pub const MIN_ARTIFACT_SIZE: usize = 4 + 2 + 1 + 16 + 8 + 1 + 32 + 64;
pub const FLAG_SEALED: u8 = 0b00000001;
pub const ZERO_PUBKEY: [u8; 32] = [0u8; 32];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArtifactState {
Draft,
Signed,
Sealed,
}
#[derive(Debug, Clone)]
pub struct Artifact {
pub version: u16,
pub flags: u8,
pub artifact_id: ArtifactId,
pub timestamp: DateTime<Utc>,
pub intent: Intent,
pub author_pubkey: [u8; 32],
pub signature: [u8; 64],
}
impl Artifact {
pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
if bytes.len() != MIN_ARTIFACT_SIZE {
return Err(Error::FileTooSmall {
expected: MIN_ARTIFACT_SIZE,
actual: bytes.len(),
});
}
let mut cursor = 0;
let magic = &bytes[cursor..cursor + 4];
if magic != MAGIC {
return Err(Error::InvalidMagic);
}
cursor += 4;
let version = u16::from_le_bytes([bytes[cursor], bytes[cursor + 1]]);
if version != VERSION {
return Err(Error::UnsupportedVersion(version));
}
cursor += 2;
let flags = bytes[cursor];
cursor += 1;
let mut artifact_id_bytes = [0u8; 16];
artifact_id_bytes.copy_from_slice(&bytes[cursor..cursor + 16]);
let artifact_id = ArtifactId::from_bytes(artifact_id_bytes);
cursor += 16;
let timestamp_secs =
u64::from_le_bytes(bytes[cursor..cursor + 8].try_into().unwrap());
let timestamp = Utc
.timestamp_opt(timestamp_secs as i64, 0)
.single()
.ok_or_else(|| Error::UnexpectedEof {
field: "timestamp".to_string(),
})?;
cursor += 8;
let intent = Intent::from_u8(bytes[cursor])?;
cursor += 1;
let mut author_pubkey = [0u8; 32];
author_pubkey.copy_from_slice(&bytes[cursor..cursor + 32]);
cursor += 32;
let mut signature = [0u8; 64];
signature.copy_from_slice(&bytes[cursor..cursor + 64]);
Ok(Artifact {
version,
flags,
artifact_id,
timestamp,
intent,
author_pubkey,
signature,
})
}
pub fn signing_bytes(&self) -> Vec<u8> {
let mut bytes = Vec::with_capacity(63);
bytes.extend_from_slice(MAGIC);
bytes.extend_from_slice(&self.version.to_le_bytes());
bytes.extend_from_slice(&self.artifact_id.0);
bytes.extend_from_slice(&(self.timestamp.timestamp() as u64).to_le_bytes());
bytes.push(self.intent.to_u8());
bytes.extend_from_slice(&self.author_pubkey);
bytes
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut bytes = Vec::with_capacity(MIN_ARTIFACT_SIZE);
bytes.extend_from_slice(MAGIC);
bytes.extend_from_slice(&self.version.to_le_bytes());
bytes.push(self.flags);
bytes.extend_from_slice(&self.artifact_id.0);
bytes.extend_from_slice(&(self.timestamp.timestamp() as u64).to_le_bytes());
bytes.push(self.intent.to_u8());
bytes.extend_from_slice(&self.author_pubkey);
bytes.extend_from_slice(&self.signature);
bytes
}
pub fn write_to<W: Write>(&self, writer: &mut W) -> Result<()> {
writer.write_all(&self.to_bytes())?;
Ok(())
}
pub fn read_from<R: Read>(reader: &mut R) -> Result<Self> {
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes)?;
Self::from_bytes(&bytes)
}
pub fn author_fingerprint(&self) -> KeyFingerprint {
KeyFingerprint::new(&self.author_pubkey)
}
pub fn is_sealed(&self) -> bool {
(self.flags & FLAG_SEALED) != 0
}
pub fn has_signature(&self) -> bool {
self.signature != [0u8; 64]
}
pub fn has_author_pubkey(&self) -> bool {
self.author_pubkey != ZERO_PUBKEY
}
pub fn state(&self) -> ArtifactState {
if !self.has_signature() {
ArtifactState::Draft
} else if self.is_sealed() {
ArtifactState::Sealed
} else {
ArtifactState::Signed
}
}
pub fn with_seal_flag(mut self) -> Self {
self.flags |= FLAG_SEALED;
self
}
pub fn without_seal_flag(mut self) -> Self {
self.flags &= !FLAG_SEALED;
self
}
pub fn without_signature(mut self) -> Self {
self.signature = [0u8; 64];
self
}
}
pub struct ArtifactBuilder {
artifact_id: ArtifactId,
timestamp: DateTime<Utc>,
intent: Intent,
author_pubkey: [u8; 32],
flags: u8,
}
impl ArtifactBuilder {
pub fn new(intent: Intent, author_pubkey: [u8; 32]) -> Self {
Self {
artifact_id: ArtifactId::new(),
timestamp: Utc::now(),
intent,
author_pubkey,
flags: 0,
}
}
pub fn with_artifact_id(mut self, id: ArtifactId) -> Self {
self.artifact_id = id;
self
}
pub fn with_timestamp(mut self, timestamp: DateTime<Utc>) -> Self {
self.timestamp = timestamp;
self
}
pub fn with_flags(mut self, flags: u8) -> Self {
self.flags = flags;
self
}
pub fn build_unsigned(&self) -> UnsignedArtifact {
UnsignedArtifact {
version: VERSION,
flags: self.flags,
artifact_id: self.artifact_id,
timestamp: self.timestamp,
intent: self.intent,
author_pubkey: self.author_pubkey,
}
}
}
pub struct UnsignedArtifact {
pub version: u16,
pub flags: u8,
pub artifact_id: ArtifactId,
pub timestamp: DateTime<Utc>,
pub intent: Intent,
pub author_pubkey: [u8; 32],
}
impl UnsignedArtifact {
pub fn signing_bytes(&self) -> Vec<u8> {
let mut bytes = Vec::with_capacity(63);
bytes.extend_from_slice(MAGIC);
bytes.extend_from_slice(&self.version.to_le_bytes());
bytes.extend_from_slice(&self.artifact_id.0);
bytes.extend_from_slice(&(self.timestamp.timestamp() as u64).to_le_bytes());
bytes.push(self.intent.to_u8());
bytes.extend_from_slice(&self.author_pubkey);
bytes
}
pub fn with_signature(self, signature: [u8; 64]) -> Artifact {
Artifact {
version: self.version,
flags: self.flags,
artifact_id: self.artifact_id,
timestamp: self.timestamp,
intent: self.intent,
author_pubkey: self.author_pubkey,
signature,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_artifact_roundtrip() {
let artifact_id = ArtifactId::new();
let timestamp = Utc::now();
let intent = Intent::Lab;
let author_pubkey = [42u8; 32];
let signature = [99u8; 64];
let artifact = Artifact {
version: VERSION,
flags: 0,
artifact_id,
timestamp,
intent,
author_pubkey,
signature,
};
let bytes = artifact.to_bytes();
let parsed = Artifact::from_bytes(&bytes).unwrap();
assert_eq!(parsed.version, artifact.version);
assert_eq!(parsed.artifact_id, artifact.artifact_id);
assert_eq!(parsed.intent, artifact.intent);
assert_eq!(parsed.author_pubkey, artifact.author_pubkey);
assert_eq!(parsed.signature, artifact.signature);
}
#[test]
fn test_invalid_magic() {
let mut bytes = vec![0u8; MIN_ARTIFACT_SIZE];
bytes[0..4].copy_from_slice(b"NOPE");
assert!(matches!(
Artifact::from_bytes(&bytes),
Err(Error::InvalidMagic)
));
}
#[test]
fn test_file_too_small() {
let bytes = vec![0u8; 10];
assert!(matches!(
Artifact::from_bytes(&bytes),
Err(Error::FileTooSmall { .. })
));
}
}