Skip to main content

agentis_pay/commands/
brcode.rs

1use anyhow::{Result, anyhow, bail};
2
3use crate::commands::BrcodeArgs;
4use crate::display::{CommandName, OutputFormat, emit_data, truncate_text};
5use serde::Serialize;
6
7#[derive(Clone, Serialize)]
8pub(crate) struct BrcodeField {
9    tag: String,
10    value: String,
11    #[serde(skip_serializing_if = "Option::is_none")]
12    children: Option<Vec<BrcodeField>>,
13}
14
15#[derive(Clone, Serialize)]
16pub(crate) struct BrcodeDecodeResponse {
17    raw: String,
18    field_count: usize,
19    fields: Vec<BrcodeField>,
20}
21
22pub(crate) fn decode_payload(code: &str) -> Result<BrcodeDecodeResponse> {
23    let fields = parse_brcode(code)?;
24    Ok(BrcodeDecodeResponse {
25        raw: code.to_string(),
26        field_count: fields.len(),
27        fields,
28    })
29}
30
31pub async fn decode(args: BrcodeArgs, output: OutputFormat, quiet: bool) -> Result<()> {
32    let response = decode_payload(&args.code)?;
33
34    if let OutputFormat::Text = output {
35        if quiet {
36            return Ok(());
37        }
38
39        println!("{}:", args.code);
40        println!("  raw_length: {}", args.code.len());
41        println!("  fields: {}", response.field_count);
42        render_fields(response.fields.iter(), 1);
43        return Ok(());
44    }
45
46    emit_data(CommandName::BrcodeDecode, &response, output, quiet)?;
47    Ok(())
48}
49
50fn render_fields(fields: std::slice::Iter<BrcodeField>, depth: usize) {
51    let indent = "  ".repeat(depth);
52    for field in fields {
53        let label = field_label(&field.tag);
54        if let Some(children) = field.children.as_ref() {
55            println!("{indent}- [{}] {}", field.tag, label);
56            render_fields(children.iter(), depth + 1);
57            println!(
58                "{}  raw_value: {}",
59                indent,
60                truncate_text(field.value.as_str(), 72)
61            );
62        } else {
63            println!("{indent}- {}: {}", field.tag, field_labelized(field));
64        }
65    }
66}
67
68fn field_labelized(field: &BrcodeField) -> String {
69    if field.value.is_empty() {
70        field_label(field.tag.as_str()).to_string()
71    } else {
72        format!(
73            "{} => {}",
74            field_label(field.tag.as_str()),
75            sanitize_text(&field.value)
76        )
77    }
78}
79
80fn field_label(tag: &str) -> &'static str {
81    match tag {
82        "00" => "Payload Format Indicator",
83        "01" => "Point of Initiation Method",
84        "26" => "Merchant Account Information",
85        "52" => "Merchant Category Code",
86        "53" => "Transaction Currency",
87        "54" => "Transaction Amount",
88        "58" => "Country Code",
89        "59" => "Merchant Name",
90        "60" => "Merchant City",
91        "62" => "Additional Data Field Template",
92        "63" => "CRC16",
93        "80" => "Transaction Details",
94        _ => "Field",
95    }
96}
97
98fn sanitize_text(value: &str) -> String {
99    value.replace(['\n', '\r'], " ")
100}
101
102fn parse_brcode(raw: &str) -> Result<Vec<BrcodeField>> {
103    let payload = raw.trim();
104    if payload.is_empty() {
105        bail!("brcode payload cannot be empty");
106    }
107
108    let mut cursor = 0usize;
109    let mut fields = Vec::new();
110
111    while cursor < payload.len() {
112        if cursor + 4 > payload.len() {
113            bail!("invalid brcode format: incomplete TLV header");
114        }
115
116        let tag = &payload[cursor..cursor + 2];
117        if !tag.chars().all(|c| c.is_ascii_digit()) {
118            bail!("invalid tag {tag} at position {cursor}");
119        }
120        let raw_length = &payload[cursor + 2..cursor + 4];
121        if !raw_length.chars().all(|c| c.is_ascii_digit()) {
122            bail!("invalid length for tag {tag} at position {}", cursor + 2);
123        }
124        let length: usize = raw_length
125            .parse()
126            .map_err(|_| anyhow!("invalid length for tag {tag}: {raw_length}"))?;
127
128        let value_start = cursor + 4;
129        let value_end = value_start + length;
130        if value_end > payload.len() {
131            bail!("invalid length for tag {tag}: expected {length} bytes");
132        }
133        let value = &payload[value_start..value_end];
134
135        let children = if let Ok(nested_fields) = try_parse_nested(value) {
136            if nested_fields.is_empty() {
137                None
138            } else {
139                Some(nested_fields)
140            }
141        } else {
142            None
143        };
144
145        fields.push(BrcodeField {
146            tag: tag.to_string(),
147            value: value.to_string(),
148            children,
149        });
150
151        cursor = value_end;
152    }
153
154    Ok(fields)
155}
156
157fn try_parse_nested(raw: &str) -> Result<Vec<BrcodeField>> {
158    parse_brcode(raw).and_then(|fields| {
159        if fields.is_empty() {
160            bail!("no nested fields");
161        }
162        Ok(fields)
163    })
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    #[test]
171    fn parse_simple_brcode() {
172        let payload = "0002010102010204abcd";
173        let fields = parse_brcode(payload).expect("valid brcode");
174        assert_eq!(fields.len(), 3);
175        assert_eq!(fields[0].tag, "00");
176        assert_eq!(fields[0].value, "01");
177        assert_eq!(fields[1].tag, "01");
178        assert_eq!(fields[2].tag, "02");
179    }
180
181    #[test]
182    fn parse_nested_brcode() {
183        let nested = "0102AA0203BBB";
184        let payload = format!("26{len:02}{nested}0102XX", len = nested.len());
185        let fields = parse_brcode(&payload).expect("valid brcode");
186        assert_eq!(fields.len(), 2);
187        assert!(fields[0].children.as_ref().is_some());
188        assert_eq!(fields[1].tag, "01");
189    }
190}