1use super::schema::{self, ResourceType};
5use anyhow::{bail, ensure, Context, Result};
6use serde_json::{json, Map, Value};
7use std::cmp::Ordering;
8
9pub const MAX_FILTER_BYTES: usize = 4096;
10const MAX_DEPTH: usize = 16;
11const MAX_NODES: usize = 128;
12
13#[derive(Debug, Clone, PartialEq)]
15pub struct AttrPath {
16 pub urn: Option<String>,
17 pub attr: String,
18 pub sub: Option<String>,
19}
20
21fn name_ok(s: &str) -> bool {
22 let mut c = s.chars();
23 c.next()
24 .is_some_and(|f| f.is_ascii_alphabetic() || f == '$')
25 && c.all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '-' || ch == '$')
26 && s.len() <= 128
27}
28
29impl AttrPath {
30 pub fn parse(s: &str) -> Result<Self> {
31 ensure!(
32 !s.is_empty() && s.len() <= 512,
33 "empty or overlong attribute path"
34 );
35 if s.contains(':') && schema::schema(s).is_some() {
37 return Ok(Self {
38 urn: Some(s.to_owned()),
39 attr: String::new(),
40 sub: None,
41 });
42 }
43 let (urn, rest) = match s.rfind(':') {
44 Some(i) => (Some(s[..i].to_owned()), &s[i + 1..]),
45 None => (None, s),
46 };
47 if let Some(u) = &urn {
48 ensure!(
49 u.to_ascii_lowercase().starts_with("urn:"),
50 "attribute path {s:?} has a prefix that is not a URN"
51 );
52 }
53 let (attr, sub) = match rest.split_once('.') {
54 Some((a, b)) => (a, Some(b)),
55 None => (rest, None),
56 };
57 ensure!(name_ok(attr), "invalid attribute name in {s:?}");
58 if let Some(b) = sub {
59 ensure!(name_ok(b), "invalid sub-attribute name in {s:?}");
60 }
61 Ok(Self {
62 urn,
63 attr: attr.to_owned(),
64 sub: sub.map(str::to_owned),
65 })
66 }
67
68 pub fn to_json(&self) -> Value {
69 let attr = (!self.attr.is_empty()).then_some(&self.attr);
70 json!({"schema": self.urn, "attribute": attr, "sub_attribute": self.sub})
71 }
72
73 fn definition(&self, rt: &ResourceType) -> Option<&'static Value> {
74 schema::attribute(rt, self.urn.as_deref(), &self.attr, self.sub.as_deref())
75 }
76
77 fn container<'a>(&self, rt: &ResourceType, resource: &'a Value) -> Option<&'a Value> {
79 match &self.urn {
80 Some(u) if !u.eq_ignore_ascii_case(rt.schema) => schema::member(resource, u),
81 _ => Some(resource),
82 }
83 }
84}
85
86#[derive(Debug, Clone, PartialEq)]
87pub enum Filter {
88 Compare {
89 path: AttrPath,
90 op: String,
91 value: Value,
92 },
93 Present(AttrPath),
94 And(Box<Filter>, Box<Filter>),
95 Or(Box<Filter>, Box<Filter>),
96 Not(Box<Filter>),
97 ValuePath {
100 path: AttrPath,
101 filter: Box<Filter>,
102 },
103}
104
105const COMPARE_OPS: &[&str] = &["eq", "ne", "co", "sw", "ew", "gt", "ge", "lt", "le"];
106
107#[derive(Debug, Clone, PartialEq)]
108enum Tok {
109 Word(String),
110 Str(String),
111 Open,
112 Close,
113 LBracket,
114 RBracket,
115}
116
117fn tokenize(s: &str) -> Result<Vec<Tok>> {
118 let mut out = Vec::new();
119 let b = s.as_bytes();
120 let mut i = 0;
121 while i < b.len() {
122 match b[i] {
123 b' ' | b'\t' | b'\n' | b'\r' => i += 1,
124 b'(' => {
125 out.push(Tok::Open);
126 i += 1
127 }
128 b')' => {
129 out.push(Tok::Close);
130 i += 1
131 }
132 b'[' => {
133 out.push(Tok::LBracket);
134 i += 1
135 }
136 b']' => {
137 out.push(Tok::RBracket);
138 i += 1
139 }
140 b'"' => {
141 let start = i;
142 i += 1;
143 while i < b.len() && b[i] != b'"' {
144 if b[i] == b'\\' {
145 i += 1;
146 }
147 i += 1;
148 }
149 ensure!(i < b.len(), "unterminated string in filter");
150 let lit: String = serde_json::from_str(&s[start..=i])
151 .context("invalid string literal in filter")?;
152 out.push(Tok::Str(lit));
153 i += 1;
154 }
155 _ => {
156 let start = i;
157 while i < b.len() && !b" \t\n\r()[]\"".contains(&b[i]) {
158 i += 1;
159 }
160 out.push(Tok::Word(s[start..i].to_owned()));
161 }
162 }
163 ensure!(out.len() <= 4 * MAX_NODES, "filter has too many tokens");
164 }
165 Ok(out)
166}
167
168struct Parser {
169 toks: Vec<Tok>,
170 pos: usize,
171 nodes: usize,
172}
173
174impl Parser {
175 fn peek(&self) -> Option<&Tok> {
176 self.toks.get(self.pos)
177 }
178 fn keyword(&self, k: &str) -> bool {
179 matches!(self.peek(), Some(Tok::Word(w)) if w.eq_ignore_ascii_case(k))
180 }
181 fn node(&mut self) -> Result<()> {
182 self.nodes += 1;
183 ensure!(self.nodes <= MAX_NODES, "filter has too many expressions");
184 Ok(())
185 }
186 fn or(&mut self, depth: usize, in_value_path: bool) -> Result<Filter> {
187 ensure!(depth <= MAX_DEPTH, "filter nests too deeply");
188 let mut left = self.and(depth, in_value_path)?;
189 while self.keyword("or") {
190 self.pos += 1;
191 self.node()?;
192 let right = self.and(depth, in_value_path)?;
193 left = Filter::Or(Box::new(left), Box::new(right));
194 }
195 Ok(left)
196 }
197 fn and(&mut self, depth: usize, in_value_path: bool) -> Result<Filter> {
198 let mut left = self.unary(depth, in_value_path)?;
199 while self.keyword("and") {
200 self.pos += 1;
201 self.node()?;
202 let right = self.unary(depth, in_value_path)?;
203 left = Filter::And(Box::new(left), Box::new(right));
204 }
205 Ok(left)
206 }
207 fn unary(&mut self, depth: usize, in_value_path: bool) -> Result<Filter> {
208 self.node()?;
209 if self.keyword("not") {
210 self.pos += 1;
211 ensure!(self.peek() == Some(&Tok::Open), "not must be followed by (");
212 self.pos += 1;
213 let inner = self.or(depth + 1, in_value_path)?;
214 ensure!(self.peek() == Some(&Tok::Close), "missing ) after not(");
215 self.pos += 1;
216 return Ok(Filter::Not(Box::new(inner)));
217 }
218 if self.peek() == Some(&Tok::Open) {
219 self.pos += 1;
220 let inner = self.or(depth + 1, in_value_path)?;
221 ensure!(self.peek() == Some(&Tok::Close), "missing )");
222 self.pos += 1;
223 return Ok(inner);
224 }
225 let Some(Tok::Word(w)) = self.peek().cloned() else {
226 bail!("expected an attribute path");
227 };
228 self.pos += 1;
229 let path = AttrPath::parse(&w)?;
230 if self.peek() == Some(&Tok::LBracket) {
231 ensure!(!in_value_path, "a value filter cannot contain another");
232 ensure!(
233 path.sub.is_none(),
234 "a value path names an attribute, not a sub-attribute"
235 );
236 self.pos += 1;
237 let inner = self.or(depth + 1, true)?;
238 ensure!(self.peek() == Some(&Tok::RBracket), "missing ]");
239 self.pos += 1;
240 return Ok(Filter::ValuePath {
241 path,
242 filter: Box::new(inner),
243 });
244 }
245 let Some(Tok::Word(op)) = self.peek().cloned() else {
246 bail!("expected an operator after {w}");
247 };
248 let op = op.to_ascii_lowercase();
249 self.pos += 1;
250 if op == "pr" {
251 return Ok(Filter::Present(path));
252 }
253 ensure!(COMPARE_OPS.contains(&op.as_str()), "unknown operator {op}");
254 let value = match self.peek().cloned() {
255 Some(Tok::Str(s)) => Value::String(s),
256 Some(Tok::Word(v)) => match v.as_str() {
257 "true" => Value::Bool(true),
258 "false" => Value::Bool(false),
259 "null" => Value::Null,
260 n => serde_json::from_str::<serde_json::Number>(n)
261 .map(Value::Number)
262 .map_err(|_| anyhow::anyhow!("{n} is not a valid comparison value"))?,
263 },
264 _ => bail!("expected a comparison value"),
265 };
266 self.pos += 1;
267 Ok(Filter::Compare { path, op, value })
268 }
269}
270
271pub fn parse_filter(s: &str) -> Result<Filter> {
272 ensure!(
273 s.len() <= MAX_FILTER_BYTES,
274 "filter over {MAX_FILTER_BYTES} bytes"
275 );
276 let mut p = Parser {
277 toks: tokenize(s)?,
278 pos: 0,
279 nodes: 0,
280 };
281 ensure!(!p.toks.is_empty(), "empty filter");
282 let f = p.or(0, false)?;
283 ensure!(p.pos == p.toks.len(), "unexpected text after the filter");
284 Ok(f)
285}
286
287impl Filter {
288 pub fn to_json(&self) -> Value {
289 match self {
290 Filter::Compare { path, op, value } => {
291 json!({"op": op, "path": path.to_json(), "value": value})
292 }
293 Filter::Present(path) => json!({"op": "pr", "path": path.to_json()}),
294 Filter::And(a, b) => json!({"and": [a.to_json(), b.to_json()]}),
295 Filter::Or(a, b) => json!({"or": [a.to_json(), b.to_json()]}),
296 Filter::Not(f) => json!({"not": f.to_json()}),
297 Filter::ValuePath { path, filter } => {
298 json!({"value_path": path.to_json(), "filter": filter.to_json()})
299 }
300 }
301 }
302}
303
304#[derive(Clone, Copy)]
307enum Scope<'a> {
308 Resource(&'a ResourceType),
309 Element(&'a ResourceType, &'a AttrPath),
310}
311
312fn values<'a>(scope: Scope, path: &AttrPath, obj: &'a Value) -> (Vec<&'a Value>, bool) {
313 let (base, def, inner_sub) = match scope {
314 Scope::Resource(rt) => (
315 path.container(rt, obj),
316 path.definition(rt),
317 path.sub.as_deref(),
318 ),
319 Scope::Element(rt, parent) => (
320 Some(obj),
321 schema::attribute(rt, parent.urn.as_deref(), &parent.attr, Some(&path.attr)),
322 None,
323 ),
324 };
325 let exact = schema::case_exact(def);
326 let Some(base) = base else {
327 return (vec![], exact);
328 };
329 if path.attr.is_empty() {
330 return (vec![base], exact);
331 }
332 let Some(v) = schema::member(base, &path.attr) else {
333 return (vec![], exact);
334 };
335 let mut out = Vec::new();
336 let mut push = |x: &'a Value| match (inner_sub, x) {
337 (Some(s), Value::Object(_)) => {
338 if let Some(y) = schema::member(x, s) {
339 out.push(y)
340 }
341 }
342 (Some(_), _) => {}
343 (None, Value::Object(_)) => {
345 if let Some(y) = schema::member(x, "value") {
346 out.push(y)
347 }
348 }
349 (None, _) => out.push(x),
350 };
351 match v {
352 Value::Array(items) => items.iter().for_each(&mut push),
353 other => push(other),
354 }
355 (out, exact)
356}
357
358fn text(v: &Value, exact: bool) -> Option<String> {
359 v.as_str().map(|s| {
360 if exact {
361 s.to_owned()
362 } else {
363 s.to_lowercase()
364 }
365 })
366}
367
368fn compare(op: &str, actual: &Value, expected: &Value, exact: bool) -> bool {
369 if let (Some(a), Some(e)) = (text(actual, exact), text(expected, exact)) {
370 return match op {
371 "eq" => a == e,
372 "ne" => a != e,
373 "co" => a.contains(&e),
374 "sw" => a.starts_with(&e),
375 "ew" => a.ends_with(&e),
376 "gt" => a > e,
377 "ge" => a >= e,
378 "lt" => a < e,
379 "le" => a <= e,
380 _ => false,
381 };
382 }
383 if let (Some(a), Some(e)) = (actual.as_f64(), expected.as_f64()) {
384 return match op {
385 "eq" => a == e,
386 "ne" => a != e,
387 "gt" => a > e,
388 "ge" => a >= e,
389 "lt" => a < e,
390 "le" => a <= e,
391 _ => false,
392 };
393 }
394 match op {
395 "eq" => actual == expected,
396 "ne" => actual != expected,
397 _ => false,
398 }
399}
400
401fn present(v: &Value) -> bool {
402 match v {
403 Value::Null => false,
404 Value::String(s) => !s.is_empty(),
405 Value::Array(a) => !a.is_empty(),
406 Value::Object(o) => !o.is_empty(),
407 _ => true,
408 }
409}
410
411fn eval_in(f: &Filter, scope: Scope, obj: &Value) -> bool {
412 match f {
413 Filter::And(a, b) => eval_in(a, scope, obj) && eval_in(b, scope, obj),
414 Filter::Or(a, b) => eval_in(a, scope, obj) || eval_in(b, scope, obj),
415 Filter::Not(x) => !eval_in(x, scope, obj),
416 Filter::Present(path) => values(scope, path, obj).0.into_iter().any(present),
417 Filter::Compare { path, op, value } => {
418 let (vals, exact) = values(scope, path, obj);
419 if vals.is_empty() {
420 return op == "ne" && !value.is_null() || (op == "eq" && value.is_null());
421 }
422 vals.into_iter().any(|v| compare(op, v, value, exact))
423 }
424 Filter::ValuePath { path, filter } => {
425 let Scope::Resource(rt) = scope else {
426 return false;
427 };
428 let Some(attr) = path
429 .container(rt, obj)
430 .and_then(|c| schema::member(c, &path.attr))
431 else {
432 return false;
433 };
434 let elements: Vec<&Value> = match attr {
435 Value::Array(a) => a.iter().collect(),
436 other => vec![other],
437 };
438 elements
439 .into_iter()
440 .any(|e| eval_in(filter, Scope::Element(rt, path), e))
441 }
442 }
443}
444
445pub fn matches(f: &Filter, rt: &ResourceType, resource: &Value) -> bool {
446 eval_in(f, Scope::Resource(rt), resource)
447}
448
449#[derive(Debug, Clone, PartialEq)]
452pub struct PatchPath {
453 pub path: AttrPath,
454 pub filter: Option<Filter>,
455 pub sub_after_filter: Option<String>,
456}
457
458impl PatchPath {
459 pub fn parse(s: &str) -> Result<Self> {
460 ensure!(
461 s.len() <= MAX_FILTER_BYTES,
462 "path over {MAX_FILTER_BYTES} bytes"
463 );
464 match s.find('[') {
465 None => Ok(Self {
466 path: AttrPath::parse(s)?,
467 filter: None,
468 sub_after_filter: None,
469 }),
470 Some(i) => {
471 let close = s.rfind(']').context("missing ] in path")?;
472 ensure!(close > i, "malformed value path");
473 let path = AttrPath::parse(&s[..i])?;
474 ensure!(
475 path.sub.is_none(),
476 "a value path names an attribute, not a sub-attribute"
477 );
478 let mut p = Parser {
479 toks: tokenize(&s[i + 1..close])?,
480 pos: 0,
481 nodes: 0,
482 };
483 let filter = p.or(0, true)?;
484 ensure!(p.pos == p.toks.len(), "unexpected text in the value filter");
485 let rest = &s[close + 1..];
486 let sub = match rest.strip_prefix('.') {
487 Some(sub) => {
488 ensure!(name_ok(sub), "invalid sub-attribute after the value filter");
489 Some(sub.to_owned())
490 }
491 None => {
492 ensure!(rest.is_empty(), "unexpected text after the value filter");
493 None
494 }
495 };
496 Ok(Self {
497 path,
498 filter: Some(filter),
499 sub_after_filter: sub,
500 })
501 }
502 }
503 }
504
505 pub fn to_json(&self) -> Value {
506 json!({"path": self.path.to_json(), "filter": self.filter.as_ref().map(Filter::to_json), "sub_attribute_after_filter": self.sub_after_filter})
507 }
508}
509
510pub fn sort(resources: &mut [Value], rt: &ResourceType, path: &AttrPath, descending: bool) {
512 let key = |r: &Value| -> Option<Value> {
513 let (vals, exact) = values(Scope::Resource(rt), path, r);
514 vals.into_iter().next().map(|v| match text(v, exact) {
515 Some(t) => Value::String(t),
516 None => v.clone(),
517 })
518 };
519 resources.sort_by(|a, b| {
520 let (ka, kb) = (key(a), key(b));
521 let ord = match (&ka, &kb) {
522 (None, None) => Ordering::Equal,
523 (None, _) => return Ordering::Greater,
524 (_, None) => return Ordering::Less,
525 (Some(x), Some(y)) => match (x, y) {
526 (Value::String(p), Value::String(q)) => p.cmp(q),
527 _ => x
528 .as_f64()
529 .partial_cmp(&y.as_f64())
530 .unwrap_or(Ordering::Equal),
531 },
532 };
533 if descending {
534 ord.reverse()
535 } else {
536 ord
537 }
538 });
539}
540
541pub fn project(
544 resource: &Value,
545 rt: &ResourceType,
546 attributes: &[AttrPath],
547 excluded: &[AttrPath],
548) -> Value {
549 let Value::Object(src) = resource else {
550 return resource.clone();
551 };
552 let mut out = src.clone();
553 for (urn, name) in schema::names_returned(rt, "never") {
554 remove(
555 &mut out,
556 rt,
557 &AttrPath {
558 urn: urn.map(str::to_owned),
559 attr: name,
560 sub: None,
561 },
562 );
563 }
564 let always: Vec<AttrPath> = schema::names_returned(rt, "always")
565 .into_iter()
566 .map(|(urn, name)| AttrPath {
567 urn: urn.map(str::to_owned),
568 attr: name,
569 sub: None,
570 })
571 .collect();
572 if !attributes.is_empty() {
573 let mut kept = Map::new();
574 for p in always.iter().chain(attributes) {
575 copy(&out, &mut kept, rt, p);
576 }
577 return Value::Object(kept);
578 }
579 for p in excluded {
580 if !always.iter().any(|a| {
581 a.attr.eq_ignore_ascii_case(&p.attr)
582 && p.sub.is_none()
583 && a.urn.is_none() == p.urn.is_none()
584 }) {
585 remove(&mut out, rt, p);
586 }
587 }
588 Value::Object(out)
589}
590
591fn is_extension(rt: &ResourceType, p: &AttrPath) -> Option<String> {
592 p.urn
593 .as_ref()
594 .filter(|u| !u.eq_ignore_ascii_case(rt.schema))
595 .cloned()
596}
597
598fn copy(src: &Map<String, Value>, dst: &mut Map<String, Value>, rt: &ResourceType, p: &AttrPath) {
599 let src_v = Value::Object(src.clone());
600 if let Some(ext) = is_extension(rt, p) {
601 let Some(key) = schema::member_key(&src_v, &ext) else {
602 return;
603 };
604 if p.attr.is_empty() {
605 dst.insert(key.clone(), src[&key].clone());
606 return;
607 }
608 let inner_src = src[&key].as_object().cloned().unwrap_or_default();
609 let entry = dst.entry(key).or_insert_with(|| json!({}));
610 if let Value::Object(inner_dst) = entry {
611 copy(
612 &inner_src,
613 inner_dst,
614 rt,
615 &AttrPath {
616 urn: None,
617 attr: p.attr.clone(),
618 sub: p.sub.clone(),
619 },
620 );
621 }
622 return;
623 }
624 let Some(key) = schema::member_key(&src_v, &p.attr) else {
625 return;
626 };
627 let value = &src[&key];
628 match &p.sub {
629 None => {
630 dst.insert(key, value.clone());
631 }
632 Some(sub) => {
633 let pick = |o: &Value| -> Option<Value> {
634 let k = schema::member_key(o, sub)?;
635 let mut m = Map::new();
636 m.insert(k.clone(), o[&k].clone());
637 Some(Value::Object(m))
638 };
639 let picked = match value {
640 Value::Array(items) => Value::Array(items.iter().filter_map(pick).collect()),
641 Value::Object(_) => pick(value).unwrap_or_else(|| json!({})),
642 _ => return,
643 };
644 match (dst.get_mut(&key), picked) {
645 (Some(Value::Object(existing)), Value::Object(more)) => existing.extend(more),
646 (_, picked) => {
647 dst.insert(key, picked);
648 }
649 }
650 }
651 }
652}
653
654fn remove(obj: &mut Map<String, Value>, rt: &ResourceType, p: &AttrPath) {
655 if let Some(ext) = is_extension(rt, p) {
656 let key = schema::member_key(&Value::Object(obj.clone()), &ext);
657 if p.attr.is_empty() {
658 if let Some(k) = key {
659 obj.remove(&k);
660 }
661 return;
662 }
663 if let Some(Value::Object(inner)) = key.and_then(|k| obj.get_mut(&k)) {
664 remove(
665 inner,
666 rt,
667 &AttrPath {
668 urn: None,
669 attr: p.attr.clone(),
670 sub: p.sub.clone(),
671 },
672 );
673 }
674 return;
675 }
676 let Some(key) = schema::member_key(&Value::Object(obj.clone()), &p.attr) else {
677 return;
678 };
679 match &p.sub {
680 None => {
681 obj.remove(&key);
682 }
683 Some(sub) => {
684 let strip = |o: &mut Value| {
685 if let Some(k) = schema::member_key(o, sub) {
686 o.as_object_mut().map(|m| m.remove(&k));
687 }
688 };
689 match obj.get_mut(&key) {
690 Some(Value::Array(items)) => items.iter_mut().for_each(strip),
691 Some(v @ Value::Object(_)) => strip(v),
692 _ => {}
693 }
694 }
695 }
696}
697
698pub fn parse_list(s: Option<&str>) -> Result<Vec<AttrPath>> {
699 match s.filter(|s| !s.trim().is_empty()) {
700 None => Ok(vec![]),
701 Some(s) => {
702 let parts: Vec<&str> = s.split(',').map(str::trim).collect();
703 ensure!(parts.len() <= 64, "at most 64 attribute names");
704 parts.into_iter().map(AttrPath::parse).collect()
705 }
706 }
707}