netget/server/restconf/
path.rs1use anyhow::{bail, ensure, Result};
4use serde_json::{json, Value as Json};
5
6pub const MAX_SEGMENTS: usize = 64;
7pub const MAX_PATH: usize = 4096;
8
9#[derive(Debug, Clone, PartialEq)]
10pub struct Segment {
11 pub module: Option<String>,
12 pub name: String,
13 pub keys: Option<Vec<String>>,
14}
15
16fn identifier(s: &str) -> bool {
17 !s.is_empty()
18 && s.len() <= 128
19 && s.chars()
20 .next()
21 .is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
22 && s.chars()
23 .all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | '.'))
24}
25
26fn decode(s: &str) -> Result<String> {
27 let bytes = s.as_bytes();
28 let mut out = Vec::with_capacity(bytes.len());
29 let mut i = 0;
30 while i < bytes.len() {
31 if bytes[i] == b'%' {
32 ensure!(i + 2 < bytes.len(), "a truncated percent escape");
33 let hex = std::str::from_utf8(&bytes[i + 1..i + 3])?;
34 out.push(
35 u8::from_str_radix(hex, 16).map_err(|_| anyhow::anyhow!("a bad percent escape"))?,
36 );
37 i += 3;
38 } else {
39 out.push(bytes[i]);
40 i += 1;
41 }
42 }
43 Ok(String::from_utf8(out)?)
44}
45
46pub fn parse(path: &str) -> Result<Vec<Segment>> {
48 ensure!(
49 path.len() <= MAX_PATH,
50 "the path is longer than {MAX_PATH} bytes"
51 );
52 if path.is_empty() {
53 return Ok(vec![]);
54 }
55 let mut out = Vec::new();
56 for raw in path.split('/') {
57 ensure!(
58 out.len() < MAX_SEGMENTS,
59 "more than {MAX_SEGMENTS} path segments"
60 );
61 ensure!(!raw.is_empty(), "an empty path segment");
62 let (ident, keys) = match raw.split_once('=') {
63 Some((i, k)) => (
64 i,
65 Some(k.split(',').map(decode).collect::<Result<Vec<_>>>()?),
66 ),
67 None => (raw, None),
68 };
69 let (module, name) = match ident.split_once(':') {
70 Some((m, n)) => (Some(m.to_owned()), n.to_owned()),
71 None => (None, ident.to_owned()),
72 };
73 if let Some(m) = &module {
74 ensure!(identifier(m), "{m:?} is not a module name");
75 } else if out.is_empty() {
76 bail!("the first path segment names its module, e.g. example:{ident}");
77 }
78 let module_root =
81 name.is_empty() && module.is_some() && keys.is_none() && path.split('/').count() == 1;
82 ensure!(
83 module_root || identifier(&name),
84 "{name:?} is not a node name"
85 );
86 out.push(Segment { module, name, keys });
87 }
88 Ok(out)
89}
90
91pub fn to_json(segments: &[Segment]) -> Json {
92 json!(segments
93 .iter()
94 .map(|s| {
95 let mut v = json!({"name": s.name});
96 if let Some(m) = &s.module {
97 v["module"] = json!(m);
98 }
99 if let Some(k) = &s.keys {
100 v["keys"] = json!(k);
101 }
102 v
103 })
104 .collect::<Vec<_>>())
105}
106
107pub fn target_module(segments: &[Segment]) -> Option<&str> {
109 segments.iter().rev().find_map(|s| s.module.as_deref())
110}
111
112pub fn encode_key(k: &str) -> String {
114 k.bytes()
115 .map(|b| {
116 if b.is_ascii_alphanumeric() || b"-._~".contains(&b) {
117 (b as char).to_string()
118 } else {
119 format!("%{b:02X}")
120 }
121 })
122 .collect()
123}