use crate::compressor::shared::SharedBrotliError;
use super::prefix::{MAX_PREFIX_DICTIONARIES, MAX_PREFIX_SEGMENT_BYTES, PrefixSources};
#[cfg(any(test, feature = "diagnostics"))]
use super::prepared::HASH_INPUT_BYTES;
use super::prepared::PreparedPrefix;
#[derive(Debug, Default)]
pub(crate) struct SharedDictionaryData {
prefix: PrefixSources,
}
impl SharedDictionaryData {
pub(crate) const fn prefix(&self) -> &PrefixSources {
&self.prefix
}
pub(crate) fn source_size(&self) -> usize {
self.prefix.total_len() as usize
}
fn allocated_size(&self) -> usize {
let segments = self.prefix.segment_count();
self.source_size() + segments * size_of::<Box<[u8]>>() + segments * size_of::<u64>()
}
}
#[derive(Debug, Default)]
pub(crate) struct PreparedDictionaryIndexes {
prefixes: Box<[PreparedPrefix]>,
}
impl PreparedDictionaryIndexes {
pub(crate) fn prefix(&self, index: usize) -> Option<&PreparedPrefix> {
self.prefixes.get(index)
}
fn allocated_size(&self) -> usize {
self.prefixes
.iter()
.map(PreparedPrefix::allocated_size)
.sum()
}
}
#[cfg(any(test, feature = "diagnostics"))]
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub(crate) struct PrefixMatch {
pub(crate) offset: u64,
pub(crate) length: usize,
}
#[derive(Debug, Default)]
pub(crate) struct SharedContextInner {
#[cfg(feature = "experimental")]
pub(crate) static_index: Option<super::static_index::StaticIndex>,
dictionaries: SharedDictionaryData,
prepared: PreparedDictionaryIndexes,
}
impl SharedContextInner {
pub(crate) fn new(
attachments: Vec<Box<[u8]>>,
limits: &Budget,
) -> Result<Self, SharedBrotliError> {
let limit = limits.max_attachments.min(MAX_PREFIX_DICTIONARIES);
if attachments.len() > limit {
return Err(SharedBrotliError::TooManyPrefixDictionaries {
attached: attachments.len(),
limit,
});
}
let mut total = 0u64;
for attachment in &attachments {
let length = attachment.len() as u64;
if length > MAX_PREFIX_SEGMENT_BYTES {
return Err(SharedBrotliError::DictionaryTooLarge {
bytes: length,
limit: MAX_PREFIX_SEGMENT_BYTES,
});
}
total += length;
}
if total > limits.max_prefix_bytes {
return Err(SharedBrotliError::DictionaryTooLarge {
bytes: total,
limit: limits.max_prefix_bytes,
});
}
if total > limits.max_total_source_bytes {
return Err(SharedBrotliError::DictionaryTooLarge {
bytes: total,
limit: limits.max_total_source_bytes,
});
}
let estimate = estimate_allocation(&attachments).unwrap_or(u64::MAX);
if estimate > limits.max_allocated_bytes {
return Err(SharedBrotliError::SharedContextTooLarge {
bytes: estimate,
limit: limits.max_allocated_bytes,
});
}
let prefixes: Vec<PreparedPrefix> = attachments
.iter()
.map(|attachment| PreparedPrefix::new(attachment))
.collect();
Ok(Self {
#[cfg(feature = "experimental")]
static_index: None,
dictionaries: SharedDictionaryData {
prefix: PrefixSources::new(attachments),
},
prepared: PreparedDictionaryIndexes {
prefixes: prefixes.into_boxed_slice(),
},
})
}
pub(crate) const fn dictionaries(&self) -> &SharedDictionaryData {
&self.dictionaries
}
pub(crate) fn prepared_prefix(&self, index: usize) -> Option<&PreparedPrefix> {
self.prepared.prefix(index)
}
#[cfg(test)]
pub(crate) fn is_empty(&self) -> bool {
self.dictionaries.prefix().is_empty()
}
pub(crate) fn allocated_size(&self) -> usize {
let bytes =
size_of::<Self>() + self.dictionaries.allocated_size() + self.prepared.allocated_size();
#[cfg(feature = "experimental")]
let bytes = bytes
+ self
.static_index
.as_ref()
.map_or(0, super::static_index::StaticIndex::allocated_size);
bytes
}
#[cfg(any(test, feature = "diagnostics"))]
pub(crate) fn longest_prefix_match(&self, input: &[u8]) -> Option<PrefixMatch> {
let head = u64::from_le_bytes(*input.first_chunk::<HASH_INPUT_BYTES>()?);
let sources = self.dictionaries.prefix();
let mut best: Option<PrefixMatch> = None;
for attachment in 0..sources.segment_count() {
let Some(index) = self.prepared.prefix(attachment) else {
continue;
};
let base = sources.segment_start(attachment);
for candidate in index.candidates(head) {
let offset = base + u64::from(candidate);
let length = sources.match_length(offset, &[], input, input.len());
if length > best.map_or(0, |found: PrefixMatch| found.length) {
best = Some(PrefixMatch { offset, length });
}
}
}
best
}
}
#[derive(Copy, Clone, Debug)]
pub(crate) struct Budget {
pub(crate) max_total_source_bytes: u64,
pub(crate) max_prefix_bytes: u64,
pub(crate) max_allocated_bytes: u64,
pub(crate) max_attachments: usize,
}
fn estimate_allocation(attachments: &[Box<[u8]>]) -> Option<u64> {
const PER_ATTACHMENT: u64 = (size_of::<Box<[u8]>>() + size_of::<u64>()) as u64;
let mut total = size_of::<SharedContextInner>() as u64;
for attachment in attachments {
let length = attachment.len() as u64;
total = total.checked_add(length)?.checked_add(PER_ATTACHMENT)?;
total = total.checked_add(PreparedPrefix::allocation_bound(attachment.len())?)?;
}
Some(total)
}
#[cfg(test)]
mod tests {
use super::*;
const GENEROUS: Budget = Budget {
max_total_source_bytes: u64::MAX,
max_prefix_bytes: u64::MAX,
max_allocated_bytes: u64::MAX,
max_attachments: MAX_PREFIX_DICTIONARIES,
};
fn attach(segments: &[&[u8]]) -> Vec<Box<[u8]>> {
segments
.iter()
.map(|segment| segment.to_vec().into_boxed_slice())
.collect()
}
fn context(segments: &[&[u8]]) -> SharedContextInner {
SharedContextInner::new(attach(segments), &GENEROUS).expect("prepared")
}
fn items(context: &SharedContextInner, attachment: usize) -> usize {
context
.prepared
.prefix(attachment)
.map_or(0, PreparedPrefix::item_count)
}
fn search(context: &SharedContextInner, input: &[u8]) -> Option<PrefixMatch> {
context.longest_prefix_match(input)
}
#[test]
fn an_empty_context_owns_nothing_but_itself() {
let context = SharedContextInner::default();
assert!(context.is_empty());
assert_eq!(context.dictionaries().source_size(), 0);
assert_eq!(context.dictionaries().prefix().segment_count(), 0);
assert!(context.prepared.prefix(0).is_none());
assert_eq!(context.allocated_size(), size_of::<SharedContextInner>());
assert_eq!(search(&context, b"anything at all"), None);
}
#[test]
fn attachments_keep_their_order_and_their_indexes() {
let first: &[u8] = b"the quick brown fox";
let second: &[u8] = b"jumps over the lazy dog";
let context = context(&[first, second]);
assert!(!context.is_empty());
assert_eq!(
context.dictionaries().source_size(),
first.len() + second.len()
);
assert_eq!(context.dictionaries().prefix().segment(0), first);
assert_eq!(context.dictionaries().prefix().segment(1), second);
assert_eq!(items(&context, 0), first.len() - 8 + 1);
assert_eq!(items(&context, 1), second.len() - 8 + 1);
assert!(context.prepared.prefix(2).is_none());
assert!(context.allocated_size() > context.dictionaries().source_size());
}
#[test]
fn too_many_attachments_are_refused_before_anything_is_built() {
let segments = vec![b"payload".as_slice(); MAX_PREFIX_DICTIONARIES + 1];
assert!(matches!(
SharedContextInner::new(attach(&segments), &GENEROUS),
Err(SharedBrotliError::TooManyPrefixDictionaries {
attached: 16,
limit: 15
})
));
let segments = vec![b"payload".as_slice(); MAX_PREFIX_DICTIONARIES];
assert!(SharedContextInner::new(attach(&segments), &GENEROUS).is_ok());
}
#[test]
fn a_prefix_past_its_limit_is_refused() {
let limits = Budget {
max_prefix_bytes: 8,
..GENEROUS
};
assert!(matches!(
SharedContextInner::new(attach(&[b"nine byte".as_slice()]), &limits),
Err(SharedBrotliError::DictionaryTooLarge { bytes: 9, limit: 8 })
));
let limits = Budget {
max_total_source_bytes: 8,
..GENEROUS
};
assert!(matches!(
SharedContextInner::new(attach(&[b"four".as_slice(), b"five!".as_slice()]), &limits),
Err(SharedBrotliError::DictionaryTooLarge { bytes: 9, limit: 8 })
));
}
#[test]
fn an_allocation_past_its_limit_is_refused() {
let limits = Budget {
max_allocated_bytes: 1024,
..GENEROUS
};
assert!(matches!(
SharedContextInner::new(attach(&[b"eight!!!".as_slice()]), &limits),
Err(SharedBrotliError::SharedContextTooLarge { .. })
));
assert!(SharedContextInner::new(attach(&[b"seven!!".as_slice()]), &limits).is_ok());
}
#[test]
fn the_estimate_is_never_smaller_than_the_context_it_predicts() {
let cases: Vec<Vec<Box<[u8]>>> = vec![
Vec::new(),
attach(&[b"short".as_slice()]),
vec![b"a".to_vec().into_boxed_slice(); MAX_PREFIX_DICTIONARIES],
vec![
b"the quick brown fox jumps over the lazy dog"
.to_vec()
.into_boxed_slice(),
vec![b'z'; 40_000].into_boxed_slice(),
],
];
for attachments in cases {
let estimate = estimate_allocation(&attachments).expect("no overflow");
let context = SharedContextInner::new(attachments, &GENEROUS).expect("built");
assert!(
estimate >= context.allocated_size() as u64,
"estimate {estimate} under {}",
context.allocated_size()
);
}
}
#[test]
fn a_search_needs_eight_bytes_to_probe_with() {
let context = context(&[b"the quick brown fox".as_slice()]);
for len in 0..8usize {
assert_eq!(search(&context, &b"the quick"[..len]), None);
}
assert_eq!(
search(&context, b"the quic"),
Some(PrefixMatch {
offset: 0,
length: 8
})
);
}
#[test]
fn a_search_finds_the_longest_match_and_where_it_is() {
let context = context(&[b"the quick brown fox".as_slice()]);
assert_eq!(
search(&context, b"quick brown foxes"),
Some(PrefixMatch {
offset: 4,
length: 15
})
);
assert_eq!(search(&context, b"nothing here at all"), None);
}
#[test]
fn a_search_crosses_the_seam_between_attachments() {
let context = context(&[
b"the quick brown fox jum".as_slice(),
b"ps over the lazy dog".as_slice(),
]);
assert_eq!(
search(&context, b"brown fox jumps ov"),
Some(PrefixMatch {
offset: 10,
length: 18
})
);
}
#[test]
fn a_search_prefers_the_longest_match_over_the_nearest() {
let context = context(&[
b"a brown fox jumped once".as_slice(),
b"a brown fox jumps twice".as_slice(),
]);
let found = search(&context, b"a brown fox jumps twice more").expect("a match");
assert_eq!(found.offset, 23);
assert_eq!(found.length, 23);
}
}