// APDU transaction log parsing and reporting.
pub(all) struct TxStep {
direction : String
bytes : Array[Int]
apdu : Apdu?
response : ApduResponse?
tlv : Array[Tlv]
note : String
}
pub(all) struct TxReport {
steps : Array[TxStep]
errors : Array[String]
pan : String?
expiry : String?
cardholder : String?
label : String?
}
fn describe_apdu_error(err : ApduError) -> String {
match err {
Truncated => "truncated APDU"
BadLength => "bad APDU length"
LeTooLarge(n) => "Le too large " + n.to_string()
DataTooLarge(n) => "data too large " + n.to_string()
}
}
pub fn parse_transaction_log(text : String) -> TxReport {
let steps : Array[TxStep] = Array::new()
let errors : Array[String] = Array::new()
let mut pan : String? = None
let mut expiry : String? = None
let mut cardholder : String? = None
let mut label : String? = None
for raw_line in text.split("\n").to_array() {
let line = raw_line.trim_space().to_string()
if line.length() == 0 || line.has_prefix("#") {
continue
}
let direction = if line.has_prefix(">>") {
Some(">>")
} else if line.has_prefix("C:") {
Some(">>")
} else if line.has_prefix("<<") {
Some("<<")
} else if line.has_prefix("R:") {
Some("<<")
} else if line.has_prefix("send:") {
Some(">>")
} else if line.has_prefix("resp:") {
Some("<<")
} else {
None
}
match direction {
None => errors.push("unrecognized line: " + line)
Some(dir) => {
let body = line.substring(start=(if line.has_prefix("send:") || line.has_prefix("resp:") { 5 } else { 2 }))
let bytes = match hex_decode(body.trim_space().to_string()) {
Some(values) => values
None => {
errors.push("invalid hex in line: " + line)
continue
}
}
if dir == ">>" {
let apdu = try {
Some(parse_apdu(bytes))
} catch {
err => {
errors.push(describe_apdu_error(err))
None
}
}
let note = match apdu {
Some(command) => command_name(command)
None => "unparseable command"
}
steps.push({
direction: dir,
bytes,
apdu,
response: None,
tlv: [],
note,
})
} else {
let response = try {
Some(parse_response(bytes))
} catch {
err => {
errors.push(describe_apdu_error(err))
None
}
}
let tlv = match response {
Some(resp) =>
if resp.data.length() > 0 {
try {
parse_tlv(resp.data)
} catch {
_ => []
}
} else {
[]
}
None => []
}
let card = card_data_from_tlv(tlv)
if pan is None {
pan = card.pan
}
if expiry is None {
expiry = card.expiry
}
if cardholder is None {
cardholder = card.cardholder
}
if label is None {
label = card.label
}
let note = match response {
Some(resp) =>
status_description(resp.sw) + " (" + hex_int(resp.sw, 4) + ")"
None => "unparseable response"
}
steps.push({
direction: dir,
bytes,
apdu: None,
response,
tlv,
note,
})
}
}
}
}
{ steps, errors, pan, expiry, cardholder, label }
}
pub fn render_transaction(report : TxReport) -> String {
let out = StringBuilder()
out.write_string(
"steps: " + report.steps.length().to_string() + " errors: " +
report.errors.length().to_string() + "\n",
)
for i in 0.. 0 {
out.write_string(" tags:")
for tag in collect_tags(step.tlv) {
out.write_string(" " + tag_to_hex(tag) + "(" + tag_name(tag) + ")")
}
out.write_string("\n")
}
}
match report.pan {
Some(value) => {
let luhn = if luhn_valid(value) { "luhn ok" } else { "luhn failed" }
out.write_string("PAN: " + mask_pan(value) + " (" + luhn + ")\n")
}
None => ()
}
match report.expiry {
Some(value) => out.write_string("expiry: " + format_expiry(value) + "\n")
None => ()
}
match report.cardholder {
Some(value) => out.write_string("cardholder: " + value + "\n")
None => ()
}
match report.label {
Some(value) => out.write_string("label: " + value + "\n")
None => ()
}
for error in report.errors {
out.write_string("error: " + error + "\n")
}
out.to_string()
}
fn json_escape(text : String) -> String {
let out = StringBuilder()
for i in 0..> 4))
out.write_char(hex_nibble_char(c))
} else {
out.write_char(Char::from_int(c))
}
}
out.to_string()
}
fn json_string(text : String) -> String {
"\"" + json_escape(text) + "\""
}
fn quoted_array(items : Array[String]) -> String {
"[" + items.join(",") + "]"
}
fn step_json(index : Int, step : TxStep) -> String {
let fields : Array[String] = [
"\"index\":" + index.to_string(),
"\"direction\":" + json_string(step.direction),
"\"bytes\":" + json_string(hex_encode(step.bytes)),
"\"note\":" + json_string(step.note),
]
match step.apdu {
Some(command) =>
fields.push("\"command\":" + json_string(command_name(command)))
None => ()
}
match step.response {
Some(response) => {
fields.push("\"sw\":" + json_string(hex_int(response.sw, 4)))
fields.push("\"status\":" + json_string(status_description(response.sw)))
}
None => ()
}
if step.tlv.length() > 0 {
let tags : Array[String] = Array::new()
for tag in collect_tags(step.tlv) {
tags.push(json_string(tag_to_hex(tag)))
}
fields.push("\"tags\":" + quoted_array(tags))
}
"{" + fields.join(",") + "}"
}
pub fn render_transaction_json(report : TxReport) -> String {
let steps : Array[String] = Array::new()
for i in 0.. {
fields.push("\"pan\":" + json_string(mask_pan(value)))
let luhn = if luhn_valid(value) { "true" } else { "false" }
fields.push("\"luhn\":" + luhn)
}
None => ()
}
match report.expiry {
Some(value) => fields.push("\"expiry\":" + json_string(format_expiry(value)))
None => ()
}
match report.cardholder {
Some(value) => fields.push("\"cardholder\":" + json_string(value))
None => ()
}
match report.label {
Some(value) => fields.push("\"label\":" + json_string(value))
None => ()
}
"{" + fields.join(",") + "}"
}
pub fn render_transaction_csv(report : TxReport) -> String {
let out = StringBuilder()
out.write_string("index,direction,command,status,sw,bytes\n")
for i in 0.. command_name(apdu)
None => ""
}
let status = match step.response {
Some(response) => status_description(response.sw)
None => ""
}
let sw = match step.response {
Some(response) => hex_int(response.sw, 4)
None => ""
}
out.write_string(
(i + 1).to_string() + "," + csv_field(step.direction) + "," +
csv_field(command) + "," + csv_field(status) + "," + csv_field(sw) +
"," + csv_field(hex_encode(step.bytes)) + "\n",
)
}
out.to_string()
}
fn csv_field(text : String) -> String {
let out = StringBuilder()
out.write_char('"')
for i in 0.. TxSummary {
let mut commands = 0
let mut responses = 0
let mut successes = 0
let mut errors = 0
for step in report.steps {
match step.apdu {
Some(_) => commands = commands + 1
None => ()
}
match step.response {
Some(response) => {
responses = responses + 1
if response.sw == 0x9000 {
successes = successes + 1
} else {
errors = errors + 1
}
}
None => ()
}
}
{ commands, responses, successes, errors }
}