use crate::{DialectHint, Document, parse_generic};
const STRONG_SIGNAL: i32 = 3;
const MODERATE_SIGNAL: i32 = 2;
const WEAK_SIGNAL: i32 = 1;
const MIN_CONFIDENCE_SCORE: i32 = 3;
const MARGIN_FACTOR: i32 = 2;
pub fn detect_dialect(input: &str) -> DialectHint {
let mut fortios: i32 = 0;
let mut junos: i32 = 0;
let mut nxos: i32 = 0;
let mut eos: i32 = 0;
let mut iosxe: i32 = 0;
for raw_line in input.lines() {
let line = raw_line.trim();
if line.is_empty() || line == "!" {
continue;
}
if line.starts_with("config ")
&& !line.contains('{')
&& line.split_whitespace().count() >= 2
{
fortios += STRONG_SIGNAL;
}
if line.starts_with("edit ") {
fortios += STRONG_SIGNAL;
}
if line == "end" {
fortios += MODERATE_SIGNAL;
}
if line == "next" {
fortios += MODERATE_SIGNAL;
}
if line.starts_with("set ") || line.starts_with("unset ") {
let second = line.split_whitespace().nth(1).unwrap_or("");
if is_junos_stanza_name(second) {
junos += STRONG_SIGNAL;
} else {
fortios += WEAK_SIGNAL;
}
}
if line.ends_with('{') {
junos += MODERATE_SIGNAL;
}
if line == "}" || line.ends_with("};") {
junos += MODERATE_SIGNAL;
}
if line.ends_with(';') && !line.ends_with("};") {
junos += WEAK_SIGNAL;
}
if is_junos_stanza_name(line.split_whitespace().next().unwrap_or("")) {
junos += STRONG_SIGNAL;
}
if line.starts_with("feature ") {
nxos += STRONG_SIGNAL;
}
if line.starts_with("interface ") {
let iface = line.trim_start_matches("interface ");
if iface.starts_with("Ethernet") && iface.contains('/') {
nxos += STRONG_SIGNAL;
} else if iface.starts_with("Ethernet") || iface.starts_with("Management") {
eos += MODERATE_SIGNAL;
}
}
if line.starts_with("vpc ") {
nxos += STRONG_SIGNAL;
}
if line.starts_with("role name ") {
nxos += MODERATE_SIGNAL;
}
if line.starts_with("ip access-list extended ") {
iosxe += STRONG_SIGNAL;
}
if line.starts_with("ip address ") {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 4 && looks_like_dotted_mask(parts[3]) {
iosxe += MODERATE_SIGNAL;
}
}
if line.contains(" mask ") && line.starts_with("network ") {
iosxe += MODERATE_SIGNAL;
}
if (line.starts_with("permit ") || line.starts_with("deny "))
&& line.split_whitespace().any(looks_like_dotted_mask)
{
iosxe += WEAK_SIGNAL;
}
if line.starts_with("ip access-list ") && !line.contains("extended") {
eos += MODERATE_SIGNAL;
}
if let Some(first) = line.split_whitespace().next()
&& first.parse::<u32>().is_ok()
&& (line.contains(" permit ") || line.contains(" deny "))
{
eos += WEAK_SIGNAL;
}
if line.starts_with("ip address ") {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 3 && parts[2].contains('/') {
eos += MODERATE_SIGNAL;
}
}
}
let candidates = [
("fortios", fortios),
("junos", junos),
("nxos", nxos),
("eos", eos),
("iosxe", iosxe),
];
let mut sorted = candidates;
sorted.sort_by_key(|c| std::cmp::Reverse(c.1));
let (best_name, best_score) = sorted[0];
let (_, second_score) = sorted[1];
if best_score < MIN_CONFIDENCE_SCORE {
return DialectHint::Generic;
}
if best_score < second_score * MARGIN_FACTOR {
return DialectHint::Generic;
}
DialectHint::Named(best_name.to_string())
}
pub fn auto_parse(input: &str) -> Document {
let hint = detect_dialect(input);
let mut doc = parse_generic(input);
doc.metadata.dialect_hint = hint;
doc
}
fn is_junos_stanza_name(name: &str) -> bool {
matches!(
name,
"interfaces"
| "protocols"
| "policy-options"
| "routing-options"
| "forwarding-options"
| "class-of-service"
| "system"
| "security"
| "firewall"
| "vlans"
| "chassis"
| "snmp"
| "applications"
| "groups"
| "routing-instances"
)
}
fn looks_like_dotted_mask(s: &str) -> bool {
let parts: Vec<&str> = s.split('.').collect();
parts.len() == 4 && parts.iter().all(|p| p.parse::<u8>().is_ok())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detect_fortios() {
let input = "\
config system global
set hostname \"FortiGate-01\"
set timezone 04
end
config firewall address
edit \"web-server\"
set type ipmask
set subnet 10.0.1.10 255.255.255.255
next
end
";
assert_eq!(detect_dialect(input), DialectHint::Named("fortios".into()));
}
#[test]
fn detect_junos_hierarchical() {
let input = "\
interfaces {
ge-0/0/0 {
description \"uplink\";
mtu 9216;
unit 0 {
family inet {
address 192.0.2.2/30;
}
}
}
}
";
assert_eq!(detect_dialect(input), DialectHint::Named("junos".into()));
}
#[test]
fn detect_junos_set_style() {
let input = "\
set interfaces ge-0/0/0 description \"uplink\"
set interfaces ge-0/0/0 mtu 9216
set interfaces ge-0/0/0 unit 0 family inet address 192.0.2.2/30
set protocols bgp group EBGP type external
set routing-options static route 0.0.0.0/0 next-hop 10.0.0.1
";
assert_eq!(detect_dialect(input), DialectHint::Named("junos".into()));
}
#[test]
fn detect_nxos() {
let input = "\
hostname n9k-leaf-01
!
feature bgp
feature interface-vlan
feature lacp
!
vlan 10
name SERVERS
!
interface Ethernet1/1
description uplink-spine-a
mtu 9216
ip address 192.0.2.2/31
no shutdown
!
router bgp 65001
router-id 10.255.255.1
";
assert_eq!(detect_dialect(input), DialectHint::Named("nxos".into()));
}
#[test]
fn detect_eos() {
let input = "\
hostname leaf-01
interface Ethernet1
description uplink-spine-a
mtu 9214
ip address 192.0.2.2/31
no shutdown
router bgp 65000
router-id 10.255.255.1
ip access-list ACL-EDGE-IN
10 permit tcp 10.10.1.0/24 any eq https
20 permit tcp 10.10.1.0/24 any eq ssh
90 deny ip any any log
";
assert_eq!(detect_dialect(input), DialectHint::Named("eos".into()));
}
#[test]
fn detect_iosxe() {
let input = "\
interface GigabitEthernet0/0/0
description uplink-core-a
mtu 9216
ip address 192.0.2.2 255.255.255.252
no shutdown
router bgp 65000
bgp log-neighbor-changes
address-family ipv4 unicast
network 10.10.1.0 mask 255.255.255.0
ip access-list extended ACL-EDGE-IN
permit tcp 10.10.1.0 0.0.0.255 any eq 443
deny ip any any log
";
assert_eq!(detect_dialect(input), DialectHint::Named("iosxe".into()));
}
#[test]
fn detect_generic_for_empty() {
assert_eq!(detect_dialect(""), DialectHint::Generic);
}
#[test]
fn detect_generic_for_plain_text() {
let input = "\
hostname router
# a comment
some random config line
";
assert_eq!(detect_dialect(input), DialectHint::Generic);
}
#[test]
fn detect_generic_when_ambiguous() {
let input = "\
set hostname myrouter
interface Ethernet1
";
assert_eq!(detect_dialect(input), DialectHint::Generic);
}
#[test]
fn detect_generic_on_exact_tie() {
let input = "\
feature ospf
ip access-list ACL-IN
10 permit tcp any any
";
assert_eq!(detect_dialect(input), DialectHint::Generic);
}
#[test]
fn detect_at_minimum_score_single_strong_signal() {
let input = "feature ospf\n";
assert_eq!(detect_dialect(input), DialectHint::Named("nxos".into()));
}
#[test]
fn detect_below_minimum_score_single_moderate_signal() {
let input = "role name admin\n";
assert_eq!(detect_dialect(input), DialectHint::Generic);
}
#[test]
fn detect_margin_exact_boundary_passes() {
let input = "\
ip access-list extended ACL-IN
permit tcp any 0.0.0.255 any
interface Ethernet1
";
assert_eq!(detect_dialect(input), DialectHint::Named("iosxe".into()));
}
#[test]
fn detect_margin_just_below_boundary_fails() {
let input = "\
ip access-list extended ACL-IN
interface Ethernet1
";
assert_eq!(detect_dialect(input), DialectHint::Generic);
}
#[test]
fn detect_clear_winner_no_runner_up() {
let input = "\
feature bgp
feature ospf
";
assert_eq!(detect_dialect(input), DialectHint::Named("nxos".into()));
}
#[test]
fn detect_strong_signal_drowned_by_cross_dialect_noise() {
let input = "\
feature ospf
interfaces {
mtu 9216;
}
";
assert_eq!(detect_dialect(input), DialectHint::Named("junos".into()));
}
#[test]
fn detect_two_moderate_signals_reach_margin() {
let input = "\
end
next
";
assert_eq!(detect_dialect(input), DialectHint::Named("fortios".into()));
}
#[test]
fn detect_only_weak_signals_below_threshold() {
let input = "\
mtu 9216;
description uplink;
";
assert_eq!(detect_dialect(input), DialectHint::Generic);
}
#[test]
fn detect_three_weak_signals_reach_threshold() {
let input = "\
mtu 9216;
description uplink;
no-readvertise;
";
assert_eq!(detect_dialect(input), DialectHint::Named("junos".into()));
}
#[test]
fn auto_parse_sets_dialect_hint() {
let input = "\
interfaces {
ge-0/0/0 {
mtu 9216;
}
}
";
let doc = auto_parse(input);
assert_eq!(
doc.metadata.dialect_hint,
DialectHint::Named("junos".into())
);
}
#[test]
fn auto_parse_generic_fallback() {
let doc = auto_parse("hostname router\n");
assert_eq!(doc.metadata.dialect_hint, DialectHint::Generic);
}
#[test]
fn auto_parse_preserves_content() {
let input = "interface Ethernet1\n description uplink\n";
let doc = auto_parse(input);
assert_eq!(doc.render(), input);
}
}