///|
/// A VRP together with the trust anchor label reported by the validator.
pub(all) struct VrpRecord {
vrp : Vrp
trust_anchor : String
} derive(Eq, Debug)
///|
pub(all) struct CsvDiagnostic {
line : Int
message : String
} derive(Eq, Debug)
///|
pub(all) struct VrpCsvResult {
records : Array[VrpRecord]
diagnostics : Array[CsvDiagnostic]
} derive(Eq, Debug)
///|
pub fn VrpCsvResult::is_valid(self : VrpCsvResult) -> Bool {
self.diagnostics.is_empty()
}
///|
fn parse_csv_row(line : StringView) -> Result[Array[String], String] {
let chars : Array[Char] = []
for ch in line {
chars.push(ch)
}
let fields : Array[String] = []
let current : Array[Char] = []
let mut index = 0
let mut quoted = false
let mut quote_closed = false
while index < chars.length() {
let ch = chars[index]
if quoted {
if ch == '"' {
if index + 1 < chars.length() && chars[index + 1] == '"' {
current.push('"')
index = index + 2
continue
}
quoted = false
quote_closed = true
} else {
current.push(ch)
}
} else if quote_closed {
if ch != ',' {
return Err("unexpected character after closing quote")
}
fields.push(String::from_array(current))
current.clear()
quote_closed = false
} else if ch == ',' {
fields.push(String::from_array(current))
current.clear()
} else if ch == '"' {
if !current.is_empty() {
return Err("quote must begin at the start of a field")
}
quoted = true
} else {
current.push(ch)
}
index = index + 1
}
if quoted {
return Err("unterminated quoted field")
}
fields.push(String::from_array(current))
Ok(fields)
}
///|
fn is_routinator_header(fields : Array[String]) -> Bool {
fields.length() == 4 &&
fields[0] == "ASN" &&
fields[1] == "IP Prefix" &&
fields[2] == "Max Length" &&
fields[3] == "Trust Anchor"
}
///|
fn decimal_digit(ch : Char) -> UInt? {
match ch {
'0' => Some(0U)
'1' => Some(1U)
'2' => Some(2U)
'3' => Some(3U)
'4' => Some(4U)
'5' => Some(5U)
'6' => Some(6U)
'7' => Some(7U)
'8' => Some(8U)
'9' => Some(9U)
_ => None
}
}
///|
fn parse_asn(text : StringView) -> Result[UInt, String] {
let body = if text.has_prefix("AS") { text[2:] } else { text }
if body.is_empty() {
return Err("ASN is empty")
}
let mut value = 0U
for ch in body {
let digit = match decimal_digit(ch) {
Some(value) => value
None => return Err("invalid ASN: \{text}")
}
if value > (0xffffffffU - digit) / 10U {
return Err("ASN is out of range: \{text}")
}
value = value * 10U + digit
}
Ok(value)
}
///|
fn contains_colon(text : StringView) -> Bool {
for ch in text {
if ch == ':' {
return true
}
}
false
}
///|
fn parse_vrp_fields(fields : Array[String]) -> Result[VrpRecord, String] {
if fields.length() != 4 {
return Err("expected four CSV fields")
}
let asn = match parse_asn(fields[0]) {
Ok(value) => value
Err(message) => return Err(message)
}
if contains_colon(fields[1]) {
return Err("IPv6 VRPs are not supported yet")
}
let prefix = match Ipv4Prefix::parse(fields[1]) {
Ok(value) => value
Err(message) => return Err(message)
}
let max_length = match parse_int_part(fields[2], "maximum length") {
Ok(value) => value
Err(message) => return Err(message)
}
if fields[3].is_empty() {
return Err("trust anchor is empty")
}
let payload = match Vrp::new(prefix, max_length, asn) {
Ok(value) => value
Err(message) => return Err(message)
}
Ok({ vrp: payload, trust_anchor: fields[3], })
}
///|
/// Parse Routinator `csv` or `csvcompat` VRP output.
///
/// Invalid data rows are retained as line-numbered diagnostics while later
/// rows continue to be processed.
pub fn parse_vrp_csv(source : StringView) -> VrpCsvResult {
let records : Array[VrpRecord] = []
let diagnostics : Array[CsvDiagnostic] = []
let mut line_number = 0
let mut header_seen = false
for raw_line in source.split("\n") {
line_number = line_number + 1
let line = raw_line.trim(chars="\r")
if line.trim().is_empty() {
continue
}
let fields = match parse_csv_row(line) {
Ok(value) => value
Err(message) => {
diagnostics.push({ line: line_number, message, })
continue
}
}
if !header_seen {
header_seen = true
if !is_routinator_header(fields) {
diagnostics.push({
line: line_number,
message: "expected Routinator CSV header",
})
}
continue
}
match parse_vrp_fields(fields) {
Ok(record) => records.push(record)
Err(message) => diagnostics.push({ line: line_number, message, })
}
}
if !header_seen {
diagnostics.push({ line: 1, message: "CSV input is empty", })
}
{ records, diagnostics, }
}