use crate::error::{Error, Result};
use sigstore_types::Sha256Hash;
#[derive(Debug, Clone, Default)]
pub struct HashTile {
pub nodes: Vec<Sha256Hash>,
}
impl HashTile {
pub fn new() -> Self {
Self { nodes: Vec::new() }
}
pub fn with_nodes(nodes: Vec<Sha256Hash>) -> Self {
Self { nodes }
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut result = Vec::with_capacity(self.nodes.len() * 32);
for node in &self.nodes {
result.extend_from_slice(node.as_bytes());
}
result
}
pub fn from_bytes(data: &[u8]) -> Result<Self> {
if !data.len().is_multiple_of(32) {
return Err(Error::InvalidEntry(format!(
"hash tile length {} is not a multiple of 32",
data.len()
)));
}
let mut nodes = Vec::with_capacity(data.len() / 32);
for chunk in data.chunks_exact(32) {
let hash = Sha256Hash::try_from_slice(chunk)
.map_err(|e| Error::InvalidEntry(format!("invalid hash: {}", e)))?;
nodes.push(hash);
}
Ok(Self { nodes })
}
pub fn len(&self) -> usize {
self.nodes.len()
}
pub fn is_empty(&self) -> bool {
self.nodes.is_empty()
}
}
#[derive(Debug, Clone, Default)]
pub struct EntryBundle {
pub entries: Vec<crate::types::EntryData>,
}
impl EntryBundle {
pub fn new() -> Self {
Self {
entries: Vec::new(),
}
}
pub fn with_entries(entries: Vec<crate::types::EntryData>) -> Self {
Self { entries }
}
pub fn to_bytes(&self) -> Vec<u8> {
let total_size: usize = self.entries.iter().map(|e| 2 + e.len()).sum();
let mut result = Vec::with_capacity(total_size);
for entry in &self.entries {
let len = entry.len() as u16;
result.extend_from_slice(&len.to_be_bytes());
result.extend_from_slice(entry.as_bytes());
}
result
}
pub fn from_bytes(data: &[u8]) -> Result<Self> {
use crate::types::EntryData;
let mut entries = Vec::new();
let mut offset = 0;
while offset < data.len() {
if offset + 2 > data.len() {
return Err(Error::InvalidEntry(format!(
"truncated entry bundle at offset {}",
offset
)));
}
let len = u16::from_be_bytes([data[offset], data[offset + 1]]) as usize;
offset += 2;
if offset + len > data.len() {
return Err(Error::InvalidEntry(format!(
"entry at offset {} claims {} bytes but only {} available",
offset - 2,
len,
data.len() - offset
)));
}
entries.push(EntryData::new(data[offset..offset + len].to_vec()));
offset += len;
}
Ok(Self { entries })
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn push(&mut self, entry: crate::types::EntryData) {
self.entries.push(entry);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::EntryData;
#[test]
fn test_hash_tile_roundtrip() {
let nodes = vec![
Sha256Hash::from_bytes([1u8; 32]),
Sha256Hash::from_bytes([2u8; 32]),
Sha256Hash::from_bytes([3u8; 32]),
];
let tile = HashTile::with_nodes(nodes.clone());
let bytes = tile.to_bytes();
assert_eq!(bytes.len(), 96);
let parsed = HashTile::from_bytes(&bytes).unwrap();
assert_eq!(parsed.nodes, nodes);
}
#[test]
fn test_hash_tile_empty() {
let tile = HashTile::new();
let bytes = tile.to_bytes();
assert!(bytes.is_empty());
let parsed = HashTile::from_bytes(&bytes).unwrap();
assert!(parsed.is_empty());
}
#[test]
fn test_hash_tile_invalid_length() {
let data = vec![0u8; 33]; assert!(HashTile::from_bytes(&data).is_err());
}
#[test]
fn test_entry_bundle_roundtrip() {
let entries = vec![
EntryData::from("hello"),
EntryData::from("world"),
EntryData::from("test entry with more data"),
];
let bundle = EntryBundle::with_entries(entries);
let bytes = bundle.to_bytes();
let parsed = EntryBundle::from_bytes(&bytes).unwrap();
assert_eq!(parsed.entries.len(), 3);
assert_eq!(parsed.entries[0].as_bytes(), b"hello");
assert_eq!(parsed.entries[1].as_bytes(), b"world");
assert_eq!(parsed.entries[2].as_bytes(), b"test entry with more data");
}
#[test]
fn test_entry_bundle_empty() {
let bundle = EntryBundle::new();
let bytes = bundle.to_bytes();
assert!(bytes.is_empty());
let parsed = EntryBundle::from_bytes(&bytes).unwrap();
assert!(parsed.is_empty());
}
#[test]
fn test_entry_bundle_single_entry() {
let entry = EntryData::from("single entry");
let bundle = EntryBundle::with_entries(vec![entry]);
let bytes = bundle.to_bytes();
assert_eq!(bytes.len(), 14);
assert_eq!(&bytes[0..2], &[0, 12]);
let parsed = EntryBundle::from_bytes(&bytes).unwrap();
assert_eq!(parsed.entries.len(), 1);
assert_eq!(parsed.entries[0].as_bytes(), b"single entry");
}
#[test]
fn test_entry_bundle_truncated() {
let data = vec![0, 10];
assert!(EntryBundle::from_bytes(&data).is_err());
let data = vec![0];
assert!(EntryBundle::from_bytes(&data).is_err());
}
}