///|
/// Validate content according to a data-element character class.
pub fn validate_data_kind(
field : Int,
kind : DataKind,
value : String,
) -> Result[Unit, IsoError] {
match kind {
Numeric => validate_numeric_text(field, value)
Alpha => validate_alpha_text(field, value)
Alphanumeric => validate_alphanumeric_text(field, value, false)
AlphanumericSpecial => validate_alphanumeric_text(field, value, true)
Track2Data => validate_track2_text(field, value)
BinaryHex => validate_binary_hex_text(field, value)
AnyText => validate_printable_text(field, value)
}
}
///|
/// Decimal-only field validation.
pub fn validate_numeric_text(
field : Int,
value : String,
) -> Result[Unit, IsoError] {
if value.length() == 0 {
return Ok(())
}
for i = 0; i < value.length(); i = i + 1 {
if !ascii_digit(value[i]) {
return Err(InvalidCharacter(field, i, "numeric"))
}
}
Ok(())
}
///|
/// Alphabetic-only field validation, including space.
pub fn validate_alpha_text(
field : Int,
value : String,
) -> Result[Unit, IsoError] {
for i = 0; i < value.length(); i = i + 1 {
let c = value[i]
if !ascii_alpha(c) && c != ' ' {
return Err(InvalidCharacter(field, i, "alphabetic"))
}
}
Ok(())
}
///|
/// Alphanumeric validation with optional ISO 8583 special characters.
pub fn validate_alphanumeric_text(
field : Int,
value : String,
allow_special : Bool,
) -> Result[Unit, IsoError] {
for i = 0; i < value.length(); i = i + 1 {
let c = value[i]
let common = ascii_alnum(c) || c == ' '
let special = c == '.' ||
c == ',' ||
c == '-' ||
c == '/' ||
c == ':' ||
c == '+' ||
c == '(' ||
c == ')' ||
c == '?'
if !common && !(allow_special && special) {
return Err(InvalidCharacter(field, i, "alphanumeric"))
}
}
Ok(())
}
///|
/// Printable seven-bit ASCII validation.
pub fn validate_printable_text(
field : Int,
value : String,
) -> Result[Unit, IsoError] {
for i = 0; i < value.length(); i = i + 1 {
let code = value[i].to_int()
if code < 32 || code > 126 {
return Err(InvalidCharacter(field, i, "printable ASCII"))
}
}
Ok(())
}
///|
/// Track 2 accepts digits, one separator, and common pad markers.
pub fn validate_track2_text(
field : Int,
value : String,
) -> Result[Unit, IsoError] {
let mut separators = 0
for i = 0; i < value.length(); i = i + 1 {
let c = value[i]
if c == '=' || c == 'D' {
separators += 1
if separators > 1 {
return Err(InvalidTrack2("multiple field separators"))
}
} else if c == 'F' && i == value.length() - 1 {
()
} else if !ascii_digit(c) {
return Err(InvalidCharacter(field, i, "Track 2"))
}
}
if separators != 1 {
return Err(InvalidTrack2("missing field separator"))
}
Ok(())
}
///|
/// Hexadecimal API representation for binary fields.
pub fn validate_binary_hex_text(
field : Int,
value : String,
) -> Result[Unit, IsoError] {
if value.length() % 2 != 0 {
return Err(InvalidLength(field, value.length() + 1, value.length()))
}
for i = 0; i < value.length(); i = i + 1 {
if hex_nibble(value[i]) is None {
return Err(InvalidCharacter(field, i, "hexadecimal"))
}
}
Ok(())
}
///|
/// Logical field length used by a length prefix.
pub fn field_logical_length(spec : FieldSpec, value : String) -> Int {
match spec.content_encoding {
BinaryContent => value.length() / 2
_ => value.length()
}
}
///|
/// Maximum logical length for the field.
pub fn field_max_logical_length(spec : FieldSpec) -> Int {
match spec.content_encoding {
BinaryContent => spec.max_length / 2
_ => spec.max_length
}
}
///|
/// Minimum logical length for the field.
pub fn field_min_logical_length(spec : FieldSpec) -> Int {
match spec.content_encoding {
BinaryContent => spec.min_length / 2
_ => spec.min_length
}
}
///|
/// Validate and canonicalize a value, applying fixed-field padding.
pub fn canonical_field_value(
spec : FieldSpec,
value : String,
) -> Result[String, IsoError] {
match validate_data_kind(spec.number, spec.data_kind, value) {
Err(error) => return Err(error)
Ok(_) => ()
}
let logical = field_logical_length(spec, value)
let maximum = field_max_logical_length(spec)
let minimum = field_min_logical_length(spec)
if logical > maximum {
return Err(FieldTooLong(spec.number, maximum, logical))
}
match spec.length_kind {
Fixed => {
if logical < minimum {
return Err(FieldTooShort(spec.number, minimum, logical))
}
if logical == maximum {
return Ok(value)
}
match spec.pad_direction {
NoPadding => Err(InvalidLength(spec.number, maximum, logical))
PadLeft =>
if spec.content_encoding == BinaryContent {
let fill = spec.pad_char.to_int().unsafe_to_char()
Ok(left_pad(value, spec.max_length, fill.to_int().to_uint16()))
} else {
Ok(left_pad(value, spec.max_length, spec.pad_char))
}
PadRight =>
if spec.content_encoding == BinaryContent {
let fill = spec.pad_char.to_int().unsafe_to_char()
Ok(right_pad(value, spec.max_length, fill.to_int().to_uint16()))
} else {
Ok(right_pad(value, spec.max_length, spec.pad_char))
}
}
}
Llvar | Lllvar =>
if logical < minimum {
Err(FieldTooShort(spec.number, minimum, logical))
} else {
Ok(value)
}
}
}
///|
/// Validate a message against every referenced field specification.
pub fn validate_message_fields(
message : IsoMessage,
packager : Packager,
) -> Array[ValidationIssue] {
let issues : Array[ValidationIssue] = []
for entry in message.fields {
match packager.spec(entry.number) {
None =>
issues.push({
code: "UNKNOWN_FIELD",
field: entry.number,
message: "packager has no definition for field \{entry.number}",
severity: "error",
})
Some(spec) =>
match canonical_field_value(spec, entry.value) {
Ok(_) => ()
Err(error) =>
issues.push({
code: error.code(),
field: entry.number,
message: error.message(),
severity: "error",
})
}
}
}
issues
}
///|
/// True when no error-level field validation findings exist.
pub fn fields_are_valid(message : IsoMessage, packager : Packager) -> Bool {
validate_message_fields(message, packager).length() == 0
}