use assay_common::limits::{LimitExceeded, LimitKind};
#[derive(Debug, Clone, Copy)]
pub struct ReplayLimits {
pub max_source_bytes: u64,
pub max_decoded_bytes: u64,
pub max_manifest_bytes: u64,
pub max_member_bytes: u64,
pub max_path_len: usize,
pub max_entries: usize,
pub max_manifest_json_depth: usize,
}
impl Default for ReplayLimits {
fn default() -> Self {
Self {
max_source_bytes: 100 * 1024 * 1024, max_decoded_bytes: 1024 * 1024 * 1024, max_manifest_bytes: 10 * 1024 * 1024, max_member_bytes: 500 * 1024 * 1024, max_path_len: 256,
max_entries: 100_000,
max_manifest_json_depth: 64,
}
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ReplayIngestError {
#[error("replay bundle exceeded {kind} limit of {limit}")]
SourceCeiling { kind: LimitKind, limit: u64 },
#[error("replay bundle member exceeded {kind} limit of {limit}")]
MemberCeiling { kind: LimitKind, limit: u64 },
#[error("replay bundle entry path exceeds the configured maximum length of {limit}")]
PathTooLong { limit: usize },
#[error("replay bundle entry count exceeds limit {limit}")]
TooManyEntries { limit: usize },
#[error("replay bundle manifest JSON nesting exceeds the configured maximum depth of {limit}")]
ManifestTooDeep { limit: usize },
}
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
#[non_exhaustive]
pub enum ReplayContractError {
#[error("replay bundle contains duplicate entry paths")]
DuplicatePath,
#[error("replay bundle contains more than one manifest")]
DuplicateManifest,
}
pub(crate) fn classify_source_ceiling(err: &std::io::Error) -> Option<ReplayIngestError> {
let cause = LimitExceeded::from_io(err)?;
Some(ReplayIngestError::SourceCeiling {
kind: cause.kind,
limit: cause.limit,
})
}
pub(crate) fn classify_member_ceiling(err: &std::io::Error) -> Option<ReplayIngestError> {
let cause = LimitExceeded::from_io(err)?;
Some(match cause.kind {
LimitKind::MemberBytes => ReplayIngestError::MemberCeiling {
kind: cause.kind,
limit: cause.limit,
},
LimitKind::SourceBytes | LimitKind::DecodedBytes | LimitKind::LineBytes => {
ReplayIngestError::SourceCeiling {
kind: cause.kind,
limit: cause.limit,
}
}
})
}
pub(crate) fn check_manifest_json_depth(
data: &[u8],
max_depth: usize,
) -> Result<(), ReplayIngestError> {
let mut depth = 0usize;
let mut in_string = false;
let mut escaped = false;
for &b in data {
if in_string {
if escaped {
escaped = false;
} else if b == b'\\' {
escaped = true;
} else if b == b'"' {
in_string = false;
}
continue;
}
match b {
b'"' => in_string = true,
b'{' | b'[' => {
depth += 1;
if depth > max_depth {
return Err(ReplayIngestError::ManifestTooDeep { limit: max_depth });
}
}
b'}' | b']' => depth = depth.saturating_sub(1),
_ => {}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use assay_common::limits::{LimitExceeded, LimitKind};
fn make_io(kind: LimitKind, limit: u64) -> std::io::Error {
std::io::Error::other(LimitExceeded { kind, limit })
}
#[test]
fn source_ceiling_is_recovered_from_the_typed_cause() {
let io = make_io(LimitKind::DecodedBytes, 1024);
match classify_source_ceiling(&io) {
Some(ReplayIngestError::SourceCeiling { kind, limit }) => {
assert_eq!(kind, LimitKind::DecodedBytes);
assert_eq!(limit, 1024);
}
other => panic!("expected SourceCeiling, got {other:?}"),
}
}
#[test]
fn member_ceiling_carries_the_dimension_and_not_the_member_name() {
let io = make_io(LimitKind::MemberBytes, 42);
match classify_member_ceiling(&io) {
Some(ReplayIngestError::MemberCeiling { kind, limit }) => {
assert_eq!(kind, LimitKind::MemberBytes);
assert_eq!(limit, 42);
}
other => panic!("expected MemberCeiling, got {other:?}"),
}
let rendered = ReplayIngestError::MemberCeiling {
kind: LimitKind::MemberBytes,
limit: 42,
}
.to_string();
assert!(
!rendered.contains('/'),
"no archive path may appear: {rendered}"
);
}
#[test]
fn a_non_ceiling_io_error_is_not_promoted() {
let io = std::io::Error::other("something else entirely");
assert!(classify_source_ceiling(&io).is_none());
assert!(classify_member_ceiling(&io).is_none());
}
}