///|
/// Length-prefix format used by common ISO 8583 TCP integrations.
pub(all) enum FrameHeader {
BinaryBigEndian16
AsciiDecimal4
} derive(Eq, Debug)
///|
/// Incremental decoder for length-prefixed byte streams.
pub(all) struct StreamDecoder {
header : FrameHeader
max_frame : Int
mut buffer : Array[Byte]
} derive(Debug)
///|
/// Create an incremental decoder with an explicit payload limit.
pub fn stream_decoder(
header : FrameHeader,
max_frame : Int,
) -> Result[StreamDecoder, IsoError] {
let format_limit = frame_format_limit(header)
if max_frame <= 0 || max_frame > format_limit {
return Err(
InvalidStreamFrame(
"maximum frame must be in 1..\{format_limit}, got \{max_frame}",
),
)
}
Ok({ header, max_frame, buffer: [], })
}
///|
/// Maximum payload encodable by the selected header.
fn frame_format_limit(header : FrameHeader) -> Int {
match header {
BinaryBigEndian16 => 65535
AsciiDecimal4 => 9999
}
}
///|
/// Header width in bytes.
pub fn frame_header_width(header : FrameHeader) -> Int {
match header {
BinaryBigEndian16 => 2
AsciiDecimal4 => 4
}
}
///|
/// Prefix one payload with the selected length format.
pub fn encode_frame(
header : FrameHeader,
payload : Bytes,
) -> Result[Bytes, IsoError] {
let length = payload.length()
if length <= 0 {
return Err(InvalidStreamFrame("empty payloads are not valid ISO messages"))
}
let limit = frame_format_limit(header)
if length > limit {
return Err(
InvalidStreamFrame(
"payload length \{length} exceeds \{limit} for selected header",
),
)
}
let output : Array[Byte] = []
match header {
BinaryBigEndian16 => {
output.push(((length >> 8) & 255).to_byte())
output.push((length & 255).to_byte())
}
AsciiDecimal4 => {
let text = match decimal_width(length, 4) {
Ok(value) => value
Err(_) =>
return Err(InvalidStreamFrame("payload length does not fit ASCII4"))
}
for i = 0; i < text.length(); i = i + 1 {
output.push(text[i].to_int().to_byte())
}
}
}
append_stream_bytes(output, payload)
Ok(Bytes::from_array(output))
}
///|
/// Decode a header from the beginning of a byte buffer.
fn decode_frame_length(
header : FrameHeader,
buffer : Array[Byte],
) -> Result[Int, IsoError] {
match header {
BinaryBigEndian16 => Ok((buffer[0].to_int() << 8) | buffer[1].to_int())
AsciiDecimal4 => {
let mut length = 0
for i = 0; i < 4; i = i + 1 {
let digit = buffer[i].to_int() - 48
if digit < 0 || digit > 9 {
return Err(
InvalidStreamFrame(
"ASCII4 header contains a non-decimal byte at offset \{i}",
),
)
}
length = length * 10 + digit
}
Ok(length)
}
}
}
///|
/// Feed an arbitrary stream chunk and return every complete payload.
pub fn StreamDecoder::feed(
self : StreamDecoder,
chunk : Bytes,
) -> Result[Array[Bytes], IsoError] {
append_stream_bytes(self.buffer, chunk)
let frames : Array[Bytes] = []
let width = frame_header_width(self.header)
let mut consumed = 0
while self.buffer.length() - consumed >= width {
let header_buffer : Array[Byte] = []
for i = 0; i < width; i = i + 1 {
header_buffer.push(self.buffer[consumed + i])
}
let length = match decode_frame_length(self.header, header_buffer) {
Ok(value) => value
Err(error) => {
self.buffer = []
return Err(error)
}
}
if length <= 0 {
self.buffer = []
return Err(InvalidStreamFrame("zero-length frame is not allowed"))
}
if length > self.max_frame {
self.buffer = []
return Err(
InvalidStreamFrame(
"declared payload length \{length} exceeds configured maximum \{self.max_frame}",
),
)
}
let frame_end = consumed + width + length
if frame_end > self.buffer.length() {
break
}
frames.push(
Bytes::makei(length, fn(i) { self.buffer[consumed + width + i] }),
)
consumed = frame_end
}
if consumed > 0 {
let remaining : Array[Byte] = []
for i = consumed; i < self.buffer.length(); i = i + 1 {
remaining.push(self.buffer[i])
}
self.buffer = remaining
}
Ok(frames)
}
///|
/// Number of undecoded bytes currently retained.
pub fn StreamDecoder::buffered_bytes(self : StreamDecoder) -> Int {
self.buffer.length()
}
///|
/// Drop a partial frame after a connection reset.
pub fn StreamDecoder::reset(self : StreamDecoder) -> Unit {
self.buffer = []
}
///|
/// Assert that the stream ended on a complete frame boundary.
pub fn StreamDecoder::finish(self : StreamDecoder) -> Result[Unit, IsoError] {
if self.buffer.length() == 0 {
return Ok(())
}
let width = frame_header_width(self.header)
if self.buffer.length() < width {
return Err(
InvalidStreamFrame(
"stream ended in a partial \{width}-byte length header",
),
)
}
let length = match decode_frame_length(self.header, self.buffer) {
Ok(value) => value
Err(error) => return Err(error)
}
let actual = self.buffer.length() - width
Err(
InvalidStreamFrame("stream ended with \{actual} of \{length} payload bytes"),
)
}
///|
/// Pack an ISO message and add a transport length header.
pub fn pack_framed_message(
packager : Packager,
profile : WireProfile,
header : FrameHeader,
message : IsoMessage,
) -> Result[Bytes, IsoError] {
let payload = match pack_message(packager, profile, message) {
Ok(value) => value
Err(error) => return Err(error)
}
encode_frame(header, payload)
}
///|
/// Decode exactly one framed message and reject extra frames or partial data.
pub fn unpack_framed_message(
packager : Packager,
profile : WireProfile,
header : FrameHeader,
frame : Bytes,
) -> Result[IsoMessage, IsoError] {
let decoder = match stream_decoder(header, frame_format_limit(header)) {
Ok(value) => value
Err(error) => return Err(error)
}
let payloads = match decoder.feed(frame) {
Ok(value) => value
Err(error) => return Err(error)
}
match decoder.finish() {
Err(error) => return Err(error)
Ok(_) => ()
}
if payloads.length() != 1 {
return Err(
InvalidStreamFrame(
"expected exactly one frame, decoded \{payloads.length()}",
),
)
}
unpack_message(packager, profile, payloads[0])
}
///|
fn append_stream_bytes(target : Array[Byte], source : Bytes) -> Unit {
for byte in source {
target.push(byte)
}
}