#![forbid(unsafe_code)]
use crate::core::cost::{CostBreakdown, Policy};
use crate::core::extent::ChunkId;
use crate::core::limits::Limits;
use crate::core::materialize::{DecoderContext, MaterializeError, materialize_to_vec};
use crate::core::representation::Representation;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ObjectKind {
Data,
Model,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ObjectRecord {
pub id: ChunkId,
pub kind: ObjectKind,
pub payload: Vec<u8>,
}
impl ObjectRecord {
pub fn data(payload: Vec<u8>) -> Self {
let id = ChunkId::of(&payload);
Self {
id,
kind: ObjectKind::Data,
payload,
}
}
pub fn model(payload: Vec<u8>) -> Self {
let id = ChunkId::of(&payload);
Self {
id,
kind: ObjectKind::Model,
payload,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Candidate {
pub representation: Representation,
pub objects: Vec<ObjectRecord>,
pub cost: CostBreakdown,
pub content_id: ChunkId,
}
pub trait Encoder {
fn name(&self) -> &'static str;
fn encode(&self, input: &[u8], ctx: &CandidateContext<'_>) -> Vec<Candidate>;
}
impl Candidate {
pub fn total(&self, policy: &Policy) -> u128 {
self.cost.total(policy)
}
}
#[derive(Debug, Clone)]
pub struct BaseChunk {
pub id: ChunkId,
pub bytes: Vec<u8>,
pub depth: u8,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DedupHit {
pub id: ChunkId,
}
#[derive(Debug)]
pub struct CandidateContext<'a> {
pub limits: &'a Limits,
pub policy: &'a Policy,
pub content_id: ChunkId,
pub bases: &'a [BaseChunk],
pub dedup: Option<DedupHit>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CandidateError {
BadChunkClass(u64),
NoCandidate,
ValidationFailed,
Materialize(MaterializeError),
BudgetExceeded,
}
impl std::fmt::Display for CandidateError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{self:?}")
}
}
impl std::error::Error for CandidateError {}
pub fn validate_candidate(
candidate: &Candidate,
target: &[u8],
resolver: &dyn DecoderContext,
limits: &Limits,
) -> Result<(), CandidateError> {
if candidate.content_id != ChunkId::of(target) {
return Err(CandidateError::ValidationFailed);
}
let out = materialize_to_vec(&candidate.representation, resolver, limits)
.map_err(CandidateError::Materialize)?;
if out.len() != target.len() || out != target {
return Err(CandidateError::ValidationFailed);
}
Ok(())
}
pub fn pick_cheapest<'a>(candidates: &'a [Candidate], policy: &Policy) -> Option<&'a Candidate> {
candidates.iter().min_by_key(|c| c.total(policy))
}
pub fn raw_candidate(input: &[u8], content_id: ChunkId, limits: &Limits) -> Option<Candidate> {
if input.len() as u64 > limits.max_chunk_size {
return None;
}
let obj = ObjectRecord::data(input.to_vec());
let rep = Representation::Raw {
obj: obj.id,
len: input.len() as u64,
};
let split = crate::core::cost::ByteSplit {
reference: 32,
..Default::default()
};
let cost = account_objects(
crate::core::cost::estimate(&rep, &split, 0),
std::slice::from_ref(&obj),
);
Some(Candidate {
representation: rep,
objects: vec![obj],
cost,
content_id,
})
}
pub fn zero_candidate(input: &[u8], content_id: ChunkId, limits: &Limits) -> Option<Candidate> {
if input.len() as u64 > limits.max_chunk_size {
return None;
}
if input.iter().any(|&b| b != 0) {
return None;
}
let rep = Representation::Zero {
len: input.len() as u64,
};
let cost = crate::core::cost::estimate(&rep, &Default::default(), 0);
Some(Candidate {
representation: rep,
objects: Vec::new(),
cost,
content_id,
})
}
pub fn fill_candidate(input: &[u8], content_id: ChunkId) -> Option<Candidate> {
let value = *input.first()?;
if input.iter().any(|&b| b != value) {
return None;
}
let rep = Representation::Fill {
value,
len: input.len() as u64,
};
let cost = crate::core::cost::estimate(&rep, &Default::default(), 0);
Some(Candidate {
representation: rep,
objects: Vec::new(),
cost,
content_id,
})
}
pub fn inline_candidate(input: &[u8], content_id: ChunkId, limits: &Limits) -> Option<Candidate> {
if input.len() as u64 > limits.max_inline_bytes || input.is_empty() {
return None;
}
let rep = Representation::Inline {
data: input.to_vec(),
};
let cost = crate::core::cost::estimate(&rep, &Default::default(), 0);
Some(Candidate {
representation: rep,
objects: Vec::new(),
cost,
content_id,
})
}
pub fn exact_ref_candidate(
target: ChunkId,
content_id: ChunkId,
len: u64,
target_len: u64,
limits: &Limits,
) -> Option<Candidate> {
if target.is_zero() || len > limits.max_chunk_size || len > target_len {
return None;
}
let rep = Representation::ExactRef {
target,
off: 0,
len,
};
let split = crate::core::cost::ByteSplit {
reference: 32,
..Default::default()
};
let cost = crate::core::cost::estimate(&rep, &split, 0);
Some(Candidate {
representation: rep,
objects: Vec::new(),
cost,
content_id,
})
}
pub fn account_objects(mut cost: CostBreakdown, objects: &[ObjectRecord]) -> CostBreakdown {
for o in objects {
if o.kind == ObjectKind::Data {
cost.object_payload_bytes = cost
.object_payload_bytes
.saturating_add(o.payload.len() as u64);
}
}
cost
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn raw_candidate_always_valid() {
let limits = Limits::default();
let policy = Policy::default();
let data: Vec<u8> = (0..1024u32).map(|i| (i % 251) as u8).collect();
let cid = ChunkId::of(&data);
let cand = raw_candidate(&data, cid, &limits).unwrap();
assert_eq!(cand.representation.len(), 1024);
assert_eq!(cand.total(&policy), cand.cost.total(&policy));
let map: std::collections::HashMap<ChunkId, Vec<u8>> = cand
.objects
.iter()
.map(|o| (o.id, o.payload.clone()))
.collect();
let resolver = crate::tests::helpers::MemResolver::from_map(map);
validate_candidate(&cand, &data, &resolver, &limits).unwrap();
}
#[test]
fn zero_candidate_only_for_zeros() {
let limits = Limits::default();
let zeros = vec![0u8; 4096];
let cid = ChunkId::of(&zeros);
let cand = zero_candidate(&zeros, cid, &limits).unwrap();
assert_eq!(cand.representation.len(), 4096);
let not_zeros = vec![1u8; 4096];
let cid2 = ChunkId::of(¬_zeros);
assert!(zero_candidate(¬_zeros, cid2, &limits).is_none());
}
#[test]
fn pick_cheapest_prefers_zero() {
let limits = Limits::default();
let policy = Policy::default();
let zeros = vec![0u8; 4096];
let cid = ChunkId::of(&zeros);
let z = zero_candidate(&zeros, cid, &limits).unwrap();
let r = raw_candidate(&zeros, cid, &limits).unwrap();
let cands = [r.clone(), z.clone()];
let best = pick_cheapest(&cands, &policy).unwrap();
assert!(matches!(best.representation, Representation::Zero { .. }));
}
}