#[cfg(any(test, feature = "test-support"))]
use crate::kernel_header::RefWidth;
use crate::nurbs::reader::{checked_knot_layout, BsplineMarker, Nullable};
use crate::sab::Token;
#[derive(Clone, Copy)]
pub struct Cur<'a> {
toks: &'a [Token],
pos: usize,
}
impl<'a> Cur<'a> {
pub fn at(toks: &'a [Token], pos: usize) -> Self {
Self { toks, pos }
}
pub fn pos(&self) -> usize {
self.pos
}
pub(super) fn set_pos(&mut self, pos: usize) {
self.pos = pos;
}
pub(super) fn toks(&self) -> &'a [Token] {
self.toks
}
pub(super) fn peek(&self) -> Option<&'a Token> {
self.toks.get(self.pos)
}
pub(super) fn at_scope_end(&self) -> bool {
self.pos + 1 == self.toks.len()
&& matches!(self.toks.get(self.pos), Some(Token::SubtypeClose))
}
pub(super) fn bump(&mut self) -> Option<&'a Token> {
let token = self.toks.get(self.pos)?;
self.pos += 1;
Some(token)
}
pub(super) fn rest(&self) -> &'a [Token] {
self.toks.get(self.pos..).unwrap_or(&[])
}
pub(super) fn take_f64(&mut self) -> Option<f64> {
match self.peek()? {
Token::Double(value) => {
self.pos += 1;
Some(*value)
}
_ => None,
}
}
pub(super) fn take_long(&mut self) -> Option<i64> {
match self.peek()? {
Token::Long(value) => {
self.pos += 1;
Some(*value)
}
_ => None,
}
}
pub(super) fn take_enum(&mut self) -> Option<i64> {
match self.peek()? {
Token::Enum(value) => {
self.pos += 1;
Some(*value)
}
_ => None,
}
}
pub(super) fn take_bool(&mut self) -> Option<bool> {
match self.peek()? {
Token::True => {
self.pos += 1;
Some(true)
}
Token::False => {
self.pos += 1;
Some(false)
}
_ => None,
}
}
pub(super) fn take_str(&mut self) -> Option<&'a str> {
match self.peek()? {
Token::Str(value) => {
self.pos += 1;
Some(value)
}
_ => None,
}
}
pub(super) fn take_ident(&mut self) -> Option<&'a str> {
match self.peek()? {
Token::Ident(value) | Token::SubIdent(value) => {
self.pos += 1;
Some(value)
}
_ => None,
}
}
pub(super) fn take_position(&mut self) -> Option<[f64; 3]> {
match self.peek()? {
Token::Position(value) => {
self.pos += 1;
Some(*value)
}
_ => None,
}
}
pub(super) fn take_vector3(&mut self) -> Option<[f64; 3]> {
match self.peek()? {
Token::Vector3(value) => {
self.pos += 1;
Some(*value)
}
_ => None,
}
}
pub(super) fn take_float_array(
&mut self,
ctx: &cadmpeg_core::decode::DecodeContext<'_>,
) -> Option<Result<Vec<f64>, cadmpeg_core::CodecError>> {
let mark = self.pos;
let Some(count) = self.take_long().and_then(|c| usize::try_from(c).ok()) else {
self.pos = mark;
return None;
};
let mut values = match ctx.collection_vec(count, "ASM counted float array") {
Ok(values) => values,
Err(error) => return Some(Err(error)),
};
for _ in 0..count {
let Some(value) = self.take_f64() else {
self.pos = mark;
return None;
};
values.push(value);
}
Some(Ok(values))
}
pub(super) fn take_range_value(&mut self) -> Option<f64> {
let mark = self.pos;
if matches!(self.peek(), Some(Token::True | Token::False)) {
self.pos += 1;
}
let Some(value) = self.take_f64() else {
self.pos = mark;
return None;
};
Some(value)
}
pub(super) fn take_optional_range_value(&mut self) -> Option<Nullable<f64>> {
let mark = self.pos;
match self.peek()? {
Token::True => {
self.pos += 1;
let Some(value) = self.take_f64() else {
self.pos = mark;
return None;
};
Some(Nullable::Value(value))
}
Token::False => {
self.pos += 1;
Some(Nullable::Null)
}
Token::Double(_) => self.take_f64().map(Nullable::Value),
_ => None,
}
}
}
pub(super) fn take_knot_table(
ctx: &cadmpeg_core::decode::DecodeContext<'_>,
cur: &mut Cur<'_>,
n: usize,
degree: i64,
) -> Option<Result<(Vec<f64>, usize), cadmpeg_core::CodecError>> {
if n > cur.rest().len() / 2 {
return None;
}
let Some(work) = cadmpeg_core::decode::u64_from_index(n).checked_mul(3) else {
return Some(Err(ctx.refuse_codec_limit(
"ASM unique knot work",
u64::MAX,
u64::MAX,
)));
};
if let Err(error) = ctx.charge_work(work, "read ASM unique knot pairs") {
return Some(Err(error));
}
let mut scratch = match ctx.reserve_scoped(0, "ASM knot expansion inputs") {
Ok(scratch) => scratch,
Err(error) => return Some(Err(error)),
};
let mut values = match scratch.with_storage(|| ctx.collection_vec(n, "ASM unique knot values"))
{
Ok(values) => values,
Err(error) => return Some(Err(error)),
};
let mut mults = match scratch.with_storage(|| ctx.collection_vec(n, "ASM knot multiplicities"))
{
Ok(mults) => mults,
Err(error) => return Some(Err(error)),
};
for _ in 0..n {
values.push(cur.take_f64()?);
mults.push(cur.take_long()?);
}
let expansion = checked_knot_layout(&mults, degree)?;
if let Err(error) = ctx.charge_work(
cadmpeg_core::decode::u64_from_index(expansion.expanded_len()),
"expand ASM knots",
) {
return Some(Err(error));
}
let mut expanded = match ctx.collection_vec(expansion.expanded_len(), "ASM expanded knots") {
Ok(expanded) => expanded,
Err(error) => return Some(Err(error)),
};
for (index, (value, multiplicity)) in values.iter().zip(&mults).enumerate() {
let run_length = usize::try_from(*multiplicity).ok()?
+ usize::from(index == 0 || index + 1 == mults.len());
for _ in 0..run_length {
expanded.push(*value);
}
}
Some(Ok((expanded, expansion.n_poles)))
}
pub(super) fn marker_at(toks: &[Token], pos: usize) -> Option<BsplineMarker> {
match toks.get(pos)? {
Token::Ident(name) if name == "nubs" => Some(BsplineMarker::Nubs),
Token::Ident(name) if name == "nurbs" => Some(BsplineMarker::Nurbs),
_ => None,
}
}
pub(super) fn owned_marker_positions(
ctx: &cadmpeg_core::decode::DecodeContext<'_>,
toks: &[Token],
) -> Option<Result<Vec<usize>, cadmpeg_core::CodecError>> {
let (out, balanced) = match walk_owned_markers(ctx, toks) {
Ok(value) => value,
Err(error) => return Some(Err(error)),
};
balanced.then_some(Ok(out))
}
fn walk_owned_markers(
ctx: &cadmpeg_core::decode::DecodeContext<'_>,
toks: &[Token],
) -> Result<(Vec<usize>, bool), cadmpeg_core::CodecError> {
let mut out = Vec::new();
let mut depth = 0usize;
let mut outer = usize::from(matches!(toks.first(), Some(Token::SubtypeOpen)));
for (pos, token) in toks.iter().enumerate().skip(outer) {
match token {
Token::SubtypeOpen => depth += 1,
Token::SubtypeClose => match depth.checked_sub(1) {
Some(next) => depth = next,
None => match outer.checked_sub(1) {
Some(next) => outer = next,
None => return Ok((out, false)),
},
},
_ => {
if depth == 0 && marker_at(toks, pos).is_some() {
ctx.push_vec(&mut out, pos, "ASM owned spline markers")?;
}
}
}
}
Ok((out, true))
}
pub(super) fn owned_subtype_defs<'a>(
ctx: &cadmpeg_core::decode::DecodeContext<'_>,
toks: &'a [Token],
) -> Option<Result<Vec<(usize, &'a str)>, cadmpeg_core::CodecError>> {
let mut owned = Vec::new();
let mut depth = 0usize;
for (pos, token) in toks.iter().enumerate() {
match token {
Token::SubtypeOpen => {
if depth == 0 {
if let Some(Token::Ident(name) | Token::SubIdent(name)) = toks.get(pos + 1) {
if let Err(error) = ctx.push_vec(
&mut owned,
(pos, name.as_str()),
"ASM owned subtype definitions",
) {
return Some(Err(error));
}
}
}
depth += 1;
}
Token::SubtypeClose => depth = depth.checked_sub(1)?,
_ => {}
}
}
Some(Ok(owned))
}
pub(super) fn find_owned_subtype_marker<'n>(
ctx: &cadmpeg_core::decode::DecodeContext<'_>,
toks: &[Token],
names: &[&'n str],
) -> Option<Result<(usize, &'n str), cadmpeg_core::CodecError>> {
let mut owned_storage = match ctx.reserve_scoped(0, "ASM owned construction index") {
Ok(storage) => storage,
Err(error) => return Some(Err(error)),
};
let owned = match owned_storage.with_storage(|| owned_subtype_defs(ctx, toks).transpose()) {
Ok(Some(value)) => value,
Ok(None) => return None,
Err(error) => return Some(Err(error)),
};
names.iter().copied().find_map(|name| {
owned
.iter()
.find(|(_, owned_name)| *owned_name == name)
.map(|(start, _)| Ok((*start, name)))
})
}
pub fn owned_construction_subtype(
ctx: &cadmpeg_core::decode::DecodeContext<'_>,
toks: &[Token],
) -> Option<Result<String, cadmpeg_core::CodecError>> {
let mut owned_storage = match ctx.reserve_scoped(0, "ASM owned construction index") {
Ok(storage) => storage,
Err(error) => return Some(Err(error)),
};
let owned = match owned_storage.with_storage(|| owned_subtype_defs(ctx, toks).transpose()) {
Ok(Some(value)) => value,
Ok(None) => return None,
Err(error) => return Some(Err(error)),
};
owned
.into_iter()
.map(|(_, name)| name)
.find(|name| *name != "ref")
.map(|name| {
let name = canonical_intcurve_kind(name);
ctx.copy_retained_text(name, "ASM construction subtype name")
})
}
pub(super) fn cache_scope<'a>(
ctx: &cadmpeg_core::decode::DecodeContext<'_>,
toks: &'a [Token],
) -> Option<Result<&'a [Token], cadmpeg_core::CodecError>> {
let mut constructions = 0usize;
let mut cache_bearing = Vec::new();
let mut owned_storage = match ctx.reserve_scoped(0, "ASM owned construction index") {
Ok(storage) => storage,
Err(error) => return Some(Err(error)),
};
let owned = match owned_storage.with_storage(|| owned_subtype_defs(ctx, toks).transpose()) {
Ok(Some(value)) => value,
Ok(None) => return None,
Err(error) => return Some(Err(error)),
};
for (start, _) in owned.into_iter().filter(|(_, name)| *name != "ref") {
constructions += 1;
let Some(scope) = subtype_span(toks, start) else {
continue;
};
let markers = match scope.owned_marker_positions(ctx) {
Ok(value) => value,
Err(error) => return Some(Err(error)),
};
if !markers.is_empty() {
if let Err(error) = ctx.push_vec(
&mut cache_bearing,
scope.tokens(),
"ASM cache bearing scopes",
) {
return Some(Err(error));
}
}
}
match (cache_bearing.as_slice(), constructions) {
([scope], _) => Some(Ok(scope)),
([], 0) => Some(Ok(toks)),
_ => None,
}
}
fn canonical_intcurve_kind(name: &str) -> &str {
super::subtypes::INTCURVE_ALIASES
.iter()
.find_map(|(modern, legacy)| (*legacy == name).then_some(*modern))
.unwrap_or(name)
}
pub(super) fn find_owned_intcurve_subtype(
ctx: &cadmpeg_core::decode::DecodeContext<'_>,
toks: &[Token],
modern: &str,
) -> Option<Result<usize, cadmpeg_core::CodecError>> {
if modern.is_empty() {
return None;
}
let legacy = super::subtypes::INTCURVE_ALIASES
.iter()
.find_map(|(name, alias)| (*name == modern).then_some(*alias));
let found = match legacy {
Some(legacy) => find_owned_subtype_marker(ctx, toks, &[modern, legacy]),
None => find_owned_subtype_marker(ctx, toks, &[modern]),
};
found.map(|result| result.map(|(marker, _)| marker))
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SubtypeScope<'a> {
tokens: &'a [Token],
}
impl<'a> SubtypeScope<'a> {
pub fn tokens(&self) -> &'a [Token] {
self.tokens
}
pub fn interior(&self) -> &'a [Token] {
&self.tokens[1..self.tokens.len() - 1]
}
pub fn owned_marker_positions(
&self,
ctx: &cadmpeg_core::decode::DecodeContext<'_>,
) -> Result<Vec<usize>, cadmpeg_core::CodecError> {
Ok(walk_owned_markers(ctx, self.tokens)?.0)
}
}
pub(super) fn subtype_span(toks: &[Token], start: usize) -> Option<SubtypeScope<'_>> {
matches!(toks.get(start), Some(Token::SubtypeOpen)).then_some(())?;
let mut depth = 0usize;
for (pos, token) in toks.iter().enumerate().skip(start) {
match token {
Token::SubtypeOpen => depth += 1,
Token::SubtypeClose => {
depth = depth.checked_sub(1)?;
if depth == 0 {
return Some(SubtypeScope {
tokens: toks.get(start..=pos)?,
});
}
}
_ => {}
}
}
None
}
pub(super) fn subtype_refs(toks: &[Token]) -> impl Iterator<Item = usize> + '_ {
toks.iter().enumerate().filter_map(|(pos, token)| {
if !matches!(token, Token::SubtypeOpen) {
return None;
}
match (toks.get(pos + 1), toks.get(pos + 2)) {
(Some(Token::Ident(name)), Some(Token::Long(index)))
if name == "ref" && *index >= 0 =>
{
usize::try_from(*index).ok()
}
(Some(Token::Long(index)), Some(Token::SubtypeClose)) if *index >= 0 => {
usize::try_from(*index).ok()
}
_ => None,
}
})
}
pub fn payload_subtype_toks<'r>(
record: &'r crate::sab::Record,
chunk_index: usize,
expected: &str,
) -> Option<SubtypeScope<'r>> {
let mut chunk = 0usize;
let mut open = None;
for (pos, token) in record.tokens.iter().enumerate() {
if token.is_payload_ident() {
continue;
}
if chunk == chunk_index {
open = Some(pos);
break;
}
chunk += 1;
}
let open = open?;
if !matches!(record.tokens.get(open), Some(Token::SubtypeOpen)) {
return None;
}
let (Token::Ident(name) | Token::SubIdent(name)) = record.tokens.get(open + 1)? else {
return None;
};
if name != expected {
return None;
}
subtype_span(&record.tokens, open)
}
pub struct SubtypeTable {
defs: Vec<(std::sync::Arc<[Token]>, usize)>,
save_format_version: Option<u32>,
}
impl SubtypeTable {
pub fn from_records(
ctx: &cadmpeg_core::decode::DecodeContext<'_>,
records: &[crate::sab::Record],
) -> Result<Self, cadmpeg_core::CodecError> {
let mut defs = Vec::new();
for record in records {
for (pos, token) in record.tokens.iter().enumerate() {
if matches!(token, Token::SubtypeOpen) {
if let Some(Token::Ident(name) | Token::SubIdent(name)) =
record.tokens.get(pos + 1)
{
if name != "ref" {
ctx.reserve_vec(&mut defs, 1, "index ASM subtype definitions")?;
defs.push((record.tokens.clone(), pos));
}
}
}
}
}
Ok(Self {
defs,
save_format_version: None,
})
}
#[must_use]
pub fn with_save_format_version(mut self, version: Option<u32>) -> Self {
self.save_format_version = version;
self
}
pub(super) fn save_format_version(&self) -> Option<u32> {
self.save_format_version
}
pub(super) fn span(&self, index: usize) -> Option<SubtypeScope<'_>> {
let (tokens, token_pos) = self.defs.get(index)?;
subtype_span(tokens, *token_pos)
}
}
pub(crate) fn admit_subtype_references(
ctx: &cadmpeg_core::decode::DecodeContext<'_>,
records: &[crate::sab::Record],
table: &SubtypeTable,
) -> Result<(), cadmpeg_core::CodecError> {
for record in records {
ctx.charge_work(
cadmpeg_core::decode::u64_from_index(record.tokens.len()),
"scan ASM subtype references",
)?;
let mut visited = std::collections::HashSet::new();
let mut pending = Vec::new();
let mut scratch = ctx.reserve_scoped(0, "walk ASM subtype references")?;
let root = (subtype_refs(&record.tokens), None);
ctx.push_scoped_vec(&mut scratch, &mut pending, root, "walk ASM subtype stack")?;
while let Some((references, _guard)) = pending.last_mut() {
let Some(index) = references.next() else {
pending.pop();
continue;
};
ctx.charge_work(1, "follow ASM subtype reference")?;
if visited.contains(&index) {
continue;
}
let visited_slot_bytes =
cadmpeg_core::decode::u64_from_index(std::mem::size_of::<usize>());
scratch.grow(visited_slot_bytes)?;
ctx.insert_hash_set(&mut visited, index, "visit ASM subtype reference")?;
let Some((tokens, _)) = table.defs.get(index) else {
continue;
};
ctx.charge_work(
cadmpeg_core::decode::u64_from_index(tokens.len()),
"scan ASM subtype definition",
)?;
let Some(target) = table.span(index) else {
continue;
};
let guard = ctx.enter_nested("follow ASM subtype reference")?;
let frame = (subtype_refs(target.tokens()), Some(guard));
ctx.push_scoped_vec(&mut scratch, &mut pending, frame, "walk ASM subtype stack")?;
}
}
Ok(())
}
#[cfg(any(test, feature = "test-support"))]
pub fn lex_test_span(
bytes: &[u8],
ref_width: RefWidth,
) -> Result<std::sync::Arc<[Token]>, crate::stream_error::StreamError> {
let mut wrapped = vec![0x0d, 1, b'x'];
wrapped.extend_from_slice(bytes);
wrapped.push(0x11);
let records = crate::test_support::sab::frame(&wrapped, 0, wrapped.len(), ref_width)?;
let [record]: [crate::sab::Record; 1] =
records
.try_into()
.map_err(|records: Vec<_>| crate::stream_error::StreamError {
format: crate::stream_error::StreamFormat::Binary,
offset: 0,
reason: format!(
"bare payload frames as {} records, expected one",
records.len()
),
})?;
Ok(record.tokens)
}
#[cfg(any(test, feature = "test-support"))]
pub fn test_table(
bytes: &[u8],
ref_width: RefWidth,
) -> Result<SubtypeTable, crate::stream_error::StreamError> {
let record = crate::sab::Record {
index: 0,
name: String::new(),
tokens: lex_test_span(bytes, ref_width)?,
offset: 0,
len: 0,
};
let arena = cadmpeg_core::decode::DecodeArena::new();
let policy = cadmpeg_core::decode::DecodePolicy::service();
let (ctx, _) = cadmpeg_core::decode::DecodeContext::from_root_bytes(&[], &arena, &policy)
.map_err(|error| crate::stream_error::StreamError {
format: crate::stream_error::StreamFormat::Binary,
offset: 0,
reason: error.to_string(),
})?;
SubtypeTable::from_records(&ctx, &[record]).map_err(|error| crate::stream_error::StreamError {
format: crate::stream_error::StreamFormat::Binary,
offset: 0,
reason: error.to_string(),
})
}
#[cfg(test)]
mod tests {
use super::{
cache_scope as cache_scope_ctx, lex_test_span, marker_at,
owned_construction_subtype as owned_construction_subtype_ctx,
owned_marker_positions as owned_marker_positions_ctx,
owned_subtype_defs as owned_subtype_defs_ctx, subtype_refs, subtype_span, test_table, Cur,
};
use crate::kernel_header::RefWidth;
use crate::nurbs::reader::BsplineMarker;
use crate::sab::Token;
fn with_ctx<T>(f: impl FnOnce(&cadmpeg_core::decode::DecodeContext<'_>) -> T) -> T {
let arena = cadmpeg_core::decode::DecodeArena::new();
let policy = cadmpeg_core::decode::DecodePolicy::service();
let (ctx, _) =
cadmpeg_core::decode::DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
f(&ctx)
}
fn owned_subtype_defs(toks: &[Token]) -> Option<Vec<(usize, &str)>> {
with_ctx(|ctx| owned_subtype_defs_ctx(ctx, toks).transpose().unwrap())
}
fn owned_marker_positions(toks: &[Token]) -> Option<Vec<usize>> {
with_ctx(|ctx| owned_marker_positions_ctx(ctx, toks).transpose().unwrap())
}
fn owned_construction_subtype(toks: &[Token]) -> Option<String> {
with_ctx(|ctx| {
owned_construction_subtype_ctx(ctx, toks)
.transpose()
.unwrap()
})
}
#[test]
fn owned_construction_subtype_refuses_retained_string_limit() {
use cadmpeg_core::decode::{DecodeArena, DecodeContext, DecodePolicy, ResourceDimension};
use cadmpeg_core::CodecError;
let tokens = [
Token::SubtypeOpen,
Token::Ident("arbitrary".into()),
Token::SubtypeClose,
];
let arena = DecodeArena::new();
let mut policy = DecodePolicy::service();
policy.limits.max_retained_bytes = 8;
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy)
.expect("empty test input fits input limit");
let error = owned_construction_subtype_ctx(&ctx, &tokens)
.expect("subtype exists")
.expect_err("subtype name exceeds retained limit");
let CodecError::ResourceLimit(refusal) = error else {
panic!("expected resource refusal, got {error:?}");
};
assert_eq!(refusal.dimension, ResourceDimension::RetainedBytes);
assert_eq!(refusal.operation, "ASM construction subtype name");
}
fn cache_scope(toks: &[Token]) -> Option<&[Token]> {
with_ctx(|ctx| cache_scope_ctx(ctx, toks).transpose().unwrap())
}
#[test]
fn owned_marker_vector_refuses_collection_limit() {
use cadmpeg_core::decode::{DecodeArena, DecodeContext, DecodePolicy, ResourceDimension};
use cadmpeg_core::CodecError;
let arena = DecodeArena::new();
let mut policy = DecodePolicy::service();
policy.limits.max_collection_items = 0;
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
let tokens = [
Token::SubtypeOpen,
Token::Ident("nubs".into()),
Token::SubtypeClose,
];
let error = owned_marker_positions_ctx(&ctx, &tokens)
.unwrap()
.unwrap_err();
let CodecError::ResourceLimit(limit) = error else {
panic!("expected resource refusal: {error:?}")
};
assert_eq!(limit.dimension, ResourceDimension::CollectionItems);
assert_eq!(limit.operation, "ASM owned spline markers");
}
#[test]
fn owned_subtype_vector_refuses_collection_limit() {
use cadmpeg_core::decode::{DecodeArena, DecodeContext, DecodePolicy, ResourceDimension};
use cadmpeg_core::CodecError;
let arena = DecodeArena::new();
let mut policy = DecodePolicy::service();
policy.limits.max_collection_items = 0;
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
let tokens = [
Token::SubtypeOpen,
Token::Ident("exactcur".into()),
Token::SubtypeClose,
];
let error = owned_subtype_defs_ctx(&ctx, &tokens).unwrap().unwrap_err();
let CodecError::ResourceLimit(limit) = error else {
panic!("expected resource refusal: {error:?}")
};
assert_eq!(limit.dimension, ResourceDimension::CollectionItems);
assert_eq!(limit.operation, "ASM owned subtype definitions");
}
#[test]
fn cache_bearing_vector_refuses_collection_limit() {
use cadmpeg_core::decode::{DecodeArena, DecodeContext, DecodePolicy, ResourceDimension};
use cadmpeg_core::CodecError;
let arena = DecodeArena::new();
let mut policy = DecodePolicy::service();
policy.limits.max_collection_items = 2;
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
let tokens = [
Token::SubtypeOpen,
Token::Ident("exactcur".into()),
Token::Ident("nubs".into()),
Token::SubtypeClose,
];
let error = cache_scope_ctx(&ctx, &tokens).unwrap().unwrap_err();
let CodecError::ResourceLimit(limit) = error else {
panic!("expected resource refusal: {error:?}")
};
assert_eq!(limit.dimension, ResourceDimension::CollectionItems);
assert_eq!(limit.operation, "ASM cache bearing scopes");
}
#[test]
fn subtype_table_vector_refuses_collection_limit() {
use cadmpeg_core::decode::{DecodeArena, DecodeContext, DecodePolicy, ResourceDimension};
use cadmpeg_core::CodecError;
let arena = DecodeArena::new();
let mut policy = DecodePolicy::service();
policy.limits.max_collection_items = 0;
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
let record = crate::sab::Record {
index: 0,
name: "spline".into(),
tokens: vec![
Token::SubtypeOpen,
Token::Ident("exactcur".into()),
Token::SubtypeClose,
]
.into(),
offset: 0,
len: 0,
};
let error = super::SubtypeTable::from_records(&ctx, &[record])
.err()
.expect("resource refusal");
let CodecError::ResourceLimit(limit) = error else {
panic!("expected resource refusal: {error:?}")
};
assert_eq!(limit.dimension, ResourceDimension::CollectionItems);
assert_eq!(limit.operation, "index ASM subtype definitions");
}
#[test]
fn subtype_reference_walk_refuses_depth_before_following_next_definition() {
use cadmpeg_core::decode::{DecodeArena, DecodeContext, DecodePolicy, ResourceDimension};
use cadmpeg_core::CodecError;
let records: Vec<crate::sab::Record> = (0..3)
.map(|index| {
let mut tokens = vec![Token::SubtypeOpen, Token::Ident("node".into())];
if index < 2 {
tokens.extend([
Token::SubtypeOpen,
Token::Ident("ref".into()),
Token::Long(index + 1),
Token::SubtypeClose,
]);
}
tokens.push(Token::SubtypeClose);
crate::sab::Record {
index: usize::try_from(index).expect("test value fits"),
name: "node".into(),
tokens: tokens.into(),
offset: 0,
len: 0,
}
})
.collect();
let table = with_ctx(|ctx| super::SubtypeTable::from_records(ctx, &records).unwrap());
let arena = DecodeArena::new();
let mut policy = DecodePolicy::service();
policy.limits.max_recursion_depth = 1;
let (ctx, _) = DecodeContext::from_root_bytes(&[0], &arena, &policy).unwrap();
let error = super::admit_subtype_references(&ctx, &records, &table)
.expect_err("second followed reference exceeds depth one");
let CodecError::ResourceLimit(limit) = error else {
panic!("expected resource refusal, got {error:?}");
};
assert_eq!(limit.dimension, ResourceDimension::RecursionDepth);
assert_eq!(limit.operation, "follow ASM subtype reference");
let arena = DecodeArena::new();
let policy = DecodePolicy::service();
let (ctx, _) = DecodeContext::from_root_bytes(&[0], &arena, &policy).unwrap();
super::admit_subtype_references(&ctx, &records, &table)
.expect("service profile admits finite chain");
}
#[test]
fn subtype_reference_walk_refuses_work_before_following_reference() {
use cadmpeg_core::decode::{DecodeArena, DecodeContext, DecodePolicy, ResourceDimension};
use cadmpeg_core::CodecError;
let tokens: std::sync::Arc<[Token]> = vec![
Token::SubtypeOpen,
Token::Ident("node".into()),
Token::SubtypeOpen,
Token::Ident("ref".into()),
Token::Long(0),
Token::SubtypeClose,
Token::SubtypeClose,
]
.into();
let record = crate::sab::Record {
index: 0,
name: "node".into(),
tokens,
offset: 0,
len: 0,
};
let records = [record];
let table = with_ctx(|ctx| super::SubtypeTable::from_records(ctx, &records).unwrap());
let arena = DecodeArena::new();
let mut policy = DecodePolicy::service();
policy.limits.max_work_units =
cadmpeg_core::decode::u64_from_index(records[0].tokens.len());
let (ctx, _) = DecodeContext::from_root_bytes(&[0], &arena, &policy).unwrap();
let error = super::admit_subtype_references(&ctx, &records, &table)
.expect_err("following the reference exceeds scan work");
let CodecError::ResourceLimit(limit) = error else {
panic!("expected resource refusal, got {error:?}");
};
assert_eq!(limit.dimension, ResourceDimension::WorkUnits);
assert_eq!(limit.operation, "follow ASM subtype reference");
}
fn ident(name: &str) -> Token {
Token::Ident(name.to_string())
}
#[test]
fn bare_payload_admission_preserves_empty_and_single_token_payloads() {
for width in [RefWidth::Four, RefWidth::Eight] {
assert!(lex_test_span(&[], width).unwrap().is_empty());
assert_eq!(&*lex_test_span(&[0x0a], width).unwrap(), &[Token::True]);
}
}
#[test]
fn bare_payload_admission_refuses_truncated_tokens() {
for width in [RefWidth::Four, RefWidth::Eight] {
assert!(lex_test_span(&[0x06], width).is_err());
assert!(test_table(&[0x06], width).is_err());
}
}
#[test]
fn bare_payload_admission_refuses_an_extra_record() {
for width in [RefWidth::Four, RefWidth::Eight] {
let bytes = [0x11, 0x0d, 1, b'y'];
let error = lex_test_span(&bytes, width).unwrap_err();
assert!(error.reason.contains("2 records"), "{error}");
assert!(test_table(&bytes, width).is_err());
}
}
#[test]
fn cursor_take_methods_do_not_advance_on_mismatch() {
let toks = [Token::Double(2.5), Token::True, Token::Long(7)];
let mut cur = Cur::at(&toks, 0);
assert_eq!(cur.take_long(), None);
assert_eq!(cur.take_f64(), Some(2.5));
assert_eq!(cur.take_bool(), Some(true));
assert_eq!(cur.take_long(), Some(7));
assert_eq!(cur.bump(), None);
}
#[test]
fn float_array_restores_position_on_a_truncated_body() {
let arena = cadmpeg_core::decode::DecodeArena::new();
let (ctx, _) = cadmpeg_core::decode::DecodeContext::from_root_bytes(
&[],
&arena,
&cadmpeg_core::decode::DecodePolicy::default(),
)
.expect("test decode context");
let toks = [Token::Long(2), Token::Double(1.0), Token::True];
let mut cur = Cur::at(&toks, 0);
assert_eq!(
cur.take_float_array(&ctx)
.transpose()
.expect("resource allocation"),
None
);
assert_eq!(cur.pos(), 0);
}
#[test]
fn counted_float_array_refuses_collection_limit_before_reading_values() {
use cadmpeg_core::decode::{DecodeArena, DecodeContext, DecodePolicy, ResourceDimension};
use cadmpeg_core::CodecError;
let arena = DecodeArena::new();
let mut policy = DecodePolicy::service();
policy.limits.max_collection_items = 1;
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
let toks = [Token::Long(2), Token::Double(1.0), Token::Double(2.0)];
let mut cur = Cur::at(&toks, 0);
let error = cur
.take_float_array(&ctx)
.expect("counted float array")
.expect_err("two floats exceed one item");
let CodecError::ResourceLimit(limit) = error else {
panic!("expected collection refusal: {error:?}");
};
assert_eq!(limit.dimension, ResourceDimension::CollectionItems);
assert_eq!(cur.pos(), 1);
}
#[test]
fn scope_end_requires_the_terminal_close() {
let toks = [Token::SubtypeOpen, ident("x"), Token::SubtypeClose];
assert!(Cur::at(&toks, 2).at_scope_end());
assert!(!Cur::at(&toks, 1).at_scope_end());
let trailing = [
Token::SubtypeOpen,
ident("x"),
Token::SubtypeClose,
Token::Double(1.0),
];
assert!(!Cur::at(&trailing, 2).at_scope_end());
}
#[test]
fn owned_defs_skip_nested_constructions() {
let toks = [
Token::SubtypeOpen,
ident("exactcur"),
Token::SubtypeOpen,
ident("ref"),
Token::Long(3),
Token::SubtypeClose,
Token::SubtypeClose,
];
assert_eq!(owned_subtype_defs(&toks), Some(vec![(0, "exactcur")]));
assert_eq!(subtype_refs(&toks).collect::<Vec<_>>(), vec![3]);
assert_eq!(
owned_construction_subtype(&toks),
Some("exact_int_cur".to_string())
);
assert_eq!(
subtype_span(&toks, 2).map(|scope| scope.tokens()),
Some(&toks[2..=5])
);
}
#[test]
fn owned_markers_ignore_nested_scopes_and_a_leading_open() {
let toks = [
Token::SubtypeOpen,
ident("nubs"),
Token::SubtypeOpen,
ident("nurbs"),
Token::SubtypeClose,
];
assert_eq!(owned_marker_positions(&toks), Some(vec![1]));
assert_eq!(marker_at(&toks, 1), Some(BsplineMarker::Nubs));
assert_eq!(marker_at(&toks, 3), Some(BsplineMarker::Nurbs));
}
#[test]
fn a_scope_yields_its_owned_markers_with_no_refusal_to_answer() {
let toks = [
Token::SubtypeOpen,
ident("exactcur"),
ident("nubs"),
Token::SubtypeOpen,
ident("ref"),
Token::SubtypeOpen,
ident("nurbs"),
Token::SubtypeClose,
Token::SubtypeClose,
ident("nurbs"),
Token::SubtypeClose,
];
let scope = subtype_span(&toks, 0).expect("balanced scope");
let owned: Vec<usize> = with_ctx(|ctx| scope.owned_marker_positions(ctx).unwrap());
assert_eq!(owned, vec![2, 9]);
assert_eq!(scope.tokens(), &toks[..]);
}
#[test]
fn a_span_that_does_not_open_at_start_is_not_a_scope() {
let toks = [
ident("x"),
Token::SubtypeOpen,
ident("exactcur"),
Token::SubtypeClose,
];
assert_eq!(subtype_span(&toks, 0), None);
assert_eq!(subtype_span(&toks, 2), None);
assert_eq!(subtype_span(&toks, 3), None);
assert_eq!(subtype_span(&toks, 4), None);
let scope = subtype_span(&toks, 1).expect("balanced scope");
assert_eq!(scope.tokens(), &toks[1..=3]);
assert_eq!(scope.interior(), &toks[2..3]);
}
#[test]
fn a_first_nested_scope_does_not_own_the_outer_scopes_markers() {
let toks = [
Token::SubtypeOpen,
ident("exactcur"),
Token::SubtypeOpen,
ident("support"),
ident("nubs"),
Token::SubtypeClose,
ident("nurbs"),
Token::SubtypeClose,
];
let scope = subtype_span(&toks, 0).expect("balanced scope");
assert_eq!(
with_ctx(|ctx| scope.owned_marker_positions(ctx).unwrap()),
vec![6]
);
assert_eq!(scope.interior(), &toks[1..7]);
assert_eq!(owned_marker_positions(&toks[2..7]), Some(vec![2, 4]));
}
#[test]
fn one_cache_bearing_scope_is_the_cache_scope_and_two_are_ambiguous() {
let one = [
Token::SubtypeOpen,
ident("exactcur"),
ident("nubs"),
Token::SubtypeClose,
Token::SubtypeOpen,
ident("ref"),
Token::Long(3),
Token::SubtypeClose,
];
assert_eq!(cache_scope(&one), Some(&one[0..=3]));
let two = [
Token::SubtypeOpen,
ident("exactcur"),
ident("nubs"),
Token::SubtypeClose,
Token::SubtypeOpen,
ident("exactcur"),
ident("nurbs"),
Token::SubtypeClose,
];
assert_eq!(cache_scope(&two), None);
let bare = [
Token::SubtypeOpen,
ident("exactcur"),
Token::SubtypeClose,
Token::SubtypeOpen,
ident("exactcur"),
ident("nubs"),
Token::SubtypeClose,
];
assert_eq!(cache_scope(&bare), Some(&bare[3..=6]));
}
#[test]
fn an_unbalanced_close_refuses_the_cache_scope_rather_than_moving_to_another() {
let toks = [
Token::SubtypeOpen,
ident("exactcur"),
ident("nubs"),
Token::SubtypeClose,
Token::SubtypeClose,
Token::SubtypeOpen,
ident("exactcur"),
ident("nurbs"),
Token::SubtypeClose,
];
assert_eq!(owned_subtype_defs(&toks), None);
assert_eq!(cache_scope(&toks), None);
}
#[test]
fn one_unbalanced_close_refuses_the_owned_walks() {
let toks = [
Token::SubtypeOpen,
ident("exactcur"),
Token::SubtypeOpen,
ident("ref"),
Token::SubtypeClose,
Token::SubtypeClose,
Token::SubtypeClose,
ident("nurbs"),
];
assert_eq!(owned_marker_positions(&toks), None);
assert_eq!(owned_subtype_defs(&toks), None);
}
#[test]
fn subtype_table_walks_wide_strings_at_the_stream_ref_width() {
fn t_ident(b: &mut Vec<u8>, s: &str) {
b.push(0x0d);
b.push(u8::try_from(s.len()).expect("test value fits"));
b.extend_from_slice(s.as_bytes());
}
for ref_width in [
crate::kernel_header::RefWidth::Four,
crate::kernel_header::RefWidth::Eight,
] {
let payload = [b'0', b'1', b'2', b'3', 0x0f, 0x0d, 0x01, b'x'];
let mut active = Vec::new();
t_ident(&mut active, "tspl");
active.push(0x09);
active.extend_from_slice(&payload.len().to_le_bytes()[..ref_width.bytes()]);
active.extend_from_slice(&payload);
active.extend_from_slice(b"\x0f\x0d\x08real_def\x10");
active.push(0x11);
let records = crate::test_support::sab::frame(&active, 0, active.len(), ref_width)
.expect("wide-string record frames at its declared width");
let table = with_ctx(|ctx| super::SubtypeTable::from_records(ctx, &records).unwrap());
assert_eq!(table.defs.len(), 1);
assert!(matches!(table.span(0).expect("real definition").tokens(),
[Token::SubtypeOpen, Token::Ident(name), Token::SubtypeClose] if name == "real_def"));
}
}
}