pix_brcode_parser/
validation.rs1use crc::{Crc, Algorithm};
4use crate::error::{BRCodeError, Result};
5
6pub const CRC16: Crc<u16> = Crc::<u16>::new(&Algorithm {
9 width: 16,
10 poly: 0x1021,
11 init: 0xffff,
12 refin: false,
13 refout: false,
14 xorout: 0x0000,
15 check: 0x29b1,
16 residue: 0x0000,
17});
18
19fn calculate_crc16_simple(data: &[u8]) -> u16 {
21 let mut crc: u16 = 0xFFFF;
22
23 for &byte in data {
24 crc ^= (byte as u16) << 8;
25 for _ in 0..8 {
26 if crc & 0x8000 != 0 {
27 crc = (crc << 1) ^ 0x1021;
28 } else {
29 crc <<= 1;
30 }
31 }
32 }
33
34 crc
35}
36
37pub fn validate_crc16(brcode: &str) -> Result<bool> {
41 if brcode.len() < 4 {
42 return Err(BRCodeError::invalid_format("BR Code too short for CRC validation"));
43 }
44
45 let crc_pos = brcode.rfind("63");
47 let crc_pos = match crc_pos {
48 Some(pos) => pos,
49 None => return Err(BRCodeError::missing_field("CRC16 field (tag 63)")),
50 };
51
52 let payload = &brcode[..crc_pos + 2]; let crc_length_str = &brcode[crc_pos + 2..crc_pos + 4];
55 let crc_length: usize = crc_length_str.parse()
56 .map_err(|_| BRCodeError::invalid_format("Invalid CRC length field"))?;
57
58 if crc_length != 4 {
59 return Err(BRCodeError::InvalidFieldLength {
60 tag: "63".to_string(),
61 expected: 4,
62 actual: crc_length,
63 });
64 }
65
66 let expected_crc = &brcode[crc_pos + 4..crc_pos + 8];
67 let payload_with_length = format!("{}04", payload);
68
69 let calculated_crc = calculate_crc16_simple(payload_with_length.as_bytes());
71 let calculated_crc_hex = format!("{:04X}", calculated_crc);
72
73 Ok(calculated_crc_hex == expected_crc.to_uppercase())
74}
75
76pub fn calculate_crc16(payload: &str) -> String {
78 let payload_with_crc_field = format!("{}6304", payload);
79 let checksum = calculate_crc16_simple(payload_with_crc_field.as_bytes());
80 format!("{:04X}", checksum)
81}
82
83pub fn validate_pix_key(key: &str) -> Result<()> {
85 if key.is_empty() {
86 return Err(BRCodeError::InvalidPixKey("PIX key cannot be empty".to_string()));
87 }
88
89 if key.len() > 77 { return Err(BRCodeError::InvalidPixKey("PIX key too long".to_string()));
92 }
93
94 if !key.chars().all(|c| c.is_ascii() && !c.is_ascii_control()) {
96 return Err(BRCodeError::InvalidPixKey("PIX key contains invalid characters".to_string()));
97 }
98
99 Ok(())
100}
101
102pub fn validate_merchant_name(name: &str) -> Result<()> {
104 if name.is_empty() {
105 return Err(BRCodeError::missing_field("Merchant name"));
106 }
107
108 if name.len() > 25 {
109 return Err(BRCodeError::field_too_long("59", 25, name.len()));
110 }
111
112 Ok(())
113}
114
115pub fn validate_merchant_city(city: &str) -> Result<()> {
117 if city.is_empty() {
118 return Err(BRCodeError::missing_field("Merchant city"));
119 }
120
121 if city.len() > 15 {
122 return Err(BRCodeError::field_too_long("60", 15, city.len()));
123 }
124
125 Ok(())
126}
127
128pub fn validate_transaction_amount(amount: &str) -> Result<()> {
130 if amount.is_empty() {
131 return Ok(()); }
133
134 amount.parse::<f64>()
136 .map_err(|_| BRCodeError::ParseNumericError(format!("Invalid amount: {}", amount)))?;
137
138 Ok(())
139}
140
141pub fn validate_gui(gui: &str) -> Result<()> {
143 if gui != "br.gov.bcb.pix" {
144 return Err(BRCodeError::InvalidGui(gui.to_string()));
145 }
146 Ok(())
147}
148
149pub fn validate_currency(currency: &str) -> Result<()> {
151 if currency != "986" {
152 return Err(BRCodeError::InvalidCurrency(currency.to_string()));
153 }
154 Ok(())
155}
156
157pub fn validate_country_code(country: &str) -> Result<()> {
159 if country != "BR" {
160 return Err(BRCodeError::InvalidCountryCode(country.to_string()));
161 }
162 Ok(())
163}
164
165pub fn validate_payload_format(format: &str) -> Result<()> {
167 if format != "01" {
168 return Err(BRCodeError::InvalidPayloadFormat(format.to_string()));
169 }
170 Ok(())
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176
177 #[test]
178 fn test_crc16_calculation() {
179 let payload = "00020126580014br.gov.bcb.pix0136123e4567-e12b-12d1-a456-426614174000520400005303986540510.005802BR5913FULANO DE TAL6008BRASILIA62070503***";
180 let expected_crc = "36D9"; let calculated_crc = calculate_crc16(payload);
182 assert_eq!(calculated_crc, expected_crc);
183 }
184
185 #[test]
186 fn test_crc16_validation_valid() {
187 let brcode = "00020126580014br.gov.bcb.pix0136123e4567-e12b-12d1-a456-426614174000520400005303986540510.005802BR5913FULANO DE TAL6008BRASILIA62070503***630436D9";
188 assert!(validate_crc16(brcode).unwrap());
189 }
190
191 #[test]
192 fn test_crc16_validation_invalid() {
193 let brcode = "00020126580014br.gov.bcb.pix0136123e4567-e12b-12d1-a456-426614174000520400005303986540510.005802BR5913FULANO DE TAL6008BRASILIA62070503***6304WXYZ";
194 assert!(!validate_crc16(brcode).unwrap());
195 }
196
197 #[test]
198 fn test_validate_pix_key() {
199 assert!(validate_pix_key("123e4567-e89b-12d3-a456-426614174000").is_ok());
200 assert!(validate_pix_key("user@example.com").is_ok());
201 assert!(validate_pix_key("").is_err());
202 }
203
204 #[test]
205 fn test_validate_merchant_name() {
206 assert!(validate_merchant_name("FULANO DE TAL").is_ok());
207 assert!(validate_merchant_name("").is_err());
208 assert!(validate_merchant_name("A".repeat(30).as_str()).is_err());
209 }
210
211 #[test]
212 fn test_validate_gui() {
213 assert!(validate_gui("br.gov.bcb.pix").is_ok());
214 assert!(validate_gui("invalid.gui").is_err());
215 }
216}