mod common;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use mib_rs::load::{Loader, load};
use mib_rs::mib::Mib;
use mib_rs::source::dir as dir_source;
use mib_rs::types::{BaseType, DiagnosticConfig, Kind, Language, ResolverStrictness};
use common::{FixtureNode, corpus_dir};
const FIXTURE_MODULES: &[&str] = &["IF-MIB", "SNMPv2-MIB", "IP-MIB", "ENTITY-MIB", "BRIDGE-MIB"];
fn fixture_path(module: &str) -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("testdata/fixtures/netsnmp")
.join(format!("{module}.json"))
}
fn load_fixture(module: &str) -> HashMap<String, FixtureNode> {
let path = fixture_path(module);
let data = std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("failed to read fixture {}: {e}", path.display()));
serde_json::from_str(&data)
.unwrap_or_else(|e| panic!("failed to parse fixture {}: {e}", path.display()))
}
fn load_fixture_mib() -> Mib {
let dir = corpus_dir();
let src = dir_source(&dir).expect("failed to create corpus source");
let opts = Loader::new()
.source(src)
.resolver_strictness(ResolverStrictness::Permissive)
.diagnostic_config(DiagnosticConfig::silent())
.modules(FIXTURE_MODULES.iter().copied());
load(opts).expect("load failed")
}
fn normalize_base_type(base: BaseType) -> &'static str {
match base {
BaseType::Integer32 => "Integer32",
BaseType::Unsigned32 => "Unsigned32",
BaseType::Counter32 => "Counter32",
BaseType::Counter64 => "Counter64",
BaseType::Gauge32 => "Gauge32",
BaseType::TimeTicks => "TimeTicks",
BaseType::IpAddress => "IpAddress",
BaseType::OctetString => "OCTET STRING",
BaseType::ObjectIdentifier => "OBJECT IDENTIFIER",
BaseType::Bits => "BITS",
BaseType::Opaque => "Opaque",
BaseType::Sequence => "OTHER",
_ => "unknown",
}
}
fn normalize_node_type(mib: &Mib, name: &str) -> String {
if let Some(obj_id) = mib.object_by_name(name) {
let obj = mib.raw().object(obj_id);
match obj.type_id() {
Some(tid) => {
let base = mib.raw().type_(tid).effective_base(mib.raw().types_slice());
normalize_base_type(base).to_string()
}
None => "OTHER".to_string(),
}
} else if mib.notification_by_name(name).is_some() {
"NOTIFICATION-TYPE".to_string()
} else if let Some(gid) = mib.group_by_name(name) {
if mib.raw().group(gid).is_notification_group() {
"NOTIFICATION-GROUP".to_string()
} else {
"OBJECT-GROUP".to_string()
}
} else if mib.compliance_by_name(name).is_some() {
"MODULE-COMPLIANCE".to_string()
} else if mib.capability_by_name(name).is_some() {
"AGENT-CAPABILITIES".to_string()
} else if let Some(node_id) = mib.node_by_name(name) {
if let Some(mod_id) = mib.raw().effective_module(node_id) {
let module = mib.raw().module(mod_id);
if let Some(mod_oid) = module.oid() {
let node_oid = mib.raw().tree().oid_of(node_id);
if mod_oid == node_oid {
return "MODULE-IDENTITY".to_string();
}
}
}
"OTHER".to_string()
} else {
"OTHER".to_string()
}
}
fn normalize_kind(kind: Kind) -> &'static str {
match kind {
Kind::Table => "table",
Kind::Row => "row",
Kind::Column => "column",
Kind::Scalar => "scalar",
_ => "",
}
}
fn normalize_enums(nvs: &[mib_rs::mib::NamedValue]) -> HashMap<i64, String> {
nvs.iter().map(|nv| (nv.value, nv.label.clone())).collect()
}
fn fixture_enum_map(m: &HashMap<String, String>) -> HashMap<i64, String> {
m.iter()
.map(|(k, v)| (k.parse::<i64>().unwrap(), v.clone()))
.collect()
}
fn normalize_type_name(t: &str) -> &str {
match t {
"INTEGER" | "Integer32" => "Integer32",
"COUNTER" | "Counter" | "Counter32" => "Counter32",
"GAUGE" | "Gauge" | "Gauge32" => "Gauge32",
"UNSIGNED32" | "Unsigned32" | "UInteger32" => "Unsigned32",
"TIMETICKS" | "TimeTicks" => "TimeTicks",
"IPADDR" | "IpAddress" => "IpAddress",
"OCTETSTR" | "OCTET STRING" | "OctetString" => "OCTET STRING",
"OBJID" | "OBJECT IDENTIFIER" | "ObjectIdentifier" => "OBJECT IDENTIFIER",
"COUNTER64" | "Counter64" => "Counter64",
"BITS" | "BITSTRING" => "BITS",
"OPAQUE" | "Opaque" => "Opaque",
other => other,
}
}
fn types_equivalent(a: &str, b: &str) -> bool {
a == b || normalize_type_name(a) == normalize_type_name(b)
}
fn signed_equiv(a: i64, b: i64) -> bool {
if a == b {
return true;
}
if a >= 0 && b < 0 && a == b.wrapping_add(1 << 32) {
return true;
}
if b >= 0 && a < 0 && b == a.wrapping_add(1 << 32) {
return true;
}
false
}
fn ranges_equivalent(gomib_ranges: &[(i64, i64)], fixture_ranges: &[(i64, i64)]) -> bool {
if gomib_ranges.len() != fixture_ranges.len() {
return false;
}
let mut g: Vec<_> = gomib_ranges.to_vec();
let mut f: Vec<_> = fixture_ranges.to_vec();
g.sort();
f.sort();
g.iter().zip(f.iter()).all(|(a, b)| {
(a.0 == b.0 && a.1 == b.1) || (signed_equiv(a.0, b.0) && signed_equiv(a.1, b.1))
})
}
fn status_equivalent(a: &str, b: &str) -> bool {
if a == b {
return true;
}
matches!((a, b), ("mandatory", "current") | ("current", "mandatory"))
}
fn access_equivalent(a: &str, b: &str, is_smiv1: bool) -> bool {
if a == b {
return true;
}
if is_smiv1 {
return matches!(
(a, b),
("read-write", "read-create") | ("read-create", "read-write")
);
}
false
}
fn hints_equivalent(a: &str, b: &str) -> bool {
a == b || a.trim().eq_ignore_ascii_case(b.trim())
}
fn defval_equivalent(a: &str, b: &str) -> bool {
if a == b {
return true;
}
let a_norm = a.trim().trim_matches(|c| c == '"' || c == '\'');
let b_norm = b.trim().trim_matches(|c| c == '"' || c == '\'');
if a_norm == b_norm {
return true;
}
if (is_hex_zeros(a_norm) && b_norm == "0") || (is_hex_zeros(b_norm) && a_norm == "0") {
return true;
}
if (is_hex_all_ones(a_norm) && b_norm == "-1") || (is_hex_all_ones(b_norm) && a_norm == "-1") {
return true;
}
false
}
fn is_hex_zeros(s: &str) -> bool {
let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) else {
return false;
};
!hex.is_empty() && hex.chars().all(|c| c == '0')
}
fn is_hex_all_ones(s: &str) -> bool {
let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) else {
return false;
};
!hex.is_empty() && hex.chars().all(|c| c == 'F' || c == 'f')
}
fn reference_equivalent(a: &str, b: &str) -> bool {
if a == b {
return true;
}
normalize_whitespace(a) == normalize_whitespace(b)
}
fn normalize_whitespace(s: &str) -> String {
s.split_whitespace().collect::<Vec<_>>().join(" ")
}
fn is_object_type_node(fn_: &FixtureNode) -> bool {
!matches!(
fn_.typ.as_str(),
"" | "OTHER"
| "NOTIFICATION-TYPE"
| "TRAP-TYPE"
| "MODULE-IDENTITY"
| "MODULE-COMPLIANCE"
| "OBJECT-GROUP"
| "NOTIFICATION-GROUP"
| "AGENT-CAPABILITIES"
| "OBJECT-IDENTITY"
)
}
fn is_notification_node(fn_: &FixtureNode) -> bool {
fn_.node_type == "NOTIFICATION-TYPE" || fn_.node_type == "TRAP-TYPE"
}
fn for_each_fixture_node(
mib: &Mib,
filter: impl Fn(&FixtureNode) -> bool,
check: impl Fn(&Mib, &str, &FixtureNode, &mut Vec<String>),
) -> Vec<String> {
let mut failures = Vec::new();
for &module in FIXTURE_MODULES {
let fixture = load_fixture(module);
for fn_ in fixture.values() {
if !filter(fn_) {
continue;
}
check(mib, module, fn_, &mut failures);
}
}
failures
}
#[test]
fn fixture_node_type() {
let mib = load_fixture_mib();
let failures = for_each_fixture_node(
&mib,
|_| true,
|mib, _, fn_, failures| {
let got = normalize_node_type(mib, &fn_.name);
if !types_equivalent(&got, &fn_.node_type) {
failures.push(format!(
"{}: node type: got={got:?} fixture={:?}",
fn_.name, fn_.node_type
));
}
},
);
assert!(
failures.is_empty(),
"node type divergences:\n{}",
failures.join("\n")
);
}
#[test]
fn fixture_oids() {
let mib = load_fixture_mib();
let failures = for_each_fixture_node(
&mib,
|_| true,
|mib, _, fn_, failures| {
let Some(node_id) = mib.node_by_name(&fn_.name) else {
failures.push(format!("{}: not found in mib-rs", fn_.name));
return;
};
let got = mib.raw().tree().oid_of(node_id).to_string();
if got != fn_.oid {
failures.push(format!("{}: OID: got={got} fixture={}", fn_.name, fn_.oid));
}
},
);
assert!(
failures.is_empty(),
"OID divergences:\n{}",
failures.join("\n")
);
}
#[test]
fn fixture_types() {
let mib = load_fixture_mib();
let failures = for_each_fixture_node(&mib, is_object_type_node, |mib, _, fn_, failures| {
let Some(obj_id) = mib.object_by_name(&fn_.name) else {
failures.push(format!("{}: object not found", fn_.name));
return;
};
let obj = mib.raw().object(obj_id);
let got_type = match obj.type_id() {
Some(tid) => {
let base = mib.raw().type_(tid).effective_base(mib.raw().types_slice());
normalize_base_type(base).to_string()
}
None => String::new(),
};
if !types_equivalent(&got_type, &fn_.typ) {
failures.push(format!(
"{}: type: got={got_type:?} fixture={:?}",
fn_.name, fn_.typ
));
}
let got_tc = match obj.type_id() {
Some(tid) => {
let t = mib.raw().type_(tid);
if t.is_textual_convention() {
t.name().to_string()
} else {
String::new()
}
}
None => String::new(),
};
if (!fn_.tc_name.is_empty() || !got_tc.is_empty()) && got_tc != fn_.tc_name {
failures.push(format!(
"{}: TC name: got={got_tc:?} fixture={:?}",
fn_.name, fn_.tc_name
));
}
let got_hint = obj.effective_display_hint();
if (!fn_.hint.is_empty() || !got_hint.is_empty()) && !hints_equivalent(got_hint, &fn_.hint)
{
failures.push(format!(
"{}: hint: got={got_hint:?} fixture={:?}",
fn_.name, fn_.hint
));
}
});
assert!(
failures.is_empty(),
"type divergences:\n{}",
failures.join("\n")
);
}
#[test]
fn fixture_enums() {
let mib = load_fixture_mib();
let failures = for_each_fixture_node(
&mib,
|fn_| is_object_type_node(fn_) && !fn_.enum_values.is_empty(),
|mib, _, fn_, failures| {
let Some(obj_id) = mib.object_by_name(&fn_.name) else {
failures.push(format!("{}: object not found", fn_.name));
return;
};
let obj = mib.raw().object(obj_id);
let got = normalize_enums(obj.effective_enums());
let expected = fixture_enum_map(&fn_.enum_values);
if got != expected {
failures.push(format!(
"{}: enums: got={got:?} fixture={expected:?}",
fn_.name
));
}
},
);
assert!(
failures.is_empty(),
"enum divergences:\n{}",
failures.join("\n")
);
}
#[test]
fn fixture_bits() {
let mib = load_fixture_mib();
let failures = for_each_fixture_node(
&mib,
|fn_| is_object_type_node(fn_) && !fn_.bit_values.is_empty(),
|mib, _, fn_, failures| {
let Some(obj_id) = mib.object_by_name(&fn_.name) else {
failures.push(format!("{}: object not found", fn_.name));
return;
};
let obj = mib.raw().object(obj_id);
let got = normalize_enums(obj.effective_bits());
let expected = fixture_enum_map(&fn_.bit_values);
if got != expected {
failures.push(format!(
"{}: bits: got={got:?} fixture={expected:?}",
fn_.name
));
}
},
);
assert!(
failures.is_empty(),
"bits divergences:\n{}",
failures.join("\n")
);
}
#[test]
fn fixture_tables() {
let mib = load_fixture_mib();
let failures = for_each_fixture_node(
&mib,
|fn_| !fn_.kind.is_empty() || fn_.indexes.is_some() || !fn_.augments.is_empty(),
|mib, _, fn_, failures| {
let Some(obj_id) = mib.object_by_name(&fn_.name) else {
failures.push(format!("{}: object not found", fn_.name));
return;
};
let obj = mib.raw().object(obj_id);
if !fn_.kind.is_empty() {
let got_kind = normalize_kind(obj.kind(mib.raw().tree()));
if got_kind != fn_.kind {
failures.push(format!(
"{}: kind: got={got_kind:?} fixture={:?}",
fn_.name, fn_.kind
));
}
}
if let Some(ref fix_indexes) = fn_.indexes {
let got_indexes: Vec<_> = obj
.index()
.iter()
.filter_map(|e| {
let name = if let Some(oid) = e.object {
mib.raw().object(oid).name().to_string()
} else if !e.name.is_empty() {
e.name.clone()
} else {
return None;
};
Some((name, e.implied))
})
.collect();
let fix: Vec<_> = fix_indexes
.iter()
.map(|i| (i.name.clone(), i.implied))
.collect();
if got_indexes != fix {
failures.push(format!(
"{}: indexes: got={got_indexes:?} fixture={fix:?}",
fn_.name
));
}
}
if !fn_.augments.is_empty() {
let got_aug = match obj.augments() {
Some(aug_id) => mib.raw().object(aug_id).name().to_string(),
None => String::new(),
};
if got_aug != fn_.augments {
failures.push(format!(
"{}: augments: got={got_aug:?} fixture={:?}",
fn_.name, fn_.augments
));
}
}
},
);
assert!(
failures.is_empty(),
"table divergences:\n{}",
failures.join("\n")
);
}
#[test]
fn fixture_access() {
let mib = load_fixture_mib();
let failures = for_each_fixture_node(
&mib,
|fn_| !fn_.access.is_empty(),
|mib, _, fn_, failures| {
let Some(obj_id) = mib.object_by_name(&fn_.name) else {
failures.push(format!("{}: object not found", fn_.name));
return;
};
let obj = mib.raw().object(obj_id);
let got = obj.access().to_string();
let is_smiv1 = obj
.module()
.map(|mid| mib.raw().module(mid).language() == Language::SMIv1)
.unwrap_or(false);
if !access_equivalent(&got, &fn_.access, is_smiv1) {
failures.push(format!(
"{}: access: got={got:?} fixture={:?}",
fn_.name, fn_.access
));
}
},
);
assert!(
failures.is_empty(),
"access divergences:\n{}",
failures.join("\n")
);
}
#[test]
fn fixture_status() {
let mib = load_fixture_mib();
let failures = for_each_fixture_node(
&mib,
|fn_| !fn_.status.is_empty(),
|mib, _, fn_, failures| {
let got = if let Some(obj_id) = mib.object_by_name(&fn_.name) {
mib.raw().object(obj_id).status().to_string()
} else if let Some(notif_id) = mib.notification_by_name(&fn_.name) {
mib.raw().notification(notif_id).status().to_string()
} else {
failures.push(format!("{}: not found", fn_.name));
return;
};
if !status_equivalent(&got, &fn_.status) {
failures.push(format!(
"{}: status: got={got:?} fixture={:?}",
fn_.name, fn_.status
));
}
},
);
assert!(
failures.is_empty(),
"status divergences:\n{}",
failures.join("\n")
);
}
#[test]
fn fixture_ranges() {
let mib = load_fixture_mib();
let failures = for_each_fixture_node(
&mib,
|fn_| is_object_type_node(fn_) && fn_.ranges.as_ref().is_some_and(|r| !r.is_empty()),
|mib, _, fn_, failures| {
let Some(obj_id) = mib.object_by_name(&fn_.name) else {
failures.push(format!("{}: object not found", fn_.name));
return;
};
let obj = mib.raw().object(obj_id);
let mut got: Vec<(i64, i64)> = Vec::new();
for r in obj.effective_ranges() {
if let (Some(min), Some(max)) = (r.min.as_i64(), r.max.as_i64()) {
got.push((min, max));
}
}
for r in obj.effective_sizes() {
if let (Some(min), Some(max)) = (r.min.as_i64(), r.max.as_i64()) {
got.push((min, max));
}
}
let fix: Vec<(i64, i64)> = fn_
.ranges
.as_ref()
.unwrap()
.iter()
.map(|r| (r.low, r.high))
.collect();
if !ranges_equivalent(&got, &fix) {
failures.push(format!("{}: ranges: got={got:?} fixture={fix:?}", fn_.name));
}
},
);
assert!(
failures.is_empty(),
"range divergences:\n{}",
failures.join("\n")
);
}
#[test]
fn fixture_notifications() {
let mib = load_fixture_mib();
let failures = for_each_fixture_node(&mib, is_notification_node, |mib, _, fn_, failures| {
let Some(notif_id) = mib.notification_by_name(&fn_.name) else {
failures.push(format!("{}: notification not found", fn_.name));
return;
};
let notif = mib.raw().notification(notif_id);
if let Some(node_id) = notif.node() {
let got_oid = mib.raw().tree().oid_of(node_id).to_string();
if got_oid != fn_.oid {
failures.push(format!(
"{}: notification OID: got={got_oid} fixture={}",
fn_.name, fn_.oid
));
}
}
if let Some(ref fix_varbinds) = fn_.varbinds {
let got: Vec<String> = notif
.objects()
.iter()
.map(|&oid| mib.raw().object(oid).name().to_string())
.collect();
if got != *fix_varbinds {
failures.push(format!(
"{}: varbinds: got={got:?} fixture={fix_varbinds:?}",
fn_.name
));
}
}
if !fn_.status.is_empty() {
let got = notif.status().to_string();
if !status_equivalent(&got, &fn_.status) {
failures.push(format!(
"{}: notification status: got={got:?} fixture={:?}",
fn_.name, fn_.status
));
}
}
});
assert!(
failures.is_empty(),
"notification divergences:\n{}",
failures.join("\n")
);
}
#[test]
fn fixture_units() {
let mib = load_fixture_mib();
let failures = for_each_fixture_node(
&mib,
|fn_| is_object_type_node(fn_) && !fn_.units.is_empty(),
|mib, _, fn_, failures| {
let Some(obj_id) = mib.object_by_name(&fn_.name) else {
failures.push(format!("{}: object not found", fn_.name));
return;
};
let got = mib.raw().object(obj_id).units();
if got != fn_.units {
failures.push(format!(
"{}: units: got={got:?} fixture={:?}",
fn_.name, fn_.units
));
}
},
);
assert!(
failures.is_empty(),
"units divergences:\n{}",
failures.join("\n")
);
}
#[test]
fn fixture_defval() {
let mib = load_fixture_mib();
let failures = for_each_fixture_node(&mib, is_object_type_node, |mib, _, fn_, failures| {
let Some(obj_id) = mib.object_by_name(&fn_.name) else {
failures.push(format!("{}: object not found", fn_.name));
return;
};
let obj = mib.raw().object(obj_id);
let got = match obj.default_value() {
Some(dv) if !dv.is_unset() => dv.to_string(),
_ => String::new(),
};
if fn_.default_value.is_empty() && got.is_empty() {
return;
}
if !fn_.default_value.is_empty() && got.is_empty() {
failures.push(format!(
"{}: defval: mib-rs has none, fixture={:?}",
fn_.name, fn_.default_value
));
return;
}
if fn_.default_value.is_empty() && !got.is_empty() {
failures.push(format!(
"{}: defval: mib-rs={got:?}, fixture has none",
fn_.name
));
return;
}
if !defval_equivalent(&got, &fn_.default_value) {
failures.push(format!(
"{}: defval: got={got:?} fixture={:?}",
fn_.name, fn_.default_value
));
}
});
assert!(
failures.is_empty(),
"defval divergences:\n{}",
failures.join("\n")
);
}
#[test]
fn fixture_reference() {
let mib = load_fixture_mib();
let failures = for_each_fixture_node(
&mib,
|fn_| !fn_.reference.is_empty(),
|mib, _, fn_, failures| {
let got = if let Some(obj_id) = mib.object_by_name(&fn_.name) {
mib.raw().object(obj_id).reference().to_string()
} else if let Some(notif_id) = mib.notification_by_name(&fn_.name) {
mib.raw().notification(notif_id).reference().to_string()
} else {
failures.push(format!("{}: not found", fn_.name));
return;
};
if !reference_equivalent(&got, &fn_.reference) {
failures.push(format!(
"{}: reference: got={got:?} fixture={:?}",
fn_.name, fn_.reference
));
}
},
);
assert!(
failures.is_empty(),
"reference divergences:\n{}",
failures.join("\n")
);
}
#[test]
fn fixture_module() {
let mib = load_fixture_mib();
let failures = for_each_fixture_node(
&mib,
|fn_| !fn_.module.is_empty(),
|mib, _, fn_, failures| {
let Some(node_id) = mib.node_by_name(&fn_.name) else {
failures.push(format!("{}: not found", fn_.name));
return;
};
let got = match mib.raw().effective_module(node_id) {
Some(mid) => mib.raw().module(mid).name().to_string(),
None => String::new(),
};
if got != fn_.module {
failures.push(format!(
"{}: module: got={got:?} fixture={:?}",
fn_.name, fn_.module
));
}
},
);
assert!(
failures.is_empty(),
"module divergences:\n{}",
failures.join("\n")
);
}