iso8583_core/
validation.rs1use crate::error::{ISO8583Error, Result};
4use crate::field::{Field, FieldLength, FieldType, FieldValue};
5use crate::message::ISO8583Message;
6
7pub struct Validator;
9
10impl Validator {
11 pub fn validate_pan(pan: &str) -> bool {
29 let pan: String = pan.chars().filter(|c| c.is_ascii_digit()).collect();
31
32 if pan.len() < 13 || pan.len() > 19 {
34 return false;
35 }
36
37 Self::luhn_check(&pan)
38 }
39
40 fn luhn_check(number: &str) -> bool {
42 let mut sum = 0;
43 let mut double = false;
44
45 for ch in number.chars().rev() {
47 if let Some(digit) = ch.to_digit(10) {
48 let mut digit = digit;
49
50 if double {
51 digit *= 2;
52 if digit > 9 {
53 digit -= 9; }
55 }
56
57 sum += digit;
58 double = !double;
59 } else {
60 return false; }
62 }
63
64 sum % 10 == 0
65 }
66
67 pub fn validate_field_format(field: Field, value: &FieldValue) -> Result<()> {
69 let def = field.definition();
70
71 match value {
72 FieldValue::String(s) => {
73 match def.field_type {
75 FieldType::Numeric => {
76 if !s.chars().all(|c: char| c.is_ascii_digit()) {
77 return Err(ISO8583Error::invalid_field_value(
78 field.number(),
79 "Field must be numeric",
80 ));
81 }
82 }
83 FieldType::Alpha => {
84 if !s.chars().all(|c: char| c.is_ascii_alphabetic() || c == ' ') {
85 return Err(ISO8583Error::invalid_field_value(
86 field.number(),
87 "Field must be alphabetic",
88 ));
89 }
90 }
91 _ => {} }
93
94 match def.length {
96 FieldLength::Fixed(len) => {
97 if s.len() != len {
98 return Err(ISO8583Error::field_length_mismatch(
99 field.number(),
100 len,
101 s.len(),
102 ));
103 }
104 }
105 FieldLength::LLVar(max_len) | FieldLength::LLLVar(max_len) => {
106 if s.len() > max_len {
107 return Err(ISO8583Error::invalid_field_value(
108 field.number(),
109 format!("Field exceeds maximum length of {}", max_len),
110 ));
111 }
112 }
113 }
114 }
115 FieldValue::Binary(_) => {
116 }
118 }
119
120 Ok(())
121 }
122
123 pub fn validate_field_value(field: Field, value: &FieldValue) -> Result<()> {
125 match field {
126 Field::PrimaryAccountNumber => {
127 if let Some(pan) = value.as_string() {
128 if !Self::validate_pan(pan) {
129 return Err(ISO8583Error::LuhnCheckFailed);
130 }
131 }
132 }
133 Field::ResponseCode => {
134 if let Some(code) = value.as_string() {
135 if code.len() != 2 {
136 return Err(ISO8583Error::invalid_field_value(
137 39,
138 "Response code must be 2 characters",
139 ));
140 }
141 }
142 }
143 Field::TransactionAmount | Field::SettlementAmount => {
144 if let Some(amount) = value.as_string() {
145 if !amount.chars().all(|c: char| c.is_ascii_digit()) {
146 return Err(ISO8583Error::invalid_field_value(
147 field.number(),
148 "Amount must be numeric",
149 ));
150 }
151 if amount.chars().all(|c: char| c == '0') {
153 return Err(ISO8583Error::invalid_field_value(
154 field.number(),
155 "Amount cannot be zero",
156 ));
157 }
158 }
159 }
160 _ => {}
161 }
162
163 Ok(())
164 }
165
166 pub fn validate_required_fields(msg: &ISO8583Message) -> Result<()> {
168 let common_required = vec![
170 Field::ProcessingCode,
171 Field::SystemTraceAuditNumber,
172 Field::LocalTransactionTime,
173 Field::LocalTransactionDate,
174 ];
175
176 for field in common_required {
177 if msg.get_field(field).is_none() {
178 return Err(ISO8583Error::MissingRequiredField(field.number()));
179 }
180 }
181
182 if msg.mti.is_request() {
184 if msg.mti.class == crate::mti::MessageClass::Financial
186 || msg.mti.class == crate::mti::MessageClass::Authorization
187 {
188 if msg.get_field(Field::PrimaryAccountNumber).is_none() {
189 return Err(ISO8583Error::MissingRequiredField(2));
190 }
191 if msg.get_field(Field::TransactionAmount).is_none() {
192 return Err(ISO8583Error::MissingRequiredField(4));
193 }
194 }
195 }
196
197 if msg.mti.is_response() {
198 if msg.get_field(Field::ResponseCode).is_none() {
200 return Err(ISO8583Error::MissingRequiredField(39));
201 }
202 }
203
204 Ok(())
205 }
206
207 pub fn validate_date_mmdd(date: &str) -> bool {
209 if date.len() != 4 {
210 return false;
211 }
212
213 if let Ok(month) = date[0..2].parse::<u32>() {
214 if let Ok(day) = date[2..4].parse::<u32>() {
215 return (1..=12).contains(&month) && (1..=31).contains(&day);
216 }
217 }
218
219 false
220 }
221
222 pub fn validate_time_hhmmss(time: &str) -> bool {
224 if time.len() != 6 {
225 return false;
226 }
227
228 if let Ok(hour) = time[0..2].parse::<u32>() {
229 if let Ok(minute) = time[2..4].parse::<u32>() {
230 if let Ok(second) = time[4..6].parse::<u32>() {
231 return hour < 24 && minute < 60 && second < 60;
232 }
233 }
234 }
235
236 false
237 }
238
239 pub fn validate_currency_code(code: &str) -> bool {
241 code.len() == 3 && code.chars().all(|c| c.is_ascii_digit())
242 }
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248
249 #[test]
250 fn test_luhn_valid() {
251 assert!(Validator::validate_pan("4111111111111111")); assert!(Validator::validate_pan("5500000000000004")); assert!(Validator::validate_pan("340000000000009")); }
256
257 #[test]
258 fn test_luhn_invalid() {
259 assert!(!Validator::validate_pan("4111111111111112")); assert!(!Validator::validate_pan("1234567890123456")); assert!(!Validator::validate_pan("0000000000000001")); }
263
264 #[test]
265 fn test_luhn_with_spaces() {
266 assert!(Validator::validate_pan("4111 1111 1111 1111")); }
268
269 #[test]
270 fn test_pan_length() {
271 assert!(!Validator::validate_pan("123")); assert!(!Validator::validate_pan("12345678901234567890")); }
274
275 #[test]
276 fn test_validate_date_mmdd() {
277 assert!(Validator::validate_date_mmdd("0101")); assert!(Validator::validate_date_mmdd("1231")); assert!(!Validator::validate_date_mmdd("1301")); assert!(!Validator::validate_date_mmdd("0132")); assert!(!Validator::validate_date_mmdd("123")); }
283
284 #[test]
285 fn test_validate_time_hhmmss() {
286 assert!(Validator::validate_time_hhmmss("000000")); assert!(Validator::validate_time_hhmmss("235959")); assert!(Validator::validate_time_hhmmss("120000")); assert!(!Validator::validate_time_hhmmss("240000")); assert!(!Validator::validate_time_hhmmss("126000")); assert!(!Validator::validate_time_hhmmss("120060")); }
293
294 #[test]
295 fn test_validate_currency_code() {
296 assert!(Validator::validate_currency_code("840")); assert!(Validator::validate_currency_code("566")); assert!(Validator::validate_currency_code("978")); assert!(!Validator::validate_currency_code("USD")); assert!(!Validator::validate_currency_code("84")); }
302}