mod as2org;
mod hegemony;
mod population;
mod sibling_orgs;
use crate::errors::{data_sources, load_methods, modules};
use crate::peeringdb::{Network, Peeringdb};
use crate::{BgpkitCommons, BgpkitCommonsError, LazyLoadable, Result};
use ipnet::{IpNet, Ipv4Net, Ipv6Net};
use serde::{Deserialize, Serialize};
use sibling_orgs::SiblingOrgsUtils;
use std::collections::HashMap;
use std::io::{BufRead, Read};
use tracing::{info, warn};
pub use hegemony::HegemonyData;
pub use population::AsnPopulationData;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DelegatedInfo {
pub registry: String,
pub country: String,
pub date: String,
pub status: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IrrAsnInfo {
pub as_name: String,
pub descr: Vec<String>,
pub source: String,
pub mnt_by: Vec<String>,
pub route_prefixes: Vec<Ipv4Net>,
pub route6_prefixes: Vec<Ipv6Net>,
pub member_of_sets: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AsInfo {
pub asn: u32,
pub name: String,
pub country: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub as2org: Option<As2orgInfo>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub population: Option<AsnPopulationData>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hegemony: Option<HegemonyData>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub peeringdb: Option<Network>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub delegated: Option<DelegatedInfo>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub irr: Vec<IrrAsnInfo>,
}
impl AsInfo {
pub fn get_preferred_name(&self) -> String {
if let Some(peeringdb_data) = &self.peeringdb {
if let Some(name) = &peeringdb_data.name {
if !name.is_empty() {
return name.clone();
}
}
}
if let Some(as2org_info) = &self.as2org {
if !as2org_info.org_name.is_empty() {
return as2org_info.org_name.clone();
}
}
self.name.clone()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct As2orgInfo {
pub name: String,
pub country: String,
pub org_id: String,
pub org_name: String,
}
const RIPE_RIS_ASN_TXT_URL: &str = "https://ftp.ripe.net/ripe/asnames/asn.txt";
const BGPKIT_ASN_TXT_MIRROR_URL: &str = "https://data.bgpkit.com/commons/asn.txt";
const BGPKIT_ASNINFO_URL: &str = "https://data.bgpkit.com/commons/asinfo.jsonl";
#[derive(Debug, Clone, Default)]
pub struct IrrSourceConfig {
sources: Vec<String>,
}
impl IrrSourceConfig {
pub fn sources(names: &[&str]) -> Result<Self> {
Self::only(names)
}
pub fn only(names: &[&str]) -> Result<Self> {
if names.is_empty() {
return Err(BgpkitCommonsError::invalid_format(
"IRR source selection",
"[]",
"explicit source selection must not be empty",
));
}
let selected = crate::irr::sources_by_name(names)?;
Ok(Self {
sources: selected
.into_iter()
.map(|source| source.name.to_string())
.collect(),
})
}
pub fn all() -> Self {
Self {
sources: Vec::new(),
}
}
fn resolve(&self) -> Result<Vec<crate::irr::IrrSource>> {
if self.sources.is_empty() {
Ok(crate::irr::all_sources())
} else {
let names = self.sources.iter().map(String::as_str).collect::<Vec<_>>();
crate::irr::sources_by_name(&names)
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum AsInfoProfile {
Minimum,
#[default]
Default,
Full,
}
impl AsInfoProfile {
pub fn builder(self) -> AsInfoBuilder {
match self {
AsInfoProfile::Minimum => AsInfoBuilder::new(),
AsInfoProfile::Default => AsInfoBuilder::new()
.with_as2org()
.with_population()
.with_hegemony()
.with_peeringdb(),
AsInfoProfile::Full => AsInfoBuilder::new()
.with_as2org()
.with_population()
.with_hegemony()
.with_peeringdb()
.with_delegated()
.with_irr()
.with_irr_route_prefixes(),
}
}
}
#[derive(Default)]
pub struct AsInfoBuilder {
load_as2org: bool,
load_population: bool,
load_hegemony: bool,
load_peeringdb: bool,
load_delegated: bool,
load_irr: bool,
irr_config: IrrSourceConfig,
irr_route_prefixes: bool,
}
impl AsInfoBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn with_as2org(mut self) -> Self {
self.load_as2org = true;
self
}
pub fn with_population(mut self) -> Self {
self.load_population = true;
self
}
pub fn with_hegemony(mut self) -> Self {
self.load_hegemony = true;
self
}
pub fn with_peeringdb(mut self) -> Self {
self.load_peeringdb = true;
self
}
pub fn with_delegated(mut self) -> Self {
self.load_delegated = true;
self
}
pub fn with_irr(mut self) -> Self {
self.load_irr = true;
self
}
pub fn with_irr_sources(mut self, config: IrrSourceConfig) -> Self {
self.load_irr = true;
self.irr_config = config;
self
}
pub fn with_irr_route_prefixes(mut self) -> Self {
self.irr_route_prefixes = true;
self
}
pub fn with_all(mut self) -> Self {
self.load_as2org = true;
self.load_population = true;
self.load_hegemony = true;
self.load_peeringdb = true;
self.load_delegated = true;
self.load_irr = true;
self.irr_config = IrrSourceConfig::all();
self.irr_route_prefixes = true;
self
}
pub fn build(self) -> Result<AsInfoUtils> {
AsInfoUtils::from_builder(&self)
}
fn config(&self) -> Result<AsInfoLoadConfig> {
Ok(AsInfoLoadConfig {
load_as2org: self.load_as2org,
load_population: self.load_population,
load_hegemony: self.load_hegemony,
load_peeringdb: self.load_peeringdb,
load_delegated: self.load_delegated,
load_irr: self.load_irr,
irr_sources: self.irr_config.resolve()?,
irr_route_prefixes: self.irr_route_prefixes,
})
}
}
#[derive(Debug, Clone)]
struct AsInfoLoadConfig {
load_as2org: bool,
load_population: bool,
load_hegemony: bool,
load_peeringdb: bool,
load_delegated: bool,
load_irr: bool,
irr_sources: Vec<crate::irr::IrrSource>,
irr_route_prefixes: bool,
}
pub struct AsInfoUtils {
pub asinfo_map: HashMap<u32, AsInfo>,
pub sibling_orgs: Option<SiblingOrgsUtils>,
config: AsInfoLoadConfig,
}
impl AsInfoUtils {
fn from_builder(builder: &AsInfoBuilder) -> Result<Self> {
let config = builder.config()?;
let asinfo_map = get_asinfo_map(&config)?;
let sibling_orgs = if config.load_as2org {
Some(SiblingOrgsUtils::new()?)
} else {
None
};
Ok(AsInfoUtils {
asinfo_map,
sibling_orgs,
config,
})
}
pub fn new_from_cached() -> Result<Self> {
let asinfo_map = get_asinfo_map_cached()?;
let sibling_orgs = Some(SiblingOrgsUtils::new()?);
Ok(AsInfoUtils {
asinfo_map,
sibling_orgs,
config: AsInfoLoadConfig {
load_as2org: true,
load_population: true,
load_hegemony: true,
load_peeringdb: true,
load_delegated: true,
load_irr: true,
irr_sources: crate::irr::all_sources(),
irr_route_prefixes: false,
},
})
}
pub fn reload(&mut self) -> Result<()> {
self.asinfo_map = get_asinfo_map(&self.config)?;
Ok(())
}
pub fn get(&self, asn: u32) -> Option<&AsInfo> {
self.asinfo_map.get(&asn)
}
}
impl LazyLoadable for AsInfoUtils {
fn reload(&mut self) -> Result<()> {
self.reload()
}
fn is_loaded(&self) -> bool {
!self.asinfo_map.is_empty()
}
fn loading_status(&self) -> &'static str {
if self.is_loaded() {
"ASInfo data loaded"
} else {
"ASInfo data not loaded"
}
}
}
pub fn get_asinfo_map_cached() -> Result<HashMap<u32, AsInfo>> {
info!("loading asinfo from previously generated BGPKIT cache file...");
let mut asnames_map = HashMap::new();
let reader = oneio::get_reader(BGPKIT_ASNINFO_URL)?;
for line in std::io::BufReader::new(reader).lines() {
let line = line?;
if line.trim().is_empty() {
continue;
}
let asinfo: AsInfo = serde_json::from_str(&line)?;
asnames_map.insert(asinfo.asn, asinfo);
}
Ok(asnames_map)
}
fn project_delegated_record(
record: crate::delegated::DelegatedRecord,
map: &mut HashMap<u32, DelegatedInfo>,
) {
if record.record_type != "asn" {
return;
}
let status = record.status.trim();
if status != "allocated" && status != "assigned" {
return;
}
let cc = record.country.trim();
if cc.is_empty() || cc == "*" {
return;
}
let (Ok(start), Ok(count)) = (record.start.parse::<u64>(), record.value.parse::<u64>()) else {
return;
};
let registry = record.registry.trim().to_lowercase();
let country = cc.to_uppercase();
let date = record.date.trim().to_string();
for asn in start..start.saturating_add(count) {
if asn > u32::MAX as u64 {
break;
}
let asn = asn as u32;
if (64512..=65534).contains(&asn) || asn >= 4_200_000_000 {
continue;
}
map.entry(asn).or_insert(DelegatedInfo {
registry: registry.clone(),
country: country.clone(),
date: date.clone(),
status: status.to_string(),
});
}
}
#[cfg(test)]
fn project_delegated_stats(text: &str, map: &mut HashMap<u32, DelegatedInfo>) {
for record in crate::delegated::parse_reader(text.as_bytes()).flatten() {
project_delegated_record(record, map);
}
}
#[allow(clippy::type_complexity)]
fn lookup_enrichment(
asn: u32,
as2org_utils: Option<&as2org::As2org>,
population_utils: Option<&population::AsnPopulation>,
hegemony_utils: Option<&hegemony::Hegemony>,
peeringdb_utils: Option<&Peeringdb>,
) -> (
Option<As2orgInfo>,
Option<AsnPopulationData>,
Option<HegemonyData>,
Option<Network>,
) {
let as2org = as2org_utils.and_then(|as2org_data| {
as2org_data.get_as_info(asn).map(|info| As2orgInfo {
name: info.name.clone(),
country: info.country_code.clone(),
org_id: info.org_id.clone(),
org_name: info.org_name.clone(),
})
});
let population = population_utils.and_then(|p| p.get(asn));
let hegemony = hegemony_utils.and_then(|h| h.get_score(asn).cloned());
let peeringdb = peeringdb_utils.and_then(|h| h.get_network(asn).cloned());
(as2org, population, hegemony, peeringdb)
}
fn fill_delegated_data(
asnames_map: &mut HashMap<u32, AsInfo>,
as2org_utils: Option<&as2org::As2org>,
population_utils: Option<&population::AsnPopulation>,
hegemony_utils: Option<&hegemony::Hegemony>,
peeringdb_utils: Option<&Peeringdb>,
) {
let mut delegated: HashMap<u32, DelegatedInfo> = HashMap::new();
for url in crate::delegated::RIR_DELEGATED_STATS_URLS {
match crate::delegated::fetch(url) {
Ok(reader) => {
for record in crate::delegated::parse_reader(reader) {
match record {
Ok(record) => project_delegated_record(record, &mut delegated),
Err(e) => warn!("failed to parse delegated stats from {url}: {e}"),
}
}
}
Err(e) => warn!("failed to load delegated stats from {}: {}", url, e),
}
}
attach_delegated_data(
asnames_map,
delegated,
as2org_utils,
population_utils,
hegemony_utils,
peeringdb_utils,
);
}
fn attach_delegated_data(
asnames_map: &mut HashMap<u32, AsInfo>,
delegated: HashMap<u32, DelegatedInfo>,
as2org_utils: Option<&as2org::As2org>,
population_utils: Option<&population::AsnPopulation>,
hegemony_utils: Option<&hegemony::Hegemony>,
peeringdb_utils: Option<&Peeringdb>,
) {
let mut new_entries = 0usize;
let mut attached = 0usize;
for (asn, delegated_info) in delegated {
asnames_map
.entry(asn)
.and_modify(|info| {
info.delegated = Some(delegated_info.clone());
attached += 1;
})
.or_insert_with(|| {
new_entries += 1;
let (as2org, population, hegemony, peeringdb) = lookup_enrichment(
asn,
as2org_utils,
population_utils,
hegemony_utils,
peeringdb_utils,
);
AsInfo {
asn,
name: "UNKNOWN".to_string(),
country: delegated_info.country.clone(),
as2org,
population,
hegemony,
peeringdb,
delegated: Some(delegated_info.clone()),
irr: Vec::new(),
}
});
}
info!(
"delegated stats: {attached} existing entries enriched, {new_entries} new entries created"
);
}
fn enrich_from_irr(
asnames_map: &mut HashMap<u32, AsInfo>,
irr_sources: &[crate::irr::IrrSource],
collect_route_prefixes: bool,
) {
use crate::irr::sources::DumpFormat;
use crate::irr::types::{IrrObject, IrrObjectType};
use std::collections::HashMap as StdMap;
let mut per_source: StdMap<String, StdMap<u32, IrrAsnInfoBuilder>> = StdMap::new();
let mut parsed_urls: std::collections::HashSet<String> = std::collections::HashSet::new();
let wanted_types: Vec<IrrObjectType> = if collect_route_prefixes {
vec![
IrrObjectType::AutNum,
IrrObjectType::Route,
IrrObjectType::Route6,
IrrObjectType::AsSet,
]
} else {
vec![IrrObjectType::AutNum, IrrObjectType::AsSet]
};
for source in irr_sources.iter().cloned() {
let source_name = source.name.to_string();
let mut urls_to_parse: Vec<(String, Vec<IrrObjectType>)> = Vec::new();
if source.format == DumpFormat::WholeDb {
let url = source.dump_urls(IrrObjectType::AutNum);
if let Some(dump) = url.first() {
urls_to_parse.push((dump.url.clone(), wanted_types.to_vec()));
}
} else {
for obj_type in &wanted_types {
for dump in source.dump_urls(*obj_type) {
urls_to_parse.push((dump.url.clone(), vec![*obj_type]));
}
}
}
for (url, _types_for_url) in urls_to_parse {
if parsed_urls.contains(&url) {
continue;
}
parsed_urls.insert(url.clone());
let sn = source_name.clone();
match crate::irr::parse_dump(
&crate::irr::IrrDumpUrl {
url: url.clone(),
transport: source.transport,
format: source.format,
},
|obj| {
let source_map = per_source.entry(sn.clone()).or_default();
match &obj {
IrrObject::AutNum(a) => {
let entry = source_map.entry(a.asn).or_default();
entry.source = a.source.clone();
entry.as_name = a.as_name.clone();
entry.descr = a.descr.clone();
if let Some(mnt) = a.extra.get("mnt-by") {
entry.mnt_by = mnt.clone();
}
}
IrrObject::Route(r) if collect_route_prefixes => {
let entry = source_map.entry(r.origin).or_default();
if entry.source.is_empty() {
entry.source = r.source.clone();
}
if let IpNet::V4(prefix) = r.prefix {
entry.route_prefixes.push(prefix);
}
}
IrrObject::Route6(r) if collect_route_prefixes => {
let entry = source_map.entry(r.origin).or_default();
if entry.source.is_empty() {
entry.source = r.source.clone();
}
if let IpNet::V6(prefix) = r.prefix {
entry.route6_prefixes.push(prefix);
}
}
IrrObject::AsSet(s) => {
let set_name = s.name.clone();
for &member_asn in &s.members {
let entry = source_map.entry(member_asn).or_default();
if entry.source.is_empty() {
entry.source = s.source.clone();
}
entry.member_of_sets.push(set_name.clone());
}
}
_ => {}
}
},
) {
Ok(stats) => info!(
"IRR from {source_name} ({url}): {} objects extracted",
stats.extracted
),
Err(e) => warn!("failed to load IRR from {source_name} ({url}): {e}"),
}
}
}
attach_irr_data(asnames_map, per_source, irr_sources);
}
fn attach_irr_data(
asnames_map: &mut HashMap<u32, AsInfo>,
per_source: std::collections::HashMap<
String,
std::collections::HashMap<u32, IrrAsnInfoBuilder>,
>,
irr_sources: &[crate::irr::IrrSource],
) {
let mut irr_attached = 0usize;
for (asn, info) in asnames_map.iter_mut() {
let mut irr_entries: Vec<IrrAsnInfo> = Vec::new();
for source in irr_sources.iter().cloned() {
if let Some(source_map) = per_source.get(source.name) {
if let Some(builder) = source_map.get(asn) {
irr_entries.push(builder.clone().build());
}
}
}
if !irr_entries.is_empty() {
info.irr = irr_entries;
irr_attached += 1;
}
}
info!("IRR data attached to {irr_attached} ASNs");
}
#[derive(Debug, Clone, Default)]
struct IrrAsnInfoBuilder {
as_name: String,
descr: Vec<String>,
source: String,
mnt_by: Vec<String>,
route_prefixes: Vec<Ipv4Net>,
route6_prefixes: Vec<Ipv6Net>,
member_of_sets: Vec<String>,
}
impl IrrAsnInfoBuilder {
fn build(self) -> IrrAsnInfo {
IrrAsnInfo {
as_name: self.as_name,
descr: self.descr,
source: self.source,
mnt_by: self.mnt_by,
route_prefixes: self.route_prefixes,
route6_prefixes: self.route6_prefixes,
member_of_sets: self.member_of_sets,
}
}
}
fn get_asinfo_map(config: &AsInfoLoadConfig) -> Result<HashMap<u32, AsInfo>> {
let load_as2org = config.load_as2org;
let load_population = config.load_population;
let load_hegemony = config.load_hegemony;
let load_peeringdb = config.load_peeringdb;
let read_text = |url: &str| -> Result<String> {
let mut text = String::new();
oneio::get_reader(url)?.read_to_string(&mut text)?;
Ok(text)
};
let text = match read_text(BGPKIT_ASN_TXT_MIRROR_URL) {
Ok(t) => t,
Err(_) => match read_text(RIPE_RIS_ASN_TXT_URL) {
Ok(t) => t,
Err(e) => {
return Err(BgpkitCommonsError::data_source_error(
data_sources::BGPKIT,
format!(
"error reading asinfo (neither mirror or original works): {}",
e
),
));
}
},
};
let as2org_utils = if load_as2org {
info!("loading as2org data from CAIDA...");
match as2org::As2org::new(None) {
Ok(data) => Some(data),
Err(e) => {
warn!("failed to load as2org data, proceeding without it: {e}");
None
}
}
} else {
None
};
let population_utils = if load_population {
info!("loading ASN population data from APNIC...");
match population::AsnPopulation::new() {
Ok(data) => Some(data),
Err(e) => {
warn!("failed to load population data, proceeding without it: {e}");
None
}
}
} else {
None
};
let hegemony_utils = if load_hegemony {
info!("loading IIJ IHR hegemony score data from BGPKIT mirror...");
match hegemony::Hegemony::new() {
Ok(data) => Some(data),
Err(e) => {
warn!("failed to load hegemony data, proceeding without it: {e}");
None
}
}
} else {
None
};
let peeringdb_utils = if load_peeringdb {
info!("loading peeringdb data...");
match Peeringdb::new_networks_only() {
Ok(data) => Some(data),
Err(e) => {
warn!(
"failed to load peeringdb data, proceeding without it: {e} \
(hint: set PEERINGDB_API_KEY to avoid rate limiting)"
);
None
}
}
} else {
None
};
let asnames = text
.lines()
.filter_map(|line| {
let (asn_str, name_country_str) = match line.split_once(' ') {
Some((asn, name)) => (asn, name),
None => return None,
};
let (name_str, country_str) = match name_country_str.rsplit_once(", ") {
Some((name, country)) => (name, country),
None => return None,
};
let asn = asn_str.parse::<u32>().unwrap();
let (as2org, population, hegemony, peeringdb) = lookup_enrichment(
asn,
as2org_utils.as_ref(),
population_utils.as_ref(),
hegemony_utils.as_ref(),
peeringdb_utils.as_ref(),
);
Some(AsInfo {
asn,
name: name_str.to_string(),
country: country_str.to_string(),
as2org,
population,
hegemony,
peeringdb,
delegated: None,
irr: Vec::new(),
})
})
.collect::<Vec<AsInfo>>();
let mut asnames_map = HashMap::new();
for asname in asnames {
asnames_map.insert(asname.asn, asname);
}
if config.load_delegated {
info!("loading delegated stats data...");
fill_delegated_data(
&mut asnames_map,
as2org_utils.as_ref(),
population_utils.as_ref(),
hegemony_utils.as_ref(),
peeringdb_utils.as_ref(),
);
}
if config.load_irr {
info!("enriching from IRR data...");
enrich_from_irr(
&mut asnames_map,
&config.irr_sources,
config.irr_route_prefixes,
);
}
Ok(asnames_map)
}
impl BgpkitCommons {
pub fn asinfo_all(&self) -> Result<HashMap<u32, AsInfo>> {
if self.asinfo.is_none() {
return Err(BgpkitCommonsError::module_not_loaded(
modules::ASINFO,
load_methods::LOAD_ASINFO,
));
}
Ok(self.asinfo.as_ref().unwrap().asinfo_map.clone())
}
pub fn asinfo_get(&self, asn: u32) -> Result<Option<AsInfo>> {
if self.asinfo.is_none() {
return Err(BgpkitCommonsError::module_not_loaded(
modules::ASINFO,
load_methods::LOAD_ASINFO,
));
}
Ok(self.asinfo.as_ref().unwrap().get(asn).cloned())
}
pub fn asinfo_are_siblings(&self, asn1: u32, asn2: u32) -> Result<bool> {
if self.asinfo.is_none() {
return Err(BgpkitCommonsError::module_not_loaded(
modules::ASINFO,
load_methods::LOAD_ASINFO,
));
}
if !self.asinfo.as_ref().unwrap().config.load_as2org {
return Err(BgpkitCommonsError::module_not_configured(
modules::ASINFO,
"as2org data",
"load_asinfo() with as2org=true",
));
}
let info_1_opt = self.asinfo_get(asn1)?;
let info_2_opt = self.asinfo_get(asn2)?;
if let (Some(info1), Some(info2)) = (info_1_opt, info_2_opt) {
if let (Some(org1), Some(org2)) = (info1.as2org, info2.as2org) {
let org_id_1 = org1.org_id;
let org_id_2 = org2.org_id;
return Ok(org_id_1 == org_id_2
|| self
.asinfo
.as_ref()
.and_then(|a| a.sibling_orgs.as_ref())
.map(|s| s.are_sibling_orgs(org_id_1.as_str(), org_id_2.as_str()))
.unwrap_or(false));
}
}
Ok(false)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn cc(map: &HashMap<u32, DelegatedInfo>, asn: u32) -> Option<&str> {
map.get(&asn).map(|d| d.country.as_str())
}
#[test]
fn test_parse_delegated_stats_basic() {
let text = "\
2|ripencc|ZZ|209|20250704|00000000+00000000+00000000|UTF-8
ripencc|*|asn|*|39634|summary
ripencc|GB|asn|219157|1|20260722|allocated
ripencc|DE|asn|219125|1|20260728|allocated
arin||asn|212|1||reserved|
arin|*|asn|*|32843|summary
arin|US|asn|402598|1|20260604|assigned|
apnic|BD|asn|154708|1|20260609|allocated
ripencc|NL|asn|1000|4|19970901|allocated
ripencc|NL|ipv4|185.0.0.0|65536|20000101|allocated
";
let mut map = HashMap::new();
project_delegated_stats(text, &mut map);
assert_eq!(cc(&map, 219157), Some("GB"));
assert_eq!(cc(&map, 219125), Some("DE"));
assert_eq!(cc(&map, 402598), Some("US"));
assert_eq!(cc(&map, 154708), Some("BD"));
assert_eq!(cc(&map, 1000), Some("NL"));
assert_eq!(cc(&map, 1003), Some("NL"));
assert!(!map.contains_key(&1004));
assert!(!map.contains_key(&212));
assert_eq!(map.len(), 8);
let info = &map[&219157];
assert_eq!(info.registry, "ripencc");
assert_eq!(info.status, "allocated");
assert_eq!(info.date, "20260722");
}
#[test]
fn test_parse_delegated_stats_skips_private_and_invalid() {
let text = "\
arin|US|asn|64512|1023|19891201|reserved
arin|US|asn|4200000000|9999|19891201|reserved
arin|US|asn|notanumber|1|20200101|allocated
arin|US|asn|123|notacount|20200101|allocated
";
let mut map = HashMap::new();
project_delegated_stats(text, &mut map);
assert!(map.is_empty());
}
#[test]
fn test_parse_delegated_stats_status_filter() {
let text = "\
arin|US|asn|300000|1|20200101|reserved
arin|US|asn|300001|1|20200101|available
arin|US|asn|300002|1|20200101|allocated
arin|US|asn|300003|1|20200101|assigned
";
let mut map = HashMap::new();
project_delegated_stats(text, &mut map);
assert!(!map.contains_key(&300000));
assert!(!map.contains_key(&300001));
assert_eq!(cc(&map, 300002), Some("US"));
assert_eq!(cc(&map, 300003), Some("US"));
assert_eq!(map.len(), 2);
}
#[test]
fn test_parse_delegated_stats_private_boundary() {
let text = "\
arin|US|asn|65535|1|19891201|allocated
arin|US|asn|65534|1|19891201|allocated
arin|US|asn|64496|1|19891201|allocated
arin|US|asn|4199999999|1|19891201|allocated
arin|US|asn|4200000000|1|19891201|allocated
";
let mut map = HashMap::new();
project_delegated_stats(text, &mut map);
assert_eq!(cc(&map, 65535), Some("US"));
assert!(!map.contains_key(&65534));
assert_eq!(cc(&map, 64496), Some("US"));
assert_eq!(cc(&map, 4199999999), Some("US"));
assert!(!map.contains_key(&4200000000));
}
#[test]
fn test_parse_delegated_stats_case_normalization() {
let text = "lacnic|br|asn|269000|1|20150101|allocated\n";
let mut map = HashMap::new();
project_delegated_stats(text, &mut map);
assert_eq!(cc(&map, 269000), Some("BR"));
assert_eq!(map[&269000].registry, "lacnic");
}
#[test]
fn test_parse_delegated_stats_malformed_lines() {
let text = "\
# comment line
ripencc|GB|asn
ripencc|GB|ipv6|2001:db8::|32|20200101|allocated
some garbage line with no pipes at all
|GB|asn|100|1|20200101|allocated
ripencc|GB|asn|100|1|20200101
ripencc|GB|asn|100|1|20200101|allocated|extra|fields|ok
";
let mut map = HashMap::new();
project_delegated_stats(text, &mut map);
assert_eq!(cc(&map, 100), Some("GB"));
assert_eq!(map.len(), 1);
}
#[test]
fn test_profiles_match_asninfo_v1_and_full_uses_all_sources() {
let minimum = AsInfoProfile::Minimum.builder().config().unwrap();
assert!(!minimum.load_as2org);
assert!(!minimum.load_population);
assert!(!minimum.load_hegemony);
assert!(!minimum.load_peeringdb);
assert!(!minimum.load_delegated);
assert!(!minimum.load_irr);
let default = AsInfoProfile::Default.builder().config().unwrap();
assert!(default.load_as2org);
assert!(default.load_population);
assert!(default.load_hegemony);
assert!(default.load_peeringdb);
assert!(!default.load_delegated);
assert!(!default.load_irr);
let full = AsInfoProfile::Full.builder().config().unwrap();
assert!(full.load_delegated);
assert!(full.load_irr);
assert!(full.irr_route_prefixes);
assert_eq!(full.irr_sources.len(), crate::irr::all_sources().len());
let all = AsInfoBuilder::new().with_all().config().unwrap();
assert!(all.irr_route_prefixes);
assert_eq!(all.irr_sources.len(), crate::irr::all_sources().len());
}
#[test]
fn test_custom_irr_sources_are_validated() {
assert!(IrrSourceConfig::only(&[]).is_err());
assert!(IrrSourceConfig::sources(&[]).is_err());
assert!(IrrSourceConfig::sources(&["RIPE", "NOT-A-REGISTRY"]).is_err());
let selected = IrrSourceConfig::sources(&["RIPE", "RADB"]).unwrap();
let config = AsInfoBuilder::new()
.with_irr_sources(selected)
.config()
.unwrap();
assert_eq!(
config
.irr_sources
.iter()
.map(|source| source.name)
.collect::<Vec<_>>(),
vec!["RIPE", "RADB"]
);
}
#[test]
fn delegated_enrichment_never_overwrites_name_or_country() {
let mut map = HashMap::new();
map.insert(
13335,
AsInfo {
asn: 13335,
name: "CLOUDFLARENET".to_string(),
country: "US".to_string(),
as2org: None,
population: None,
hegemony: None,
peeringdb: None,
delegated: None,
irr: Vec::new(),
},
);
let mut delegated = HashMap::new();
delegated.insert(
13335,
DelegatedInfo {
registry: "ripencc".to_string(),
country: "GB".to_string(),
date: "20260722".to_string(),
status: "allocated".to_string(),
},
);
delegated.insert(
400644,
DelegatedInfo {
registry: "arin".to_string(),
country: "US".to_string(),
date: "20200101".to_string(),
status: "allocated".to_string(),
},
);
attach_delegated_data(&mut map, delegated, None, None, None, None);
let existing = &map[&13335];
assert_eq!(existing.name, "CLOUDFLARENET");
assert_eq!(existing.country, "US");
assert_eq!(existing.delegated.as_ref().unwrap().registry, "ripencc");
let new_entry = &map[&400644];
assert_eq!(new_entry.name, "UNKNOWN");
assert_eq!(new_entry.country, "US");
assert_eq!(new_entry.delegated.as_ref().unwrap().registry, "arin");
}
#[test]
fn irr_enrichment_never_overwrites_name_or_country() {
let mut map = HashMap::new();
map.insert(
13335,
AsInfo {
asn: 13335,
name: "CLOUDFLARENET".to_string(),
country: "US".to_string(),
as2org: None,
population: None,
hegemony: None,
peeringdb: None,
delegated: None,
irr: Vec::new(),
},
);
let mut per_source: std::collections::HashMap<
String,
std::collections::HashMap<u32, IrrAsnInfoBuilder>,
> = std::collections::HashMap::new();
let mut builder = IrrAsnInfoBuilder::default();
builder.source = "RIPE".to_string();
builder.as_name = "CLOUDFLARE-NET".to_string();
per_source.insert("RIPE".to_string(), [(13335, builder)].into_iter().collect());
let ripe = crate::irr::sources::all_sources()
.into_iter()
.find(|source| source.name == "RIPE")
.unwrap();
attach_irr_data(&mut map, per_source, &[ripe]);
let info = &map[&13335];
assert_eq!(info.name, "CLOUDFLARENET");
assert_eq!(info.country, "US");
assert_eq!(info.irr.len(), 1);
assert_eq!(info.irr[0].as_name, "CLOUDFLARE-NET");
assert_eq!(info.irr[0].source, "RIPE");
}
}