cadmpeg-codec-step 0.6.1

Read and write ISO 10303-21 STEP Part 21 files for AP203, AP214, and AP242.
Documentation
// SPDX-License-Identifier: Apache-2.0
//! Resource refusals from STEP PMI string fields.

use std::collections::{BTreeMap, BTreeSet};

use cadmpeg_core::decode::{DecodeArena, DecodeContext, DecodePolicy, ResourceDimension};
use cadmpeg_core::CodecError;
use cadmpeg_ir::report::loss::LossNote;

fn source(records: &str) -> String {
    format!("ISO-10303-21;HEADER;FILE_DESCRIPTION(('test'),'2;1');FILE_NAME('','',(''),(''),'','','');FILE_SCHEMA(('AP242'));ENDSEC;DATA;{records}ENDSEC;END-ISO-10303-21;")
}

fn pmi_result(records: &str, retained_limit: u64) -> Result<(), CodecError> {
    let source = source(records);
    let (exchange, _) =
        crate::test_support::with_service_context(source.as_bytes(), crate::parse::parse_inner)
            .expect("valid PMI exchange");
    let arena = DecodeArena::new();
    let mut policy = DecodePolicy::service();
    policy.limits.max_retained_bytes = retained_limit;
    let (ctx, _) = DecodeContext::from_root_bytes(source.as_bytes(), &arena, &policy)
        .expect("root fits retained policy");
    let mut ir = cadmpeg_ir::document::CadIr::empty();
    let (geometry, _) = crate::reader::geometry::decode(&exchange, &mut ir, &ctx)?;
    let index = crate::reader::index::CarrierIndex::from_ir(&ir, &ctx)?;
    let topology = crate::reader::topology::decode(&exchange, &mut ir, &index, &ctx)?;
    super::super::decode(&exchange, &geometry.value, &topology.value, &mut ir, &ctx)?;
    Ok(())
}

macro_rules! pmi_string_limit_test {
    ($name:ident, $records:literal, $limit:expr) => {
        #[test]
        fn $name() {
            assert!(matches!(
                Err::<(), CodecError>(cadmpeg_test_support::refusal::resource_limit_at(ResourceDimension::RetainedBytes, "step_string_text", |cap| pmi_result($records, cap))),
                Err(CodecError::ResourceLimit(refusal))
                    if refusal.dimension == ResourceDimension::RetainedBytes
                        && refusal.operation == "step_string_text"
            ));
        }
    };
}

pmi_string_limit_test!(
    datum_identification_refuses_retained_limit,
    "#1=DATUM('identifier');",
    1
);
pmi_string_limit_test!(
    datum_name_refuses_retained_limit,
    "#1=(DATUM('') SHAPE_ASPECT('datum name','',#2,.F.));#2=ITEM();",
    1
);
pmi_string_limit_test!(
    datum_target_form_refuses_retained_limit,
    "#1=DATUM_TARGET('','rectangle',#2,.F.,'');#2=ITEM();",
    1
);
pmi_string_limit_test!(
    datum_target_identification_refuses_retained_limit,
    "#1=DATUM_TARGET('','',#2,.F.,'identifier');#2=ITEM();",
    1
);
pmi_string_limit_test!(
    datum_target_name_refuses_retained_limit,
    "#1=DATUM_TARGET('target name','',#2,.F.,'');#2=ITEM();",
    1
);
pmi_string_limit_test!(
    datum_system_name_refuses_retained_limit,
    "#1=DATUM_SYSTEM('system name','',#2,.F.,());#2=ITEM();",
    1
);
pmi_string_limit_test!(
    dimension_name_refuses_retained_limit,
    "#1=DIMENSIONAL_SIZE(#2,'dimension name');#2=ITEM();",
    1
);
pmi_string_limit_test!(
    dimension_category_refuses_retained_limit,
    "#1=DIMENSIONAL_SIZE_WITH_DATUM_FEATURE(#2,'diameter');#2=ITEM();",
    8
);
pmi_string_limit_test!(
    limits_form_variance_refuses_retained_limit,
    "#1=PLUS_MINUS_TOLERANCE(#2);#2=LIMITS_AND_FITS('form variance','','','');",
    1
);
pmi_string_limit_test!(
    limits_zone_variance_refuses_retained_limit,
    "#1=PLUS_MINUS_TOLERANCE(#2);#2=LIMITS_AND_FITS('','zone variance','','');",
    1
);
pmi_string_limit_test!(
    limits_grade_refuses_retained_limit,
    "#1=PLUS_MINUS_TOLERANCE(#2);#2=LIMITS_AND_FITS('','','grade value','');",
    1
);
pmi_string_limit_test!(
    limits_source_refuses_retained_limit,
    "#1=PLUS_MINUS_TOLERANCE(#2);#2=LIMITS_AND_FITS('','','','source value');",
    1
);
pmi_string_limit_test!(geometric_tolerance_name_refuses_retained_limit, "#1=(LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.));#2=(LENGTH_MEASURE_WITH_UNIT() MEASURE_WITH_UNIT(LENGTH_MEASURE(0.05),#1));#3=ITEM();#4=FLATNESS_TOLERANCE('tol name','',#2,#3);", 1);
pmi_string_limit_test!(
    presentation_annotation_name_refuses_retained_limit,
    "#1=ANNOTATION_TEXT_OCCURRENCE('annotation name',());",
    1
);

#[test]
fn annotation_text_refuses_retained_limit() {
    let source = source("#1=TEXT_LITERAL('annotation text',$,'left',.RIGHT.,$);");
    let (exchange, _) =
        crate::test_support::with_service_context(source.as_bytes(), crate::parse::parse_inner)
            .expect("valid text exchange");
    let error = cadmpeg_test_support::refusal::resource_limit_at(
        ResourceDimension::RetainedBytes,
        "step_string_text",
        |cap| {
            let arena = DecodeArena::new();
            let mut policy = DecodePolicy::service();
            policy.limits.max_retained_bytes = cap;
            let (ctx, _) = DecodeContext::from_root_bytes(source.as_bytes(), &arena, &policy)
                .expect("root fits retained policy");
            let mut visited = BTreeSet::new();
            let mut candidates = BTreeMap::new();
            let mut losses = Vec::<LossNote>::new();
            super::super::collect_annotation_text(
                1,
                &exchange,
                &mut visited,
                &mut candidates,
                &mut losses,
                0,
                &ctx,
            )
        },
    );
    assert!(
        matches!(Err::<(), CodecError>(error), Err(CodecError::ResourceLimit(refusal))
            if refusal.dimension == ResourceDimension::RetainedBytes
                && refusal.operation == "step_string_text")
    );
}

#[test]
fn measure_item_name_refuses_retained_limit() {
    let source = source("#1=(MEASURE_REPRESENTATION_ITEM() REPRESENTATION_ITEM('measure name'));");
    let (exchange, _) =
        crate::test_support::with_service_context(source.as_bytes(), crate::parse::parse_inner)
            .expect("valid measure exchange");
    let arena = DecodeArena::new();
    let mut policy = DecodePolicy::service();
    policy.limits.max_retained_bytes = 1;
    let (ctx, _) = DecodeContext::from_root_bytes(source.as_bytes(), &arena, &policy)
        .expect("root fits retained policy");
    let record = exchange.records().get(&1).expect("measure record");
    assert!(matches!(
        super::super::measure_item_name(1, record, &exchange, &mut Vec::new(), &ctx),
        Err(CodecError::ResourceLimit(refusal))
            if refusal.dimension == ResourceDimension::RetainedBytes
                && refusal.operation == "step_string_text"
    ));
}

fn annotation_collection_result(limit: u64, consume_text: bool) -> Result<(), CodecError> {
    let source = source("#1=TEXT_LITERAL('annotation text',$,'left',.RIGHT.,$);");
    let (exchange, _) =
        crate::test_support::with_service_context(source.as_bytes(), crate::parse::parse_inner)
            .expect("valid text exchange");
    let arena = DecodeArena::new();
    let mut policy = DecodePolicy::service();
    policy.limits.max_collection_items = limit;
    let (ctx, _) = DecodeContext::from_root_bytes(source.as_bytes(), &arena, &policy)
        .expect("root fits collection policy");
    let mut visited = BTreeSet::new();
    let mut losses = Vec::new();
    if consume_text {
        super::super::find_annotation_text(
            1,
            &exchange,
            &mut visited,
            &mut BTreeSet::new(),
            &mut losses,
            0,
            &ctx,
        )?;
    } else {
        super::super::collect_annotation_text(
            1,
            &exchange,
            &mut visited,
            &mut BTreeMap::new(),
            &mut losses,
            0,
            &ctx,
        )?;
    }
    Ok(())
}

macro_rules! annotation_collection_limit_test {
    ($name:ident, $limit:expr, $consume:expr, $operation:literal) => {
        #[test]
        fn $name() {
            assert!(matches!(
                annotation_collection_result($limit, $consume),
                Err(CodecError::ResourceLimit(refusal))
                    if refusal.dimension == ResourceDimension::CollectionItems
                        && refusal.operation == $operation
            ));
        }
    };
}

annotation_collection_limit_test!(
    annotation_text_visited_refuses_collection_limit,
    0,
    false,
    "step_pmi_annotation_text_visited"
);
annotation_collection_limit_test!(
    annotation_text_candidates_refuse_collection_limit,
    1,
    false,
    "step_pmi_annotation_text_candidates"
);
annotation_collection_limit_test!(
    annotation_text_used_refuses_collection_limit,
    2,
    true,
    "step_pmi_annotation_text_used"
);

#[test]
fn characteristic_measure_values_refuse_collection_limit() {
    let source = source("#1=ITEM();");
    let (exchange, _) =
        crate::test_support::with_service_context(source.as_bytes(), crate::parse::parse_inner)
            .expect("valid exchange");
    let arena = DecodeArena::new();
    let mut policy = DecodePolicy::service();
    policy.limits.max_collection_items = 0;
    let (ctx, _) = DecodeContext::from_root_bytes(source.as_bytes(), &arena, &policy)
        .expect("root fits collection policy");
    let mut losses = Vec::new();
    let mut measurements = super::super::MeasureContext {
        length_scale: 1.0,
        angle_scale: 1.0,
        graph_limit: 64,
        losses: &mut losses,
    };
    let value = crate::parse::Value::Real(
        cadmpeg_ir::scalar::FiniteReal::new(1.0).expect("finite fixture"),
    );
    assert!(matches!(
        super::super::characteristic_measure_values(&super::super::MeasureParameters::Items(std::slice::from_ref(&value)), &exchange, &mut measurements, &ctx),
        Err(CodecError::ResourceLimit(refusal))
            if refusal.dimension == ResourceDimension::CollectionItems
                && refusal.operation == "step_pmi_measure_values"
    ));
}

#[test]
fn characteristic_value_map_refuses_collection_limit() {
    const RECORDS: &str = "#1=(LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.));#2=(GEOMETRIC_REPRESENTATION_CONTEXT(3) GLOBAL_UNIT_ASSIGNED_CONTEXT((#1)) REPRESENTATION_CONTEXT('model','3D'));#5=PRODUCT_DEFINITION_SHAPE('PMI shape','',#99);#6=SHAPE_ASPECT('feature','',#5,.T.);#10=DIMENSIONAL_SIZE(#6,'width');#13=(LENGTH_MEASURE_WITH_UNIT() MEASURE_REPRESENTATION_ITEM() MEASURE_WITH_UNIT(POSITIVE_LENGTH_MEASURE(5.0),#1) REPRESENTATION_ITEM('nominal value'));#14=SHAPE_DIMENSION_REPRESENTATION('value',(#13),#2);#15=DIMENSIONAL_CHARACTERISTIC_REPRESENTATION(#10,#14);#99=ITEM();";
    let source = source(RECORDS);
    let (exchange, _) =
        crate::test_support::with_service_context(source.as_bytes(), crate::parse::parse_inner)
            .expect("valid characteristic exchange");
    let arena = DecodeArena::new();
    let refused = (0..512).any(|limit| {
        let mut policy = DecodePolicy::service();
        policy.limits.max_collection_items = limit;
        let (ctx, _) = DecodeContext::from_root_bytes(source.as_bytes(), &arena, &policy)
            .expect("root fits collection policy");
        let mut ir = cadmpeg_ir::document::CadIr::empty();
        let Ok((geometry, _)) = crate::reader::geometry::decode(&exchange, &mut ir, &ctx) else {
            return false;
        };
        let mut losses = Vec::new();
        matches!(
            super::super::characteristic_values(&exchange, &geometry.value, &mut losses, 64, &ctx),
            Err(CodecError::ResourceLimit(refusal))
                if refusal.dimension == ResourceDimension::CollectionItems
                    && refusal.operation == "step_pmi_characteristic_values"
        )
    });
    assert!(
        refused,
        "no collection limit refused the characteristic map entry"
    );
}