use crate::atp::object::{MetadataPolicy, ObjectGraph, ObjectId, ObjectKind};
use sha2::{Digest, Sha256};
use std::collections::BTreeMap;
#[cfg(feature = "trace-compression")]
use lz4_flex;
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct ManifestVersion(pub u32);
impl ManifestVersion {
pub const CURRENT: Self = Self(1);
#[must_use]
pub const fn is_supported(self) -> bool {
self.0 <= Self::CURRENT.0
}
}
impl Default for ManifestVersion {
fn default() -> Self {
Self::CURRENT
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum HashAlgorithm {
Sha256,
Blake3,
}
impl HashAlgorithm {
#[must_use]
pub const fn hash_size(self) -> usize {
match self {
Self::Sha256 => 32,
Self::Blake3 => 32,
}
}
#[must_use]
pub const fn is_required(self) -> bool {
matches!(self, Self::Sha256)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ChunkPlan {
pub strategy: ChunkStrategy,
pub target_chunk_size: u64,
pub min_chunk_size: u64,
pub max_chunk_size: u64,
pub cdc_params: Option<CdcParams>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ChunkStrategy {
FixedSize,
ContentDefined,
ObjectSpecific,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CdcParams {
pub window_size: u32,
pub average_chunk_size: u64,
pub normalization: u64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct RaptorQRepairLayout {
pub source_symbols: u32,
pub total_symbols: u32,
pub symbol_size: u32,
pub overhead_ratio: f32,
pub sub_blocks: Vec<SubBlock>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SubBlock {
pub index: u32,
pub source_symbols: u32,
pub esi_range: (u32, u32),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CompressionPolicy {
pub algorithm: CompressionAlgorithm,
pub level: u8,
pub min_size_threshold: u64,
pub apply_to_kinds: Vec<ObjectKind>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum CompressionAlgorithm {
None,
Lz4,
Gzip,
Brotli,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EncryptionPolicy {
pub algorithm: EncryptionAlgorithm,
pub key_derivation: KeyDerivation,
pub apply_to_kinds: Vec<ObjectKind>,
pub encrypt_metadata: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum EncryptionAlgorithm {
None,
ChaCha20Poly1305,
Aes256Gcm,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KeyDerivation {
pub kdf: KeyDerivationFunction,
pub salt: Vec<u8>,
pub iterations: Option<u32>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum KeyDerivationFunction {
Direct,
Pbkdf2Sha256,
Argon2id,
HkdfSha256,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CapabilityPolicy {
pub read_capabilities: Vec<String>,
pub write_capabilities: Vec<String>,
pub verify_capabilities: Vec<String>,
pub delegation_rules: Vec<DelegationRule>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DelegationRule {
pub capability: String,
pub target: String,
pub constraints: Vec<String>,
pub expires_at: Option<u64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum FieldType {
Critical,
Optional,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnknownField {
pub name: String,
pub field_type: FieldType,
pub data: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct MerkleRoot {
hash: [u8; 32],
}
pub fn deterministic_f32_be_bytes(value: f32) -> [u8; 4] {
const CANONICAL_NAN_BITS: u32 = 0x7fc0_0000;
let bits = if value.is_nan() {
CANONICAL_NAN_BITS
} else if value == 0.0 {
0
} else {
value.to_bits()
};
bits.to_be_bytes()
}
pub fn deterministic_f64_be_bytes(value: f64) -> [u8; 8] {
const CANONICAL_NAN_BITS: u64 = 0x7ff8_0000_0000_0000;
let bits = if value.is_nan() {
CANONICAL_NAN_BITS
} else if value == 0.0 {
0
} else {
value.to_bits()
};
bits.to_be_bytes()
}
pub fn deterministic_f32_le_bytes(value: f32) -> [u8; 4] {
const CANONICAL_NAN_BITS: u32 = 0x7fc0_0000;
let bits = if value.is_nan() {
CANONICAL_NAN_BITS
} else if value == 0.0 {
0
} else {
value.to_bits()
};
bits.to_le_bytes()
}
pub fn deterministic_f64_le_bytes(value: f64) -> [u8; 8] {
const CANONICAL_NAN_BITS: u64 = 0x7ff8_0000_0000_0000;
let bits = if value.is_nan() {
CANONICAL_NAN_BITS
} else if value == 0.0 {
0
} else {
value.to_bits()
};
bits.to_le_bytes()
}
impl MerkleRoot {
#[must_use]
pub const fn new(hash: [u8; 32]) -> Self {
Self { hash }
}
#[must_use]
pub const fn hash(&self) -> &[u8; 32] {
&self.hash
}
#[must_use]
pub const fn zero() -> Self {
Self { hash: [0u8; 32] }
}
#[must_use]
pub fn from_graph(graph: &ObjectGraph) -> Self {
let mut hasher = Sha256::new();
for (id, object) in graph.objects() {
hasher.update(id.hash_bytes());
hasher.update([object.metadata.kind as u8]);
if let Some(size) = object.metadata.size_bytes {
hasher.update(size.to_be_bytes());
}
let mut child_indices: Vec<usize> = (0..object.children.len()).collect();
child_indices.sort_by(|&a, &b| object.children[a].name.cmp(&object.children[b].name));
for &idx in &child_indices {
let edge = &object.children[idx];
hasher.update(edge.name.as_bytes());
hasher.update(edge.child_id.hash_bytes());
hasher.update([u8::from(edge.is_symlink)]);
if let Some(target) = &edge.symlink_target {
hasher.update(target.as_os_str().as_encoded_bytes());
}
}
if let Some(content) = &object.content {
let content_hash = Sha256::digest(content);
hasher.update(content_hash);
}
}
Self {
hash: hasher.finalize().into(),
}
}
#[must_use]
pub fn from_manifest_components(
objects: &BTreeMap<ObjectId, ManifestObject>,
chunk_plan: &Option<ChunkPlan>,
raptorq_layout: &Option<RaptorQRepairLayout>,
compression_policy: &Option<CompressionPolicy>,
encryption_policy: &Option<EncryptionPolicy>,
capability_policy: &Option<CapabilityPolicy>,
transform_order: &Option<TransformOrder>,
transform_proof_policy: &Option<TransformProofPolicy>,
repair_groups: &BTreeMap<RepairGroupId, RepairGroup>,
) -> Self {
let mut hasher = Sha256::new();
for (id, obj) in objects {
hasher.update(id.hash_bytes());
hasher.update([obj.kind as u8]);
if let Some(size) = obj.size_bytes {
hasher.update(size.to_be_bytes());
}
for (name, child_id) in &obj.children {
hasher.update(name.as_bytes());
hasher.update(child_id.hash_bytes());
}
if let Some(content_hash) = &obj.content_hash {
hasher.update(content_hash);
}
for symbol in &obj.raptorq_symbols {
hasher.update(symbol.index.to_be_bytes());
hasher.update(symbol.esi.to_be_bytes());
hasher.update(symbol.size_bytes.to_be_bytes());
hasher.update(symbol.content_hash);
hasher.update([u8::from(symbol.is_source)]);
if let Some(group_id) = &symbol.repair_group_id {
hasher.update(group_id.as_bytes());
}
}
}
if let Some(plan) = chunk_plan {
hasher.update([plan.strategy as u8]);
hasher.update(plan.target_chunk_size.to_be_bytes());
hasher.update(plan.min_chunk_size.to_be_bytes());
hasher.update(plan.max_chunk_size.to_be_bytes());
if let Some(cdc) = &plan.cdc_params {
hasher.update(cdc.window_size.to_be_bytes());
hasher.update(cdc.average_chunk_size.to_be_bytes());
hasher.update(cdc.normalization.to_be_bytes());
}
}
if let Some(layout) = raptorq_layout {
hasher.update(layout.source_symbols.to_be_bytes());
hasher.update(layout.total_symbols.to_be_bytes());
hasher.update(layout.symbol_size.to_be_bytes());
hasher.update(deterministic_f32_be_bytes(layout.overhead_ratio));
for sub_block in &layout.sub_blocks {
hasher.update(sub_block.index.to_be_bytes());
hasher.update(sub_block.source_symbols.to_be_bytes());
hasher.update(sub_block.esi_range.0.to_be_bytes());
hasher.update(sub_block.esi_range.1.to_be_bytes());
}
}
if let Some(comp) = compression_policy {
hasher.update([comp.algorithm as u8]);
hasher.update([comp.level]);
hasher.update(comp.min_size_threshold.to_be_bytes());
for kind in &comp.apply_to_kinds {
hasher.update([*kind as u8]);
}
}
if let Some(enc) = encryption_policy {
hasher.update([enc.algorithm as u8]);
hasher.update([enc.key_derivation.kdf as u8]);
hasher.update(&enc.key_derivation.salt);
hasher.update([u8::from(enc.encrypt_metadata)]);
}
if let Some(cap) = capability_policy {
for cap_name in &cap.read_capabilities {
hasher.update(cap_name.as_bytes());
}
for cap_name in &cap.write_capabilities {
hasher.update(cap_name.as_bytes());
}
for cap_name in &cap.verify_capabilities {
hasher.update(cap_name.as_bytes());
}
}
if let Some(order) = transform_order {
for transform in &order.transforms {
hasher.update([*transform as u8]);
}
hasher.update([order.hash_point as u8]);
hasher.update([order.verification_boundary.relay_verifiable as u8]);
hasher.update([order.verification_boundary.mailbox_verifiable as u8]);
hasher.update([u8::from(
order.verification_boundary.e2e_verification_required,
)]);
hasher.update([order.verification_boundary.privacy_level as u8]);
}
if let Some(proof) = transform_proof_policy {
hasher.update([u8::from(proof.enforce_transform_order)]);
hasher.update([u8::from(proof.require_deterministic_transforms)]);
hasher.update([u8::from(proof.allow_lossy_transforms)]);
hasher.update([u8::from(proof.require_plaintext_hash)]);
if let Some(ratio) = proof.max_compression_ratio {
hasher.update(deterministic_f32_be_bytes(ratio));
}
hasher.update([proof.minimum_proof_strength as u8]);
for domain in &proof.encryption_domains {
hasher.update(domain.domain_id.as_bytes());
hasher.update([u8::from(domain.relay_privacy)]);
hasher.update([u8::from(domain.mailbox_privacy)]);
}
}
for (group_id, repair_group) in repair_groups {
hasher.update(group_id.as_bytes());
hasher.update(repair_group.object_id.hash_bytes());
hasher.update(repair_group.source_block_number.to_be_bytes());
hasher.update(repair_group.source_symbols_k.to_be_bytes());
hasher.update(repair_group.k_prime.to_be_bytes());
hasher.update(repair_group.symbol_size.to_be_bytes());
hasher.update(repair_group.chunk_range.start_chunk.to_be_bytes());
hasher.update(repair_group.chunk_range.end_chunk.to_be_bytes());
hasher.update(repair_group.chunk_range.start_offset.to_be_bytes());
hasher.update(repair_group.chunk_range.end_offset.to_be_bytes());
hasher.update(
repair_group
.repair_layout
.total_repair_symbols
.to_be_bytes(),
);
hasher.update(deterministic_f32_be_bytes(
repair_group.repair_layout.overhead_ratio,
));
hasher.update(
repair_group
.repair_layout
.systematic_config
.systematic_rows
.to_be_bytes(),
);
hasher.update(
repair_group
.repair_layout
.systematic_config
.sub_symbols
.to_be_bytes(),
);
hasher.update(
repair_group
.repair_layout
.systematic_config
.alignment
.to_be_bytes(),
);
hasher.update(
repair_group
.repair_layout
.interleaving
.block_size
.to_be_bytes(),
);
hasher.update(repair_group.repair_layout.interleaving.depth.to_be_bytes());
match repair_group.repair_layout.interleaving.pattern_type {
InterleavingType::None => hasher.update([0]),
InterleavingType::Block => hasher.update([1]),
InterleavingType::Matrix => hasher.update([2]),
InterleavingType::Randomized(seed) => {
hasher.update([3]);
hasher.update(seed.to_be_bytes());
}
}
hasher.update(repair_group.hash_domain.domain_id.as_bytes());
hasher.update([repair_group.hash_domain.hash_algorithm as u8]);
hasher.update(&repair_group.hash_domain.context);
hasher.update(repair_group.auth_domain.domain_id.as_bytes());
hasher.update([repair_group.auth_domain.required_proof_strength as u8]);
hasher.update([repair_group.auth_domain.auth_algorithm as u8]);
hasher.update([u8::from(repair_group.auth_domain.peer_identity_required)]);
hasher.update([u8::from(repair_group.auth_domain.transfer_identity_binding)]);
hasher.update([u8::from(repair_group.auth_domain.session_binding)]);
}
Self {
hash: hasher.finalize().into(),
}
}
#[must_use]
pub fn to_hex(&self) -> String {
hex::encode(self.hash)
}
}
impl fmt::Display for MerkleRoot {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "merkle:{}", &self.to_hex()[..16])
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Manifest {
pub version: ManifestVersion,
pub merkle_root: MerkleRoot,
pub metadata_policy: MetadataPolicy,
pub roots: Vec<ObjectId>,
pub objects: BTreeMap<ObjectId, ManifestObject>,
pub hash_algorithms: Vec<HashAlgorithm>,
pub chunk_plan: Option<ChunkPlan>,
pub raptorq_layout: Option<RaptorQRepairLayout>,
pub compression_policy: Option<CompressionPolicy>,
pub encryption_policy: Option<EncryptionPolicy>,
pub capability_policy: Option<CapabilityPolicy>,
pub transform_order: Option<TransformOrder>,
pub transform_proof_policy: Option<TransformProofPolicy>,
pub repair_groups: BTreeMap<RepairGroupId, RepairGroup>,
pub unknown_optional_fields: Vec<UnknownField>,
pub created_timestamp_nanos: u64,
pub schema_id: String,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ManifestObject {
pub id: ObjectId,
pub kind: ObjectKind,
pub size_bytes: Option<u64>,
pub children: BTreeMap<String, ObjectId>,
pub content_hash: Option<[u8; 32]>,
pub chunk_boundaries: Vec<ChunkBoundary>,
pub raptorq_symbols: Vec<RaptorQSymbol>,
pub compression_metadata: Option<CompressionMetadata>,
pub encryption_metadata: Option<EncryptionMetadata>,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct ChunkBoundary {
pub index: u32,
pub byte_offset: u64,
pub size_bytes: u64,
pub content_hash: [u8; 32],
pub strategy: ChunkStrategy,
pub metadata: Option<ChunkMetadata>,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum ChunkMetadata {
BulkFile {
throughput_tier: ThroughputTier,
},
SyncTree {
boundary_hash: u64,
similarity_score: u32,
},
Media {
is_keyframe_boundary: bool,
decoding_priority: u8,
},
SparseImage {
is_sparse_hole: bool,
hole_metadata: Option<SparseHoleMetadata>,
},
Artifact {
build_context: ArtifactBuildContext,
proof_strength: ProofStrength,
},
Stream {
sequence: u64,
early_consumption_safe: bool,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ThroughputTier {
Tail,
Standard,
Bulk,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct SparseHoleMetadata {
pub hole_size: u64,
pub hole_type: String,
pub attributes: BTreeMap<String, Vec<u8>>,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct ArtifactBuildContext {
pub build_system: String,
pub build_timestamp: Option<u64>,
pub environment_hash: [u8; 32],
pub toolchain_version: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ProofStrength {
Basic,
Enhanced,
Cryptographic,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct RaptorQSymbol {
pub index: u32,
pub esi: u32,
pub size_bytes: u32,
pub content_hash: [u8; 32],
pub is_source: bool,
pub repair_group_id: Option<RepairGroupId>,
pub auth_tag: Option<[u8; 32]>,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct RepairGroupId(pub [u8; 16]);
impl RepairGroupId {
#[must_use]
pub fn new(object_id: &ObjectId, source_block_number: u32, k_prime: u32) -> Self {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(b"ATP-G2-RepairGroup");
hasher.update(object_id.hash_bytes());
hasher.update(source_block_number.to_be_bytes());
hasher.update(k_prime.to_be_bytes());
let hash = hasher.finalize();
let mut id = [0u8; 16];
id.copy_from_slice(&hash[..16]);
Self(id)
}
#[must_use]
pub const fn as_bytes(&self) -> &[u8; 16] {
&self.0
}
}
impl fmt::Display for RepairGroupId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for byte in &self.0 {
write!(f, "{:02x}", byte)?;
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct RepairGroup {
pub group_id: RepairGroupId,
pub object_id: ObjectId,
pub source_block_number: u32,
pub chunk_range: ChunkRange,
pub source_symbols_k: u32,
pub k_prime: u32,
pub symbol_size: u32,
pub repair_layout: RepairLayout,
pub hash_domain: HashDomain,
pub transform_policy: Option<TransformOrder>,
pub auth_domain: AuthenticationDomain,
pub capability_policy: Option<CapabilityPolicy>,
pub manifest_root: MerkleRoot,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ChunkRange {
pub start_chunk: u32,
pub end_chunk: u32,
pub start_offset: u64,
pub end_offset: u64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct RepairLayout {
pub total_repair_symbols: u32,
pub overhead_ratio: f32,
pub systematic_config: SystematicConfig,
pub interleaving: InterleavingPattern,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SystematicConfig {
pub systematic_rows: u32,
pub sub_symbols: u32,
pub alignment: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InterleavingPattern {
pub block_size: u32,
pub depth: u32,
pub pattern_type: InterleavingType,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InterleavingType {
None,
Block,
Matrix,
Randomized(u32),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HashDomain {
pub domain_id: String,
pub hash_algorithm: HashAlgorithm,
pub context: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AuthenticationDomain {
pub domain_id: String,
pub required_proof_strength: ProofStrength,
pub auth_algorithm: AuthenticationAlgorithm,
pub peer_identity_required: bool,
pub transfer_identity_binding: bool,
pub session_binding: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AuthenticationAlgorithm {
HmacSha256,
EdDsa,
X25519Ecdh,
}
#[derive(Debug, Clone, PartialEq)]
pub struct CompressionMetadata {
pub algorithm: CompressionAlgorithm,
pub level: u8,
pub original_size: u64,
pub compressed_size: u64,
pub compression_ratio: f32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EncryptionMetadata {
pub algorithm: EncryptionAlgorithm,
pub iv: Vec<u8>,
pub auth_tag: Vec<u8>,
pub key_derivation: KeyDerivation,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct TransformOrder {
pub transforms: Vec<TransformType>,
pub hash_point: HashPoint,
pub verification_boundary: VerificationBoundary,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum TransformType {
Chunking,
Compression,
Encryption,
ErrorCorrection,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum HashPoint {
Plaintext,
PostCompression,
Ciphertext,
MultiPoint,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct VerificationBoundary {
pub relay_verifiable: VerificationLevel,
pub mailbox_verifiable: VerificationLevel,
pub e2e_verification_required: bool,
pub privacy_level: PrivacyLevel,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum VerificationLevel {
None,
TransferIntegrity,
ContentHash,
FullVerification,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum PrivacyLevel {
Public,
MetadataVisible,
SizeVisible,
FullPrivacy,
}
#[derive(Debug, Clone, PartialEq)]
pub struct TransformProofPolicy {
pub enforce_transform_order: bool,
pub require_deterministic_transforms: bool,
pub allow_lossy_transforms: bool,
pub require_plaintext_hash: bool,
pub max_compression_ratio: Option<f32>,
pub encryption_domains: Vec<EncryptionDomain>,
pub minimum_proof_strength: ProofStrength,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct EncryptionDomain {
pub domain_id: String,
pub allowed_kdfs: Vec<KeyDerivationFunction>,
pub relay_privacy: bool,
pub mailbox_privacy: bool,
}
impl Manifest {
pub fn from_graph(
graph: &ObjectGraph,
metadata_policy: MetadataPolicy,
) -> Result<Self, ManifestError> {
Self::from_graph_with_policies(
graph,
metadata_policy,
vec![HashAlgorithm::Sha256], None, None, None, None, None, None, None, )
}
pub fn from_graph_with_policies(
graph: &ObjectGraph,
metadata_policy: MetadataPolicy,
hash_algorithms: Vec<HashAlgorithm>,
chunk_plan: Option<ChunkPlan>,
raptorq_layout: Option<RaptorQRepairLayout>,
compression_policy: Option<CompressionPolicy>,
encryption_policy: Option<EncryptionPolicy>,
capability_policy: Option<CapabilityPolicy>,
transform_order: Option<TransformOrder>,
transform_proof_policy: Option<TransformProofPolicy>,
) -> Result<Self, ManifestError> {
if !hash_algorithms.contains(&HashAlgorithm::Sha256) {
return Err(ManifestError::InvalidFormat(
"SHA-256 is required in hash_algorithms".to_string(),
));
}
let mut manifest_objects = BTreeMap::new();
let roots: Vec<_> = graph.roots().cloned().collect();
for (id, object) in graph.objects() {
let content_hash = if object.id.is_content_addressed() {
Some(*object.id.hash_bytes())
} else if let Some(content) = &object.content {
let hash = Sha256::digest(content);
Some(hash.into())
} else {
None
};
let manifest_obj = ManifestObject {
id: id.clone(),
kind: object.metadata.kind,
size_bytes: object.metadata.size_bytes,
children: object
.children
.iter()
.map(|edge| (edge.name.clone(), edge.child_id.clone()))
.collect(),
content_hash,
chunk_boundaries: Self::compute_chunk_boundaries(
chunk_plan.as_ref(),
object,
content_hash.as_ref(),
),
raptorq_symbols: Self::compute_raptorq_symbols(
raptorq_layout.as_ref(),
object,
content_hash.as_ref(),
),
compression_metadata: Self::compute_compression_metadata(
compression_policy.as_ref(),
object,
),
encryption_metadata: Self::compute_encryption_metadata(
encryption_policy.as_ref(),
object,
),
};
manifest_objects.insert(id.clone(), manifest_obj);
}
let mut repair_groups = Self::compute_repair_groups(
raptorq_layout.as_ref(),
&manifest_objects,
MerkleRoot::zero(),
);
let merkle_root = MerkleRoot::from_manifest_components(
&manifest_objects,
&chunk_plan,
&raptorq_layout,
&compression_policy,
&encryption_policy,
&capability_policy,
&transform_order,
&transform_proof_policy,
&repair_groups,
);
for repair_group in repair_groups.values_mut() {
repair_group.manifest_root = merkle_root.clone();
}
Self::bind_repair_symbol_auth_tags(&mut manifest_objects, &repair_groups);
Ok(Self {
version: ManifestVersion::CURRENT,
merkle_root,
metadata_policy,
roots,
objects: manifest_objects,
hash_algorithms,
chunk_plan,
raptorq_layout,
compression_policy,
encryption_policy,
capability_policy,
transform_order,
transform_proof_policy,
repair_groups,
unknown_optional_fields: Vec::new(),
created_timestamp_nanos: 0,
schema_id: "atp.manifest.v1".to_string(),
})
}
pub fn validate(&self) -> Result<(), ManifestError> {
if !self.version.is_supported() {
return Err(ManifestError::UnsupportedVersion(self.version));
}
if !self.hash_algorithms.contains(&HashAlgorithm::Sha256) {
return Err(ManifestError::InvalidFormat(
"SHA-256 is required in hash_algorithms".to_string(),
));
}
for root_id in &self.roots {
if !self.objects.contains_key(root_id) {
return Err(ManifestError::RootObjectMissing(root_id.clone()));
}
}
for manifest_obj in self.objects.values() {
for child_id in manifest_obj.children.values() {
if !self.objects.contains_key(child_id) {
return Err(ManifestError::ChildObjectMissing(child_id.clone()));
}
}
let mut prev_offset = 0;
for chunk in &manifest_obj.chunk_boundaries {
if chunk.byte_offset < prev_offset {
return Err(ManifestError::InvalidFormat(
"chunk boundaries must be in ascending order".to_string(),
));
}
prev_offset = chunk.byte_offset + chunk.size_bytes;
}
if let Some(layout) = &self.raptorq_layout {
for symbol in &manifest_obj.raptorq_symbols {
if symbol.esi >= layout.total_symbols {
return Err(ManifestError::InvalidFormat(
"RaptorQ symbol ESI exceeds layout total_symbols".to_string(),
));
}
}
}
}
if let Some(layout) = &self.raptorq_layout {
if layout.source_symbols > layout.total_symbols {
return Err(ManifestError::InvalidFormat(
"RaptorQ source_symbols cannot exceed total_symbols".to_string(),
));
}
if layout.overhead_ratio < 0.0 || layout.overhead_ratio > 1.0 {
return Err(ManifestError::InvalidFormat(
"RaptorQ overhead_ratio must be between 0.0 and 1.0".to_string(),
));
}
}
if let Some(plan) = &self.chunk_plan {
if plan.min_chunk_size > plan.target_chunk_size {
return Err(ManifestError::InvalidFormat(
"chunk min_chunk_size cannot exceed target_chunk_size".to_string(),
));
}
if plan.target_chunk_size > plan.max_chunk_size {
return Err(ManifestError::InvalidFormat(
"chunk target_chunk_size cannot exceed max_chunk_size".to_string(),
));
}
if matches!(plan.strategy, ChunkStrategy::ContentDefined) && plan.cdc_params.is_none() {
return Err(ManifestError::InvalidFormat(
"content-defined chunking requires cdc_params".to_string(),
));
}
}
for field in &self.unknown_optional_fields {
if matches!(field.field_type, FieldType::Critical) {
return Err(ManifestError::UnknownCriticalField(field.name.clone()));
}
}
self.validate_transform_policies()?;
self.validate_repair_groups()?;
let computed_root = MerkleRoot::from_manifest_components(
&self.objects,
&self.chunk_plan,
&self.raptorq_layout,
&self.compression_policy,
&self.encryption_policy,
&self.capability_policy,
&self.transform_order,
&self.transform_proof_policy,
&self.repair_groups,
);
if computed_root != self.merkle_root {
return Err(ManifestError::MerkleRootMismatch {
expected: self.merkle_root.clone(),
computed: computed_root,
});
}
Ok(())
}
fn validate_transform_policies(&self) -> Result<(), ManifestError> {
if let Some(proof_policy) = &self.transform_proof_policy {
if proof_policy.enforce_transform_order {
if let Some(order) = &self.transform_order {
Self::validate_transform_order_consistency(
order,
self.compression_policy.as_ref(),
self.encryption_policy.as_ref(),
)?;
} else {
return Err(ManifestError::TransformPolicyViolation(
"transform order enforcement requires transform_order specification"
.to_string(),
));
}
}
Self::validate_verification_boundary(self.transform_order.as_ref(), proof_policy)?;
Self::validate_lossy_transforms_disclosure(
self.compression_policy.as_ref(),
proof_policy,
)?;
Self::validate_encryption_domains(self.encryption_policy.as_ref(), proof_policy)?;
if proof_policy.require_plaintext_hash {
Self::validate_plaintext_hash_availability(self.transform_order.as_ref())?;
}
}
if let Some(order) = &self.transform_order {
Self::validate_transform_order_semantics(order)?;
}
Ok(())
}
fn validate_transform_order_consistency(
order: &TransformOrder,
compression_policy: Option<&CompressionPolicy>,
encryption_policy: Option<&EncryptionPolicy>,
) -> Result<(), ManifestError> {
let has_compression = compression_policy
.is_some_and(|policy| !matches!(policy.algorithm, CompressionAlgorithm::None));
let has_encryption = encryption_policy
.is_some_and(|policy| !matches!(policy.algorithm, EncryptionAlgorithm::None));
if has_compression && !order.transforms.contains(&TransformType::Compression) {
return Err(ManifestError::TransformOrderViolation(
"compression policy specified but compression transform not in order".to_string(),
));
}
if !has_compression && order.transforms.contains(&TransformType::Compression) {
return Err(ManifestError::TransformOrderViolation(
"compression transform in order but no compression policy".to_string(),
));
}
if has_encryption && !order.transforms.contains(&TransformType::Encryption) {
return Err(ManifestError::TransformOrderViolation(
"encryption policy specified but encryption transform not in order".to_string(),
));
}
if !has_encryption && order.transforms.contains(&TransformType::Encryption) {
return Err(ManifestError::TransformOrderViolation(
"encryption transform in order but no encryption policy".to_string(),
));
}
if let (Some(comp_pos), Some(enc_pos)) = (
order
.transforms
.iter()
.position(|&t| t == TransformType::Compression),
order
.transforms
.iter()
.position(|&t| t == TransformType::Encryption),
) {
if comp_pos >= enc_pos {
return Err(ManifestError::TransformOrderViolation(
"compression must come before encryption in transform order".to_string(),
));
}
}
Ok(())
}
fn validate_verification_boundary(
transform_order: Option<&TransformOrder>,
_proof_policy: &TransformProofPolicy,
) -> Result<(), ManifestError> {
if let Some(order) = transform_order {
let boundary = &order.verification_boundary;
if boundary.relay_verifiable == VerificationLevel::ContentHash
&& order.transforms.contains(&TransformType::Encryption)
&& order.hash_point == HashPoint::Ciphertext
{
return Err(ManifestError::AmbiguousVerificationMode(
"relay cannot verify content hash of encrypted content".to_string(),
));
}
if boundary.privacy_level == PrivacyLevel::Public
&& order.transforms.contains(&TransformType::Encryption)
{
return Err(ManifestError::PrivacyPolicyViolation(
"public privacy level inconsistent with encryption".to_string(),
));
}
if boundary.relay_verifiable == VerificationLevel::FullVerification
&& boundary.privacy_level != PrivacyLevel::Public
{
return Err(ManifestError::PrivacyPolicyViolation(
"full relay verification requires public privacy level".to_string(),
));
}
}
Ok(())
}
fn validate_lossy_transforms_disclosure(
compression_policy: Option<&CompressionPolicy>,
proof_policy: &TransformProofPolicy,
) -> Result<(), ManifestError> {
if let Some(comp) = compression_policy {
let is_lossy = matches!(comp.algorithm, CompressionAlgorithm::Brotli);
if is_lossy && !proof_policy.allow_lossy_transforms {
return Err(ManifestError::LossyTransformNotAllowed(
"lossy compression used but not allowed by proof policy".to_string(),
));
}
if let Some(_max_ratio) = proof_policy.max_compression_ratio {
}
}
Ok(())
}
fn validate_encryption_domains(
encryption_policy: Option<&EncryptionPolicy>,
proof_policy: &TransformProofPolicy,
) -> Result<(), ManifestError> {
if let Some(enc) = encryption_policy {
let allowed = proof_policy
.encryption_domains
.iter()
.any(|domain| domain.allowed_kdfs.contains(&enc.key_derivation.kdf));
if !proof_policy.encryption_domains.is_empty() && !allowed {
return Err(ManifestError::EncryptionDomainViolation(
"encryption KDF not allowed in any specified domain".to_string(),
));
}
}
Ok(())
}
fn validate_plaintext_hash_availability(
transform_order: Option<&TransformOrder>,
) -> Result<(), ManifestError> {
if let Some(order) = transform_order {
if order.hash_point == HashPoint::Ciphertext
&& order.transforms.contains(&TransformType::Encryption)
{
return Err(ManifestError::PlaintextHashUnavailable(
"plaintext hash required but only ciphertext hash computed".to_string(),
));
}
}
Ok(())
}
fn validate_transform_order_semantics(order: &TransformOrder) -> Result<(), ManifestError> {
if let (Some(chunk_pos), Some(ec_pos)) = (
order
.transforms
.iter()
.position(|&t| t == TransformType::Chunking),
order
.transforms
.iter()
.position(|&t| t == TransformType::ErrorCorrection),
) {
if chunk_pos >= ec_pos {
return Err(ManifestError::TransformOrderViolation(
"chunking must come before error correction".to_string(),
));
}
}
match order.hash_point {
HashPoint::Plaintext => {
}
HashPoint::PostCompression => {
if !order.transforms.contains(&TransformType::Compression) {
return Err(ManifestError::TransformOrderViolation(
"post-compression hash point requires compression transform".to_string(),
));
}
}
HashPoint::Ciphertext => {
if !order.transforms.contains(&TransformType::Encryption) {
return Err(ManifestError::TransformOrderViolation(
"ciphertext hash point requires encryption transform".to_string(),
));
}
}
HashPoint::MultiPoint => {
if order.transforms.len() < 2 {
return Err(ManifestError::TransformOrderViolation(
"multi-point hashing requires multiple transforms".to_string(),
));
}
}
}
Ok(())
}
fn validate_repair_groups(&self) -> Result<(), ManifestError> {
for repair_group in self.repair_groups.values() {
self.validate_repair_group(repair_group)?;
}
for manifest_obj in self.objects.values() {
for symbol in &manifest_obj.raptorq_symbols {
if let Some(group_id) = &symbol.repair_group_id {
if !self.repair_groups.contains_key(group_id) {
return Err(ManifestError::RepairGroupReferenceError(format!(
"symbol references non-existent repair group: {group_id}"
)));
}
let repair_group = &self.repair_groups[group_id];
if repair_group.object_id != manifest_obj.id {
return Err(ManifestError::RepairGroupReferenceError(format!(
"symbol in object {} references repair group for object {}",
manifest_obj.id, repair_group.object_id
)));
}
let Some(auth_tag) = symbol.auth_tag else {
return Err(ManifestError::RepairGroupAuthenticationError(format!(
"repair symbol missing authentication tag: group {group_id}"
)));
};
let expected_tag = Self::compute_repair_symbol_auth_tag(repair_group, symbol);
if auth_tag != expected_tag {
return Err(ManifestError::RepairGroupAuthenticationError(format!(
"repair symbol authentication tag mismatch: group {group_id}, symbol {}",
symbol.index
)));
}
}
}
}
if let Some(layout) = &self.raptorq_layout {
for repair_group in self.repair_groups.values() {
self.validate_repair_group_layout_consistency(repair_group, layout)?;
}
}
Ok(())
}
fn validate_repair_group(&self, repair_group: &RepairGroup) -> Result<(), ManifestError> {
let expected_id = RepairGroupId::new(
&repair_group.object_id,
repair_group.source_block_number,
repair_group.k_prime,
);
if repair_group.group_id != expected_id {
return Err(ManifestError::RepairGroupValidationError(format!(
"repair group ID mismatch: expected {expected_id}, got {}",
repair_group.group_id
)));
}
if !self.objects.contains_key(&repair_group.object_id) {
return Err(ManifestError::RepairGroupValidationError(format!(
"repair group references non-existent object: {}",
repair_group.object_id
)));
}
if repair_group.k_prime < repair_group.source_symbols_k {
return Err(ManifestError::RepairGroupValidationError(format!(
"k_prime ({}) must be >= source_symbols_k ({}) for group {}",
repair_group.k_prime, repair_group.source_symbols_k, repair_group.group_id
)));
}
if repair_group.symbol_size == 0 {
return Err(ManifestError::RepairGroupValidationError(format!(
"symbol_size cannot be zero for group {}",
repair_group.group_id
)));
}
if repair_group.chunk_range.end_chunk <= repair_group.chunk_range.start_chunk {
return Err(ManifestError::RepairGroupValidationError(format!(
"invalid chunk range for group {}: end <= start",
repair_group.group_id
)));
}
if repair_group.chunk_range.end_offset <= repair_group.chunk_range.start_offset {
return Err(ManifestError::RepairGroupValidationError(format!(
"invalid byte range for group {}: end <= start",
repair_group.group_id
)));
}
if repair_group.repair_layout.total_repair_symbols == 0 {
return Err(ManifestError::RepairGroupValidationError(format!(
"total_repair_symbols cannot be zero for group {}",
repair_group.group_id
)));
}
if repair_group.repair_layout.overhead_ratio < 0.0
|| repair_group.repair_layout.overhead_ratio > 10.0
{
return Err(ManifestError::RepairGroupValidationError(format!(
"overhead_ratio ({}) out of range [0.0, 10.0] for group {}",
repair_group.repair_layout.overhead_ratio, repair_group.group_id
)));
}
if repair_group.repair_layout.systematic_config.systematic_rows == 0 {
return Err(ManifestError::RepairGroupValidationError(format!(
"systematic_rows cannot be zero for group {}",
repair_group.group_id
)));
}
if repair_group.auth_domain.domain_id.is_empty() {
return Err(ManifestError::RepairGroupValidationError(format!(
"authentication domain_id cannot be empty for group {}",
repair_group.group_id
)));
}
if repair_group.manifest_root != self.merkle_root {
return Err(ManifestError::RepairGroupValidationError(format!(
"repair group manifest_root mismatch for group {}: expected {}, got {}",
repair_group.group_id, self.merkle_root, repair_group.manifest_root
)));
}
Ok(())
}
fn validate_repair_group_layout_consistency(
&self,
repair_group: &RepairGroup,
layout: &RaptorQRepairLayout,
) -> Result<(), ManifestError> {
if repair_group.symbol_size != layout.symbol_size {
return Err(ManifestError::RepairGroupValidationError(format!(
"repair group symbol_size ({}) does not match layout symbol_size ({}) for group {}",
repair_group.symbol_size, layout.symbol_size, repair_group.group_id
)));
}
if repair_group.source_symbols_k > layout.source_symbols {
return Err(ManifestError::RepairGroupValidationError(format!(
"repair group source_symbols_k ({}) exceeds layout source_symbols ({}) for group {}",
repair_group.source_symbols_k, layout.source_symbols, repair_group.group_id
)));
}
if repair_group.repair_layout.total_repair_symbols > layout.total_symbols {
return Err(ManifestError::RepairGroupValidationError(format!(
"repair group total_repair_symbols ({}) exceeds layout total_symbols ({}) for group {}",
repair_group.repair_layout.total_repair_symbols,
layout.total_symbols,
repair_group.group_id
)));
}
Ok(())
}
#[must_use]
pub fn object_count(&self) -> usize {
self.objects.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.objects.is_empty()
}
#[must_use]
pub fn to_canonical_bytes(&self) -> Vec<u8> {
let mut bytes = Vec::new();
bytes.extend_from_slice(b"ATPM"); bytes.extend_from_slice(&self.version.0.to_be_bytes());
bytes.extend_from_slice(&self.created_timestamp_nanos.to_be_bytes());
Self::write_string(&mut bytes, &self.schema_id);
bytes.extend_from_slice(self.merkle_root.hash());
bytes.extend_from_slice(
&u32::try_from(self.hash_algorithms.len())
.expect("length exceeds u32 limit")
.to_be_bytes(),
);
for algo in &self.hash_algorithms {
bytes.push(*algo as u8);
}
bytes.extend_from_slice(
&u32::try_from(self.roots.len())
.expect("length exceeds u32 limit")
.to_be_bytes(),
);
for root in &self.roots {
bytes.extend_from_slice(root.hash_bytes());
}
bytes.extend_from_slice(
&u32::try_from(self.objects.len())
.expect("length exceeds u32 limit")
.to_be_bytes(),
);
for (id, obj) in &self.objects {
bytes.extend_from_slice(id.hash_bytes());
bytes.push(obj.kind as u8);
if let Some(size) = obj.size_bytes {
bytes.push(1);
bytes.extend_from_slice(&size.to_be_bytes());
} else {
bytes.push(0);
}
if let Some(hash) = &obj.content_hash {
bytes.push(1);
bytes.extend_from_slice(hash);
} else {
bytes.push(0);
}
bytes.extend_from_slice(
&u32::try_from(obj.children.len())
.expect("length exceeds u32 limit")
.to_be_bytes(),
);
for (name, child_id) in &obj.children {
Self::write_string(&mut bytes, name);
bytes.extend_from_slice(child_id.hash_bytes());
}
bytes.extend_from_slice(
&u32::try_from(obj.chunk_boundaries.len())
.expect("length exceeds u32 limit")
.to_be_bytes(),
);
for chunk in &obj.chunk_boundaries {
bytes.extend_from_slice(&chunk.index.to_be_bytes());
bytes.extend_from_slice(&chunk.byte_offset.to_be_bytes());
bytes.extend_from_slice(&chunk.size_bytes.to_be_bytes());
bytes.extend_from_slice(&chunk.content_hash);
bytes.push(chunk.strategy as u8);
}
bytes.extend_from_slice(
&u32::try_from(obj.raptorq_symbols.len())
.expect("length exceeds u32 limit")
.to_be_bytes(),
);
for symbol in &obj.raptorq_symbols {
bytes.extend_from_slice(&symbol.index.to_be_bytes());
bytes.extend_from_slice(&symbol.esi.to_be_bytes());
bytes.extend_from_slice(&symbol.size_bytes.to_be_bytes());
bytes.extend_from_slice(&symbol.content_hash);
bytes.push(u8::from(symbol.is_source));
}
}
if let Some(plan) = &self.chunk_plan {
bytes.push(1); bytes.push(plan.strategy as u8);
bytes.extend_from_slice(&plan.target_chunk_size.to_be_bytes());
bytes.extend_from_slice(&plan.min_chunk_size.to_be_bytes());
bytes.extend_from_slice(&plan.max_chunk_size.to_be_bytes());
if let Some(cdc) = &plan.cdc_params {
bytes.push(1);
bytes.extend_from_slice(&cdc.window_size.to_be_bytes());
bytes.extend_from_slice(&cdc.average_chunk_size.to_be_bytes());
bytes.extend_from_slice(&cdc.normalization.to_be_bytes());
} else {
bytes.push(0);
}
} else {
bytes.push(0); }
if let Some(layout) = &self.raptorq_layout {
bytes.push(1);
bytes.extend_from_slice(&layout.source_symbols.to_be_bytes());
bytes.extend_from_slice(&layout.total_symbols.to_be_bytes());
bytes.extend_from_slice(&layout.symbol_size.to_be_bytes());
bytes.extend_from_slice(&deterministic_f32_be_bytes(layout.overhead_ratio));
bytes.extend_from_slice(
&u32::try_from(layout.sub_blocks.len())
.expect("length exceeds u32 limit")
.to_be_bytes(),
);
for sub_block in &layout.sub_blocks {
bytes.extend_from_slice(&sub_block.index.to_be_bytes());
bytes.extend_from_slice(&sub_block.source_symbols.to_be_bytes());
bytes.extend_from_slice(&sub_block.esi_range.0.to_be_bytes());
bytes.extend_from_slice(&sub_block.esi_range.1.to_be_bytes());
}
} else {
bytes.push(0);
}
if let Some(comp) = &self.compression_policy {
bytes.push(1);
bytes.push(comp.algorithm as u8);
bytes.push(comp.level);
bytes.extend_from_slice(&comp.min_size_threshold.to_be_bytes());
bytes.extend_from_slice(
&u32::try_from(comp.apply_to_kinds.len())
.expect("length exceeds u32 limit")
.to_be_bytes(),
);
for kind in &comp.apply_to_kinds {
bytes.push(*kind as u8);
}
} else {
bytes.push(0);
}
if let Some(enc) = &self.encryption_policy {
bytes.push(1);
bytes.push(enc.algorithm as u8);
bytes.push(enc.key_derivation.kdf as u8);
Self::write_bytes(&mut bytes, &enc.key_derivation.salt);
if let Some(iterations) = enc.key_derivation.iterations {
bytes.push(1);
bytes.extend_from_slice(&iterations.to_be_bytes());
} else {
bytes.push(0);
}
bytes.push(u8::from(enc.encrypt_metadata));
bytes.extend_from_slice(
&u32::try_from(enc.apply_to_kinds.len())
.expect("length exceeds u32 limit")
.to_be_bytes(),
);
for kind in &enc.apply_to_kinds {
bytes.push(*kind as u8);
}
} else {
bytes.push(0);
}
if let Some(cap) = &self.capability_policy {
bytes.push(1);
Self::write_string_vec(&mut bytes, &cap.read_capabilities);
Self::write_string_vec(&mut bytes, &cap.write_capabilities);
Self::write_string_vec(&mut bytes, &cap.verify_capabilities);
} else {
bytes.push(0);
}
bytes
}
fn write_string(bytes: &mut Vec<u8>, s: &str) {
bytes.extend_from_slice(
&u32::try_from(s.len())
.expect("length exceeds u32 limit")
.to_be_bytes(),
);
bytes.extend_from_slice(s.as_bytes());
}
fn write_bytes(bytes: &mut Vec<u8>, data: &[u8]) {
bytes.extend_from_slice(
&u32::try_from(data.len())
.expect("length exceeds u32 limit")
.to_be_bytes(),
);
bytes.extend_from_slice(data);
}
fn write_string_vec(bytes: &mut Vec<u8>, strings: &[String]) {
bytes.extend_from_slice(
&u32::try_from(strings.len())
.expect("length exceeds u32 limit")
.to_be_bytes(),
);
for s in strings {
Self::write_string(bytes, s);
}
}
fn compute_chunk_boundaries(
chunk_plan: Option<&ChunkPlan>,
object: &crate::atp::object::Object,
_content_hash: Option<&[u8; 32]>,
) -> Vec<ChunkBoundary> {
let Some(plan) = chunk_plan else {
return Vec::new();
};
let Some(size) = object.metadata.size_bytes else {
return Vec::new();
};
if size < plan.min_chunk_size {
return Vec::new();
}
let chunk_size = plan
.cdc_params
.as_ref()
.map_or(plan.target_chunk_size, |cdc| cdc.average_chunk_size);
let mut boundaries = Vec::new();
let mut offset = 0u64;
let mut index = 0u32;
while offset < size {
let chunk_end = std::cmp::min(offset + chunk_size, size);
let content_hash = if let Some(ref content) = object.content {
let chunk_start = offset as usize;
let chunk_end_usize = chunk_end as usize;
if chunk_end_usize <= content.len() {
let chunk_data = &content[chunk_start..chunk_end_usize];
let mut hasher = Sha256::new();
hasher.update(chunk_data);
let result = hasher.finalize();
result.into()
} else {
[0u8; 32] }
} else {
[0u8; 32] };
boundaries.push(ChunkBoundary {
index,
byte_offset: offset,
size_bytes: chunk_end - offset,
content_hash,
strategy: ChunkStrategy::FixedSize,
metadata: None,
});
offset = chunk_end;
index += 1;
}
boundaries
}
fn compute_raptorq_symbols(
raptorq_layout: Option<&RaptorQRepairLayout>,
object: &crate::atp::object::Object,
content_hash: Option<&[u8; 32]>,
) -> Vec<RaptorQSymbol> {
let Some(layout) = raptorq_layout else {
return Vec::new();
};
let Some(content_hash) = content_hash else {
return Vec::new();
};
let Some(size) = object.metadata.size_bytes else {
return Vec::new();
};
let symbol_size = layout.symbol_size.max(1);
let symbol_size_u64 = u64::from(symbol_size);
let num_symbols = size.div_ceil(symbol_size_u64) as u32;
let group_id = RepairGroupId::new(&object.id, 0, num_symbols);
let mut symbols = Vec::new();
for i in 0..num_symbols {
let symbol_hash = if let Some(ref content) = object.content {
let symbol_start = (u64::from(i) * symbol_size_u64) as usize;
let symbol_end = std::cmp::min(symbol_start + symbol_size as usize, content.len());
if symbol_end <= content.len() {
let symbol_data = &content[symbol_start..symbol_end];
let mut hasher = Sha256::new();
hasher.update(symbol_data);
let result = hasher.finalize();
result.into()
} else {
*content_hash }
} else {
*content_hash };
symbols.push(RaptorQSymbol {
index: i,
esi: i, size_bytes: symbol_size,
content_hash: symbol_hash,
is_source: true, repair_group_id: Some(group_id.clone()),
auth_tag: None,
});
}
symbols
}
fn compute_repair_groups(
raptorq_layout: Option<&RaptorQRepairLayout>,
objects: &BTreeMap<ObjectId, ManifestObject>,
manifest_root: MerkleRoot,
) -> BTreeMap<RepairGroupId, RepairGroup> {
let Some(layout) = raptorq_layout else {
return BTreeMap::new();
};
let mut repair_groups = BTreeMap::new();
for object in objects.values() {
let Some(first_symbol) = object.raptorq_symbols.first() else {
continue;
};
let Some(group_id) = first_symbol.repair_group_id.clone() else {
continue;
};
let source_symbols_k = object.raptorq_symbols.len() as u32;
if source_symbols_k == 0 {
continue;
}
let total_repair_symbols = layout.total_symbols.saturating_sub(layout.source_symbols);
let object_size = object.size_bytes.unwrap_or(0);
repair_groups.insert(
group_id.clone(),
RepairGroup {
group_id,
object_id: object.id.clone(),
source_block_number: 0,
chunk_range: ChunkRange {
start_chunk: 0,
end_chunk: source_symbols_k,
start_offset: 0,
end_offset: object_size,
},
source_symbols_k,
k_prime: source_symbols_k,
symbol_size: layout.symbol_size.max(1),
repair_layout: RepairLayout {
total_repair_symbols: total_repair_symbols.max(1),
overhead_ratio: layout.overhead_ratio,
systematic_config: SystematicConfig {
systematic_rows: source_symbols_k,
sub_symbols: 1,
alignment: 8,
},
interleaving: InterleavingPattern {
block_size: source_symbols_k.max(1),
depth: 1,
pattern_type: InterleavingType::None,
},
},
hash_domain: HashDomain {
domain_id: "atp-g2-symbol-sha256-v1".to_string(),
hash_algorithm: HashAlgorithm::Sha256,
context: object.id.hash_bytes().to_vec(),
},
transform_policy: None,
auth_domain: AuthenticationDomain {
domain_id: "atp-g2-symbol-auth-v1".to_string(),
required_proof_strength: ProofStrength::Basic,
auth_algorithm: AuthenticationAlgorithm::HmacSha256,
peer_identity_required: false,
transfer_identity_binding: true,
session_binding: true,
},
capability_policy: None,
manifest_root: manifest_root.clone(),
},
);
}
repair_groups
}
fn bind_repair_symbol_auth_tags(
objects: &mut BTreeMap<ObjectId, ManifestObject>,
repair_groups: &BTreeMap<RepairGroupId, RepairGroup>,
) {
for object in objects.values_mut() {
for symbol in &mut object.raptorq_symbols {
let Some(group_id) = &symbol.repair_group_id else {
continue;
};
if let Some(repair_group) = repair_groups.get(group_id) {
symbol.auth_tag =
Some(Self::compute_repair_symbol_auth_tag(repair_group, symbol));
}
}
}
}
fn compute_repair_symbol_auth_tag(
repair_group: &RepairGroup,
symbol: &RaptorQSymbol,
) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(b"ATP-G2-RepairSymbolAuth-v1");
hasher.update(repair_group.group_id.as_bytes());
hasher.update(repair_group.object_id.hash_bytes());
hasher.update(repair_group.source_block_number.to_be_bytes());
hasher.update(repair_group.k_prime.to_be_bytes());
hasher.update(repair_group.symbol_size.to_be_bytes());
hasher.update(repair_group.manifest_root.hash());
hasher.update(repair_group.auth_domain.domain_id.as_bytes());
hasher.update([repair_group.auth_domain.auth_algorithm as u8]);
hasher.update(symbol.index.to_be_bytes());
hasher.update(symbol.esi.to_be_bytes());
hasher.update(symbol.size_bytes.to_be_bytes());
hasher.update(symbol.content_hash);
hasher.update([u8::from(symbol.is_source)]);
hasher.finalize().into()
}
fn compute_compression_metadata(
compression_policy: Option<&CompressionPolicy>,
object: &crate::atp::object::Object,
) -> Option<CompressionMetadata> {
let policy = compression_policy?;
let size = object.metadata.size_bytes?;
if size < policy.min_size_threshold {
return None;
}
if size == 0 {
return None;
}
if !policy.apply_to_kinds.contains(&object.metadata.kind) {
return None;
}
let content = object.content.as_deref()?;
if content.len() as u64 != size {
return None;
}
let compressed_len = Self::compressed_len_for_policy(policy, content)?;
Some(CompressionMetadata {
algorithm: policy.algorithm,
level: policy.level,
original_size: size,
compressed_size: compressed_len as u64,
compression_ratio: compressed_len as f32 / size as f32,
})
}
fn compressed_len_for_policy(policy: &CompressionPolicy, content: &[u8]) -> Option<usize> {
match policy.algorithm {
CompressionAlgorithm::None => None,
CompressionAlgorithm::Lz4 => Self::lz4_compressed_len(content),
CompressionAlgorithm::Gzip => Self::gzip_compressed_len(content, policy.level),
CompressionAlgorithm::Brotli => Self::brotli_compressed_len(content, policy.level),
}
}
#[cfg(feature = "trace-compression")]
fn lz4_compressed_len(content: &[u8]) -> Option<usize> {
Some(lz4_flex::compress_prepend_size(content).len())
}
#[cfg(not(feature = "trace-compression"))]
fn lz4_compressed_len(_content: &[u8]) -> Option<usize> {
None
}
#[cfg(feature = "compression")]
fn gzip_compressed_len(content: &[u8], level: u8) -> Option<usize> {
use flate2::{Compression, write::GzEncoder};
use std::io::Write;
let mut encoder = GzEncoder::new(Vec::new(), Compression::new(u32::from(level.min(9))));
encoder.write_all(content).ok()?;
Some(encoder.finish().ok()?.len())
}
#[cfg(not(feature = "compression"))]
fn gzip_compressed_len(_content: &[u8], _level: u8) -> Option<usize> {
None
}
#[cfg(feature = "compression")]
fn brotli_compressed_len(content: &[u8], level: u8) -> Option<usize> {
use std::io::Write;
let mut encoder =
brotli::CompressorWriter::new(Vec::new(), 4096, u32::from(level.min(11)), 22);
encoder.write_all(content).ok()?;
encoder.flush().ok()?;
Some(encoder.into_inner().len())
}
#[cfg(not(feature = "compression"))]
fn brotli_compressed_len(_content: &[u8], _level: u8) -> Option<usize> {
None
}
fn compute_encryption_metadata(
encryption_policy: Option<&EncryptionPolicy>,
object: &crate::atp::object::Object,
) -> Option<EncryptionMetadata> {
let policy = encryption_policy?;
if !policy.apply_to_kinds.contains(&object.metadata.kind) {
return None;
}
match policy.algorithm {
EncryptionAlgorithm::None => None,
EncryptionAlgorithm::ChaCha20Poly1305 | EncryptionAlgorithm::Aes256Gcm => {
Some(Self::derive_encryption_metadata(policy, object))
}
}
}
fn derive_encryption_metadata(
policy: &EncryptionPolicy,
object: &crate::atp::object::Object,
) -> EncryptionMetadata {
let mut iv_hasher = Sha256::new();
iv_hasher.update(b"ATP-C4-EncryptionNonce-v1");
iv_hasher.update(object.id.hash_bytes());
iv_hasher.update([policy.algorithm as u8]);
iv_hasher.update([policy.key_derivation.kdf as u8]);
iv_hasher.update(&policy.key_derivation.salt);
let iv_digest: [u8; 32] = iv_hasher.finalize().into();
let iv = iv_digest[..12].to_vec();
let mut tag_hasher = Sha256::new();
tag_hasher.update(b"ATP-C4-EncryptionMetadataTag-v1");
tag_hasher.update(object.id.hash_bytes());
tag_hasher.update([policy.algorithm as u8]);
tag_hasher.update([policy.key_derivation.kdf as u8]);
tag_hasher.update(&policy.key_derivation.salt);
tag_hasher.update(&iv);
if let Some(content) = &object.content {
tag_hasher.update(Sha256::digest(content));
}
let auth_digest: [u8; 32] = tag_hasher.finalize().into();
EncryptionMetadata {
algorithm: policy.algorithm,
iv,
auth_tag: auth_digest[..16].to_vec(),
key_derivation: policy.key_derivation.clone(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ManifestError {
UnsupportedVersion(ManifestVersion),
RootObjectMissing(ObjectId),
ChildObjectMissing(ObjectId),
InvalidFormat(String),
MerkleRootMismatch {
expected: MerkleRoot,
computed: MerkleRoot,
},
UnknownCriticalField(String),
CapabilityPolicyViolation(String),
ChunkPlanError(String),
RaptorQLayoutError(String),
CompressionPolicyError(String),
EncryptionPolicyError(String),
TransformPolicyViolation(String),
TransformOrderViolation(String),
AmbiguousVerificationMode(String),
PrivacyPolicyViolation(String),
LossyTransformNotAllowed(String),
EncryptionDomainViolation(String),
PlaintextHashUnavailable(String),
RepairGroupValidationError(String),
RepairGroupReferenceError(String),
RepairGroupAuthenticationError(String),
}
impl fmt::Display for ManifestError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnsupportedVersion(version) => {
write!(f, "unsupported manifest version: {}", version.0)
}
Self::RootObjectMissing(id) => {
write!(f, "root object missing: {id}")
}
Self::ChildObjectMissing(id) => {
write!(f, "child object missing: {id}")
}
Self::InvalidFormat(msg) => {
write!(f, "invalid manifest format: {msg}")
}
Self::MerkleRootMismatch { expected, computed } => {
write!(
f,
"merkle root mismatch: expected {expected}, computed {computed}"
)
}
Self::UnknownCriticalField(field) => {
write!(
f,
"unknown critical field: {field} (validation fails closed)"
)
}
Self::CapabilityPolicyViolation(msg) => {
write!(f, "capability policy violation: {msg}")
}
Self::ChunkPlanError(msg) => {
write!(f, "chunk plan error: {msg}")
}
Self::RaptorQLayoutError(msg) => {
write!(f, "RaptorQ layout error: {msg}")
}
Self::CompressionPolicyError(msg) => {
write!(f, "compression policy error: {msg}")
}
Self::EncryptionPolicyError(msg) => {
write!(f, "encryption policy error: {msg}")
}
Self::TransformPolicyViolation(msg) => {
write!(f, "transform policy violation: {msg}")
}
Self::TransformOrderViolation(msg) => {
write!(f, "transform order violation: {msg}")
}
Self::AmbiguousVerificationMode(msg) => {
write!(f, "ambiguous verification mode: {msg}")
}
Self::PrivacyPolicyViolation(msg) => {
write!(f, "privacy policy violation: {msg}")
}
Self::LossyTransformNotAllowed(msg) => {
write!(f, "lossy transform not allowed: {msg}")
}
Self::EncryptionDomainViolation(msg) => {
write!(f, "encryption domain violation: {msg}")
}
Self::PlaintextHashUnavailable(msg) => {
write!(f, "plaintext hash unavailable: {msg}")
}
Self::RepairGroupValidationError(msg) => {
write!(f, "repair group validation error: {msg}")
}
Self::RepairGroupReferenceError(msg) => {
write!(f, "repair group reference error: {msg}")
}
Self::RepairGroupAuthenticationError(msg) => {
write!(f, "repair group authentication error: {msg}")
}
}
}
}
impl std::error::Error for ManifestError {}
#[derive(Debug, Clone, PartialEq)]
pub struct GraphCommit {
pub id: CommitId,
pub parent: Option<CommitId>,
pub manifest: Manifest,
pub metadata: CommitMetadata,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct CommitId {
hash: [u8; 32],
}
impl CommitId {
#[must_use]
pub const fn new(hash: [u8; 32]) -> Self {
Self { hash }
}
#[must_use]
pub const fn hash(&self) -> &[u8; 32] {
&self.hash
}
#[must_use]
pub fn from_commit(manifest: &Manifest, metadata: &CommitMetadata) -> Self {
let mut hasher = Sha256::new();
let manifest_bytes = manifest.to_canonical_bytes();
hasher.update(&manifest_bytes);
hasher.update(metadata.timestamp_nanos.to_be_bytes());
hasher.update(metadata.author.as_bytes());
hasher.update(metadata.message.as_bytes());
Self {
hash: hasher.finalize().into(),
}
}
#[must_use]
pub fn to_hex(&self) -> String {
hex::encode(self.hash)
}
}
impl fmt::Display for CommitId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "commit:{}", &self.to_hex()[..16])
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CommitMetadata {
pub timestamp_nanos: u64,
pub author: String,
pub message: String,
}
impl GraphCommit {
#[must_use]
pub fn new(parent: Option<CommitId>, manifest: Manifest, metadata: CommitMetadata) -> Self {
let id = CommitId::from_commit(&manifest, &metadata);
Self {
id,
parent,
manifest,
metadata,
}
}
pub fn validate(&self) -> Result<(), ManifestError> {
self.manifest.validate()?;
let expected_id = CommitId::from_commit(&self.manifest, &self.metadata);
if self.id != expected_id {
return Err(ManifestError::InvalidFormat(
"commit ID does not match content".to_string(),
));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::atp::object::{ContentId, Object};
#[test]
fn manifest_version_support_check_works() {
assert!(ManifestVersion::CURRENT.is_supported());
assert!(ManifestVersion(0).is_supported());
assert!(!ManifestVersion(100).is_supported());
}
#[test]
fn merkle_root_from_empty_graph() {
let graph = ObjectGraph::new();
let root = MerkleRoot::from_graph(&graph);
let root2 = MerkleRoot::from_graph(&graph);
assert_eq!(root, root2);
}
#[test]
fn deterministic_f32_manifest_bytes_are_stable() {
assert_eq!(
deterministic_f32_be_bytes(10.0),
10.0_f32.to_bits().to_be_bytes()
);
assert_eq!(
deterministic_f32_be_bytes(f32::from_bits(0x7fc0_0001)),
0x7fc0_0000_u32.to_be_bytes()
);
assert_eq!(
deterministic_f32_be_bytes(-0.0),
deterministic_f32_be_bytes(0.0)
);
}
#[test]
fn deterministic_f32_be_bytes_canonical_nan() {
let nan1 = f32::from_bits(0x7fc0_0001); let nan2 = f32::from_bits(0x7ff0_1234); let nan3 = f32::NAN;
let expected = [0x7f, 0xc0, 0x00, 0x00]; assert_eq!(deterministic_f32_be_bytes(nan1), expected);
assert_eq!(deterministic_f32_be_bytes(nan2), expected);
assert_eq!(deterministic_f32_be_bytes(nan3), expected);
}
#[test]
fn deterministic_f32_le_bytes_canonical_nan() {
let nan = f32::from_bits(0x7fc0_0001);
let expected = [0x00, 0x00, 0xc0, 0x7f]; assert_eq!(deterministic_f32_le_bytes(nan), expected);
}
#[test]
fn deterministic_f32_signed_zero_normalization() {
assert_eq!(
deterministic_f32_be_bytes(0.0),
deterministic_f32_be_bytes(-0.0)
);
assert_eq!(
deterministic_f32_le_bytes(0.0),
deterministic_f32_le_bytes(-0.0)
);
assert_eq!(deterministic_f32_be_bytes(0.0), [0x00, 0x00, 0x00, 0x00]);
assert_eq!(deterministic_f32_le_bytes(0.0), [0x00, 0x00, 0x00, 0x00]);
}
#[test]
fn deterministic_f64_be_bytes_canonical_nan() {
let nan1 = f64::from_bits(0x7ff8_0000_0000_0001); let nan2 = f64::from_bits(0x7ff0_1234_5678_9abc); let nan3 = f64::NAN;
let expected = [0x7f, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; assert_eq!(deterministic_f64_be_bytes(nan1), expected);
assert_eq!(deterministic_f64_be_bytes(nan2), expected);
assert_eq!(deterministic_f64_be_bytes(nan3), expected);
}
#[test]
fn deterministic_f64_le_bytes_canonical_nan() {
let nan = f64::from_bits(0x7ff8_0000_0000_0001);
let expected = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0x7f]; assert_eq!(deterministic_f64_le_bytes(nan), expected);
}
#[test]
fn deterministic_f64_signed_zero_normalization() {
assert_eq!(
deterministic_f64_be_bytes(0.0),
deterministic_f64_be_bytes(-0.0)
);
assert_eq!(
deterministic_f64_le_bytes(0.0),
deterministic_f64_le_bytes(-0.0)
);
assert_eq!(deterministic_f64_be_bytes(0.0), [0x00; 8]);
assert_eq!(deterministic_f64_le_bytes(0.0), [0x00; 8]);
}
#[test]
fn deterministic_float_normal_values() {
let f32_val = std::f32::consts::PI;
let f64_val = std::f64::consts::PI;
assert_eq!(
deterministic_f32_be_bytes(f32_val),
f32_val.to_bits().to_be_bytes()
);
assert_eq!(
deterministic_f32_le_bytes(f32_val),
f32_val.to_bits().to_le_bytes()
);
assert_eq!(
deterministic_f64_be_bytes(f64_val),
f64_val.to_bits().to_be_bytes()
);
assert_eq!(
deterministic_f64_le_bytes(f64_val),
f64_val.to_bits().to_le_bytes()
);
}
#[test]
fn deterministic_float_infinities() {
assert_eq!(
deterministic_f32_be_bytes(f32::INFINITY),
f32::INFINITY.to_bits().to_be_bytes()
);
assert_eq!(
deterministic_f32_be_bytes(f32::NEG_INFINITY),
f32::NEG_INFINITY.to_bits().to_be_bytes()
);
assert_eq!(
deterministic_f64_be_bytes(f64::INFINITY),
f64::INFINITY.to_bits().to_be_bytes()
);
assert_eq!(
deterministic_f64_be_bytes(f64::NEG_INFINITY),
f64::NEG_INFINITY.to_bits().to_be_bytes()
);
}
#[test]
fn merkle_root_from_simple_graph() {
let mut graph = ObjectGraph::new();
let file = Object::file(b"test content".to_vec());
let _file_id = file.id.clone();
graph.add_root(file).unwrap();
let root = MerkleRoot::from_graph(&graph);
let mut graph2 = ObjectGraph::new();
let file2 = Object::file(b"test content".to_vec());
graph2.add_root(file2).unwrap();
let root2 = MerkleRoot::from_graph(&graph2);
assert_eq!(root, root2);
let mut graph3 = ObjectGraph::new();
let file3 = Object::file(b"different content".to_vec());
graph3.add_root(file3).unwrap();
let root3 = MerkleRoot::from_graph(&graph3);
assert_ne!(root, root3);
}
#[test]
fn manifest_from_graph_works() {
let mut graph = ObjectGraph::new();
let file1 = Object::file(b"content1".to_vec());
let file2 = Object::file(b"content2".to_vec());
let file1_id = file1.id.clone();
let file2_id = file2.id.clone();
graph.add_root(file1).unwrap();
graph.add_root(file2).unwrap();
let policy = MetadataPolicy::default();
let manifest = Manifest::from_graph(&graph, policy.clone()).unwrap();
assert_eq!(manifest.version, ManifestVersion::CURRENT);
assert_eq!(manifest.metadata_policy, policy);
assert_eq!(manifest.object_count(), 2);
assert_eq!(manifest.roots.len(), 2);
assert!(manifest.roots.contains(&file1_id));
assert!(manifest.roots.contains(&file2_id));
assert!(manifest.objects.contains_key(&file1_id));
assert!(manifest.objects.contains_key(&file2_id));
}
#[test]
fn manifest_validation_works() {
let mut graph = ObjectGraph::new();
let file = Object::file(b"test".to_vec());
graph.add_root(file).unwrap();
let policy = MetadataPolicy::default();
let manifest = Manifest::from_graph(&graph, policy).unwrap();
assert!(manifest.validate().is_ok());
}
#[test]
fn manifest_validation_catches_missing_root() {
let graph = ObjectGraph::new();
let policy = MetadataPolicy::default();
let mut manifest = Manifest::from_graph(&graph, policy).unwrap();
let missing_id = ObjectId::content(ContentId::from_bytes(b"missing-root"));
manifest.roots.push(missing_id.clone());
let result = manifest.validate();
assert!(matches!(result, Err(ManifestError::RootObjectMissing(id)) if id == missing_id));
}
#[test]
fn commit_creation_and_validation_works() {
let mut graph = ObjectGraph::new();
let file = Object::file(b"test content".to_vec());
graph.add_root(file).unwrap();
let policy = MetadataPolicy::default();
let manifest = Manifest::from_graph(&graph, policy).unwrap();
let metadata = CommitMetadata {
timestamp_nanos: 1234567890,
author: "test_author".to_string(),
message: "test commit".to_string(),
};
let commit = GraphCommit::new(None, manifest, metadata);
assert!(commit.validate().is_ok());
assert!(commit.parent.is_none());
}
#[test]
fn commit_with_parent_works() {
let mut graph = ObjectGraph::new();
let file = Object::file(b"test content".to_vec());
graph.add_root(file).unwrap();
let policy = MetadataPolicy::default();
let manifest = Manifest::from_graph(&graph, policy).unwrap();
let metadata = CommitMetadata {
timestamp_nanos: 1234567890,
author: "test_author".to_string(),
message: "test commit".to_string(),
};
let parent_id = CommitId::new([1; 32]);
let commit = GraphCommit::new(Some(parent_id.clone()), manifest, metadata);
assert_eq!(commit.parent, Some(parent_id));
assert!(commit.validate().is_ok());
}
#[test]
fn manifest_canonical_bytes_are_deterministic() {
let mut graph = ObjectGraph::new();
let file = Object::file(b"test content".to_vec());
graph.add_root(file).unwrap();
let policy = MetadataPolicy::default();
let manifest1 = Manifest::from_graph(&graph, policy.clone()).unwrap();
let manifest2 = Manifest::from_graph(&graph, policy).unwrap();
let bytes1 = manifest1.to_canonical_bytes();
let bytes2 = manifest2.to_canonical_bytes();
assert_eq!(bytes1, bytes2);
}
#[test]
fn commit_id_is_deterministic() {
let mut graph = ObjectGraph::new();
let file = Object::file(b"test content".to_vec());
graph.add_root(file).unwrap();
let policy = MetadataPolicy::default();
let manifest = Manifest::from_graph(&graph, policy).unwrap();
let metadata = CommitMetadata {
timestamp_nanos: 1234567890,
author: "test_author".to_string(),
message: "test commit".to_string(),
};
let id1 = CommitId::from_commit(&manifest, &metadata);
let id2 = CommitId::from_commit(&manifest, &metadata);
assert_eq!(id1, id2);
}
#[test]
fn hash_algorithm_properties() {
assert_eq!(HashAlgorithm::Sha256.hash_size(), 32);
assert_eq!(HashAlgorithm::Blake3.hash_size(), 32);
assert!(HashAlgorithm::Sha256.is_required());
assert!(!HashAlgorithm::Blake3.is_required());
}
#[test]
fn manifest_requires_sha256() {
let graph = ObjectGraph::new();
let policy = MetadataPolicy::default();
let result = Manifest::from_graph_with_policies(
&graph,
policy,
vec![HashAlgorithm::Blake3],
None,
None,
None,
None,
None,
None,
None,
);
assert!(matches!(result, Err(ManifestError::InvalidFormat(_))));
}
#[test]
fn manifest_with_chunk_plan() {
let mut graph = ObjectGraph::new();
let file = Object::file(b"large content for chunking".to_vec());
graph.add_root(file).unwrap();
let chunk_plan = ChunkPlan {
strategy: ChunkStrategy::ContentDefined,
target_chunk_size: 64 * 1024,
min_chunk_size: 32 * 1024,
max_chunk_size: 128 * 1024,
cdc_params: Some(CdcParams {
window_size: 64,
average_chunk_size: 64 * 1024,
normalization: 0x0001_0000,
}),
};
let policy = MetadataPolicy::default();
let manifest = Manifest::from_graph_with_policies(
&graph,
policy,
vec![HashAlgorithm::Sha256],
Some(chunk_plan.clone()),
None,
None,
None,
None,
None,
None,
)
.unwrap();
assert!(manifest.chunk_plan.is_some());
assert_eq!(
manifest.chunk_plan.as_ref().unwrap().strategy,
ChunkStrategy::ContentDefined
);
assert!(manifest.validate().is_ok());
}
#[test]
fn manifest_with_raptorq_layout() {
let mut graph = ObjectGraph::new();
let file = Object::file(b"content requiring FEC".to_vec());
graph.add_root(file).unwrap();
let raptorq_layout = RaptorQRepairLayout {
source_symbols: 1000,
total_symbols: 1200,
symbol_size: 1024,
overhead_ratio: 0.2,
sub_blocks: vec![SubBlock {
index: 0,
source_symbols: 1000,
esi_range: (0, 1199),
}],
};
let policy = MetadataPolicy::default();
let manifest = Manifest::from_graph_with_policies(
&graph,
policy,
vec![HashAlgorithm::Sha256],
None,
Some(raptorq_layout.clone()),
None,
None,
None,
None,
None,
)
.unwrap();
assert!(manifest.raptorq_layout.is_some());
assert_eq!(
manifest.raptorq_layout.as_ref().unwrap().source_symbols,
1000
);
assert_eq!(
manifest.raptorq_layout.as_ref().unwrap().total_symbols,
1200
);
assert!(manifest.validate().is_ok());
}
#[test]
fn manifest_with_compression_policy() {
let mut graph = ObjectGraph::new();
let file = Object::file(b"compressible content with lots of repetition".to_vec());
graph.add_root(file).unwrap();
let compression_policy = CompressionPolicy {
algorithm: CompressionAlgorithm::Lz4,
level: 6,
min_size_threshold: 1024,
apply_to_kinds: vec![ObjectKind::FileObject, ObjectKind::DatasetObject],
};
let policy = MetadataPolicy::default();
let manifest = Manifest::from_graph_with_policies(
&graph,
policy,
vec![HashAlgorithm::Sha256],
None,
None,
Some(compression_policy.clone()),
None,
None,
None,
None,
)
.unwrap();
assert!(manifest.compression_policy.is_some());
assert_eq!(
manifest.compression_policy.as_ref().unwrap().algorithm,
CompressionAlgorithm::Lz4
);
assert!(manifest.validate().is_ok());
}
#[test]
#[cfg(feature = "compression")]
fn manifest_computes_gzip_and_brotli_metadata_from_real_content() {
for algorithm in [CompressionAlgorithm::Gzip, CompressionAlgorithm::Brotli] {
let mut graph = ObjectGraph::new();
let file = Object::file(
b"manifest compression metadata metadata metadata chunk chunk chunk".repeat(8),
);
let object_id = file.id.clone();
let object_size = file.metadata.size_bytes.unwrap();
graph.add_root(file).unwrap();
let compression_policy = CompressionPolicy {
algorithm,
level: 6,
min_size_threshold: 1,
apply_to_kinds: vec![ObjectKind::FileObject],
};
let manifest = Manifest::from_graph_with_policies(
&graph,
MetadataPolicy::default(),
vec![HashAlgorithm::Sha256],
None,
None,
Some(compression_policy),
None,
None,
None,
None,
)
.unwrap();
let metadata = manifest.objects[&object_id]
.compression_metadata
.as_ref()
.expect("compression metadata should be computed from object content");
assert_eq!(metadata.algorithm, algorithm);
assert_eq!(metadata.original_size, object_size);
assert!(metadata.compressed_size > 0);
assert_eq!(
metadata.compression_ratio,
metadata.compressed_size as f32 / metadata.original_size as f32
);
}
}
#[test]
fn manifest_with_encryption_policy() {
let mut graph = ObjectGraph::new();
let file = Object::file(b"sensitive content requiring encryption".to_vec());
graph.add_root(file).unwrap();
let encryption_policy = EncryptionPolicy {
algorithm: EncryptionAlgorithm::ChaCha20Poly1305,
key_derivation: KeyDerivation {
kdf: KeyDerivationFunction::Argon2id,
salt: b"random_salt_32_bytes_long_example".to_vec(),
iterations: Some(100_000),
},
apply_to_kinds: vec![ObjectKind::FileObject],
encrypt_metadata: false,
};
let policy = MetadataPolicy::default();
let manifest = Manifest::from_graph_with_policies(
&graph,
policy,
vec![HashAlgorithm::Sha256],
None,
None,
None,
Some(encryption_policy.clone()),
None,
None,
None,
)
.unwrap();
assert!(manifest.encryption_policy.is_some());
assert_eq!(
manifest.encryption_policy.as_ref().unwrap().algorithm,
EncryptionAlgorithm::ChaCha20Poly1305
);
assert!(manifest.validate().is_ok());
}
#[test]
fn manifest_with_capability_policy() {
let mut graph = ObjectGraph::new();
let file = Object::file(b"authorized content".to_vec());
graph.add_root(file).unwrap();
let capability_policy = CapabilityPolicy {
read_capabilities: vec!["read:public".to_string(), "read:authenticated".to_string()],
write_capabilities: vec!["write:admin".to_string()],
verify_capabilities: vec!["verify:trusted".to_string()],
delegation_rules: vec![DelegationRule {
capability: "read:public".to_string(),
target: "user:*".to_string(),
constraints: vec!["time:business_hours".to_string()],
expires_at: Some(1_640_995_200_000_000_000), }],
};
let policy = MetadataPolicy::default();
let manifest = Manifest::from_graph_with_policies(
&graph,
policy,
vec![HashAlgorithm::Sha256],
None,
None,
None,
None,
Some(capability_policy.clone()),
None,
None,
)
.unwrap();
assert!(manifest.capability_policy.is_some());
assert_eq!(
manifest
.capability_policy
.as_ref()
.unwrap()
.read_capabilities
.len(),
2
);
assert!(manifest.validate().is_ok());
}
#[test]
fn chunk_plan_validation_errors() {
let graph = ObjectGraph::new();
let policy = MetadataPolicy::default();
let bad_chunk_plan = ChunkPlan {
strategy: ChunkStrategy::FixedSize,
target_chunk_size: 32 * 1024,
min_chunk_size: 64 * 1024, max_chunk_size: 128 * 1024,
cdc_params: None,
};
let manifest = Manifest::from_graph_with_policies(
&graph,
policy.clone(),
vec![HashAlgorithm::Sha256],
Some(bad_chunk_plan),
None,
None,
None,
None,
None,
None,
)
.unwrap();
assert!(matches!(
manifest.validate(),
Err(ManifestError::InvalidFormat(_))
));
let bad_chunk_plan2 = ChunkPlan {
strategy: ChunkStrategy::ContentDefined,
target_chunk_size: 64 * 1024,
min_chunk_size: 32 * 1024,
max_chunk_size: 128 * 1024,
cdc_params: None, };
let manifest2 = Manifest::from_graph_with_policies(
&graph,
policy,
vec![HashAlgorithm::Sha256],
Some(bad_chunk_plan2),
None,
None,
None,
None,
None,
None,
)
.unwrap();
assert!(matches!(
manifest2.validate(),
Err(ManifestError::InvalidFormat(_))
));
}
#[test]
fn raptorq_layout_validation_errors() {
let graph = ObjectGraph::new();
let policy = MetadataPolicy::default();
let bad_layout = RaptorQRepairLayout {
source_symbols: 1500,
total_symbols: 1000, symbol_size: 1024,
overhead_ratio: 0.2,
sub_blocks: vec![],
};
let manifest = Manifest::from_graph_with_policies(
&graph,
policy.clone(),
vec![HashAlgorithm::Sha256],
None,
Some(bad_layout),
None,
None,
None,
None,
None,
)
.unwrap();
assert!(matches!(
manifest.validate(),
Err(ManifestError::InvalidFormat(_))
));
let bad_layout2 = RaptorQRepairLayout {
source_symbols: 1000,
total_symbols: 1200,
symbol_size: 1024,
overhead_ratio: 1.5, sub_blocks: vec![],
};
let manifest2 = Manifest::from_graph_with_policies(
&graph,
policy,
vec![HashAlgorithm::Sha256],
None,
Some(bad_layout2),
None,
None,
None,
None,
None,
)
.unwrap();
assert!(matches!(
manifest2.validate(),
Err(ManifestError::InvalidFormat(_))
));
}
#[test]
fn unknown_critical_field_validation() {
let graph = ObjectGraph::new();
let policy = MetadataPolicy::default();
let mut manifest = Manifest::from_graph(&graph, policy).unwrap();
manifest.unknown_optional_fields.push(UnknownField {
name: "future_critical_feature".to_string(),
field_type: FieldType::Critical,
data: b"critical_data".to_vec(),
});
assert!(matches!(
manifest.validate(),
Err(ManifestError::UnknownCriticalField(_))
));
manifest.unknown_optional_fields[0].field_type = FieldType::Optional;
assert!(manifest.validate().is_ok());
}
#[test]
fn manifest_deterministic_across_policies() {
let mut graph = ObjectGraph::new();
let file1 = Object::file(b"content1".to_vec());
let file2 = Object::file(b"content2".to_vec());
graph.add_root(file1).unwrap();
graph.add_object(file2).unwrap();
let policy = MetadataPolicy::default();
let manifest1 = Manifest::from_graph_with_policies(
&graph,
policy.clone(),
vec![HashAlgorithm::Sha256, HashAlgorithm::Blake3],
None,
None,
None,
None,
None,
None,
None,
)
.unwrap();
let manifest2 = Manifest::from_graph_with_policies(
&graph,
policy,
vec![HashAlgorithm::Sha256, HashAlgorithm::Blake3],
None,
None,
None,
None,
None,
None,
None,
)
.unwrap();
assert_eq!(manifest1.merkle_root, manifest2.merkle_root);
assert_eq!(manifest1.objects, manifest2.objects);
assert_eq!(manifest1.hash_algorithms, manifest2.hash_algorithms);
assert_eq!(manifest1.schema_id, manifest2.schema_id);
}
#[test]
fn manifest_with_all_policies_validates() {
let mut graph = ObjectGraph::new();
let file = Object::file(b"comprehensive test content".to_vec());
graph.add_root(file).unwrap();
let chunk_plan = ChunkPlan {
strategy: ChunkStrategy::ContentDefined,
target_chunk_size: 64 * 1024,
min_chunk_size: 32 * 1024,
max_chunk_size: 128 * 1024,
cdc_params: Some(CdcParams {
window_size: 64,
average_chunk_size: 64 * 1024,
normalization: 0x0001_0000,
}),
};
let raptorq_layout = RaptorQRepairLayout {
source_symbols: 1000,
total_symbols: 1200,
symbol_size: 1024,
overhead_ratio: 0.2,
sub_blocks: vec![SubBlock {
index: 0,
source_symbols: 1000,
esi_range: (0, 1199),
}],
};
let compression_policy = CompressionPolicy {
algorithm: CompressionAlgorithm::Lz4,
level: 6,
min_size_threshold: 1024,
apply_to_kinds: vec![ObjectKind::FileObject],
};
let encryption_policy = EncryptionPolicy {
algorithm: EncryptionAlgorithm::ChaCha20Poly1305,
key_derivation: KeyDerivation {
kdf: KeyDerivationFunction::Argon2id,
salt: b"test_salt_32_bytes_long_example!".to_vec(),
iterations: Some(100_000),
},
apply_to_kinds: vec![ObjectKind::FileObject],
encrypt_metadata: false,
};
let capability_policy = CapabilityPolicy {
read_capabilities: vec!["read:authenticated".to_string()],
write_capabilities: vec!["write:admin".to_string()],
verify_capabilities: vec!["verify:trusted".to_string()],
delegation_rules: vec![],
};
let transform_order = TransformOrder {
transforms: vec![
TransformType::Chunking,
TransformType::Compression,
TransformType::Encryption,
TransformType::ErrorCorrection,
],
hash_point: HashPoint::MultiPoint,
verification_boundary: VerificationBoundary {
relay_verifiable: VerificationLevel::TransferIntegrity,
mailbox_verifiable: VerificationLevel::ContentHash,
e2e_verification_required: true,
privacy_level: PrivacyLevel::MetadataVisible,
},
};
let transform_proof_policy = TransformProofPolicy {
enforce_transform_order: true,
require_deterministic_transforms: true,
allow_lossy_transforms: false,
require_plaintext_hash: true,
max_compression_ratio: Some(10.0),
encryption_domains: vec![EncryptionDomain {
domain_id: "secure".to_string(),
allowed_kdfs: vec![KeyDerivationFunction::Argon2id],
relay_privacy: true,
mailbox_privacy: true,
}],
minimum_proof_strength: ProofStrength::Enhanced,
};
let policy = MetadataPolicy::default();
let manifest = Manifest::from_graph_with_policies(
&graph,
policy,
vec![HashAlgorithm::Sha256, HashAlgorithm::Blake3],
Some(chunk_plan),
Some(raptorq_layout),
Some(compression_policy),
Some(encryption_policy),
Some(capability_policy),
Some(transform_order),
Some(transform_proof_policy),
)
.unwrap();
assert!(manifest.validate().is_ok());
assert!(manifest.chunk_plan.is_some());
assert!(manifest.raptorq_layout.is_some());
assert!(manifest.compression_policy.is_some());
assert!(manifest.encryption_policy.is_some());
assert!(manifest.capability_policy.is_some());
assert!(manifest.transform_order.is_some());
assert!(manifest.transform_proof_policy.is_some());
assert_eq!(manifest.hash_algorithms.len(), 2);
let transform_order = manifest.transform_order.as_ref().unwrap();
assert_eq!(transform_order.transforms.len(), 4);
assert!(
transform_order
.transforms
.contains(&TransformType::Compression)
);
assert!(
transform_order
.transforms
.contains(&TransformType::Encryption)
);
assert_eq!(transform_order.hash_point, HashPoint::MultiPoint);
let proof_policy = manifest.transform_proof_policy.as_ref().unwrap();
assert!(proof_policy.enforce_transform_order);
assert!(proof_policy.require_deterministic_transforms);
assert!(!proof_policy.allow_lossy_transforms);
assert!(proof_policy.require_plaintext_hash);
let bytes = manifest.to_canonical_bytes();
assert!(bytes.starts_with(b"ATPM")); assert!(bytes.len() > 100);
assert_eq!(manifest.schema_id, "atp.manifest.v1");
}
mod atp_g2_tests {
use super::*;
use crate::atp::object::{ContentId, Object, ObjectGraph};
use std::collections::BTreeMap;
fn create_test_repair_group(object_id: ObjectId) -> RepairGroup {
let group_id = RepairGroupId::new(&object_id, 0, 1024);
RepairGroup {
group_id: group_id.clone(),
object_id: object_id.clone(),
source_block_number: 0,
chunk_range: ChunkRange {
start_chunk: 0,
end_chunk: 10,
start_offset: 0,
end_offset: 10240,
},
source_symbols_k: 1000,
k_prime: 1024,
symbol_size: 1024,
repair_layout: RepairLayout {
total_repair_symbols: 200,
overhead_ratio: 0.2,
systematic_config: SystematicConfig {
systematic_rows: 1000,
sub_symbols: 1,
alignment: 8,
},
interleaving: InterleavingPattern {
block_size: 1,
depth: 1,
pattern_type: InterleavingType::None,
},
},
hash_domain: HashDomain {
domain_id: "test-domain".to_string(),
hash_algorithm: HashAlgorithm::Sha256,
context: b"test-context".to_vec(),
},
transform_policy: None,
auth_domain: AuthenticationDomain {
domain_id: "test-auth-domain".to_string(),
required_proof_strength: ProofStrength::Basic,
auth_algorithm: AuthenticationAlgorithm::HmacSha256,
peer_identity_required: false,
transfer_identity_binding: true,
session_binding: true,
},
capability_policy: None,
manifest_root: MerkleRoot::new([0u8; 32]),
}
}
fn create_test_manifest_with_repair_groups() -> Manifest {
let mut graph = ObjectGraph::new();
let object = Object::file(vec![0u8; 10240]);
let object_id = object.id.clone();
graph.add_object(object).unwrap();
let mut manifest = Manifest::from_graph(&graph, MetadataPolicy::portable()).unwrap();
let mut repair_group = create_test_repair_group(object_id.clone());
repair_group.manifest_root = manifest.merkle_root.clone();
let group_id = repair_group.group_id.clone();
manifest
.repair_groups
.insert(group_id.clone(), repair_group);
if let Some(manifest_obj) = manifest.objects.get_mut(&object_id) {
manifest_obj.raptorq_symbols = vec![
RaptorQSymbol {
index: 0,
esi: 0,
size_bytes: 1024,
content_hash: [1u8; 32],
is_source: true,
repair_group_id: Some(group_id.clone()),
auth_tag: None,
},
RaptorQSymbol {
index: 1,
esi: 1000,
size_bytes: 1024,
content_hash: [3u8; 32],
is_source: false,
repair_group_id: Some(group_id),
auth_tag: None,
},
];
}
manifest.merkle_root = MerkleRoot::from_manifest_components(
&manifest.objects,
&manifest.chunk_plan,
&manifest.raptorq_layout,
&manifest.compression_policy,
&manifest.encryption_policy,
&manifest.capability_policy,
&manifest.transform_order,
&manifest.transform_proof_policy,
&manifest.repair_groups,
);
if let Some(repair_group) = manifest.repair_groups.values_mut().next() {
repair_group.manifest_root = manifest.merkle_root.clone();
}
Manifest::bind_repair_symbol_auth_tags(&mut manifest.objects, &manifest.repair_groups);
manifest
}
#[test]
fn test_repair_group_id_generation() {
let object_id = ObjectId::content(ContentId::new([1u8; 32]));
let group_id1 = RepairGroupId::new(&object_id, 0, 1024);
let group_id2 = RepairGroupId::new(&object_id, 0, 1024);
let group_id3 = RepairGroupId::new(&object_id, 1, 1024);
assert_eq!(group_id1, group_id2);
assert_ne!(group_id1, group_id3);
assert_eq!(group_id1.as_bytes().len(), 16);
}
#[test]
fn test_repair_group_validation_success() {
let manifest = create_test_manifest_with_repair_groups();
let result = manifest.validate();
assert!(result.is_ok(), "Validation failed: {:?}", result);
}
#[test]
fn test_repair_group_validation_invalid_group_id() {
let mut manifest = create_test_manifest_with_repair_groups();
let wrong_object_id = ObjectId::content(ContentId::new([99u8; 32]));
let correct_group_id = RepairGroupId::new(&wrong_object_id, 0, 1024);
if let Some(repair_group) = manifest.repair_groups.values_mut().next() {
repair_group.group_id = correct_group_id; }
let result = manifest.validate();
assert!(matches!(
result,
Err(ManifestError::RepairGroupValidationError(_))
));
}
#[test]
fn test_repair_group_validation_k_prime_constraint() {
let mut manifest = create_test_manifest_with_repair_groups();
if let Some(repair_group) = manifest.repair_groups.values_mut().next() {
repair_group.k_prime = 500; }
let result = manifest.validate();
assert!(matches!(
result,
Err(ManifestError::RepairGroupValidationError(_))
));
}
#[test]
fn test_repair_group_validation_symbol_reference_error() {
let mut manifest = create_test_manifest_with_repair_groups();
let missing_group_id =
RepairGroupId::new(&ObjectId::content(ContentId::new([99u8; 32])), 99, 999);
if let Some(manifest_obj) = manifest.objects.values_mut().next() {
manifest_obj.raptorq_symbols.push(RaptorQSymbol {
index: 99,
esi: 1001,
size_bytes: 1024,
content_hash: [5u8; 32],
is_source: false,
repair_group_id: Some(missing_group_id),
auth_tag: Some([6u8; 32]),
});
}
let result = manifest.validate();
assert!(matches!(
result,
Err(ManifestError::RepairGroupReferenceError(_))
));
}
#[test]
fn test_repair_group_validation_missing_auth_tag() {
let mut manifest = create_test_manifest_with_repair_groups();
if let Some(manifest_obj) = manifest.objects.values_mut().next() {
if let Some(symbol) = manifest_obj.raptorq_symbols.get_mut(0) {
symbol.auth_tag = None;
}
}
let result = manifest.validate();
assert!(matches!(
result,
Err(ManifestError::RepairGroupAuthenticationError(_))
));
}
#[test]
fn test_repair_group_validation_chunk_range_errors() {
let mut manifest = create_test_manifest_with_repair_groups();
if let Some(repair_group) = manifest.repair_groups.values_mut().next() {
repair_group.chunk_range.end_chunk = repair_group.chunk_range.start_chunk;
}
let result = manifest.validate();
assert!(matches!(
result,
Err(ManifestError::RepairGroupValidationError(_))
));
}
#[test]
fn test_repair_group_validation_zero_symbol_size() {
let mut manifest = create_test_manifest_with_repair_groups();
if let Some(repair_group) = manifest.repair_groups.values_mut().next() {
repair_group.symbol_size = 0;
}
let result = manifest.validate();
assert!(matches!(
result,
Err(ManifestError::RepairGroupValidationError(_))
));
}
#[test]
fn test_repair_group_validation_overhead_ratio_bounds() {
let mut manifest = create_test_manifest_with_repair_groups();
if let Some(repair_group) = manifest.repair_groups.values_mut().next() {
repair_group.repair_layout.overhead_ratio = -0.1;
}
let result = manifest.validate();
assert!(matches!(
result,
Err(ManifestError::RepairGroupValidationError(_))
));
if let Some(repair_group) = manifest.repair_groups.values_mut().next() {
repair_group.repair_layout.overhead_ratio = 15.0; }
let result = manifest.validate();
assert!(matches!(
result,
Err(ManifestError::RepairGroupValidationError(_))
));
}
#[test]
fn test_repair_group_validation_empty_domain_id() {
let mut manifest = create_test_manifest_with_repair_groups();
if let Some(repair_group) = manifest.repair_groups.values_mut().next() {
repair_group.auth_domain.domain_id.clear();
}
let result = manifest.validate();
assert!(matches!(
result,
Err(ManifestError::RepairGroupValidationError(_))
));
}
#[test]
fn test_repair_group_manifest_root_binding() {
let mut manifest = create_test_manifest_with_repair_groups();
if let Some(repair_group) = manifest.repair_groups.values_mut().next() {
repair_group.manifest_root = MerkleRoot::new([99u8; 32]);
}
let result = manifest.validate();
assert!(matches!(
result,
Err(ManifestError::RepairGroupValidationError(_))
));
}
#[test]
fn test_merkle_root_includes_repair_groups() {
let manifest = create_test_manifest_with_repair_groups();
let root_with_groups = MerkleRoot::from_manifest_components(
&manifest.objects,
&manifest.chunk_plan,
&manifest.raptorq_layout,
&manifest.compression_policy,
&manifest.encryption_policy,
&manifest.capability_policy,
&manifest.transform_order,
&manifest.transform_proof_policy,
&manifest.repair_groups,
);
let root_without_groups = MerkleRoot::from_manifest_components(
&manifest.objects,
&manifest.chunk_plan,
&manifest.raptorq_layout,
&manifest.compression_policy,
&manifest.encryption_policy,
&manifest.capability_policy,
&manifest.transform_order,
&manifest.transform_proof_policy,
&BTreeMap::new(),
);
assert_ne!(root_with_groups, root_without_groups);
let computed_manifest_root = MerkleRoot::from_manifest_components(
&manifest.objects,
&manifest.chunk_plan,
&manifest.raptorq_layout,
&manifest.compression_policy,
&manifest.encryption_policy,
&manifest.capability_policy,
&manifest.transform_order,
&manifest.transform_proof_policy,
&manifest.repair_groups,
);
assert_eq!(manifest.merkle_root, computed_manifest_root);
}
#[test]
fn test_repair_group_interleaving_pattern_hashing() {
let object_id = ObjectId::content(ContentId::new([1u8; 32]));
let mut repair_group1 = create_test_repair_group(object_id.clone());
repair_group1.repair_layout.interleaving.pattern_type = InterleavingType::None;
let mut repair_group2 = repair_group1.clone();
repair_group2.repair_layout.interleaving.pattern_type = InterleavingType::Block;
let mut repair_group3 = repair_group1.clone();
repair_group3.repair_layout.interleaving.pattern_type =
InterleavingType::Randomized(12345);
let groups1 = {
let mut map = BTreeMap::new();
map.insert(repair_group1.group_id.clone(), repair_group1);
map
};
let groups2 = {
let mut map = BTreeMap::new();
map.insert(repair_group2.group_id.clone(), repair_group2);
map
};
let groups3 = {
let mut map = BTreeMap::new();
map.insert(repair_group3.group_id.clone(), repair_group3);
map
};
let root1 = MerkleRoot::from_manifest_components(
&BTreeMap::new(),
&None,
&None,
&None,
&None,
&None,
&None,
&None,
&groups1,
);
let root2 = MerkleRoot::from_manifest_components(
&BTreeMap::new(),
&None,
&None,
&None,
&None,
&None,
&None,
&None,
&groups2,
);
let root3 = MerkleRoot::from_manifest_components(
&BTreeMap::new(),
&None,
&None,
&None,
&None,
&None,
&None,
&None,
&groups3,
);
assert_ne!(root1, root2);
assert_ne!(root1, root3);
assert_ne!(root2, root3);
}
}
}