1use crate::generated::common::{ApprovalRequirement, Constraint};
8
9#[derive(Clone, Debug, Default, PartialEq, Eq)]
10pub struct EvalContext {
11 pub now: String,
12 pub session_id: Option<String>,
13 pub target: Option<String>,
14 pub approver_count: Option<u32>,
15 pub device_actor: Option<String>,
16}
17
18pub fn constraints_satisfied(constraints: &[Constraint], ctx: &EvalContext) -> bool {
19 constraints.iter().all(|c| satisfies(c, ctx))
20}
21
22fn satisfies(c: &Constraint, ctx: &EvalContext) -> bool {
23 match c {
24 Constraint::TimeWindow { from, until } => {
25 if let Some(from_ts) = from {
26 if ctx.now.as_str() < from_ts.as_str() {
27 return false;
28 }
29 }
30 ctx.now.as_str() <= until.as_str()
31 }
32 Constraint::Target { patterns } => match &ctx.target {
33 Some(t) => patterns.iter().any(|p| matches_glob(p, t)),
34 None => false,
35 },
36 Constraint::Quantity { .. } => true, Constraint::Rate { .. } => true, Constraint::Session { session_id } => {
39 ctx.session_id.as_deref() == Some(session_id.as_str())
40 }
41 Constraint::Approval { approval } => matches!(
42 approval,
43 ApprovalRequirement::None | ApprovalRequirement::Conditional
44 ),
45 Constraint::Quorum { quorum, .. } => ctx.approver_count.unwrap_or(0) as i64 >= *quorum,
46 Constraint::DeviceBinding { device_actor } => {
47 ctx.device_actor.as_deref() == Some(device_actor.as_str())
48 }
49 }
50}
51
52pub fn intersect_constraints(a: &[Constraint], b: &[Constraint]) -> Vec<Constraint> {
53 let mut out: Vec<Constraint> = a.to_vec();
54 for nc in b {
55 let idx = out.iter().position(|c| same_kind(c, nc));
56 match idx {
57 Some(i) => {
58 out[i] = intersect_same(&out[i], nc);
59 }
60 None => out.push(nc.clone()),
61 }
62 }
63 out
64}
65
66fn same_kind(a: &Constraint, b: &Constraint) -> bool {
67 matches!(
68 (a, b),
69 (Constraint::TimeWindow { .. }, Constraint::TimeWindow { .. })
70 | (Constraint::Target { .. }, Constraint::Target { .. })
71 | (Constraint::Quantity { .. }, Constraint::Quantity { .. })
72 | (Constraint::Rate { .. }, Constraint::Rate { .. })
73 | (Constraint::Session { .. }, Constraint::Session { .. })
74 | (Constraint::Approval { .. }, Constraint::Approval { .. })
75 | (Constraint::Quorum { .. }, Constraint::Quorum { .. })
76 | (
77 Constraint::DeviceBinding { .. },
78 Constraint::DeviceBinding { .. }
79 )
80 )
81}
82
83fn intersect_same(a: &Constraint, b: &Constraint) -> Constraint {
84 match (a, b) {
85 (
86 Constraint::TimeWindow {
87 from: af,
88 until: au,
89 },
90 Constraint::TimeWindow {
91 from: bf,
92 until: bu,
93 },
94 ) => Constraint::TimeWindow {
95 from: pick_later(af.as_deref(), bf.as_deref()),
96 until: pick_earlier(au, bu).to_string(),
97 },
98 (Constraint::Target { patterns: ap }, Constraint::Target { patterns: bp }) => {
99 let shared: Vec<String> = ap.iter().filter(|p| bp.contains(p)).cloned().collect();
100 if shared.is_empty() {
101 let mut merged = ap.clone();
102 merged.extend(bp.iter().cloned());
103 Constraint::Target { patterns: merged }
104 } else {
105 Constraint::Target { patterns: shared }
106 }
107 }
108 (
109 Constraint::Quantity { max: am, unit: au },
110 Constraint::Quantity { max: bm, unit: bu },
111 ) => Constraint::Quantity {
112 max: (*am).min(*bm),
113 unit: au.clone().or_else(|| bu.clone()),
114 },
115 (
116 Constraint::Rate {
117 max_per_window: am,
118 window_seconds: aw,
119 },
120 Constraint::Rate {
121 max_per_window: bm,
122 window_seconds: bw,
123 },
124 ) => Constraint::Rate {
125 max_per_window: (*am).min(*bm),
126 window_seconds: (*aw).min(*bw),
127 },
128 (Constraint::Quorum { quorum: aq, of: ao }, Constraint::Quorum { quorum: bq, .. }) => {
129 Constraint::Quorum {
130 quorum: (*aq).max(*bq),
131 of: ao.clone(),
132 }
133 }
134 _ => a.clone(),
135 }
136}
137
138fn pick_later(a: Option<&str>, b: Option<&str>) -> Option<String> {
139 match (a, b) {
140 (None, None) => None,
141 (Some(x), None) => Some(x.to_string()),
142 (None, Some(y)) => Some(y.to_string()),
143 (Some(x), Some(y)) => Some(if x > y { x.to_string() } else { y.to_string() }),
144 }
145}
146
147fn pick_earlier<'a>(a: &'a str, b: &'a str) -> &'a str {
148 if a < b {
149 a
150 } else {
151 b
152 }
153}
154
155fn matches_glob(pattern: &str, value: &str) -> bool {
156 crate::glob::glob_match(pattern, value)
157}