pix-brcode-parser 0.1.0

A Rust library for parsing and validating Brazilian PIX QR codes (BR Code) following EMV QRCPS standard
Documentation
//! Parser implementation for PIX BR Code QR strings

use crate::error::{BRCodeError, Result};
use crate::types::{BRCode, MerchantAccountInfo, AdditionalData, EmvField};
use crate::validation::*;
use std::collections::HashMap;

/// Parse a PIX BR Code string into a structured BRCode
pub fn parse_brcode(input: &str) -> Result<BRCode> {
    if input.is_empty() {
        return Err(BRCodeError::invalid_format("Empty BR Code string"));
    }

    // First validate CRC16 checksum
    if !validate_crc16(input)? {
        return Err(BRCodeError::InvalidChecksum);
    }

    // Parse TLV fields
    let fields = parse_tlv_fields(input)?;
    
    // Build BRCode from parsed fields
    build_brcode_from_fields(fields)
}

/// Parse TLV (Tag-Length-Value) fields from BR Code string
fn parse_tlv_fields(input: &str) -> Result<HashMap<String, EmvField>> {
    let mut fields = HashMap::new();
    let mut pos = 0;
    
    while pos < input.len() {
        if pos + 4 > input.len() {
            return Err(BRCodeError::TlvParsingError("Insufficient data for TLV field".to_string()));
        }
        
        // Extract tag (2 digits)
        let tag = &input[pos..pos + 2];
        pos += 2;
        
        // Extract length (2 digits)
        let length_str = &input[pos..pos + 2];
        pos += 2;
        
        let length: usize = length_str.parse()
            .map_err(|_| BRCodeError::TlvParsingError(format!("Invalid length field: {}", length_str)))?;
        
        // Check if we have enough data for the value
        if pos + length > input.len() {
            return Err(BRCodeError::TlvParsingError(format!("Insufficient data for field value, tag: {}", tag)));
        }
        
        // Extract value
        let value = &input[pos..pos + length];
        pos += length;
        
        let field = EmvField {
            tag: tag.to_string(),
            length,
            value: value.to_string(),
        };
        
        fields.insert(tag.to_string(), field);
    }
    
    Ok(fields)
}

/// Build BRCode struct from parsed TLV fields
fn build_brcode_from_fields(fields: HashMap<String, EmvField>) -> Result<BRCode> {
    // Extract required fields
    let payload_format_indicator = get_required_field(&fields, "00")?;
    validate_payload_format(&payload_format_indicator.value)?;
    
    let merchant_category_code = get_required_field(&fields, "52")?;
    let transaction_currency = get_required_field(&fields, "53")?;
    validate_currency(&transaction_currency.value)?;
    
    let country_code = get_required_field(&fields, "58")?;
    validate_country_code(&country_code.value)?;
    
    let merchant_name = get_required_field(&fields, "59")?;
    validate_merchant_name(&merchant_name.value)?;
    
    let merchant_city = get_required_field(&fields, "60")?;
    validate_merchant_city(&merchant_city.value)?;
    
    let crc16 = get_required_field(&fields, "63")?;
    
    // Extract optional fields
    let point_of_initiation_method = fields.get("01").map(|f| f.value.clone());
    let transaction_amount = fields.get("54").map(|f| f.value.clone());
    
    // Validate transaction amount if present
    if let Some(ref amount) = transaction_amount {
        validate_transaction_amount(amount)?;
    }
    
    // Parse merchant account information (field 26)
    let merchant_account_info = parse_merchant_account_info(&fields)?;
    
    // Parse additional data (field 62) if present
    let additional_data = if let Some(field) = fields.get("62") {
        Some(parse_additional_data(&field.value)?)
    } else {
        None
    };
    
    Ok(BRCode {
        payload_format_indicator: payload_format_indicator.value.clone(),
        point_of_initiation_method,
        merchant_account_info,
        merchant_category_code: merchant_category_code.value.clone(),
        transaction_currency: transaction_currency.value.clone(),
        transaction_amount,
        country_code: country_code.value.clone(),
        merchant_name: merchant_name.value.clone(),
        merchant_city: merchant_city.value.clone(),
        additional_data,
        crc16: crc16.value.clone(),
    })
}

/// Parse merchant account information from field 26
fn parse_merchant_account_info(fields: &HashMap<String, EmvField>) -> Result<MerchantAccountInfo> {
    let field_26 = get_required_field(fields, "26")?;
    
    // Parse nested TLV fields within field 26
    let nested_fields = parse_tlv_fields(&field_26.value)?;
    
    // GUI field (tag 00) - should be "br.gov.bcb.pix"
    let gui_field = get_required_field(&nested_fields, "00")?;
    validate_gui(&gui_field.value)?;
    
    // PIX key field (tag 01)
    let pix_key_field = get_required_field(&nested_fields, "01")?;
    validate_pix_key(&pix_key_field.value)?;
    
    // Optional description field (tag 02)
    let description = nested_fields.get("02").map(|f| f.value.clone());
    
    // Optional URL field (tag 25) for dynamic QR codes
    let url = nested_fields.get("25").map(|f| f.value.clone());
    
    Ok(MerchantAccountInfo {
        gui: gui_field.value.clone(),
        pix_key: pix_key_field.value.clone(),
        description,
        url,
    })
}

/// Parse additional data from field 62
fn parse_additional_data(input: &str) -> Result<AdditionalData> {
    let fields = parse_tlv_fields(input)?;
    
    Ok(AdditionalData {
        bill_number: fields.get("01").map(|f| f.value.clone()),
        mobile_number: fields.get("02").map(|f| f.value.clone()),
        store_label: fields.get("03").map(|f| f.value.clone()),
        loyalty_number: fields.get("04").map(|f| f.value.clone()),
        reference_label: fields.get("05").map(|f| f.value.clone()),
        customer_label: fields.get("06").map(|f| f.value.clone()),
        terminal_label: fields.get("07").map(|f| f.value.clone()),
        purpose_of_transaction: fields.get("08").map(|f| f.value.clone()),
        additional_consumer_data_request: fields.get("09").map(|f| f.value.clone()),
    })
}

/// Get a required field from the fields map
fn get_required_field<'a>(fields: &'a HashMap<String, EmvField>, tag: &str) -> Result<&'a EmvField> {
    fields.get(tag).ok_or_else(|| BRCodeError::missing_field(format!("Required field with tag {}", tag)))
}

/// Generate a BR Code string from a BRCode struct
pub fn generate_brcode(brcode: &BRCode) -> Result<String> {
    let mut result = String::new();
    
    // Add payload format indicator
    result.push_str(&format_field("00", &brcode.payload_format_indicator));
    
    // Add point of initiation method if present
    if let Some(ref pim) = brcode.point_of_initiation_method {
        result.push_str(&format_field("01", pim));
    }
    
    // Add merchant account information (field 26)
    let mai_content = format_merchant_account_info(&brcode.merchant_account_info);
    result.push_str(&format_field("26", &mai_content));
    
    // Add merchant category code
    result.push_str(&format_field("52", &brcode.merchant_category_code));
    
    // Add transaction currency
    result.push_str(&format_field("53", &brcode.transaction_currency));
    
    // Add transaction amount if present
    if let Some(ref amount) = brcode.transaction_amount {
        result.push_str(&format_field("54", amount));
    }
    
    // Add country code
    result.push_str(&format_field("58", &brcode.country_code));
    
    // Add merchant name
    result.push_str(&format_field("59", &brcode.merchant_name));
    
    // Add merchant city
    result.push_str(&format_field("60", &brcode.merchant_city));
    
    // Add additional data if present
    if let Some(ref additional_data) = brcode.additional_data {
        let ad_content = format_additional_data(additional_data);
        if !ad_content.is_empty() {
            result.push_str(&format_field("62", &ad_content));
        }
    }
    
    // Calculate and add CRC16
    let crc = calculate_crc16(&result);
    result.push_str(&format_field("63", &crc));
    
    Ok(result)
}

/// Format a TLV field
fn format_field(tag: &str, value: &str) -> String {
    format!("{}{:02}{}", tag, value.len(), value)
}

/// Format merchant account information for field 26
fn format_merchant_account_info(mai: &MerchantAccountInfo) -> String {
    let mut result = String::new();
    
    result.push_str(&format_field("00", &mai.gui));
    result.push_str(&format_field("01", &mai.pix_key));
    
    if let Some(ref description) = mai.description {
        result.push_str(&format_field("02", description));
    }
    
    if let Some(ref url) = mai.url {
        result.push_str(&format_field("25", url));
    }
    
    result
}

/// Format additional data for field 62
fn format_additional_data(ad: &AdditionalData) -> String {
    let mut result = String::new();
    
    if let Some(ref bill_number) = ad.bill_number {
        result.push_str(&format_field("01", bill_number));
    }
    if let Some(ref mobile_number) = ad.mobile_number {
        result.push_str(&format_field("02", mobile_number));
    }
    if let Some(ref store_label) = ad.store_label {
        result.push_str(&format_field("03", store_label));
    }
    if let Some(ref loyalty_number) = ad.loyalty_number {
        result.push_str(&format_field("04", loyalty_number));
    }
    if let Some(ref reference_label) = ad.reference_label {
        result.push_str(&format_field("05", reference_label));
    }
    if let Some(ref customer_label) = ad.customer_label {
        result.push_str(&format_field("06", customer_label));
    }
    if let Some(ref terminal_label) = ad.terminal_label {
        result.push_str(&format_field("07", terminal_label));
    }
    if let Some(ref purpose_of_transaction) = ad.purpose_of_transaction {
        result.push_str(&format_field("08", purpose_of_transaction));
    }
    if let Some(ref additional_consumer_data_request) = ad.additional_consumer_data_request {
        result.push_str(&format_field("09", additional_consumer_data_request));
    }
    
    result
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_valid_static_brcode() {
        // Use the working QR code from lib.rs test
        let brcode_str = "00020126580014br.gov.bcb.pix0136123e4567-e12b-12d1-a456-426614174000520400005303986540510.005802BR5913FULANO DE TAL6008BRASILIA62070503***630436D9";
        
        let result = parse_brcode(brcode_str);
        assert!(result.is_ok());
        
        let brcode = result.unwrap();
        assert_eq!(brcode.payload_format_indicator, "01");
        assert_eq!(brcode.merchant_account_info.gui, "br.gov.bcb.pix");
        assert_eq!(brcode.merchant_account_info.pix_key, "123e4567-e12b-12d1-a456-426614174000");
        assert_eq!(brcode.transaction_currency, "986");
        assert_eq!(brcode.country_code, "BR");
        assert_eq!(brcode.merchant_name, "FULANO DE TAL");
        assert_eq!(brcode.merchant_city, "BRASILIA");
        assert_eq!(brcode.transaction_amount, Some("10.00".to_string()));
        // Note: This QR code doesn't have point_of_initiation_method field, so is_static() returns false
        // We'll just check that it parses correctly
    }

    // TLV parsing is tested through the main parse_brcode function

    #[test]
    fn test_generate_brcode() {
        let brcode = BRCode {
            payload_format_indicator: "01".to_string(),
            point_of_initiation_method: Some("11".to_string()),
            merchant_account_info: MerchantAccountInfo {
                gui: "br.gov.bcb.pix".to_string(),
                pix_key: "test@example.com".to_string(),
                description: None,
                url: None,
            },
            merchant_category_code: "0000".to_string(),
            transaction_currency: "986".to_string(),
            transaction_amount: None,
            country_code: "BR".to_string(),
            merchant_name: "TEST MERCHANT".to_string(),
            merchant_city: "TEST CITY".to_string(),
            additional_data: None,
            crc16: "".to_string(), // Will be calculated
        };
        
        let result = generate_brcode(&brcode);
        assert!(result.is_ok());
        
        let generated = result.unwrap();
        // Should be able to parse the generated code back
        let parsed = parse_brcode(&generated);
        assert!(parsed.is_ok());
    }

    #[test]
    fn test_invalid_crc() {
        let brcode_str = "00020126580014br.gov.bcb.pix0136123e4567-e12b-12d1-a456-426614174000520400005303986540510.005802BR5913FULANO DE TAL6008BRASILIA62070503***630457B9";
        
        let result = parse_brcode(brcode_str);
        assert!(matches!(result, Err(BRCodeError::InvalidChecksum)));
    }

    #[test]
    fn test_missing_required_field() {
        let brcode_str = "0002"; // Too short, missing required fields
        
        let result = parse_brcode(brcode_str);
        assert!(result.is_err());
    }
}