agentis_pay/commands/
brcode.rs1use 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}