cadmpeg-codec-step 0.6.2

Read and write ISO 10303-21 STEP Part 21 files for AP203, AP214, and AP242.
Documentation
// SPDX-License-Identifier: Apache-2.0
#![allow(clippy::unwrap_used)]
#![allow(clippy::default_trait_access)]
use super::validate_detached_cms;
use base64::{engine::general_purpose::STANDARD, Engine as _};

#[test]
fn ber_cursor_past_input_refuses_remaining_length() {
    let mut ber = super::Ber::new(b"");
    ber.at = 1;
    assert_eq!(super::require_empty(&ber), Err("BER cursor exceeds input"));
}

struct SignatureView {
    span: std::ops::Range<usize>,
    payload: std::ops::Range<usize>,
    signed: std::ops::Range<usize>,
    cms: Vec<u8>,
}

impl SignatureView {
    fn signed_alphabet_bytes(&self, input: &[u8]) -> Option<Vec<u8>> {
        Some(
            input
                .get(self.signed.clone())?
                .iter()
                .copied()
                .filter(|byte| !byte.is_ascii_control())
                .collect(),
        )
    }
}

fn sections(exchange: &crate::parse::Exchange, input: impl AsRef<[u8]>) -> Vec<SignatureView> {
    let input = input.as_ref();
    let tokens = crate::test_support::with_service_context(input, crate::test_support::lex)
        .expect("signature tokens");
    let exchange_start = tokens[0].span.start;
    exchange
        .signatures()
        .iter()
        .map(|span| {
            let section_tokens = tokens
                .iter()
                .filter(|token| token.span.start >= span.start && token.span.end <= span.end)
                .collect::<Vec<_>>();
            let payload = section_tokens[1].span.end..section_tokens[2].span.start;
            SignatureView {
                span: span.clone(),
                signed: exchange_start..span.start,
                cms: crate::test_support::with_service_context(input, |_, ctx| {
                    super::decode_payload(input, &payload, ctx)
                })
                .expect("admitted CMS payload"),
                payload,
            }
        })
        .collect()
}

#[test]
fn accepts_a_minimal_detached_signed_data_envelope() {
    let cms = [
        0x30, 0x5a, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x07, 0x02, 0xa0, 0x4d,
        0x30, 0x4b, 0x02, 0x01, 0x01, 0x31, 0x0d, 0x30, 0x0b, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01,
        0x65, 0x03, 0x04, 0x02, 0x01, 0x30, 0x0b, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d,
        0x01, 0x07, 0x01, 0x31, 0x2a, 0x30, 0x28, 0x02, 0x01, 0x01, 0x30, 0x05, 0x30, 0x00, 0x02,
        0x01, 0x01, 0x30, 0x0b, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01,
        0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00,
        0x04, 0x00,
    ];
    assert_eq!(validate_detached_cms(&cms), Ok(()));
}

const BER_CMS_INDEFINITE: &[u8] = &[
    0x30, 0x80, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x07, 0x02, 0xa0, 0x80, 0x30,
    0x80, 0x02, 0x01, 0x01, 0x31, 0x80, 0x30, 0x0b, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03,
    0x04, 0x02, 0x01, 0x00, 0x00, 0x30, 0x80, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01,
    0x07, 0x01, 0x00, 0x00, 0x31, 0x80, 0x30, 0x80, 0x02, 0x01, 0x01, 0x30, 0x80, 0x30, 0x00, 0x02,
    0x01, 0x01, 0x00, 0x00, 0x30, 0x0b, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02,
    0x01, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00,
    0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
];

#[test]
fn accepts_cms_ber_indefinite_lengths() {
    assert_eq!(validate_detached_cms(BER_CMS_INDEFINITE), Ok(()));
}

#[test]
fn accepts_ber_contextual_subject_key_identifier_and_octet_string() {
    assert_eq!(
        super::validate_signer_identifier(0x80, &[0x01, 0x02, 0x03]),
        Ok(())
    );
    assert_eq!(
        super::validate_octet_string(0x24, &[0x04, 0x01, 0xaa]),
        Ok(())
    );
}

#[test]
fn parser_retains_base64_encoded_ber_cms() {
    let payload = STANDARD.encode(BER_CMS_INDEFINITE);
    let source = format!(
        "ISO-10303-21;HEADER;FILE_DESCRIPTION(('BER CMS'),'4;2');FILE_NAME('','',(''),(''),'','','');FILE_SCHEMA(('AP242'));ENDSEC;DATA;#1=ITEM();ENDSEC;END-ISO-10303-21;SIGNATURE;{payload}\nENDSEC;"
    );
    let (exchange, diagnostics) =
        crate::test_support::with_service_context(source.as_bytes(), crate::parse::parse_inner)
            .expect("BER CMS witness");

    assert!(diagnostics.is_empty());
    assert_eq!(sections(&exchange, &source)[0].cms, BER_CMS_INDEFINITE);
}

#[test]
fn rejects_embedded_content() {
    let cms = [
        0x30, 0x5c, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x07, 0x02, 0xa0, 0x4f,
        0x30, 0x4d, 0x02, 0x01, 0x01, 0x31, 0x0d, 0x30, 0x0b, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01,
        0x65, 0x03, 0x04, 0x02, 0x01, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d,
        0x01, 0x07, 0x01, 0xa0, 0x02, 0x04, 0x00, 0x31, 0x2a, 0x30, 0x28, 0x02, 0x01, 0x01, 0x30,
        0x05, 0x30, 0x00, 0x02, 0x01, 0x01, 0x30, 0x0b, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65,
        0x03, 0x04, 0x02, 0x01, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01,
        0x01, 0x01, 0x05, 0x00, 0x04, 0x00,
    ];
    assert!(validate_detached_cms(&cms).is_err());
}

#[test]
fn parser_retains_multiple_signature_sections_after_exchange_terminator() {
    let source = b"ISO-10303-21;HEADER;FILE_DESCRIPTION(('signatures'),'4;2');FILE_NAME('','',(''),(''),'','','');FILE_SCHEMA(('AP242'));ENDSEC;DATA;#1=ITEM();ENDSEC;END-ISO-10303-21;SIGNATURE;MFoGCSqGSIb3DQEHAqBNMEsCAQExDTALBglghkgBZQMEAgEwCwYJKoZIhvcNAQcBMSowKAIBATAFMAACAQEwCwYJYIZIAWUDBAIBMA0GCSqGSIb3DQEBAQUABAA=\nENDSEC;SIGNATURE;MFoGCSqGSIb3DQEHAqBNMEsCAQExDTALBglghkgBZQMEAgEwCwYJKoZIhvcNAQcBMSowKAIBATAFMAACAQEwCwYJYIZIAWUDBAIBMA0GCSqGSIb3DQEBAQUABAA=\nENDSEC;";
    let (exchange, diagnostics) =
        crate::test_support::with_service_context(source, crate::parse::parse_inner)
            .expect("multiple signatures");

    assert!(diagnostics.is_empty());
    assert_eq!(exchange.signatures().len(), 2);
    assert!(source[exchange.signatures()[0].clone()]
        .windows(b"MFoGCSqGSIb3DQEHAqBNMEsCAQExDTALBglghkgBZQMEAgEwCwYJKoZIhvcNAQcBMSowKAIBATAFMAACAQEwCwYJYIZIAWUDBAIBMA0GCSqGSIb3DQEBAQUABAA=".len())
        .any(|bytes| bytes == b"MFoGCSqGSIb3DQEHAqBNMEsCAQExDTALBglghkgBZQMEAgEwCwYJKoZIhvcNAQcBMSowKAIBATAFMAACAQEwCwYJYIZIAWUDBAIBMA0GCSqGSIb3DQEBAQUABAA="));
    assert!(source[exchange.signatures()[1].clone()]
        .windows(b"MFoGCSqGSIb3DQEHAqBNMEsCAQExDTALBglghkgBZQMEAgEwCwYJKoZIhvcNAQcBMSowKAIBATAFMAACAQEwCwYJYIZIAWUDBAIBMA0GCSqGSIb3DQEBAQUABAA=".len())
        .any(|bytes| bytes == b"MFoGCSqGSIb3DQEHAqBNMEsCAQExDTALBglghkgBZQMEAgEwCwYJKoZIhvcNAQcBMSowKAIBATAFMAACAQEwCwYJYIZIAWUDBAIBMA0GCSqGSIb3DQEBAQUABAA="));
    assert_eq!(sections(&exchange, source).len(), 2);
    let first = &sections(&exchange, source)[0];
    let second = &sections(&exchange, source)[1];
    assert_eq!(first.signed.end, first.span.start);
    assert_eq!(second.signed.end, second.span.start);
    let first_section = &source[first.span.clone()];
    let second_signed = &source[second.signed.clone()];
    assert!(second_signed
        .windows(first_section.len())
        .any(|window| window == first_section));
}

#[test]
fn parser_accepts_signature_edge_separators_and_keeps_each_boundary() {
    let payload = "MFoGCSqGSIb3DQEHAqBNMEsCAQExDTALBglghkgBZQMEAgEwCwYJKoZIhvcNAQcBMSowKAIBATAFMAACAQEwCwYJYIZIAWUDBAIBMA0GCSqGSIb3DQEBAQUABAA=";
    let source = format!(
        "ISO-10303-21;HEADER;FILE_DESCRIPTION(('signatures'),'4;2');FILE_NAME('','',(''),(''),'','','');FILE_SCHEMA(('AP242'));ENDSEC;DATA;#1=ITEM();ENDSEC;END-ISO-10303-21;SIGNATURE;/* decoy ENDSEC; */\\N\\{payload}\\F\\/* trailing ENDSEC; */EN\nDSEC;SIGNATURE;\\F\\{payload}\\N\\ENDSEC;"
    );
    let (exchange, diagnostics) =
        crate::test_support::with_service_context(source.as_bytes(), crate::parse::parse_inner)
            .expect("edge separators");

    assert!(diagnostics.is_empty());
    assert_eq!(sections(&exchange, &source).len(), 2);
    assert_eq!(exchange.signatures().len(), 2);
    assert!(source.as_bytes()[exchange.signatures()[0].clone()].ends_with(b"DSEC;"));
    assert!(source.as_bytes()[exchange.signatures()[0].clone()]
        .windows(b"EN\nDSEC;".len())
        .any(|bytes| bytes == b"EN\nDSEC;"));
    assert!(source.as_bytes()[exchange.signatures()[1].clone()].ends_with(b"ENDSEC;"));
    assert_eq!(
        sections(&exchange, &source)[0].signed.end,
        exchange.signatures()[0].start
    );
    assert_eq!(
        sections(&exchange, &source)[1].signed.end,
        exchange.signatures()[1].start
    );
    assert_eq!(exchange.signatures()[1].start, exchange.signatures()[0].end);
    assert_eq!(sections(&exchange, &source)[0].cms.len(), 92);
    assert_eq!(sections(&exchange, &source)[1].cms.len(), 92);
}

#[test]
fn parser_does_not_treat_unseparated_endsec_text_as_a_signature_boundary() {
    let payload = "MFoGCSqGSIb3DQEHAqBNMEsCAQExDTALBglghkgBZQMEAgEwCwYJKoZIhvcNAQcBMSowKAIBATAFMAACAQEwCwYJYIZIAWUDBAIBMA0GCSqGSIb3DQEBAQUABAA=";
    let source = format!(
        "ISO-10303-21;HEADER;FILE_DESCRIPTION(('signature'),'4;2');FILE_NAME('','',(''),(''),'','','');FILE_SCHEMA(('AP242'));ENDSEC;DATA;#1=ITEM();ENDSEC;END-ISO-10303-21;SIGNATURE;{payload}ENDSEC;AAAA\nENDSEC;"
    );
    let error =
        crate::test_support::with_service_context(source.as_bytes(), crate::parse::parse_inner)
            .expect_err("unseparated ENDSEC text");

    assert!(matches!(
        error,
        crate::parse::ParseError::Lex(crate::lex::LexError { message, .. })
            if message == "invalid SIGNATURE base64 padding"
    ));
}

#[test]
fn parser_exposes_the_detached_signature_contract() {
    let source = b" /* leading trivia */ ISO-10303-21;HEADER;FILE_DESCRIPTION(('signature'),'4;2');FILE_NAME('','',(''),(''),'','','');FILE_SCHEMA(('AP242'));ENDSEC;DATA;#1=ITEM();ENDSEC;END-ISO-10303-21;SIGNATURE;MFoGCSqGSIb3DQEHAqBNMEsCAQExDTALBglghkgBZQMEAgEwCwYJKoZIhvcNAQcBMSowKAIBATAFMAACAQEwCwYJYIZIAWUDBAIBMA0GCSqGSIb3DQEBAQUABAA=\nENDSEC;";
    let (exchange, diagnostics) =
        crate::test_support::with_service_context(source, crate::parse::parse_inner)
            .expect("signature contract");

    assert!(diagnostics.is_empty());
    let section = &sections(&exchange, source)[0];
    let signed = &source[section.signed.clone()];
    assert!(signed.starts_with(b"ISO-10303-21;"));
    assert!(signed.ends_with(b"END-ISO-10303-21;"));
    assert_eq!(
        &source[section.payload.clone()],
        b"MFoGCSqGSIb3DQEHAqBNMEsCAQExDTALBglghkgBZQMEAgEwCwYJKoZIhvcNAQcBMSowKAIBATAFMAACAQEwCwYJYIZIAWUDBAIBMA0GCSqGSIb3DQEBAQUABAA=\n"
    );
    assert_eq!(section.cms.len(), 92);
    let signed_alphabet = section
        .signed_alphabet_bytes(source)
        .expect("signed source range");
    assert!(!signed_alphabet.contains(&b'\n'));
    assert!(signed_alphabet.ends_with(b"END-ISO-10303-21;"));
    assert_eq!(
        &source[section.span.clone()],
        b"SIGNATURE;MFoGCSqGSIb3DQEHAqBNMEsCAQExDTALBglghkgBZQMEAgEwCwYJKoZIhvcNAQcBMSowKAIBATAFMAACAQEwCwYJYIZIAWUDBAIBMA0GCSqGSIb3DQEBAQUABAA=\nENDSEC;"
    );
}

#[test]
fn parser_does_not_verify_detached_content_during_structural_admission() {
    let source = include_bytes!("tests/data/sg01_signature_method_selection.p21");
    let (original, original_diagnostics) =
        crate::test_support::with_service_context(source, crate::parse::parse_inner)
            .expect("original signature");
    let mut tampered_source = source.to_vec();
    let marker = b"SG-01 CMS method witness";
    let marker_start = tampered_source
        .windows(marker.len())
        .position(|window| window == marker)
        .expect("signature witness marker");
    tampered_source[marker_start] = b'T';
    let (tampered, tampered_diagnostics) =
        crate::test_support::with_service_context(&tampered_source, crate::parse::parse_inner)
            .expect("structural signature admission");

    assert!(original_diagnostics.is_empty());
    assert!(tampered_diagnostics.is_empty());
    assert_eq!(
        sections(&original, source)[0].cms,
        sections(&tampered, &tampered_source)[0].cms
    );
    assert_ne!(
        sections(&original, source)[0]
            .signed_alphabet_bytes(source)
            .expect("original signed content"),
        sections(&tampered, &tampered_source)[0]
            .signed_alphabet_bytes(&tampered_source)
            .expect("tampered signed content")
    );
}

#[test]
fn real_detached_cms_witness_remains_structural_after_source_tampering() {
    let source = include_bytes!("tests/data/sg04_openssl_detached.p21");
    let (original, original_diagnostics) =
        crate::test_support::with_service_context(source, crate::parse::parse_inner)
            .expect("real CMS witness");
    let mut tampered_source = source.to_vec();
    let marker = b"SG-04 OpenSSL detached CMS witness";
    let marker_start = tampered_source
        .windows(marker.len())
        .position(|window| window == marker)
        .expect("real CMS witness marker");
    tampered_source[marker_start] = b'X';
    let (tampered, tampered_diagnostics) =
        crate::test_support::with_service_context(&tampered_source, crate::parse::parse_inner)
            .expect("tampered CMS structure");

    assert!(original_diagnostics.is_empty());
    assert!(tampered_diagnostics.is_empty());
    assert_eq!(sections(&original, source)[0].cms.len(), 1324);
    assert_eq!(
        super::validate_detached_cms(&sections(&original, source)[0].cms),
        Ok(())
    );
    assert_eq!(
        sections(&original, source)[0].cms,
        sections(&tampered, &tampered_source)[0].cms
    );
    assert_ne!(
        sections(&original, source)[0]
            .signed_alphabet_bytes(source)
            .expect("original signed content"),
        sections(&tampered, &tampered_source)[0]
            .signed_alphabet_bytes(&tampered_source)
            .expect("tampered signed content")
    );
}

#[test]
fn signature_method_and_parameters_are_inside_cms_payload() {
    let source = include_bytes!("tests/data/sg01_signature_method_selection.p21");
    let (exchange, diagnostics) =
        crate::test_support::with_service_context(source, crate::parse::parse_inner)
            .expect("signature method witness");

    assert!(diagnostics.is_empty());
    assert_eq!(sections(&exchange, source).len(), 1);
    let section = &sections(&exchange, source)[0];
    assert_eq!(
        &source[section.span.clone()],
        b"SIGNATURE;\nMFoGCSqGSIb3DQEHAqBNMEsCAQExDTALBglghkgBZQMEAgEwCwYJKoZIhvcNAQcBMSowKAIBATAFMAACAQEwCwYJYIZIAWUDBAIBMA0GCSqGSIb3DQEBAQUABAA=\nENDSEC;"
    );
    assert_eq!(
        &source[section.span.start..section.payload.start],
        b"SIGNATURE;"
    );
    assert_eq!(&source[section.payload.end..section.span.end], b"ENDSEC;");
    assert_eq!(section.cms.len(), 92);
}

#[test]
fn parser_projects_each_signature_to_preceding_alphabet_bytes() {
    let source = include_bytes!("tests/data/sg02_signed_byte_sequence.p21");
    let (exchange, diagnostics) =
        crate::test_support::with_service_context(source, crate::parse::parse_inner)
            .expect("signed byte witness");

    assert!(diagnostics.is_empty());
    assert_eq!(sections(&exchange, source).len(), 2);
    let first = &sections(&exchange, source)[0];
    let second = &sections(&exchange, source)[1];
    assert_eq!(first.signed.start, 0);
    assert_eq!(first.signed.end, first.span.start);
    assert_eq!(second.signed.start, 0);
    assert_eq!(second.signed.end, second.span.start);
    assert_eq!(
        first.signed_alphabet_bytes(source).expect("first signed bytes"),
        b"ISO-10303-21;HEADER;FILE_DESCRIPTION(('SG-02 signed byte sequence witness'),'4;2');FILE_NAME('sg02_signed_byte_sequence','2026-08-16T00:00:00',('cadmpeg'),('cadmpeg'),'cadmpeg-step','','');FILE_SCHEMA(('AP242'));ENDSEC;DATA;#1=PRODUCT('P1','Part','',());#2=(LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.));#3=(NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.));#4=(GEOMETRIC_REPRESENTATION_CONTEXT(3) GLOBAL_UNIT_ASSIGNED_CONTEXT((#2,#3)) REPRESENTATION_CONTEXT('model','3D'));ENDSEC;END-ISO-10303-21;/*FIRST*/\\N\\"
    );
    let mut expected_second = first
        .signed_alphabet_bytes(source)
        .expect("first signed bytes")
        .clone();
    expected_second.extend_from_slice(
        b"SIGNATURE;MFoGCSqGSIb3DQEHAqBNMEsCAQExDTALBglghkgBZQMEAgEwCwYJKoZIhvcNAQcBMSowKAIBATAFMAACAQEwCwYJYIZIAWUDBAIBMA0GCSqGSIb3DQEBAQUABAA=ENDSEC;/*SECOND*/\\F\\",
    );
    assert_eq!(
        second
            .signed_alphabet_bytes(source)
            .expect("second signed bytes"),
        expected_second
    );
    assert_eq!(
        &source[first.span.clone()],
        b"SIGNATURE;\nMFoGCSqGSIb3DQEHAqBNMEsCAQExDTALBglghkgBZQMEAgEwCwYJKoZIhvcNAQcBMSowKAIBATAFMAACAQEwCwYJYIZIAWUDBAIBMA0GCSqGSIb3DQEBAQUABAA=\nENDSEC;"
    );
}

#[test]
fn parser_ignores_controls_inside_signature_terminators() {
    let source = b"ISO-10303-21;HEADER;FILE_DESCRIPTION(('signature'),'4;2');FILE_NAME('','',(''),(''),'','','');FILE_SCHEMA(('AP242'));ENDSEC;DATA;#1=ITEM();ENDSEC;END-ISO-10303-21;SIGNATURE;MFoGCSqGSIb3DQEHAqBNMEsCAQExDTALBglghkgBZQMEAgEwCwYJKoZIhvcNAQcBMSowKAIBATAFMAACAQEwCwYJYIZIAWUDBAIBMA0GCSqGSIb3DQEBAQUABAA=\nEN\nDSEC;";
    let (exchange, _) =
        crate::test_support::with_service_context(source, crate::parse::parse_inner)
            .expect("split signature terminator");
    assert_eq!(exchange.signatures().len(), 1);
}

#[test]
fn parser_rejects_invalid_signature_base64() {
    for (payload, expected_message) in [
        ("YWJjZA==!", "invalid SIGNATURE base64 padding"),
        ("YWJjZA==AAAA", "invalid SIGNATURE base64 padding"),
        ("YWJjZA=", "SIGNATURE base64 content has incomplete quantum"),
        ("YWJj ZA==", "invalid SIGNATURE base64 character"),
    ] {
        let source = format!(
            "ISO-10303-21;HEADER;FILE_DESCRIPTION(('signature'),'4;2');FILE_NAME('','',(''),(''),'','','');FILE_SCHEMA(('AP242'));ENDSEC;DATA;#1=ITEM();ENDSEC;END-ISO-10303-21;SIGNATURE;{payload}\nENDSEC;"
        );
        let error =
            crate::test_support::with_service_context(source.as_bytes(), crate::parse::parse_inner)
                .expect_err("invalid signature");
        assert!(matches!(
            error,
            crate::parse::ParseError::Lex(crate::lex::LexError { message, .. })
                if message == expected_message
        ));
    }
}