use std::fmt;
use std::fs::File;
use std::io::{self, BufRead, BufReader, Read};
use std::net::IpAddr;
use std::path::Path;
use flate2::read::MultiGzDecoder;
use ipnet::IpNet;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Origins {
raw: Box<str>,
primary: u32,
}
impl Origins {
pub fn parse(field: &str) -> Result<Self, ParseError> {
let mut first = None;
for token in field.split(['_', ',']) {
let asn: u32 = token.parse().map_err(|_| ParseError::Origins(field.to_owned()))?;
first.get_or_insert(asn);
}
let primary = first.ok_or_else(|| ParseError::Origins(field.to_owned()))?;
Ok(Self {
raw: field.into(),
primary,
})
}
pub fn raw(&self) -> &str {
&self.raw
}
pub fn primary(&self) -> u32 {
self.primary
}
pub fn asns(&self) -> impl Iterator<Item = u32> + '_ {
self.raw.split(['_', ',']).filter_map(|token| token.parse().ok())
}
pub fn is_multi_origin(&self) -> bool {
self.raw.contains('_')
}
pub fn is_as_set(&self) -> bool {
self.raw.contains(',')
}
}
impl fmt::Display for Origins {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.raw)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Entry {
pub prefix: IpNet,
pub origins: Origins,
}
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum ParseError {
#[error("expected `prefix<TAB>length<TAB>origins`, got {0:?}")]
Fields(String),
#[error("invalid address {0:?}")]
Address(String),
#[error("invalid prefix length {0:?}")]
Length(String),
#[error("invalid origin field {0:?}")]
Origins(String),
#[error("expected an {expected} prefix, got {prefix}")]
Family {
expected: &'static str,
prefix: String,
},
}
pub fn parse_line(line: &str) -> Result<Option<Entry>, ParseError> {
let line = line.trim_end_matches(['\r', '\n']);
if line.trim().is_empty() || line.starts_with('#') {
return Ok(None);
}
let mut fields = line.split('\t');
let (Some(address), Some(length), Some(origins), None) =
(fields.next(), fields.next(), fields.next(), fields.next())
else {
return Err(ParseError::Fields(line.to_owned()));
};
let address: IpAddr = address
.trim()
.parse()
.map_err(|_| ParseError::Address(address.to_owned()))?;
let length: u8 = length
.trim()
.parse()
.map_err(|_| ParseError::Length(length.to_owned()))?;
let prefix = IpNet::new(address, length).map_err(|_| ParseError::Length(length.to_string()))?;
let origins = Origins::parse(origins.trim())?;
Ok(Some(Entry { prefix, origins }))
}
#[derive(Debug, thiserror::Error)]
pub enum ReadError {
#[error(transparent)]
Io(#[from] io::Error),
#[error("line {line}: {source}")]
Parse {
line: usize,
#[source]
source: ParseError,
},
}
pub fn numbered_entries<R: BufRead>(reader: R) -> impl Iterator<Item = Result<(usize, Entry), ReadError>> {
reader.lines().enumerate().filter_map(|(index, line)| match line {
Err(error) => Some(Err(ReadError::Io(error))),
Ok(line) => match parse_line(&line) {
Ok(None) => None,
Ok(Some(entry)) => Some(Ok((index + 1, entry))),
Err(source) => Some(Err(ReadError::Parse {
line: index + 1,
source,
})),
},
})
}
pub fn entries<R: BufRead>(reader: R) -> impl Iterator<Item = Result<Entry, ReadError>> {
numbered_entries(reader).map(|item| item.map(|(_, entry)| entry))
}
pub fn open(path: &Path) -> io::Result<Box<dyn BufRead>> {
let file = File::open(path)?;
decode(BufReader::new(file))
}
pub fn decode<R: BufRead + 'static>(mut reader: R) -> io::Result<Box<dyn BufRead>> {
let magic = reader.fill_buf()?;
if magic.starts_with(&[0x1f, 0x8b]) {
Ok(Box::new(BufReader::new(MultiGzDecoder::new(reader))))
} else {
Ok(Box::new(reader))
}
}
pub fn read_to_string(path: &Path) -> io::Result<String> {
let mut text = String::new();
open(path)?.read_to_string(&mut text)?;
Ok(text)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_single_origin() {
let entry = parse_line("1.0.0.0\t24\t13335\n").unwrap().unwrap();
assert_eq!(entry.prefix, "1.0.0.0/24".parse::<IpNet>().unwrap());
assert_eq!(entry.origins.primary(), 13335);
assert_eq!(entry.origins.raw(), "13335");
assert!(!entry.origins.is_multi_origin());
assert!(!entry.origins.is_as_set());
}
#[test]
fn parses_multi_origin_and_sets() {
let moas = parse_line("1.119.178.0\t23\t4847_58851_4808").unwrap().unwrap();
assert_eq!(moas.origins.primary(), 4847);
assert_eq!(moas.origins.asns().collect::<Vec<_>>(), [4847, 58851, 4808]);
assert!(moas.origins.is_multi_origin());
let set = parse_line("2001:db8::\t32\t64496,64497").unwrap().unwrap();
assert_eq!(set.origins.primary(), 64496);
assert!(set.origins.is_as_set());
assert_eq!(set.prefix.to_string(), "2001:db8::/32");
}
#[test]
fn skips_blank_and_comment_lines() {
assert_eq!(parse_line("").unwrap(), None);
assert_eq!(parse_line(" \r\n").unwrap(), None);
assert_eq!(parse_line("# comment").unwrap(), None);
}
#[test]
fn rejects_malformed_lines() {
assert!(matches!(
parse_line("1.0.0.0 24 13335"),
Err(ParseError::Fields(_))
));
assert!(matches!(parse_line("1.0.0.0\t24"), Err(ParseError::Fields(_))));
assert!(matches!(
parse_line("1.0.0.0\t24\t1\textra"),
Err(ParseError::Fields(_))
));
assert!(matches!(
parse_line("1.0.0\t24\t13335"),
Err(ParseError::Address(_))
));
assert!(matches!(
parse_line("1.0.0.0\t33\t13335"),
Err(ParseError::Length(_))
));
assert!(matches!(
parse_line("1.0.0.0\tx\t13335"),
Err(ParseError::Length(_))
));
assert!(matches!(parse_line("1.0.0.0\t24\t"), Err(ParseError::Origins(_))));
assert!(matches!(
parse_line("1.0.0.0\t24\tAS13335"),
Err(ParseError::Origins(_))
));
assert!(matches!(
parse_line("1.0.0.0\t24\t1__2"),
Err(ParseError::Origins(_))
));
}
#[test]
fn iterates_entries_with_line_numbers() {
let text = "# header\n1.0.0.0\t24\t13335\n\nbad line\n";
let items: Vec<_> = entries(text.as_bytes()).collect();
assert_eq!(items.len(), 2);
assert!(items[0].is_ok());
match &items[1] {
Err(ReadError::Parse { line, .. }) => assert_eq!(*line, 4),
other => panic!("expected a parse error, got {other:?}"),
}
}
#[test]
fn decodes_gzip_by_magic_bytes() {
use flate2::write::GzEncoder;
use std::io::Write;
let mut encoder = GzEncoder::new(Vec::new(), flate2::Compression::fast());
encoder.write_all(b"8.8.8.0\t24\t15169\n").unwrap();
let compressed = encoder.finish().unwrap();
let reader = decode(io::Cursor::new(compressed)).unwrap();
let entries: Vec<_> = entries(reader).map(Result::unwrap).collect();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].origins.primary(), 15169);
let plain = decode(io::Cursor::new(b"8.8.8.0\t24\t15169\n".to_vec())).unwrap();
assert_eq!(entries_count(plain), 1);
}
fn entries_count(reader: Box<dyn BufRead>) -> usize {
entries(reader).count()
}
}