sccp-protocol 0.4.2

Cisco SCCP (Skinny Client Control Protocol) codec/server
Documentation
use sccp_protocol::message::catalog::{PayloadLayout, PayloadSizeBounds};
use sccp_protocol::{
    AddParticipantResponse, BoundedBytes, ClientMessage, ControlMessage, DtmfPayloadIdentity,
    DtmfPayloadRequest, Frame, FrameDecoder, MessageId, ProtocolVersion, ServerMessage,
    XmlAlarmMessage,
};

fn encoded_frame(bytes: &[u8]) -> Frame {
    FrameDecoder::new().push(bytes).unwrap().remove(0)
}

#[test]
fn dtmf_subscription_words_round_trip_without_value_loss() {
    let identity = DtmfPayloadIdentity {
        payload_type: 101,
        conference_id: 0x1122_3344,
        passthrough_party_id: 0x5566_7788,
    };
    let request = DtmfPayloadRequest {
        payload_type: identity.payload_type,
        conference_id: identity.conference_id,
        passthrough_party_id: identity.passthrough_party_id,
        dtmf_type: 2,
    };

    for message in [
        ClientMessage::SubscribeDtmfPayloadResponse(identity),
        ClientMessage::UnsubscribeDtmfPayloadResponse(identity),
    ] {
        let frame = encoded_frame(&message.encode(ProtocolVersion::V22).unwrap());
        assert_eq!(frame.payload.len(), 12);
        assert_eq!(
            ClientMessage::decode_with_version(frame, ProtocolVersion::V22).unwrap(),
            message
        );
    }

    for message in [
        ServerMessage::SubscribeDtmfPayloadRequest(request),
        ServerMessage::SubscribeDtmfPayloadError(identity),
        ServerMessage::UnsubscribeDtmfPayloadRequest(request),
        ServerMessage::UnsubscribeDtmfPayloadError(identity),
    ] {
        let frame = encoded_frame(&message.encode(ProtocolVersion::V22).unwrap());
        assert!(matches!(frame.payload.len(), 12 | 16));
        assert_eq!(
            ServerMessage::decode(frame, ProtocolVersion::V22).unwrap(),
            message
        );
    }
}

#[test]
fn add_participant_response_emits_canonical_identifier_storage() {
    let identifier = BoundedBytes::try_from(b"bridge-participant".as_slice()).unwrap();
    let message = ControlMessage::AddParticipantResponse(AddParticipantResponse {
        conference_id: 42.into(),
        call_reference: 100.into(),
        result: sccp_protocol::AddParticipantResult::Ok,
        bridge_participant_id: identifier,
    });
    let frame = encoded_frame(&message.encode(ProtocolVersion::V22).unwrap());
    assert_eq!(frame.payload.len(), 272);
    assert_eq!(&frame.payload[12..30], b"bridge-participant");
    assert!(frame.payload[30..].iter().all(|byte| *byte == 0));

    let progressive = Frame::new(
        22,
        MessageId::AddParticipantResponse.wire_value(),
        frame.payload[..30].to_vec(),
    );
    let ControlMessage::AddParticipantResponse(decoded) =
        ControlMessage::decode(progressive, ProtocolVersion::V22).unwrap()
    else {
        panic!("expected add-participant response");
    };
    assert_eq!(
        decoded.bridge_participant_id.as_bytes(),
        b"bridge-participant"
    );
}

#[test]
fn xml_alarm_preserves_every_bounded_frame_length() {
    for payload_len in [0, 1, 2_000, 2_004, 2_048] {
        let payload = vec![b'x'; payload_len];
        let decoded = ClientMessage::decode_with_version(
            Frame::new(22, MessageId::XmlAlarm.wire_value(), payload.clone()),
            ProtocolVersion::V22,
        )
        .unwrap();
        let ClientMessage::XmlAlarm(message) = &decoded else {
            panic!("expected XML alarm");
        };
        assert_eq!(message.wire_payload(), payload);
        assert_eq!(
            encoded_frame(&decoded.encode(ProtocolVersion::V22).unwrap()).payload,
            payload
        );
    }

    assert!(
        ClientMessage::decode_with_version(
            Frame::new(22, MessageId::XmlAlarm.wire_value(), vec![0; 2_049]),
            ProtocolVersion::V22,
        )
        .is_err()
    );

    let canonical = XmlAlarmMessage::from_xml(vec![b'x'; 2_000]).unwrap();
    assert_eq!(canonical.wire_payload().len(), 2_004);
    let terminated = XmlAlarmMessage::from_wire_payload(b"<alarm/>\0suffix".to_vec()).unwrap();
    assert_eq!(terminated.xml_bytes(), b"<alarm/>");
}

#[test]
fn catalog_exposes_service_bounds_without_fixed_form_labels() {
    let alarm = MessageId::XmlAlarm.contract().unwrap();
    assert_eq!(alarm.payload_layout, PayloadLayout::BoundedPreserved);
    assert_eq!(
        alarm.payload_size_bounds,
        Some(PayloadSizeBounds {
            minimum: 0,
            maximum: 2_048,
        })
    );

    let participant = MessageId::AddParticipantResponse.contract().unwrap();
    assert_eq!(participant.fixed_payload_bytes, Some(272));
    assert_eq!(
        participant.payload_size_bounds,
        Some(PayloadSizeBounds {
            minimum: 12,
            maximum: 272,
        })
    );

    assert_eq!(
        MessageId::AuditConferenceRequest
            .contract()
            .unwrap()
            .fixed_payload_bytes,
        Some(0)
    );
}