Skip to main content

netconv_core/
ir.rs

1use ipnet::IpNet;
2use serde::{Deserialize, Serialize};
3use std::net::IpAddr;
4
5// ---------------------------------------------------------------------------
6// Confidence — каждый элемент IR знает насколько точно он был распознан
7// ---------------------------------------------------------------------------
8
9#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
10pub enum Confidence {
11    /// Точное соответствие — гарантировано корректная конвертация
12    Exact,
13    /// Есть аналог, но с нюансами — объясняем в репорте
14    Approximate { note: String },
15    /// Нет аналога на целевой платформе — требует ручного решения
16    Manual { reason: String },
17    /// Парсер не распознал команду — сохраняем raw текст
18    Unknown { raw: String },
19}
20
21// ---------------------------------------------------------------------------
22// NetworkConfig — корневой объект IR
23// ---------------------------------------------------------------------------
24
25#[derive(Debug, Clone, Default, Serialize, Deserialize)]
26pub struct NetworkConfig {
27    pub hostname: Option<String>,
28    pub domain_name: Option<String>,
29    pub interfaces: Vec<Interface>,
30    pub vlans: Vec<Vlan>,
31    pub routing: RoutingConfig,
32    pub acls: Vec<Acl>,
33    pub nat: Vec<NatRule>,
34    pub ntp: Vec<NtpServer>,
35    pub dns: Vec<IpAddr>,
36    pub snmp: Option<SnmpConfig>,
37    pub aaa: Option<AaaConfig>,
38    pub stp: Option<GlobalStp>,
39    pub logging: Option<LoggingConfig>,
40    pub users: Vec<LocalUser>,
41    pub line_vty: Option<LineVty>,
42    pub ssh: Option<SshConfig>,
43    pub banner: Option<String>,
44    /// Всё что парсер не распознал — не выбрасываем, храним
45    pub unknown_blocks: Vec<UnknownBlock>,
46    /// Платформо-специфичные команды без аналога
47    pub platform_specific: Vec<UnknownBlock>,
48}
49
50// ---------------------------------------------------------------------------
51// Global STP
52// ---------------------------------------------------------------------------
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct GlobalStp {
56    pub mode: StpMode,
57    pub loopguard: bool,
58    pub portfast_default: bool,
59    pub bpduguard_default: bool,
60    pub vlan_priorities: Vec<StpVlanPriority>,
61}
62
63#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
64pub enum StpMode {
65    RapidPvst,
66    Pvst,
67    Mst,
68    Rstp,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct StpVlanPriority {
73    pub vlans: Vec<u16>,
74    pub priority: u32,
75}
76
77// ---------------------------------------------------------------------------
78// Logging
79// ---------------------------------------------------------------------------
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct LoggingConfig {
83    pub buffered_size: Option<u32>,
84    pub console_level: Option<String>,
85    pub hosts: Vec<std::net::IpAddr>,
86}
87
88// ---------------------------------------------------------------------------
89// Local users
90// ---------------------------------------------------------------------------
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct LocalUser {
94    pub name: String,
95    pub privilege: u8,
96    pub password_type: PasswordType,
97    pub password_hash: String,
98}
99
100#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
101pub enum PasswordType {
102    Plaintext,
103    Type7,  // Cisco Type 7 (reversible)
104    Md5,    // enable secret 5
105    Scrypt, // enable secret 9
106}
107
108// ---------------------------------------------------------------------------
109// Line VTY
110// ---------------------------------------------------------------------------
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct LineVty {
114    pub exec_timeout_min: u32,
115    pub exec_timeout_sec: u32,
116    pub transport_input: Vec<String>,
117    pub logging_synchronous: bool,
118}
119
120// ---------------------------------------------------------------------------
121// SSH
122// ---------------------------------------------------------------------------
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct SshConfig {
126    pub version: u8,
127    pub timeout: Option<u32>,
128    pub retries: Option<u8>,
129}
130
131// ---------------------------------------------------------------------------
132// Interface
133// ---------------------------------------------------------------------------
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct Interface {
137    /// Нормализованное имя: "GigabitEthernet0/0" → "GigabitEthernet0/0"
138    /// Рендерер сам адаптирует под целевой вендор
139    pub name: InterfaceName,
140    pub description: Option<String>,
141    pub addresses: Vec<IpAddress>,
142    pub shutdown: bool,
143    pub mtu: Option<u32>,
144    pub speed: Option<InterfaceSpeed>,
145    pub duplex: Option<Duplex>,
146    pub l2: Option<L2Config>,
147    pub helper_addresses: Vec<IpAddr>,
148    pub acl_in: Option<String>,
149    pub acl_out: Option<String>,
150    pub nat_direction: Option<NatDirection>,
151    pub hsrp: Vec<HsrpGroup>,
152    pub ospf: Option<InterfaceOspf>,
153    pub voice_vlan: Option<u16>,
154    pub storm_control: Option<StormControl>,
155    pub stp: InterfaceStp,
156    pub confidence: Confidence,
157}
158
159#[derive(Debug, Clone, Default, Serialize, Deserialize)]
160pub struct StormControl {
161    pub broadcast_level: Option<f32>,
162    pub multicast_level: Option<f32>,
163    pub unicast_level: Option<f32>,
164}
165
166#[derive(Debug, Clone, Default, Serialize, Deserialize)]
167pub struct InterfaceStp {
168    pub portfast: bool,
169    pub bpduguard: bool,
170    pub bpdufilter: bool,
171    pub guard_root: bool,
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct InterfaceName {
176    pub kind: InterfaceKind,
177    /// Слот/порт как строка — "0/0", "0/0/0", "1" и т.д.
178    pub id: String,
179    /// Оригинальное имя из конфига — для репорта
180    pub original: String,
181}
182
183#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
184pub enum InterfaceKind {
185    GigabitEthernet,
186    FastEthernet,
187    TenGigabitEthernet,
188    Loopback,
189    Vlan,
190    Tunnel,
191    Serial,
192    BundleEther,
193    Management,
194    Unknown(String),
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize)]
198pub struct IpAddress {
199    pub prefix: IpNet,
200    pub secondary: bool,
201}
202
203#[derive(Debug, Clone, Serialize, Deserialize)]
204pub enum InterfaceSpeed {
205    Mbps(u32),
206    Auto,
207}
208
209#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
210pub enum Duplex {
211    Full,
212    Half,
213    Auto,
214}
215
216#[derive(Debug, Clone, Serialize, Deserialize)]
217pub struct L2Config {
218    pub mode: L2Mode,
219    pub access_vlan: Option<u16>,
220    pub trunk_allowed: Option<Vec<u16>>,
221    pub trunk_native: Option<u16>,
222}
223
224#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
225pub enum L2Mode {
226    Access,
227    Trunk,
228}
229
230#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
231pub enum NatDirection {
232    Inside,
233    Outside,
234}
235
236// ---------------------------------------------------------------------------
237// HSRP → будет рендериться как VRRP на Huawei (Approximate)
238// ---------------------------------------------------------------------------
239
240#[derive(Debug, Clone, Serialize, Deserialize)]
241pub struct HsrpGroup {
242    pub group_id: u16,
243    pub virtual_ip: IpAddr,
244    pub priority: Option<u16>,
245    pub preempt: bool,
246    pub preempt_delay: Option<u32>,
247    pub timers: Option<HsrpTimers>,
248    pub track: Vec<HsrpTrack>,
249}
250
251#[derive(Debug, Clone, Serialize, Deserialize)]
252pub struct HsrpTimers {
253    pub hello_ms: u32,
254    pub hold_ms: u32,
255}
256
257#[derive(Debug, Clone, Serialize, Deserialize)]
258pub struct HsrpTrack {
259    pub object: u32,
260    pub decrement: u16,
261}
262
263#[derive(Debug, Clone, Serialize, Deserialize)]
264pub struct InterfaceOspf {
265    pub process_id: u32,
266    pub area: OspfArea,
267    pub cost: Option<u32>,
268    pub priority: Option<u8>,
269    pub timers: Option<OspfIfTimers>,
270    pub auth: Option<OspfAuth>,
271    pub passive: bool,
272    pub network_type: Option<OspfNetworkType>,
273}
274
275#[derive(Debug, Clone, Serialize, Deserialize)]
276pub struct OspfIfTimers {
277    pub hello_interval: u32,
278    pub dead_interval: u32,
279}
280
281// ---------------------------------------------------------------------------
282// VLAN
283// ---------------------------------------------------------------------------
284
285#[derive(Debug, Clone, Serialize, Deserialize)]
286pub struct Vlan {
287    pub id: u16,
288    pub name: Option<String>,
289    pub active: bool,
290}
291
292// ---------------------------------------------------------------------------
293// Routing
294// ---------------------------------------------------------------------------
295
296#[derive(Debug, Clone, Default, Serialize, Deserialize)]
297pub struct RoutingConfig {
298    pub static_routes: Vec<StaticRoute>,
299    pub ospf: Vec<OspfProcess>,
300    pub bgp: Option<BgpConfig>,
301    pub eigrp: Vec<EigrpProcess>,
302}
303
304#[derive(Debug, Clone, Serialize, Deserialize)]
305pub struct StaticRoute {
306    pub prefix: IpNet,
307    pub next_hop: NextHop,
308    pub distance: Option<u8>,
309    pub tag: Option<u32>,
310    pub name: Option<String>,
311    pub permanent: bool,
312}
313
314#[derive(Debug, Clone, Serialize, Deserialize)]
315pub enum NextHop {
316    Ip(IpAddr),
317    Interface(String),
318    IpAndInterface(IpAddr, String),
319    Null0,
320}
321
322#[derive(Debug, Clone, Serialize, Deserialize)]
323pub struct OspfProcess {
324    pub process_id: u32,
325    pub router_id: Option<IpAddr>,
326    pub areas: Vec<OspfAreaConfig>,
327    pub passive_interfaces: Vec<String>,
328    pub default_originate: Option<OspfDefaultOriginate>,
329    pub redistribute: Vec<OspfRedistribute>,
330    pub max_metric: bool,
331    pub auth: Option<OspfAuth>,
332    pub log_adjacency: bool,
333}
334
335#[derive(Debug, Clone, Serialize, Deserialize)]
336pub struct OspfAreaConfig {
337    pub area: OspfArea,
338    pub networks: Vec<OspfNetwork>,
339    pub area_type: OspfAreaType,
340    pub auth: Option<OspfAuth>,
341}
342
343#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
344pub enum OspfArea {
345    Backbone, // area 0
346    Normal(u32),
347    IpFormat(IpAddr), // area 0.0.0.1 формат
348}
349
350#[derive(Debug, Clone, Serialize, Deserialize)]
351pub struct OspfNetwork {
352    pub prefix: IpNet,
353    pub wildcard: bool, // если true — это wildcard маска, не prefix length
354}
355
356#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
357pub enum OspfAreaType {
358    Normal,
359    Stub,
360    StubNoSummary,
361    Nssa,
362    NssaNoSummary,
363}
364
365#[derive(Debug, Clone, Serialize, Deserialize)]
366pub enum OspfAuth {
367    Simple(String),
368    Md5 { key_id: u8, key: String },
369}
370
371#[derive(Debug, Clone, Serialize, Deserialize)]
372pub enum OspfNetworkType {
373    Broadcast,
374    PointToPoint,
375    PointToMultipoint,
376    NonBroadcast,
377}
378
379#[derive(Debug, Clone, Serialize, Deserialize)]
380pub struct OspfDefaultOriginate {
381    pub always: bool,
382    pub metric: Option<u32>,
383    pub metric_type: Option<u8>,
384}
385
386#[derive(Debug, Clone, Serialize, Deserialize)]
387pub struct OspfRedistribute {
388    pub source: RedistributeSource,
389    pub metric: Option<u32>,
390    pub metric_type: Option<u8>,
391    pub subnets: bool,
392    pub tag: Option<u32>,
393    pub route_map: Option<String>,
394}
395
396#[derive(Debug, Clone, Serialize, Deserialize)]
397pub enum RedistributeSource {
398    Connected,
399    Static,
400    Bgp(u32),
401    Eigrp(u32),
402    Rip,
403}
404
405#[derive(Debug, Clone, Serialize, Deserialize)]
406pub struct BgpConfig {
407    pub asn: u32,
408    pub router_id: Option<IpAddr>,
409    pub neighbors: Vec<BgpNeighbor>,
410    pub peer_groups: Vec<BgpPeerGroup>,
411    /// Сети из глобального контекста (без address-family)
412    pub networks: Vec<IpNet>,
413    /// address-family блоки
414    pub address_families: Vec<BgpAddressFamily>,
415    pub redistribute: Vec<OspfRedistribute>,
416    pub log_neighbor_changes: bool,
417    pub bestpath: Option<String>,
418}
419
420#[derive(Debug, Clone, Serialize, Deserialize)]
421pub struct BgpNeighbor {
422    pub address: BgpNeighborAddr,
423    pub remote_as: u32,
424    pub description: Option<String>,
425    pub update_source: Option<String>,
426    pub next_hop_self: bool,
427    pub password: Option<String>,
428    pub shutdown: bool,
429    pub peer_group: Option<String>,
430    pub route_map_in: Option<String>,
431    pub route_map_out: Option<String>,
432    pub prefix_list_in: Option<String>,
433    pub prefix_list_out: Option<String>,
434    pub soft_reconfiguration: bool,
435    pub send_community: bool,
436    pub remove_private_as: bool,
437    pub default_originate: bool,
438    pub activate: bool,
439}
440
441/// Адрес соседа — IP или имя peer-group
442#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
443pub enum BgpNeighborAddr {
444    Ip(IpAddr),
445    PeerGroup(String),
446}
447
448impl std::fmt::Display for BgpNeighborAddr {
449    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
450        match self {
451            BgpNeighborAddr::Ip(ip) => write!(f, "{}", ip),
452            BgpNeighborAddr::PeerGroup(name) => write!(f, "{}", name),
453        }
454    }
455}
456
457#[derive(Debug, Clone, Serialize, Deserialize)]
458pub struct BgpPeerGroup {
459    pub name: String,
460    pub remote_as: Option<u32>,
461    pub update_source: Option<String>,
462    pub next_hop_self: bool,
463    pub route_map_in: Option<String>,
464    pub route_map_out: Option<String>,
465    pub send_community: bool,
466}
467
468/// address-family ipv4/ipv6/vpnv4 unicast/multicast
469#[derive(Debug, Clone, Serialize, Deserialize)]
470pub struct BgpAddressFamily {
471    pub afi: BgpAfi,
472    pub safi: BgpSafi,
473    pub networks: Vec<IpNet>,
474    pub redistribute: Vec<OspfRedistribute>,
475    /// neighbor activate / no neighbor activate
476    pub activated_neighbors: Vec<BgpNeighborAddr>,
477    pub deactivated_neighbors: Vec<BgpNeighborAddr>,
478    pub neighbor_settings: Vec<BgpNeighbor>,
479    pub default_information: bool,
480    pub aggregate_addresses: Vec<BgpAggregate>,
481}
482
483#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
484pub enum BgpAfi {
485    Ipv4,
486    Ipv6,
487    Vpnv4,
488    L2vpn,
489}
490
491#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
492pub enum BgpSafi {
493    Unicast,
494    Multicast,
495    Labeled,
496    Evpn,
497}
498
499#[derive(Debug, Clone, Serialize, Deserialize)]
500pub struct BgpAggregate {
501    pub prefix: IpNet,
502    pub summary_only: bool,
503    pub as_set: bool,
504}
505
506#[derive(Debug, Clone, Serialize, Deserialize)]
507pub struct EigrpProcess {
508    pub asn: u32,
509    pub networks: Vec<OspfNetwork>,
510    pub passive_interfaces: Vec<String>,
511    pub redistribute: Vec<OspfRedistribute>,
512}
513
514// ---------------------------------------------------------------------------
515// ACL
516// ---------------------------------------------------------------------------
517
518#[derive(Debug, Clone, Serialize, Deserialize)]
519pub struct Acl {
520    pub name: AclName,
521    pub acl_type: AclType,
522    pub entries: Vec<AclEntry>,
523}
524
525#[derive(Debug, Clone, Serialize, Deserialize)]
526pub enum AclName {
527    Named(String),
528    Numbered(u32),
529}
530
531#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
532pub enum AclType {
533    Standard, // только src IP
534    Extended, // src+dst+proto+port
535}
536
537#[derive(Debug, Clone, Serialize, Deserialize)]
538pub struct AclEntry {
539    pub sequence: Option<u32>,
540    pub action: AclAction,
541    pub protocol: Option<AclProtocol>,
542    pub src: AclMatch,
543    pub dst: Option<AclMatch>,
544    pub src_port: Option<AclPort>,
545    pub dst_port: Option<AclPort>,
546    pub established: bool,
547    pub log: bool,
548    pub remark: Option<String>,
549}
550
551#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
552pub enum AclAction {
553    Permit,
554    Deny,
555}
556
557#[derive(Debug, Clone, Serialize, Deserialize)]
558pub enum AclProtocol {
559    Ip,
560    Tcp,
561    Udp,
562    Icmp,
563    Esp,
564    Ahp,
565    Number(u8),
566}
567
568#[derive(Debug, Clone, Serialize, Deserialize)]
569pub enum AclMatch {
570    Any,
571    Host(IpAddr),
572    Network { addr: IpAddr, wildcard: IpAddr },
573    Prefix(IpNet),
574}
575
576#[derive(Debug, Clone, Serialize, Deserialize)]
577pub enum AclPort {
578    Eq(u16),
579    Ne(u16),
580    Lt(u16),
581    Gt(u16),
582    Range(u16, u16),
583}
584
585// ---------------------------------------------------------------------------
586// NAT
587// ---------------------------------------------------------------------------
588
589#[derive(Debug, Clone, Serialize, Deserialize)]
590pub struct NatRule {
591    pub rule_type: NatType,
592    pub acl: Option<String>,
593    pub pool: Option<NatPool>,
594    pub interface_overload: bool,
595    pub static_entry: Option<NatStaticEntry>,
596}
597
598#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
599pub enum NatType {
600    Dynamic,  // source list ACL pool
601    Overload, // PAT
602    Static,   // фиксированный маппинг
603}
604
605#[derive(Debug, Clone, Serialize, Deserialize)]
606pub struct NatPool {
607    pub name: String,
608    pub start: IpAddr,
609    pub end: IpAddr,
610    pub prefix: Option<IpNet>,
611    pub overload: bool,
612}
613
614#[derive(Debug, Clone, Serialize, Deserialize)]
615pub struct NatStaticEntry {
616    pub local: IpAddr,
617    pub global: IpAddr,
618    pub local_port: Option<u16>,
619    pub global_port: Option<u16>,
620    pub protocol: Option<AclProtocol>,
621}
622
623// ---------------------------------------------------------------------------
624// NTP / DNS / SNMP / AAA
625// ---------------------------------------------------------------------------
626
627#[derive(Debug, Clone, Serialize, Deserialize)]
628pub struct NtpServer {
629    pub address: IpAddr,
630    pub prefer: bool,
631    pub key: Option<u32>,
632    pub source_interface: Option<String>,
633}
634
635#[derive(Debug, Clone, Serialize, Deserialize)]
636pub struct SnmpConfig {
637    pub communities: Vec<SnmpCommunity>,
638    pub location: Option<String>,
639    pub contact: Option<String>,
640    pub traps: Vec<String>,
641}
642
643#[derive(Debug, Clone, Serialize, Deserialize)]
644pub struct SnmpCommunity {
645    pub name: String,
646    pub access: SnmpAccess,
647    pub acl: Option<String>,
648}
649
650#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
651pub enum SnmpAccess {
652    Ro,
653    Rw,
654}
655
656#[derive(Debug, Clone, Serialize, Deserialize)]
657pub struct AaaConfig {
658    pub new_model: bool,
659    pub authentication: Vec<AaaMethod>,
660    pub authorization: Vec<AaaMethod>,
661}
662
663#[derive(Debug, Clone, Serialize, Deserialize)]
664pub struct AaaMethod {
665    pub list_name: String,
666    pub methods: Vec<String>,
667}
668
669// ---------------------------------------------------------------------------
670// UnknownBlock — всё нераспознанное, с контекстом
671// ---------------------------------------------------------------------------
672
673#[derive(Debug, Clone, Serialize, Deserialize)]
674pub struct UnknownBlock {
675    /// Строка в исходном конфиге
676    pub line: usize,
677    /// Контекст: в каком блоке находилась команда
678    pub context: String,
679    /// Оригинальный текст
680    pub raw: String,
681}
682
683// ---------------------------------------------------------------------------
684// Вспомогательные типы
685// ---------------------------------------------------------------------------
686
687pub type ProcessId = u32;
688
689impl Default for Interface {
690    fn default() -> Self {
691        Interface {
692            name: InterfaceName {
693                kind: InterfaceKind::Unknown(String::new()),
694                id: String::new(),
695                original: String::new(),
696            },
697            description: None,
698            addresses: vec![],
699            shutdown: false,
700            mtu: None,
701            speed: None,
702            duplex: None,
703            l2: None,
704            helper_addresses: vec![],
705            acl_in: None,
706            acl_out: None,
707            nat_direction: None,
708            hsrp: vec![],
709            ospf: None,
710            voice_vlan: None,
711            storm_control: None,
712            stp: InterfaceStp::default(),
713            confidence: Confidence::Exact,
714        }
715    }
716}
717
718impl InterfaceName {
719    pub fn parse(raw: &str) -> Self {
720        let original = raw.to_string();
721
722        // Нормализуем сокращения Cisco
723        let expanded = expand_interface_name(raw);
724
725        // Разбиваем на kind + id
726        let (kind_str, id) = split_interface_name(&expanded);
727
728        let kind = match kind_str.to_lowercase().as_str() {
729            "gigabitethernet" => InterfaceKind::GigabitEthernet,
730            "fastethernet" => InterfaceKind::FastEthernet,
731            "tengigabitethernet" | "tengige" => InterfaceKind::TenGigabitEthernet,
732            "loopback" => InterfaceKind::Loopback,
733            "vlan" => InterfaceKind::Vlan,
734            "tunnel" => InterfaceKind::Tunnel,
735            "serial" => InterfaceKind::Serial,
736            "bundle-ether" | "bundleether" => InterfaceKind::BundleEther,
737            "management" => InterfaceKind::Management,
738            other => InterfaceKind::Unknown(other.to_string()),
739        };
740
741        InterfaceName { kind, id, original }
742    }
743}
744
745fn expand_interface_name(raw: &str) -> String {
746    // Cisco сокращения → полные имена
747    let prefixes = [
748        ("Gi", "GigabitEthernet"),
749        ("Fa", "FastEthernet"),
750        ("Te", "TenGigabitEthernet"),
751        ("Lo", "Loopback"),
752        ("Tu", "Tunnel"),
753        ("Se", "Serial"),
754        ("Vl", "Vlan"),
755        ("Mg", "Management"),
756    ];
757
758    for (short, full) in &prefixes {
759        if raw.starts_with(short) && !raw.to_lowercase().starts_with(&full.to_lowercase()) {
760            return format!("{}{}", full, &raw[short.len()..]);
761        }
762    }
763    raw.to_string()
764}
765
766fn split_interface_name(name: &str) -> (&str, String) {
767    // Ищем первую цифру — всё до неё это тип, после — id
768    let pos = name.find(|c: char| c.is_ascii_digit());
769    match pos {
770        Some(i) => (&name[..i], name[i..].to_string()),
771        None => (name, String::new()),
772    }
773}
774
775impl OspfArea {
776    pub fn parse(s: &str) -> Self {
777        if let Ok(n) = s.parse::<u32>() {
778            if n == 0 {
779                OspfArea::Backbone
780            } else {
781                OspfArea::Normal(n)
782            }
783        } else if let Ok(ip) = s.parse::<IpAddr>() {
784            if ip == "0.0.0.0".parse::<IpAddr>().unwrap() {
785                OspfArea::Backbone
786            } else {
787                OspfArea::IpFormat(ip)
788            }
789        } else {
790            OspfArea::Normal(0) // fallback
791        }
792    }
793}