///|
/// Vector category used by the protocol regression runner.
pub enum VectorCategory {
ApciVector
AsduVector
SequenceVector
BoundaryVector
NegativeVector
} derive(Eq, Debug)
///|
/// Expected outcome of a conformance vector.
pub enum VectorExpectation {
ExpectDecode
ExpectReject
} derive(Eq, Debug)
///|
/// A deterministic byte-level protocol vector.
pub struct ConformanceVector {
name : String
category : VectorCategory
input : Bytes
expectation : VectorExpectation
expected_kind : FrameKind?
expected_send : Int?
expected_receive : Int?
expected_payload : Bytes?
} derive(Debug)
///|
pub fn ConformanceVector::new(
name : String,
category : VectorCategory,
input : Bytes,
expectation : VectorExpectation,
expected_kind? : FrameKind,
expected_send? : Int,
expected_receive? : Int,
expected_payload? : Bytes,
) -> Result[ConformanceVector, String] {
if name.length() == 0 {
Err("conformance vector name is empty")
} else {
Ok({
name,
category,
input,
expectation,
expected_kind,
expected_send,
expected_receive,
expected_payload,
})
}
}
///|
pub fn ConformanceVector::name(self : ConformanceVector) -> String {
self.name
}
///|
pub fn ConformanceVector::category(self : ConformanceVector) -> VectorCategory {
self.category
}
///|
pub fn ConformanceVector::input(self : ConformanceVector) -> Bytes {
self.input
}
///|
pub fn ConformanceVector::expectation(
self : ConformanceVector,
) -> VectorExpectation {
self.expectation
}
///|
pub fn ConformanceVector::expected_kind(self : ConformanceVector) -> FrameKind? {
self.expected_kind
}
///|
pub fn ConformanceVector::expected_send(self : ConformanceVector) -> Int? {
self.expected_send
}
///|
pub fn ConformanceVector::expected_receive(self : ConformanceVector) -> Int? {
self.expected_receive
}
///|
pub fn ConformanceVector::expected_payload(self : ConformanceVector) -> Bytes? {
self.expected_payload
}
///|
/// Diagnostic detail produced for a vector failure.
pub struct VectorMismatch {
vector : String
field : String
expected : String
actual : String
} derive(Eq, Debug)
///|
pub fn VectorMismatch::vector(self : VectorMismatch) -> String {
self.vector
}
///|
pub fn VectorMismatch::field(self : VectorMismatch) -> String {
self.field
}
///|
pub fn VectorMismatch::expected(self : VectorMismatch) -> String {
self.expected
}
///|
pub fn VectorMismatch::actual(self : VectorMismatch) -> String {
self.actual
}
///|
pub struct VectorResult {
name : String
passed : Bool
decoded : Bool
encoded_size : Int
mismatches : Array[VectorMismatch]
message : String
} derive(Debug)
///|
pub fn VectorResult::name(self : VectorResult) -> String {
self.name
}
///|
pub fn VectorResult::passed(self : VectorResult) -> Bool {
self.passed
}
///|
pub fn VectorResult::decoded(self : VectorResult) -> Bool {
self.decoded
}
///|
pub fn VectorResult::encoded_size(self : VectorResult) -> Int {
self.encoded_size
}
///|
pub fn VectorResult::mismatches(self : VectorResult) -> Array[VectorMismatch] {
self.mismatches.copy()
}
///|
pub fn VectorResult::message(self : VectorResult) -> String {
self.message
}
///|
pub struct VectorReport {
total : Int
passed : Int
failed : Int
rejected : Int
bytes : Int
results : Array[VectorResult]
} derive(Debug)
///|
pub fn VectorReport::total(self : VectorReport) -> Int {
self.total
}
///|
pub fn VectorReport::passed(self : VectorReport) -> Int {
self.passed
}
///|
pub fn VectorReport::failed(self : VectorReport) -> Int {
self.failed
}
///|
pub fn VectorReport::rejected(self : VectorReport) -> Int {
self.rejected
}
///|
pub fn VectorReport::bytes(self : VectorReport) -> Int {
self.bytes
}
///|
pub fn VectorReport::results(self : VectorReport) -> Array[VectorResult] {
self.results.copy()
}
///|
fn vector_frame_kind_name(kind : FrameKind) -> String {
match kind {
Information => "I"
Supervisory => "S"
Unnumbered => "U"
}
}
///|
fn mismatch(
vector : ConformanceVector,
field : String,
expected : String,
actual : String,
) -> VectorMismatch {
{ vector: vector.name(), field, expected, actual }
}
///|
fn validate_decoded(
vector : ConformanceVector,
frame : Frame,
) -> Array[VectorMismatch] {
let result : Array[VectorMismatch] = []
match vector.expected_kind() {
Some(kind) =>
if frame.kind != kind {
result.push(
mismatch(
vector,
"kind",
vector_frame_kind_name(kind),
vector_frame_kind_name(frame.kind),
),
)
}
None => ()
}
match vector.expected_send() {
Some(value) =>
if frame.send_sequence != value {
result.push(
mismatch(vector, "send-sequence", "{value}", "{frame.send_sequence}"),
)
}
None => ()
}
match vector.expected_receive() {
Some(value) =>
if frame.receive_sequence != value {
result.push(
mismatch(
vector, "receive-sequence", "{value}", "{frame.receive_sequence}",
),
)
}
None => ()
}
match vector.expected_payload() {
Some(value) =>
if frame.payload != value {
result.push(
mismatch(
vector, "payload", "{value.length()} bytes", "{frame.payload.length()} bytes",
),
)
}
None => ()
}
result
}
///|
pub fn run_conformance_vector(vector : ConformanceVector) -> VectorResult {
match decode_frame(vector.input()) {
Err(error) => {
let passed = vector.expectation() == ExpectReject
{
name: vector.name(),
passed,
decoded: false,
encoded_size: 0,
mismatches: if passed {
[]
} else {
[mismatch(vector, "decode", "valid APDU", error)]
},
message: if passed {
"expected rejection"
} else {
error
},
}
}
Ok(frame) => {
let mismatches = if vector.expectation() == ExpectReject {
[mismatch(vector, "decode", "rejected", "decoded")]
} else {
validate_decoded(vector, frame)
}
let encoded = encode_frame(frame)
let roundtrip = if encoded == vector.input() {
mismatches
} else {
let result = mismatches.copy()
result.push(
mismatch(
vector, "roundtrip", "identical APDU", "encoded bytes differ",
),
)
result
}
{
name: vector.name(),
passed: roundtrip.length() == 0,
decoded: true,
encoded_size: encoded.length(),
mismatches: roundtrip,
message: if roundtrip.length() == 0 {
"vector passed"
} else {
"vector mismatch"
},
}
}
}
}
///|
pub fn run_conformance_suite(
vectors : Array[ConformanceVector],
) -> VectorReport {
let results : Array[VectorResult] = []
let mut passed = 0
let mut failed = 0
let mut rejected = 0
let mut bytes = 0
for vector in vectors {
let result = run_conformance_vector(vector)
results.push(result)
bytes += result.encoded_size()
if result.passed() {
passed += 1
} else {
failed += 1
}
if !result.decoded() {
rejected += 1
}
}
{ total: vectors.length(), passed, failed, rejected, bytes, results }
}
///|
fn vector_ok(
name : String,
category : VectorCategory,
frame : Frame,
) -> ConformanceVector {
ConformanceVector::new(
name,
category,
encode_frame(frame),
ExpectDecode,
expected_kind=frame.kind,
expected_send=frame.send_sequence,
expected_receive=frame.receive_sequence,
expected_payload=frame.payload,
).unwrap()
}
///|
fn vector_reject(name : String, input : Bytes) -> ConformanceVector {
ConformanceVector::new(name, NegativeVector, input, ExpectReject).unwrap()
}
///|
/// Built-in vectors cover all APCI frame families and malformed boundaries.
pub fn standard_conformance_vectors() -> Array[ConformanceVector] {
let normalized = normalized_value_asdu(3, 1, 2300, 0)
let single = single_point_asdu(3, 1, 7, true, 0)
[
vector_ok("i-frame-empty-payload", ApciVector, information_frame(0, 0, b"")),
vector_ok(
"i-frame-sequence-wrap",
SequenceVector,
information_frame(32767, 32767, normalized),
),
vector_ok(
"i-frame-single-point",
AsduVector,
information_frame(12, 9, single),
),
vector_ok("s-frame-zero", ApciVector, supervisory_frame(0)),
vector_ok("s-frame-window-edge", SequenceVector, supervisory_frame(32767)),
vector_ok("u-startdt", ApciVector, unnumbered_frame(0x0007U.to_uint16())),
vector_ok("u-stopdt", ApciVector, unnumbered_frame(0x0013U.to_uint16())),
vector_ok("u-testfr", ApciVector, unnumbered_frame(0x0043U.to_uint16())),
vector_reject("short-apdu", b"\x68\x04"),
vector_reject("wrong-start-byte", b"\x67\x04\x00\x00\x00\x00"),
vector_reject("declared-length-too-long", b"\x68\x08\x00\x00\x00\x00"),
]
}
///|
/// Add a caller-owned vector while preserving deterministic ordering.
pub struct VectorCatalog {
vectors : Array[ConformanceVector]
names : Map[String, Bool]
} derive(Debug)
///|
pub fn VectorCatalog::new() -> VectorCatalog {
{ vectors: [], names: {} }
}
///|
pub fn VectorCatalog::add(
self : VectorCatalog,
vector : ConformanceVector,
) -> Result[Unit, String] {
if self.names.contains(vector.name()) {
Err("conformance vector name already exists")
} else {
self.names[vector.name()] = true
self.vectors.push(vector)
Ok(())
}
}
///|
pub fn VectorCatalog::add_all(
self : VectorCatalog,
vectors : Array[ConformanceVector],
) -> Result[Unit, String] {
for vector in vectors {
match self.add(vector) {
Ok(_) => ()
Err(error) => return Err(error)
}
}
Ok(())
}
///|
pub fn VectorCatalog::len(self : VectorCatalog) -> Int {
self.vectors.length()
}
///|
pub fn VectorCatalog::vectors(self : VectorCatalog) -> Array[ConformanceVector] {
self.vectors.copy()
}
///|
pub fn vector_category_examples() -> Array[VectorCategory] {
[ApciVector, AsduVector, SequenceVector, BoundaryVector, NegativeVector]
}
///|
pub fn vector_expectation_examples() -> Array[VectorExpectation] {
[ExpectDecode, ExpectReject]
}