pix-brcode-parser 0.1.0

A Rust library for parsing and validating Brazilian PIX QR codes (BR Code) following EMV QRCPS standard
Documentation
//! CRC16 validation and other validation utilities for PIX BR Code

use crc::{Crc, Algorithm};
use crate::error::{BRCodeError, Result};

/// CRC16 calculator for EMV QRCPS compliance (CRC-16/CCITT-FALSE)
/// Using the standard CRC-16 polynomial 0x1021 with initial value 0xFFFF
pub const CRC16: Crc<u16> = Crc::<u16>::new(&Algorithm {
    width: 16,
    poly: 0x1021,
    init: 0xffff,
    refin: false,
    refout: false,
    xorout: 0x0000,
    check: 0x29b1,
    residue: 0x0000,
});

/// Calculate CRC16 using a simple implementation that matches PIX standards
fn calculate_crc16_simple(data: &[u8]) -> u16 {
    let mut crc: u16 = 0xFFFF;
    
    for &byte in data {
        crc ^= (byte as u16) << 8;
        for _ in 0..8 {
            if crc & 0x8000 != 0 {
                crc = (crc << 1) ^ 0x1021;
            } else {
                crc <<= 1;
            }
        }
    }
    
    crc
}

/// Validate CRC16 checksum of a BR Code string
/// 
/// The CRC16 is calculated over the entire payload excluding the CRC field (tag 63)
pub fn validate_crc16(brcode: &str) -> Result<bool> {
    if brcode.len() < 4 {
        return Err(BRCodeError::invalid_format("BR Code too short for CRC validation"));
    }
    
    // Find the CRC field (tag 63)
    let crc_pos = brcode.rfind("63");
    let crc_pos = match crc_pos {
        Some(pos) => pos,
        None => return Err(BRCodeError::missing_field("CRC16 field (tag 63)")),
    };
    
    // Extract the payload without CRC and the CRC value
    let payload = &brcode[..crc_pos + 2]; // Include "63" + length
    let crc_length_str = &brcode[crc_pos + 2..crc_pos + 4];
    let crc_length: usize = crc_length_str.parse()
        .map_err(|_| BRCodeError::invalid_format("Invalid CRC length field"))?;
    
    if crc_length != 4 {
        return Err(BRCodeError::InvalidFieldLength {
            tag: "63".to_string(),
            expected: 4,
            actual: crc_length,
        });
    }
    
    let expected_crc = &brcode[crc_pos + 4..crc_pos + 8];
    let payload_with_length = format!("{}04", payload);
    
    // Calculate CRC16 over the payload
    let calculated_crc = calculate_crc16_simple(payload_with_length.as_bytes());
    let calculated_crc_hex = format!("{:04X}", calculated_crc);
    
    Ok(calculated_crc_hex == expected_crc.to_uppercase())
}

/// Calculate CRC16 checksum for a BR Code payload
pub fn calculate_crc16(payload: &str) -> String {
    let payload_with_crc_field = format!("{}6304", payload);
    let checksum = calculate_crc16_simple(payload_with_crc_field.as_bytes());
    format!("{:04X}", checksum)
}

/// Validate PIX key format
pub fn validate_pix_key(key: &str) -> Result<()> {
    if key.is_empty() {
        return Err(BRCodeError::InvalidPixKey("PIX key cannot be empty".to_string()));
    }
    
    // Basic validation - more specific validation could be added for each key type
    if key.len() > 77 { // EMV maximum length
        return Err(BRCodeError::InvalidPixKey("PIX key too long".to_string()));
    }
    
    // Check for invalid characters (basic ASCII validation)
    if !key.chars().all(|c| c.is_ascii() && !c.is_ascii_control()) {
        return Err(BRCodeError::InvalidPixKey("PIX key contains invalid characters".to_string()));
    }
    
    Ok(())
}

/// Validate merchant name format
pub fn validate_merchant_name(name: &str) -> Result<()> {
    if name.is_empty() {
        return Err(BRCodeError::missing_field("Merchant name"));
    }
    
    if name.len() > 25 {
        return Err(BRCodeError::field_too_long("59", 25, name.len()));
    }
    
    Ok(())
}

/// Validate merchant city format
pub fn validate_merchant_city(city: &str) -> Result<()> {
    if city.is_empty() {
        return Err(BRCodeError::missing_field("Merchant city"));
    }
    
    if city.len() > 15 {
        return Err(BRCodeError::field_too_long("60", 15, city.len()));
    }
    
    Ok(())
}

/// Validate transaction amount format
pub fn validate_transaction_amount(amount: &str) -> Result<()> {
    if amount.is_empty() {
        return Ok(()); // Amount is optional
    }
    
    // Check if it's a valid decimal number
    amount.parse::<f64>()
        .map_err(|_| BRCodeError::ParseNumericError(format!("Invalid amount: {}", amount)))?;
    
    Ok(())
}

/// Validate GUI field (should be "br.gov.bcb.pix")
pub fn validate_gui(gui: &str) -> Result<()> {
    if gui != "br.gov.bcb.pix" {
        return Err(BRCodeError::InvalidGui(gui.to_string()));
    }
    Ok(())
}

/// Validate currency code (should be "986" for BRL)
pub fn validate_currency(currency: &str) -> Result<()> {
    if currency != "986" {
        return Err(BRCodeError::InvalidCurrency(currency.to_string()));
    }
    Ok(())
}

/// Validate country code (should be "BR")
pub fn validate_country_code(country: &str) -> Result<()> {
    if country != "BR" {
        return Err(BRCodeError::InvalidCountryCode(country.to_string()));
    }
    Ok(())
}

/// Validate payload format indicator (should be "01")
pub fn validate_payload_format(format: &str) -> Result<()> {
    if format != "01" {
        return Err(BRCodeError::InvalidPayloadFormat(format.to_string()));
    }
    Ok(())
}

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

    #[test]
    fn test_crc16_calculation() {
        let payload = "00020126580014br.gov.bcb.pix0136123e4567-e12b-12d1-a456-426614174000520400005303986540510.005802BR5913FULANO DE TAL6008BRASILIA62070503***";
        let expected_crc = "36D9";  // Corrected expected CRC value
        let calculated_crc = calculate_crc16(payload);
        assert_eq!(calculated_crc, expected_crc);
    }

    #[test]
    fn test_crc16_validation_valid() {
        let brcode = "00020126580014br.gov.bcb.pix0136123e4567-e12b-12d1-a456-426614174000520400005303986540510.005802BR5913FULANO DE TAL6008BRASILIA62070503***630436D9";
        assert!(validate_crc16(brcode).unwrap());
    }

    #[test]
    fn test_crc16_validation_invalid() {
        let brcode = "00020126580014br.gov.bcb.pix0136123e4567-e12b-12d1-a456-426614174000520400005303986540510.005802BR5913FULANO DE TAL6008BRASILIA62070503***6304WXYZ";
        assert!(!validate_crc16(brcode).unwrap());
    }

    #[test]
    fn test_validate_pix_key() {
        assert!(validate_pix_key("123e4567-e89b-12d3-a456-426614174000").is_ok());
        assert!(validate_pix_key("user@example.com").is_ok());
        assert!(validate_pix_key("").is_err());
    }

    #[test]
    fn test_validate_merchant_name() {
        assert!(validate_merchant_name("FULANO DE TAL").is_ok());
        assert!(validate_merchant_name("").is_err());
        assert!(validate_merchant_name("A".repeat(30).as_str()).is_err());
    }

    #[test]
    fn test_validate_gui() {
        assert!(validate_gui("br.gov.bcb.pix").is_ok());
        assert!(validate_gui("invalid.gui").is_err());
    }
}