pix_brcode_parser/
parser.rs

1//! Parser implementation for PIX BR Code QR strings
2
3use crate::error::{BRCodeError, Result};
4use crate::types::{BRCode, MerchantAccountInfo, AdditionalData, EmvField};
5use crate::validation::*;
6use std::collections::HashMap;
7
8/// Parse a PIX BR Code string into a structured BRCode
9pub fn parse_brcode(input: &str) -> Result<BRCode> {
10    if input.is_empty() {
11        return Err(BRCodeError::invalid_format("Empty BR Code string"));
12    }
13
14    // First validate CRC16 checksum
15    if !validate_crc16(input)? {
16        return Err(BRCodeError::InvalidChecksum);
17    }
18
19    // Parse TLV fields
20    let fields = parse_tlv_fields(input)?;
21    
22    // Build BRCode from parsed fields
23    build_brcode_from_fields(fields)
24}
25
26/// Parse TLV (Tag-Length-Value) fields from BR Code string
27fn parse_tlv_fields(input: &str) -> Result<HashMap<String, EmvField>> {
28    let mut fields = HashMap::new();
29    let mut pos = 0;
30    
31    while pos < input.len() {
32        if pos + 4 > input.len() {
33            return Err(BRCodeError::TlvParsingError("Insufficient data for TLV field".to_string()));
34        }
35        
36        // Extract tag (2 digits)
37        let tag = &input[pos..pos + 2];
38        pos += 2;
39        
40        // Extract length (2 digits)
41        let length_str = &input[pos..pos + 2];
42        pos += 2;
43        
44        let length: usize = length_str.parse()
45            .map_err(|_| BRCodeError::TlvParsingError(format!("Invalid length field: {}", length_str)))?;
46        
47        // Check if we have enough data for the value
48        if pos + length > input.len() {
49            return Err(BRCodeError::TlvParsingError(format!("Insufficient data for field value, tag: {}", tag)));
50        }
51        
52        // Extract value
53        let value = &input[pos..pos + length];
54        pos += length;
55        
56        let field = EmvField {
57            tag: tag.to_string(),
58            length,
59            value: value.to_string(),
60        };
61        
62        fields.insert(tag.to_string(), field);
63    }
64    
65    Ok(fields)
66}
67
68/// Build BRCode struct from parsed TLV fields
69fn build_brcode_from_fields(fields: HashMap<String, EmvField>) -> Result<BRCode> {
70    // Extract required fields
71    let payload_format_indicator = get_required_field(&fields, "00")?;
72    validate_payload_format(&payload_format_indicator.value)?;
73    
74    let merchant_category_code = get_required_field(&fields, "52")?;
75    let transaction_currency = get_required_field(&fields, "53")?;
76    validate_currency(&transaction_currency.value)?;
77    
78    let country_code = get_required_field(&fields, "58")?;
79    validate_country_code(&country_code.value)?;
80    
81    let merchant_name = get_required_field(&fields, "59")?;
82    validate_merchant_name(&merchant_name.value)?;
83    
84    let merchant_city = get_required_field(&fields, "60")?;
85    validate_merchant_city(&merchant_city.value)?;
86    
87    let crc16 = get_required_field(&fields, "63")?;
88    
89    // Extract optional fields
90    let point_of_initiation_method = fields.get("01").map(|f| f.value.clone());
91    let transaction_amount = fields.get("54").map(|f| f.value.clone());
92    
93    // Validate transaction amount if present
94    if let Some(ref amount) = transaction_amount {
95        validate_transaction_amount(amount)?;
96    }
97    
98    // Parse merchant account information (field 26)
99    let merchant_account_info = parse_merchant_account_info(&fields)?;
100    
101    // Parse additional data (field 62) if present
102    let additional_data = if let Some(field) = fields.get("62") {
103        Some(parse_additional_data(&field.value)?)
104    } else {
105        None
106    };
107    
108    Ok(BRCode {
109        payload_format_indicator: payload_format_indicator.value.clone(),
110        point_of_initiation_method,
111        merchant_account_info,
112        merchant_category_code: merchant_category_code.value.clone(),
113        transaction_currency: transaction_currency.value.clone(),
114        transaction_amount,
115        country_code: country_code.value.clone(),
116        merchant_name: merchant_name.value.clone(),
117        merchant_city: merchant_city.value.clone(),
118        additional_data,
119        crc16: crc16.value.clone(),
120    })
121}
122
123/// Parse merchant account information from field 26
124fn parse_merchant_account_info(fields: &HashMap<String, EmvField>) -> Result<MerchantAccountInfo> {
125    let field_26 = get_required_field(fields, "26")?;
126    
127    // Parse nested TLV fields within field 26
128    let nested_fields = parse_tlv_fields(&field_26.value)?;
129    
130    // GUI field (tag 00) - should be "br.gov.bcb.pix"
131    let gui_field = get_required_field(&nested_fields, "00")?;
132    validate_gui(&gui_field.value)?;
133    
134    // PIX key field (tag 01)
135    let pix_key_field = get_required_field(&nested_fields, "01")?;
136    validate_pix_key(&pix_key_field.value)?;
137    
138    // Optional description field (tag 02)
139    let description = nested_fields.get("02").map(|f| f.value.clone());
140    
141    // Optional URL field (tag 25) for dynamic QR codes
142    let url = nested_fields.get("25").map(|f| f.value.clone());
143    
144    Ok(MerchantAccountInfo {
145        gui: gui_field.value.clone(),
146        pix_key: pix_key_field.value.clone(),
147        description,
148        url,
149    })
150}
151
152/// Parse additional data from field 62
153fn parse_additional_data(input: &str) -> Result<AdditionalData> {
154    let fields = parse_tlv_fields(input)?;
155    
156    Ok(AdditionalData {
157        bill_number: fields.get("01").map(|f| f.value.clone()),
158        mobile_number: fields.get("02").map(|f| f.value.clone()),
159        store_label: fields.get("03").map(|f| f.value.clone()),
160        loyalty_number: fields.get("04").map(|f| f.value.clone()),
161        reference_label: fields.get("05").map(|f| f.value.clone()),
162        customer_label: fields.get("06").map(|f| f.value.clone()),
163        terminal_label: fields.get("07").map(|f| f.value.clone()),
164        purpose_of_transaction: fields.get("08").map(|f| f.value.clone()),
165        additional_consumer_data_request: fields.get("09").map(|f| f.value.clone()),
166    })
167}
168
169/// Get a required field from the fields map
170fn get_required_field<'a>(fields: &'a HashMap<String, EmvField>, tag: &str) -> Result<&'a EmvField> {
171    fields.get(tag).ok_or_else(|| BRCodeError::missing_field(format!("Required field with tag {}", tag)))
172}
173
174/// Generate a BR Code string from a BRCode struct
175pub fn generate_brcode(brcode: &BRCode) -> Result<String> {
176    let mut result = String::new();
177    
178    // Add payload format indicator
179    result.push_str(&format_field("00", &brcode.payload_format_indicator));
180    
181    // Add point of initiation method if present
182    if let Some(ref pim) = brcode.point_of_initiation_method {
183        result.push_str(&format_field("01", pim));
184    }
185    
186    // Add merchant account information (field 26)
187    let mai_content = format_merchant_account_info(&brcode.merchant_account_info);
188    result.push_str(&format_field("26", &mai_content));
189    
190    // Add merchant category code
191    result.push_str(&format_field("52", &brcode.merchant_category_code));
192    
193    // Add transaction currency
194    result.push_str(&format_field("53", &brcode.transaction_currency));
195    
196    // Add transaction amount if present
197    if let Some(ref amount) = brcode.transaction_amount {
198        result.push_str(&format_field("54", amount));
199    }
200    
201    // Add country code
202    result.push_str(&format_field("58", &brcode.country_code));
203    
204    // Add merchant name
205    result.push_str(&format_field("59", &brcode.merchant_name));
206    
207    // Add merchant city
208    result.push_str(&format_field("60", &brcode.merchant_city));
209    
210    // Add additional data if present
211    if let Some(ref additional_data) = brcode.additional_data {
212        let ad_content = format_additional_data(additional_data);
213        if !ad_content.is_empty() {
214            result.push_str(&format_field("62", &ad_content));
215        }
216    }
217    
218    // Calculate and add CRC16
219    let crc = calculate_crc16(&result);
220    result.push_str(&format_field("63", &crc));
221    
222    Ok(result)
223}
224
225/// Format a TLV field
226fn format_field(tag: &str, value: &str) -> String {
227    format!("{}{:02}{}", tag, value.len(), value)
228}
229
230/// Format merchant account information for field 26
231fn format_merchant_account_info(mai: &MerchantAccountInfo) -> String {
232    let mut result = String::new();
233    
234    result.push_str(&format_field("00", &mai.gui));
235    result.push_str(&format_field("01", &mai.pix_key));
236    
237    if let Some(ref description) = mai.description {
238        result.push_str(&format_field("02", description));
239    }
240    
241    if let Some(ref url) = mai.url {
242        result.push_str(&format_field("25", url));
243    }
244    
245    result
246}
247
248/// Format additional data for field 62
249fn format_additional_data(ad: &AdditionalData) -> String {
250    let mut result = String::new();
251    
252    if let Some(ref bill_number) = ad.bill_number {
253        result.push_str(&format_field("01", bill_number));
254    }
255    if let Some(ref mobile_number) = ad.mobile_number {
256        result.push_str(&format_field("02", mobile_number));
257    }
258    if let Some(ref store_label) = ad.store_label {
259        result.push_str(&format_field("03", store_label));
260    }
261    if let Some(ref loyalty_number) = ad.loyalty_number {
262        result.push_str(&format_field("04", loyalty_number));
263    }
264    if let Some(ref reference_label) = ad.reference_label {
265        result.push_str(&format_field("05", reference_label));
266    }
267    if let Some(ref customer_label) = ad.customer_label {
268        result.push_str(&format_field("06", customer_label));
269    }
270    if let Some(ref terminal_label) = ad.terminal_label {
271        result.push_str(&format_field("07", terminal_label));
272    }
273    if let Some(ref purpose_of_transaction) = ad.purpose_of_transaction {
274        result.push_str(&format_field("08", purpose_of_transaction));
275    }
276    if let Some(ref additional_consumer_data_request) = ad.additional_consumer_data_request {
277        result.push_str(&format_field("09", additional_consumer_data_request));
278    }
279    
280    result
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286
287    #[test]
288    fn test_parse_valid_static_brcode() {
289        // Use the working QR code from lib.rs test
290        let brcode_str = "00020126580014br.gov.bcb.pix0136123e4567-e12b-12d1-a456-426614174000520400005303986540510.005802BR5913FULANO DE TAL6008BRASILIA62070503***630436D9";
291        
292        let result = parse_brcode(brcode_str);
293        assert!(result.is_ok());
294        
295        let brcode = result.unwrap();
296        assert_eq!(brcode.payload_format_indicator, "01");
297        assert_eq!(brcode.merchant_account_info.gui, "br.gov.bcb.pix");
298        assert_eq!(brcode.merchant_account_info.pix_key, "123e4567-e12b-12d1-a456-426614174000");
299        assert_eq!(brcode.transaction_currency, "986");
300        assert_eq!(brcode.country_code, "BR");
301        assert_eq!(brcode.merchant_name, "FULANO DE TAL");
302        assert_eq!(brcode.merchant_city, "BRASILIA");
303        assert_eq!(brcode.transaction_amount, Some("10.00".to_string()));
304        // Note: This QR code doesn't have point_of_initiation_method field, so is_static() returns false
305        // We'll just check that it parses correctly
306    }
307
308    // TLV parsing is tested through the main parse_brcode function
309
310    #[test]
311    fn test_generate_brcode() {
312        let brcode = BRCode {
313            payload_format_indicator: "01".to_string(),
314            point_of_initiation_method: Some("11".to_string()),
315            merchant_account_info: MerchantAccountInfo {
316                gui: "br.gov.bcb.pix".to_string(),
317                pix_key: "test@example.com".to_string(),
318                description: None,
319                url: None,
320            },
321            merchant_category_code: "0000".to_string(),
322            transaction_currency: "986".to_string(),
323            transaction_amount: None,
324            country_code: "BR".to_string(),
325            merchant_name: "TEST MERCHANT".to_string(),
326            merchant_city: "TEST CITY".to_string(),
327            additional_data: None,
328            crc16: "".to_string(), // Will be calculated
329        };
330        
331        let result = generate_brcode(&brcode);
332        assert!(result.is_ok());
333        
334        let generated = result.unwrap();
335        // Should be able to parse the generated code back
336        let parsed = parse_brcode(&generated);
337        assert!(parsed.is_ok());
338    }
339
340    #[test]
341    fn test_invalid_crc() {
342        let brcode_str = "00020126580014br.gov.bcb.pix0136123e4567-e12b-12d1-a456-426614174000520400005303986540510.005802BR5913FULANO DE TAL6008BRASILIA62070503***630457B9";
343        
344        let result = parse_brcode(brcode_str);
345        assert!(matches!(result, Err(BRCodeError::InvalidChecksum)));
346    }
347
348    #[test]
349    fn test_missing_required_field() {
350        let brcode_str = "0002"; // Too short, missing required fields
351        
352        let result = parse_brcode(brcode_str);
353        assert!(result.is_err());
354    }
355}