use std::collections::HashSet;
pub mod uri {
pub const BASE_1_0: &str = "urn:ietf:params:netconf:base:1.0";
pub const BASE_1_1: &str = "urn:ietf:params:netconf:base:1.1";
pub const CANDIDATE: &str = "urn:ietf:params:netconf:capability:candidate:1.0";
pub const CONFIRMED_COMMIT: &str = "urn:ietf:params:netconf:capability:confirmed-commit:1.0";
pub const CONFIRMED_COMMIT_1_1: &str = "urn:ietf:params:netconf:capability:confirmed-commit:1.1";
pub const VALIDATE: &str = "urn:ietf:params:netconf:capability:validate:1.0";
pub const VALIDATE_1_1: &str = "urn:ietf:params:netconf:capability:validate:1.1";
pub const STARTUP: &str = "urn:ietf:params:netconf:capability:startup:1.0";
pub const ROLLBACK_ON_ERROR: &str = "urn:ietf:params:netconf:capability:rollback-on-error:1.0";
pub const WRITABLE_RUNNING: &str = "urn:ietf:params:netconf:capability:writable-running:1.0";
pub const NOTIFICATION: &str = "urn:ietf:params:netconf:capability:notification:1.0";
pub const INTERLEAVE: &str = "urn:ietf:params:netconf:capability:interleave:1.0";
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NetconfVersion {
V1_0,
V1_1,
}
#[derive(Debug, Clone)]
pub struct Capabilities {
uris: HashSet<String>,
session_id: Option<u32>,
}
impl Capabilities {
pub fn new(uris: HashSet<String>, session_id: Option<u32>) -> Self {
Self { uris, session_id }
}
pub fn session_id(&self) -> Option<u32> {
self.session_id
}
pub fn supports(&self, capability_uri: &str) -> bool {
self.uris.iter().any(|uri| uri.starts_with(capability_uri))
}
pub fn negotiate_version(&self) -> Option<NetconfVersion> {
if self.supports(uri::BASE_1_1) {
return Some(NetconfVersion::V1_1);
}
if self.supports(uri::BASE_1_0) {
return Some(NetconfVersion::V1_0);
}
None
}
pub fn all_uris(&self) -> &HashSet<String> {
&self.uris
}
pub fn has_candidate(&self) -> bool {
self.supports(uri::CANDIDATE)
}
pub fn has_validate(&self) -> bool {
self.supports(uri::VALIDATE) || self.supports(uri::VALIDATE_1_1)
}
pub fn has_confirmed_commit(&self) -> bool {
self.supports(uri::CONFIRMED_COMMIT) || self.supports(uri::CONFIRMED_COMMIT_1_1)
}
pub fn has_notification(&self) -> bool {
self.supports(uri::NOTIFICATION)
}
pub fn has_interleave(&self) -> bool {
self.supports(uri::INTERLEAVE)
}
}
pub fn client_hello_xml() -> String {
format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<hello xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
<capabilities>
<capability>{}</capability>
<capability>{}</capability>
</capabilities>
</hello>"#,
uri::BASE_1_0,
uri::BASE_1_1,
)
}
pub fn parse_device_hello(xml: &str) -> Result<Capabilities, String> {
use quick_xml::events::Event;
use quick_xml::Reader;
let mut reader = Reader::from_str(xml);
let mut capabilities = HashSet::new();
let mut session_id: Option<u32> = None;
let mut in_capability = false;
let mut in_session_id = false;
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(ref tag)) | Ok(Event::Empty(ref tag)) => {
let local_name = tag.local_name();
if local_name.as_ref() == b"capability" {
in_capability = true;
} else if local_name.as_ref() == b"session-id" {
in_session_id = true;
}
}
Ok(Event::Text(ref text)) => {
if in_capability {
let cap_text = text.unescape().map_err(|e| e.to_string())?;
let trimmed = cap_text.trim().to_string();
if !trimmed.is_empty() {
capabilities.insert(trimmed);
}
} else if in_session_id {
let id_text = text.unescape().map_err(|e| e.to_string())?;
session_id = id_text.trim().parse().ok();
}
}
Ok(Event::End(ref tag)) => {
let local_name = tag.local_name();
if local_name.as_ref() == b"capability" {
in_capability = false;
} else if local_name.as_ref() == b"session-id" {
in_session_id = false;
}
}
Ok(Event::Eof) => break,
Err(e) => return Err(format!("XML parse error in hello: {e}")),
_ => {}
}
buf.clear();
}
if capabilities.is_empty() {
return Err("device hello contained no capabilities".to_string());
}
Ok(Capabilities::new(capabilities, session_id))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_device_hello_1_0_only() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<hello xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
<capabilities>
<capability>urn:ietf:params:netconf:base:1.0</capability>
<capability>urn:ietf:params:netconf:capability:candidate:1.0</capability>
</capabilities>
<session-id>42</session-id>
</hello>"#;
let caps = parse_device_hello(xml).unwrap();
assert_eq!(caps.negotiate_version(), Some(NetconfVersion::V1_0));
assert_eq!(caps.session_id(), Some(42));
assert!(caps.has_candidate());
assert!(!caps.has_validate());
}
#[test]
fn test_parse_device_hello_1_1() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<hello xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
<capabilities>
<capability>urn:ietf:params:netconf:base:1.0</capability>
<capability>urn:ietf:params:netconf:base:1.1</capability>
<capability>urn:ietf:params:netconf:capability:candidate:1.0</capability>
<capability>urn:ietf:params:netconf:capability:validate:1.1</capability>
</capabilities>
<session-id>99</session-id>
</hello>"#;
let caps = parse_device_hello(xml).unwrap();
assert_eq!(caps.negotiate_version(), Some(NetconfVersion::V1_1));
assert!(caps.has_candidate());
assert!(caps.has_validate());
}
#[test]
fn test_parse_device_hello_no_capabilities() {
let xml = r#"<hello xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
<capabilities></capabilities>
</hello>"#;
assert!(parse_device_hello(xml).is_err());
}
#[test]
fn test_parse_device_hello_no_base() {
let xml = r#"<hello xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
<capabilities>
<capability>urn:ietf:params:netconf:capability:candidate:1.0</capability>
</capabilities>
</hello>"#;
let caps = parse_device_hello(xml).unwrap();
assert_eq!(caps.negotiate_version(), None);
}
#[test]
fn test_client_hello_xml() {
let hello = client_hello_xml();
assert!(hello.contains(uri::BASE_1_0));
assert!(hello.contains(uri::BASE_1_1));
assert!(hello.contains("<hello"));
}
}