///|
/// The two Routinator CSV layouts accepted by the parser.
pub(all) enum VrpCsvStyle {
RoutinatorCsv
RoutinatorCsvCompat
} derive(Eq, Debug)
///|
fn has_line_break(text : String) -> Bool {
for ch in text {
if ch == '\n' || ch == '\r' {
return true
}
}
false
}
///|
fn quote_csv_field(text : String, always : Bool) -> String {
let mut quoted = always
if !quoted {
for ch in text {
if ch == ',' || ch == '"' {
quoted = true
break
}
}
}
if !quoted {
return text
}
let buffer = StringBuilder()
buffer.write_char('"')
for ch in text {
if ch == '"' {
buffer.write_string("\"\"")
} else {
buffer.write_char(ch)
}
}
buffer.write_char('"')
buffer.to_string()
}
///|
fn write_csv_row(
buffer : StringBuilder,
fields : Array[String],
quote_all : Bool,
) -> Unit {
for index, field in fields {
if index > 0 {
buffer.write_char(',')
}
buffer.write_string(quote_csv_field(field, quote_all))
}
buffer.write_char('\n')
}
///|
/// Write VRP records in Routinator csv or csvcompat form.
///
/// Input order is preserved and line endings are always LF. Empty trust-anchor
/// labels and embedded line breaks are rejected so the result can be parsed
/// without losing record boundaries.
pub fn write_vrp_csv(
records : Array[VrpRecord],
style : VrpCsvStyle,
) -> Result[String, String] {
let quote_all = style == RoutinatorCsvCompat
let buffer = StringBuilder()
write_csv_row(
buffer,
["ASN", "IP Prefix", "Max Length", "Trust Anchor"],
quote_all,
)
for index, record in records {
if record.trust_anchor.is_empty() {
return Err("record \{index} has an empty trust anchor")
}
if has_line_break(record.trust_anchor) {
return Err("record \{index} trust anchor contains a line break")
}
let asn = match style {
RoutinatorCsv => "AS\{record.vrp.asn()}"
RoutinatorCsvCompat => record.vrp.asn().to_string()
}
write_csv_row(
buffer,
[
asn,
record.vrp.prefix().to_string(),
record.vrp.max_length().to_string(),
record.trust_anchor,
],
quote_all,
)
}
Ok(buffer.to_string())
}