1use serde::{Deserialize, Serialize};
4
5use crate::actionability::Address;
6use crate::ids::{ArtifactId, DescriptorId};
7
8#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
10#[serde(deny_unknown_fields)]
11pub struct Screening {
12 #[serde(deserialize_with = "positive")]
13 #[schemars(range(min = 1))]
14 pub max_text_units: u64,
15 #[serde(deserialize_with = "bounded_list")]
16 #[schemars(length(min = 1, max = 32))]
17 pub entries: Vec<ScreeningEntry>,
18}
19
20#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
22#[serde(deny_unknown_fields)]
23pub struct ScreeningEntry {
24 #[serde(deserialize_with = "name")]
25 pub id: String,
26 #[serde(deserialize_with = "fields")]
27 #[schemars(length(min = 1, max = 32))]
28 pub fields: Vec<String>,
29 pub item_id: ScreeningItemId,
30 #[serde(deserialize_with = "name")]
31 pub screener: String,
32 pub on_block: OnBlock,
33 #[serde(deserialize_with = "address")]
34 pub address: Address,
35 pub on_unavailable: OnUnavailable,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
39#[serde(rename_all = "snake_case")]
40pub enum ScreeningItemId {
41 None,
42 #[serde(untagged)]
46 Field(ScreeningItemField),
47}
48
49#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
50#[serde(deny_unknown_fields)]
51pub struct ScreeningItemField {
52 #[serde(deserialize_with = "name")]
53 pub field: String,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
57#[serde(rename_all = "snake_case")]
58pub enum OnBlock {
59 Refuse,
60 DropField,
61 DropText,
62 DropItem,
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
66#[serde(rename_all = "snake_case")]
67pub enum OnUnavailable {
68 FailClosed,
69 SkipGeneration,
70 FailOpen,
71}
72
73#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
75#[serde(deny_unknown_fields)]
76pub struct PlanScreening {
77 pub screening: Screening,
78 #[serde(deserialize_with = "nonempty_steps")]
79 #[schemars(length(min = 1))]
80 pub steps: Vec<ScreeningStep>,
81}
82
83#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
84#[serde(deny_unknown_fields)]
85pub struct ScreeningStep {
86 #[serde(deserialize_with = "nonempty")]
87 pub step: String,
88 #[serde(deserialize_with = "name")]
89 pub entry: String,
90 #[serde(deserialize_with = "field_reference")]
91 pub field: String,
92 pub binding: ScreenerBinding,
93 pub consequence: OnBlock,
94}
95
96#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
97#[serde(deny_unknown_fields)]
98pub struct ScreenerBinding {
99 #[serde(deserialize_with = "name")]
100 pub backend_id: String,
101 pub artifact: ArtifactId,
102 pub descriptor: DescriptorId,
103}
104
105pub(crate) fn present<'de, D: serde::Deserializer<'de>, T: Deserialize<'de>>(
107 deserializer: D,
108) -> Result<Option<T>, D::Error> {
109 T::deserialize(deserializer).map(Some)
110}
111
112fn positive<'de, D: serde::Deserializer<'de>>(d: D) -> Result<u64, D::Error> {
113 let n = u64::deserialize(d)?;
114 if n == 0 {
115 return Err(serde::de::Error::custom("max_text_units must be positive"));
116 }
117 Ok(n)
118}
119
120fn bounded_list<'de, D: serde::Deserializer<'de>, T: Deserialize<'de>>(
121 d: D,
122) -> Result<Vec<T>, D::Error> {
123 let values = Vec::<T>::deserialize(d)?;
124 if !(1..=32).contains(&values.len()) {
125 return Err(serde::de::Error::custom("expected 1 to 32 elements"));
126 }
127 Ok(values)
128}
129
130fn valid_name(s: &str) -> bool {
132 !s.is_empty() && !s.contains(':') && !s.chars().any(char::is_whitespace)
133}
134
135fn name<'de, D: serde::Deserializer<'de>>(d: D) -> Result<String, D::Error> {
136 let s = String::deserialize(d)?;
137 if !valid_name(&s) {
138 return Err(serde::de::Error::custom("invalid screening name"));
139 }
140 Ok(s)
141}
142
143fn reference(s: &str) -> bool {
144 let Some(path) = s.strip_prefix("input:") else {
145 return false;
146 };
147 let (input, field) = match path.split_once('/') {
148 Some((input, field)) => (input.strip_suffix("[]").unwrap_or(input), Some(field)),
149 None => (path, None),
150 };
151 let component = |s: &str| valid_name(s) && !s.contains(['/', '[', ']']);
152 component(input) && field.is_none_or(component)
153}
154
155fn fields<'de, D: serde::Deserializer<'de>>(d: D) -> Result<Vec<String>, D::Error> {
156 let values: Vec<String> = bounded_list(d)?;
157 if !values.iter().all(|s| reference(s)) {
158 return Err(serde::de::Error::custom(
159 "invalid screening field reference",
160 ));
161 }
162 Ok(values)
163}
164
165fn address<'de, D: serde::Deserializer<'de>>(d: D) -> Result<Address, D::Error> {
166 let value = Address::deserialize(d)?;
167 if let Address::Override(a) = &value
170 && a.reason.is_empty()
171 {
172 return Err(serde::de::Error::custom(
173 "invalid screening address override",
174 ));
175 }
176 Ok(value)
177}
178
179fn nonempty_steps<'de, D: serde::Deserializer<'de>>(d: D) -> Result<Vec<ScreeningStep>, D::Error> {
180 let steps = Vec::<ScreeningStep>::deserialize(d)?;
181 if steps.is_empty() {
182 return Err(serde::de::Error::custom("screening steps must be nonempty"));
183 }
184 Ok(steps)
185}
186
187fn nonempty<'de, D: serde::Deserializer<'de>>(d: D) -> Result<String, D::Error> {
188 let s = String::deserialize(d)?;
189 if s.is_empty() {
190 return Err(serde::de::Error::custom(
191 "screening string must be nonempty",
192 ));
193 }
194 Ok(s)
195}
196
197fn optional_nonempty<'de, D: serde::Deserializer<'de>>(d: D) -> Result<Option<String>, D::Error> {
198 let s = Option::<String>::deserialize(d)?;
199 if s.as_ref().is_some_and(String::is_empty) {
200 return Err(serde::de::Error::custom(
201 "screening string must be nonempty",
202 ));
203 }
204 Ok(s)
205}
206
207fn field_reference<'de, D: serde::Deserializer<'de>>(d: D) -> Result<String, D::Error> {
208 let s = String::deserialize(d)?;
209 if !reference(&s) {
210 return Err(serde::de::Error::custom(
211 "invalid screening field reference",
212 ));
213 }
214 Ok(s)
215}
216
217pub const SCREEN_TEXT_TAG: &str = "rustev.screen-text/1";
219
220pub fn screen_text_digest(text: &str) -> crate::ids::ContentDigest {
221 crate::ids::ContentDigest::from_digest(crate::canonical::tagged_digest(
222 SCREEN_TEXT_TAG,
223 text.as_bytes(),
224 ))
225}
226
227pub fn withheld_screen_blocked() -> serde_json::Value {
229 serde_json::json!({"withheld": "screen_blocked"})
230}
231
232#[derive(
234 Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, schemars::JsonSchema,
235)]
236#[serde(deny_unknown_fields)]
237pub struct ScreeningNotice {
238 #[serde(deserialize_with = "name")]
239 pub entry: String,
240 #[serde(deserialize_with = "field_reference")]
241 pub field: String,
242 pub consequence: ScreenConsequence,
243 pub items: Vec<String>,
244}
245
246#[derive(
247 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, schemars::JsonSchema,
248)]
249#[serde(rename_all = "snake_case")]
250pub enum ScreenConsequence {
251 None,
252 Refuse,
253 DropField,
254 DropText,
255 DropItem,
256 FailClosed,
257 SkipGeneration,
258 FailOpen,
259}
260
261#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
262#[serde(rename_all = "snake_case", deny_unknown_fields)]
263pub enum ScreenVerdict {
264 Pass,
265 Blocked,
266 Unavailable { reason: crate::judgment::Unresolved },
267 Empty,
268}
269
270#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
272#[serde(deny_unknown_fields)]
273pub struct ScreeningEvidence {
274 #[serde(deserialize_with = "name")]
275 pub entry: String,
276 #[serde(deserialize_with = "evidence_field_reference")]
277 pub field: String,
278 #[serde(default, deserialize_with = "optional_nonempty")]
280 pub item: Option<String>,
281 pub text_digest: crate::ids::ContentDigest,
282 pub verdict: ScreenVerdict,
283 pub categories: Vec<String>,
284 pub consequence: ScreenConsequence,
285 #[serde(deserialize_with = "name")]
286 pub backend_id: String,
287 pub artifact: ArtifactId,
288 pub descriptor: DescriptorId,
289 #[serde(deserialize_with = "nonempty")]
290 pub step: String,
291}
292
293#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
294#[serde(deny_unknown_fields)]
295pub struct ScreenRequest {
296 pub cache: ScreenCache,
297 pub units: ScreenChargedUnits,
298}
299
300#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
301#[serde(rename_all = "snake_case", deny_unknown_fields)]
302pub enum ScreenCache {
303 Hit {
304 #[serde(deserialize_with = "nonempty")]
305 source_attempt: String,
306 },
307 Miss,
308 Disabled,
309}
310
311#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
312#[serde(deny_unknown_fields)]
313pub struct ScreenChargedUnits {
314 #[serde(
317 default,
318 skip_serializing_if = "Option::is_none",
319 deserialize_with = "present"
320 )]
321 #[schemars(with = "u64")]
322 pub computed: Option<u64>,
323 #[serde(
326 default,
327 skip_serializing_if = "Option::is_none",
328 deserialize_with = "present"
329 )]
330 #[schemars(with = "u64")]
331 pub reported: Option<u64>,
332 pub charged: u64,
333 pub basis: ScreenUnitBasis,
334 #[serde(
337 default,
338 skip_serializing_if = "Option::is_none",
339 deserialize_with = "present"
340 )]
341 #[schemars(with = "ScreenUnitBoundViolation")]
342 pub bound_violation: Option<ScreenUnitBoundViolation>,
343}
344
345#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
346#[serde(rename_all = "snake_case")]
347pub enum ScreenUnitBasis {
348 Reported,
349 Computed,
350 CacheHit,
351}
352
353#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
354#[serde(deny_unknown_fields)]
355pub struct ScreenUnitBoundViolation {
356 pub computed: u64,
357 pub reported: u64,
358}
359
360impl<'de> Deserialize<'de> for ScreenUnitBoundViolation {
361 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
362 #[derive(Deserialize)]
363 #[serde(deny_unknown_fields)]
364 struct Wire {
365 computed: u64,
366 reported: u64,
367 }
368
369 let wire = Wire::deserialize(d)?;
370 if wire.reported <= wire.computed {
371 return Err(serde::de::Error::custom(
372 "reported screening units must exceed the computed bound",
373 ));
374 }
375 Ok(Self {
376 computed: wire.computed,
377 reported: wire.reported,
378 })
379 }
380}
381
382pub(crate) fn unavailable_fields<'de, D: serde::Deserializer<'de>>(
383 d: D,
384) -> Result<Vec<String>, D::Error> {
385 let fields = Vec::<String>::deserialize(d)?;
386 if fields.is_empty()
387 || !fields.iter().all(|s| reference(s))
388 || fields
389 .windows(2)
390 .any(|w| w[0].as_bytes() >= w[1].as_bytes())
391 {
392 return Err(serde::de::Error::custom(
393 "screen_unavailable fields must be nonempty, valid, sorted and unique",
394 ));
395 }
396 Ok(fields)
397}
398
399pub(crate) fn evidence_list<'de, D: serde::Deserializer<'de>>(
400 d: D,
401) -> Result<Option<Vec<ScreeningEvidence>>, D::Error> {
402 let entries = Vec::<ScreeningEvidence>::deserialize(d)?;
403 if entries.is_empty() {
404 return Err(serde::de::Error::custom(
405 "screening evidence must be nonempty",
406 ));
407 }
408 Ok(Some(entries))
409}
410
411fn evidence_field_reference<'de, D: serde::Deserializer<'de>>(d: D) -> Result<String, D::Error> {
412 let s = String::deserialize(d)?;
413 let output = s
414 .strip_prefix("output:")
415 .and_then(|p| p.split_once('/'))
416 .is_some_and(|(step, field)| valid_name(step) && valid_name(field));
417 if reference(&s) || output {
418 Ok(s)
419 } else {
420 Err(serde::de::Error::custom("invalid screening evidence field"))
421 }
422}