Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions crates/netconf-proto/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -755,10 +755,14 @@ impl<T: AsyncRead + AsyncWrite + Unpin> NetConfSshClient<T> {
if let Some(RpcResponse::WellKnown(WellKnownRpcResponse::Data(data))) =
rpc_reply.reply().responses()
{
trace!("[{}] Raw <get> response for filters: `{data}`", self.peer);

let mut reader = NsReader::from_str(data);
reader.config_mut().trim_text(true);
let mut parser = crate::xml_utils::XmlParser::new(reader)?;
let filters = Filters::xml_deserialize(&mut parser)?;

trace!("[{}] Parsed filters: {filters:?}", self.peer);
return Ok(filters);
}
Err(NetConfSshClientError::UnexpectedMessage {
Expand Down Expand Up @@ -792,6 +796,10 @@ impl<T: AsyncRead + AsyncWrite + Unpin> NetConfSshClient<T> {
if let Some(RpcResponse::WellKnown(WellKnownRpcResponse::Data(data))) =
rpc_reply.reply().responses()
{
trace!(
"[{}] Raw <get> response for subscription {id}: `{data}`",
self.peer
);
// Parse the response streams if any returned, filters if any returned
// and then the subscription details
let mut reader = NsReader::from_str(data);
Expand All @@ -801,6 +809,10 @@ impl<T: AsyncRead + AsyncWrite + Unpin> NetConfSshClient<T> {
{
parser.open(Some(SUBSCRIBED_NOTIFICATIONS_NS), "subscriptions")?;
let mut subscription = Subscription::xml_deserialize(&mut parser)?;
trace!(
"[{}] Parsed subscription {id} before target-by-reference resolution: {subscription:?}",
self.peer
);
if let Target::Datastore(datastore_target) = &mut subscription.target
&& let DatastoreSelectionFilterObjects::ByReference(name) =
&datastore_target.selection
Expand All @@ -818,6 +830,13 @@ impl<T: AsyncRead + AsyncWrite + Unpin> NetConfSshClient<T> {
}
}
parser.close()?;
trace!(
"[{}] Final subscription {id}: target={:?} module_version={:?} yang_library_content_id={:?}",
self.peer,
subscription.target,
subscription.module_version,
subscription.yang_library_content_id
);
subscription
} else {
return Err(NetConfSshClientError::ParsingError(
Expand Down
94 changes: 48 additions & 46 deletions crates/netconf-proto/src/xml_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -764,7 +764,7 @@ impl<'a, R: io::BufRead> XmlParser<'a, R> {
all_namespaces.insert(prefix, String::from_utf8_lossy(ns).into_owned());
}
let path = self.tag_string()?;
let used_namespaces = Self::find_xpath_prefixes(&path);
let used_namespaces = find_xpath_prefixes(&path);
let namespaces: IndexMap<String, String> = all_namespaces
.into_iter()
.filter(|(prefix, _)| used_namespaces.contains(prefix))
Expand Down Expand Up @@ -811,59 +811,61 @@ impl<'a, R: io::BufRead> XmlParser<'a, R> {
}
}
}
}

/// Find prefixes used within an Xpath expression
fn find_xpath_prefixes(xpath: &str) -> HashSet<String> {
let mut prefixes = HashSet::new();
let mut chars = xpath.char_indices().peekable();
let mut in_single = false;
let mut in_double = false;

while let Some((i, c)) = chars.next() {
// Skip over string literals — colons inside them aren't prefixes.
if in_single {
if c == '\'' {
in_single = false;
}
continue;
/// Find the prefixes used within an Xpath expression (e.g. the `if` in
/// `/if:interfaces/if:interface`). String literals are skipped and axis
/// specifiers (`child::`) are not treated as prefixes.
pub(crate) fn find_xpath_prefixes(xpath: &str) -> HashSet<String> {
let mut prefixes = HashSet::new();
let mut chars = xpath.char_indices().peekable();
let mut in_single = false;
let mut in_double = false;

while let Some((i, c)) = chars.next() {
// Skip over string literals — colons inside them aren't prefixes.
if in_single {
if c == '\'' {
in_single = false;
}
if in_double {
if c == '"' {
in_double = false;
}
continue;
continue;
}
if in_double {
if c == '"' {
in_double = false;
}
match c {
'\'' => in_single = true,
'"' => in_double = true,
c if c.is_ascii_alphabetic() || c == '_' => {
let start = i;
let mut end = i + c.len_utf8();
while let Some(&(_, nc)) = chars.peek() {
if nc.is_ascii_alphanumeric() || nc == '_' || nc == '-' || nc == '.' {
chars.next();
end += nc.len_utf8();
} else {
break;
}
continue;
}
match c {
'\'' => in_single = true,
'"' => in_double = true,
c if c.is_ascii_alphabetic() || c == '_' => {
let start = i;
let mut end = i + c.len_utf8();
while let Some(&(_, nc)) = chars.peek() {
if nc.is_ascii_alphanumeric() || nc == '_' || nc == '-' || nc == '.' {
Comment thread
rodonile marked this conversation as resolved.
chars.next();
end += nc.len_utf8();
} else {
break;
}
// A prefix is an NCName followed by exactly one ':'
// (two colons = axis specifier like `child::`).
if let Some(&(_, ':')) = chars.peek() {
let mut look = chars.clone();
look.next();
let is_axis = matches!(look.peek(), Some(&(_, ':')));
if !is_axis {
prefixes.insert(xpath[start..end].to_string());
chars.next(); // consume the ':'
}
}
// A prefix is an NCName followed by exactly one ':'
// (two colons = axis specifier like `child::`).
if let Some(&(_, ':')) = chars.peek() {
let mut look = chars.clone();
look.next();
let is_axis = matches!(look.peek(), Some(&(_, ':')));
if !is_axis {
prefixes.insert(xpath[start..end].to_string());
chars.next(); // consume the ':'
}
}
_ => {}
}
_ => {}
}
prefixes
}
prefixes
}

/// Format a `DateTime<Utc>` as YANG `date-and-time` (RFC 3339, UTC).
Expand Down Expand Up @@ -1473,7 +1475,7 @@ mod tests {

fn assert_prefixes(expr: &str, expected: HashSet<String>) {
assert_eq!(
XmlParser::<io::Cursor<&[u8]>>::find_xpath_prefixes(expr),
find_xpath_prefixes(expr),
expected,
"unexpected prefix set for: {expr}"
);
Expand Down
14 changes: 14 additions & 0 deletions crates/netconf-proto/src/yang_push/filters.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Copyright (C) 2026-present The NetCalyx Authors.
// Copyright (C) 2026-present The NetGauze Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
Expand Down Expand Up @@ -353,6 +354,19 @@ pub struct DatastoreXPathFilter {
pub path: Box<str>,
}

impl DatastoreXPathFilter {
/// Prefixes used in the xpath `path` (e.g. the `if` in
/// `/if:interfaces/if:interface`), sorted for determinism. Axis specifiers
/// and string literals are not treated as prefixes.
pub fn path_prefixes(&self) -> Vec<String> {
let mut prefixes: Vec<String> = crate::xml_utils::find_xpath_prefixes(&self.path)
.into_iter()
.collect();
prefixes.sort_unstable();
prefixes
}
}

impl XmlSerialize for DatastoreXPathFilter {
fn xml_serialize<T: io::Write>(
&self,
Expand Down
28 changes: 28 additions & 0 deletions crates/netconf-proto/src/yang_push/tests.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Copyright (C) 2026-present The NetCalyx Authors.
// Copyright (C) 2026-present The NetGauze Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
Expand Down Expand Up @@ -782,3 +783,30 @@ fn test_yang_push_module_version_json_serde() {
let deserialized: YangPushModuleVersion = serde_json::from_value(json_value).unwrap();
assert_eq!(deserialized, modeled);
}

#[test]
fn test_datastore_xpath_filter_path_prefixes() {
// Cisco IOS-XR: prefix equals the module name, no xmlns binding declared.
let cisco = DatastoreXPathFilter {
namespaces: Box::new([]),
path: "Cisco-IOS-XR-procmem-oper:processes-memory/nodes/node/process-ids/process-id".into(),
};
assert_eq!(cisco.path_prefixes(), vec!["Cisco-IOS-XR-procmem-oper"]);

// Multi-module xpath (Huawei-style), distinct prefixes (sorted).
let multi = DatastoreXPathFilter {
namespaces: Box::new([
("devm".into(), "urn:huawei:yang:huawei-devm".into()),
("driver".into(), "urn:huawei:yang:huawei-driver".into()),
]),
path: "/devm:devm/devm:chassiss/devm:chassis/driver:power-supply-attribute".into(),
};
assert_eq!(multi.path_prefixes(), vec!["devm", "driver"]);

// Unprefixed (default-namespace) steps contribute no prefixes.
let unprefixed = DatastoreXPathFilter {
namespaces: Box::new([]),
path: "/interfaces/interface/state/counters".into(),
};
assert!(unprefixed.path_prefixes().is_empty());
}
48 changes: 48 additions & 0 deletions crates/netconf-proto/src/yanglib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
// Copyright (C) 2026-present The NetCalyx Authors.
// Copyright (C) 2025-present The NetGauze Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
// implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use crate::xml_utils::{ParsingError, XmlDeserialize, XmlParser, XmlSerialize, XmlWriter};
use crate::yangparser::{YangDependencies, extract_yang_dependencies};
use crate::{YANG_DATASTORES_NS_STR, YANG_LIBRARY_AUGMENTED_BY_NS, YANG_LIBRARY_NS};
Expand Down Expand Up @@ -110,6 +126,14 @@ impl YangLibrary {
None
}

/// Find a module by namespace, scoped to the module sets referenced by
/// `datastore_name`'s schema (RFC 8525 datastore -> schema -> module-set).
///
/// Unlike [Self::find_module], this only considers module sets reachable
/// from the given datastore, since different datastores can be backed by
/// different schemas/module-sets and may pin different revisions of the
/// same module. Returns `None` if the datastore, its schema, or a
/// matching module cannot be found.
pub fn find_module_by_datastore_and_ns(
&self,
datastore_name: &DatastoreName,
Expand All @@ -130,6 +154,30 @@ impl YangLibrary {
None
}

/// Find a module by name, scoped to the module sets referenced by
/// `datastore_name`'s schema (RFC 8525 datastore -> schema -> module-set).
///
/// Unlike [Self::find_module], this only considers module sets reachable
/// from the given datastore, since different datastores can be backed by
/// different schemas/module-sets and may pin different revisions of the
/// same module name. Returns `None` if the datastore, its schema, or a
/// matching module cannot be found.
pub fn find_module_by_datastore_and_name(
&self,
datastore_name: &DatastoreName,
name: &str,
) -> Option<&Module> {
let datastore = self.datastores().get(datastore_name)?;
let schema = self.schemas().get(datastore.schema())?;
for module_set_name in schema.modules_sets() {
let module_set = self.module_sets().get(module_set_name)?;
if let Some(module) = module_set.modules().get(name) {
return Some(module);
}
}
None
}

/// Register the YANG Lib to in the Confluent Schema Registry.
///
/// `root_schema_name` is the name of the root module to register.
Expand Down
Loading