1use crate::{
4 arena::Kind,
5 document::{Document, Graph},
6 error::{Error, ErrorKind, ReadFailure, Syntax},
7 fields::Fields,
8 model::{admit, get, named, required},
9 range::RangeSource,
10 signature::{Coverage, Signature},
11 source::Location,
12};
13use structured_asn1::memory::{Memory, OwnedText, OwnedVec, Work};
14mod classify;
15mod placement;
16
17#[derive(Debug)]
20pub enum AssessmentError<E> {
21 Pdf(ReadFailure<E>),
23 Material(crate::material::MaterialFailure),
25}
26impl<E> From<ReadFailure<E>> for AssessmentError<E> {
27 fn from(cause: ReadFailure<E>) -> Self {
28 Self::Pdf(cause)
29 }
30}
31impl<E> From<Error> for AssessmentError<E> {
32 fn from(cause: Error) -> Self {
33 Self::Pdf(cause.into())
34 }
35}
36impl<E> From<crate::range::InputError<E>> for AssessmentError<E> {
37 fn from(cause: crate::range::InputError<E>) -> Self {
38 Self::Pdf(cause.into())
39 }
40}
41pub use placement::Placement;
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum Permission {
46 NoChanges,
48 FormFill,
50 Annotations,
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum Action {
57 All,
59 Include,
61 Exclude,
63}
64
65pub struct Lock {
67 action: Action,
68 fields: OwnedVec<OwnedText, Memory>,
69 location: Location,
70}
71
72impl Lock {
73 #[must_use]
75 pub fn action(&self) -> Action {
76 self.action
77 }
78 #[must_use]
80 pub fn fields(&self) -> &[OwnedText] {
81 &self.fields
82 }
83 #[must_use]
85 pub fn location(&self) -> Location {
86 self.location
87 }
88
89 fn protects(&self, name: &str, work: &impl Work) -> crate::Result<bool> {
90 if self.action == Action::All {
91 return Ok(true);
92 }
93 let mut selected = false;
94 for field in &self.fields {
95 admit(work, self.location, name.len().min(field.len()) as u64 + 1)?;
96 if name == field.as_str()
97 || (name.starts_with(field.as_str())
98 && name.as_bytes().get(field.len()) == Some(&b'.'))
99 {
100 selected = true;
101 break;
102 }
103 }
104 Ok(if self.action == Action::Include {
105 selected
106 } else {
107 !selected
108 })
109 }
110}
111
112pub struct Transforms {
114 permission: Option<Permission>,
115 locks: OwnedVec<Lock, Memory>,
116 unsupported: bool,
117 location: Location,
118}
119
120impl Transforms {
121 #[must_use]
123 pub fn location(&self) -> Location {
124 self.location
125 }
126
127 #[must_use]
129 pub fn permission(&self) -> Option<Permission> {
130 self.permission
131 }
132 #[must_use]
134 pub fn locks(&self) -> &[Lock] {
135 &self.locks
136 }
137 #[must_use]
139 pub fn has_unsupported_transform(&self) -> bool {
140 self.unsupported
141 }
142}
143
144#[derive(Debug, Clone, Copy, PartialEq, Eq)]
146pub enum Outcome {
147 Permitted,
149 Forbidden,
151 Indeterminate,
153 Unstated,
155}
156
157#[derive(Debug, Clone, Copy)]
160pub struct Change {
161 pub identity: (u64, u16),
163 pub location: Location,
165}
166
167pub struct Assessment {
170 transforms: Transforms,
171 changes: OwnedVec<Change, Memory>,
172 outcome: Outcome,
173 source_revision: usize,
174 compared_revision: usize,
175}
176
177impl Assessment {
178 #[must_use]
180 pub fn transforms(&self) -> &Transforms {
181 &self.transforms
182 }
183 #[must_use]
185 pub fn changes(&self) -> &[Change] {
186 &self.changes
187 }
188 #[must_use]
190 pub fn outcome(&self) -> Outcome {
191 self.outcome
192 }
193 #[must_use]
195 pub fn revisions(&self) -> (usize, usize) {
196 (self.source_revision, self.compared_revision)
197 }
198}
199
200fn reference_fields(
201 arena: &crate::arena::Arena<impl Work>,
202 reference: crate::arena::NodeId,
203 work: &impl Work,
204) -> crate::Result<(crate::arena::NodeId, Option<crate::arena::NodeId>)> {
205 let at = arena.nodes[reference.0].location;
206 let method = required(arena, reference, b"TransformMethod", work)?;
207 let parameters = get(arena, reference, b"TransformParams", work)?;
208 if let Some(kind) = get(arena, reference, b"Type", work)?
209 && !named(arena, kind, b"SigRef", work)?
210 {
211 return Err(Error {
212 kind: ErrorKind::Syntax(Syntax::Dictionary),
213 location: at,
214 });
215 }
216 if let Some(parameters) = parameters {
217 if !matches!(arena.nodes[parameters.0].kind, Kind::Dictionary(_, _)) {
218 return Err(Error {
219 kind: ErrorKind::Syntax(Syntax::Dictionary),
220 location: arena.nodes[parameters.0].location,
221 });
222 }
223 for (key, expected) in [
224 (b"Type".as_slice(), b"TransformParams".as_slice()),
225 (b"V", b"1.2"),
226 ] {
227 if let Some(value) = get(arena, parameters, key, work)?
228 && !named(arena, value, expected, work)?
229 {
230 return Err(Error {
231 kind: ErrorKind::Syntax(Syntax::Dictionary),
232 location: arena.nodes[value.0].location,
233 });
234 }
235 }
236 }
237 Ok((method, parameters))
238}
239
240impl<'s, S: RangeSource> Document<'s, S> {
241 async fn field_lock(
242 &self,
243 reading: &crate::document::Reading<'s, S::Document, S::Generation>,
244 graph: &Graph<'s, S::Document, S::Generation>,
245 reference: crate::arena::NodeId,
246 parameters: Option<crate::arena::NodeId>,
247 work: &impl Work,
248 ) -> Result<Lock, ReadFailure<S::Error>> {
249 let arena = &reading.value.arena;
250 let at = arena.nodes[reference.0].location;
251 let data = required(arena, reference, b"Data", work)?;
252 let Kind::Reference { number, generation } = arena.nodes[data.0].kind else {
253 return Err(Error {
254 kind: ErrorKind::Syntax(Syntax::Dictionary),
255 location: arena.nodes[data.0].location,
256 }
257 .into());
258 };
259 let catalog = self
260 .object(graph.revision, number, generation, work)
261 .await?;
262 if catalog
263 .as_ref()
264 .is_none_or(|value| value.value.root().location() != graph.root().direct().location())
265 {
266 return Err(Error {
267 kind: ErrorKind::Ambiguous(Syntax::Dictionary),
268 location: arena.nodes[data.0].location,
269 }
270 .into());
271 }
272 let parameters = parameters.ok_or(Error {
273 kind: ErrorKind::Syntax(Syntax::Dictionary),
274 location: at,
275 })?;
276 let action = required(arena, parameters, b"Action", work)?;
277 let action = if named(arena, action, b"All", work)? {
278 Action::All
279 } else if named(arena, action, b"Include", work)? {
280 Action::Include
281 } else if named(arena, action, b"Exclude", work)? {
282 Action::Exclude
283 } else {
284 return Err(Error {
285 kind: ErrorKind::Syntax(Syntax::Name),
286 location: arena.nodes[action.0].location,
287 }
288 .into());
289 };
290 let mut names = work.vector();
291 let fields = get(arena, parameters, b"Fields", work)?;
292 if fields.is_none() && action != Action::All {
293 return Err(Error {
294 kind: ErrorKind::Syntax(Syntax::Dictionary),
295 location: at,
296 }
297 .into());
298 }
299 if let Some(fields) = fields {
300 let Kind::Array(children) = arena.nodes[fields.0].kind else {
301 return Err(Error {
302 kind: ErrorKind::Syntax(Syntax::Dictionary),
303 location: arena.nodes[fields.0].location,
304 }
305 .into());
306 };
307 let mut next = children.first;
308 while let Some(index) = next {
309 admit(work, at, 1)?;
310 let link = arena.links[index];
311 next = link.next;
312 let bytes = self.string(reading, link.value, usize::MAX, work).await?;
313 let name = crate::text::read(&bytes, arena.nodes[link.value.0].location, work)?;
314 names.try_push(name).map_err(|cause| Error {
315 kind: ErrorKind::Resource(cause),
316 location: at,
317 })?;
318 }
319 }
320 Ok(Lock {
321 action,
322 fields: names.into_owned(),
323 location: arena.nodes[parameters.0].location,
324 })
325 }
326
327 pub async fn transforms<W: Work>(
332 &self,
333 signature: Signature<'_, 's, S::Document, S::Generation>,
334 graph: &Graph<'s, S::Document, S::Generation>,
335 operation: &W,
336 ) -> Result<Transforms, ReadFailure<S::Error>> {
337 let work = self.session.query(operation);
338 let location = signature.location();
339 if !core::ptr::eq(graph.session, self.session)
340 || graph.stamp != self.stamp
341 || signature.coverage() != Coverage::Revision(graph.revision)
342 {
343 return Err(Error {
344 kind: ErrorKind::Foreign,
345 location,
346 }
347 .into());
348 }
349 self.check(&work, location)?;
350 let reading = signature.owner();
351 let arena = &reading.value.arena;
352 let mut output = Transforms {
353 permission: None,
354 locks: work.vector().into_owned(),
355 unsupported: false,
356 location,
357 };
358 let mut locks = work.vector();
359 if let Some(references) = get(arena, signature.node(), b"Reference", &work)? {
360 let Kind::Array(children) = arena.nodes[references.0].kind else {
361 return Err(Error {
362 kind: ErrorKind::Syntax(Syntax::Dictionary),
363 location: arena.nodes[references.0].location,
364 }
365 .into());
366 };
367 let mut next = children.first;
368 while let Some(index) = next {
369 admit(&work, location, 1)?;
370 let link = arena.links[index];
371 next = link.next;
372 let reference = link.value;
373 let at = arena.nodes[reference.0].location;
374 let (method, parameters) = reference_fields(arena, reference, &work)?;
375 if named(arena, method, b"DocMDP", &work)? {
376 if output.permission.is_some() {
377 return Err(Error {
378 kind: ErrorKind::Ambiguous(Syntax::Dictionary),
379 location: at,
380 }
381 .into());
382 }
383 let permission = parameters
386 .map(|node| get(arena, node, b"P", &work))
387 .transpose()?
388 .flatten()
389 .map(|node| crate::model::unsigned_number(arena, node, &work))
390 .transpose()?
391 .unwrap_or(2);
392 output.permission = Some(match permission {
393 1 => Permission::NoChanges,
394 2 => Permission::FormFill,
395 3 => Permission::Annotations,
396 _ => {
397 return Err(Error {
398 kind: ErrorKind::Syntax(Syntax::Number),
399 location: at,
400 }
401 .into());
402 }
403 });
404 } else if named(arena, method, b"FieldMDP", &work)? {
405 let lock = self
406 .field_lock(reading, graph, reference, parameters, &work)
407 .await?;
408 locks.try_push(lock).map_err(|cause| Error {
409 kind: ErrorKind::Resource(cause),
410 location: at,
411 })?;
412 } else {
413 output.unsupported = true;
414 }
415 }
416 }
417 output.locks = locks.into_owned();
418 self.check(&work, location)?;
419 Ok(output)
420 }
421
422 async fn locks_preserved<'g>(
423 &self,
424 before: &Fields<'g, 's, S::Document, S::Generation>,
425 after: &Fields<'g, 's, S::Document, S::Generation>,
426 transforms: &Transforms,
427 work: &impl Work,
428 ) -> Result<bool, ReadFailure<S::Error>> {
429 let mut preserved = true;
430 for (ordinal, field) in before.values().iter().enumerate() {
431 let at = field.object.direct().location();
432 let differs = if let Some(other) = after.get(field.name(), work)? {
433 match (before.value(ordinal, work)?, after.value(other, work)?) {
434 (None, None) => false,
435 (Some(a), Some(b)) => !self.same_graph_values(a, b, work).await?,
436 _ => true,
437 }
438 } else {
439 true
440 };
441 if differs {
442 for lock in &transforms.locks {
443 admit(work, at, 1)?;
444 if lock.protects(field.name(), work)? {
445 preserved = false;
446 }
447 }
448 }
449 }
450 for (ordinal, field) in after.values().iter().enumerate() {
451 admit(work, field.object().direct().location(), 1)?;
452 if before.get(field.name(), work)?.is_some() || after.value(ordinal, work)?.is_none() {
453 continue;
454 }
455 for lock in &transforms.locks {
456 if lock.protects(field.name(), work)? {
457 preserved = false;
458 }
459 }
460 }
461 Ok(preserved)
462 }
463
464 async fn graph_changes<W: Work>(
465 &self,
466 first: &Graph<'s, S::Document, S::Generation>,
467 second: &Graph<'s, S::Document, S::Generation>,
468 location: Location,
469 work: &W,
470 ) -> Result<OwnedVec<Change, W>, ReadFailure<S::Error>> {
471 let mut changes = work.vector();
472 for (ordinal, node) in second.nodes.iter().enumerate() {
473 admit(work, location, 1)?;
474 let old = first
475 .index
476 .get(&node.key, work)
477 .map_err(|cause| Error {
478 kind: ErrorKind::Resource(cause),
479 location,
480 })?
481 .copied();
482 let different = match (
483 old.and_then(|index| first.nodes[index].reading.as_ref()),
484 node.reading.as_ref(),
485 ) {
486 (Some(a), Some(b)) => {
487 !self
488 .same_readings_in(
489 a,
490 first.decoded(old.expect("selected original node")),
491 b,
492 second.decoded(ordinal),
493 work,
494 )
495 .await?
496 }
497 (None, None) => false,
498 _ => true,
499 };
500 if different {
501 let at = node
502 .reading
503 .as_ref()
504 .map_or(location, |reading| reading.value.root().location());
505 changes
506 .try_push(Change {
507 identity: (node.key.number, node.key.generation),
508 location: at,
509 })
510 .map_err(|cause| Error {
511 kind: ErrorKind::Resource(cause),
512 location: at,
513 })?;
514 }
515 }
516 for node in &first.nodes {
517 admit(work, location, 1)?;
518 if second
519 .index
520 .get(&node.key, work)
521 .map_err(|cause| Error {
522 kind: ErrorKind::Resource(cause),
523 location,
524 })?
525 .is_none()
526 {
527 let at = node
528 .reading
529 .as_ref()
530 .map_or(location, |reading| reading.value.root().location());
531 changes
532 .try_push(Change {
533 identity: (node.key.number, node.key.generation),
534 location: at,
535 })
536 .map_err(|cause| Error {
537 kind: ErrorKind::Resource(cause),
538 location: at,
539 })?;
540 }
541 }
542 Ok(changes)
543 }
544
545 #[expect(
552 clippy::result_large_err,
553 reason = "structural and interpreted material failures retain separate typed causes and original locations without allocation on refusal"
554 )]
555 pub async fn inspect_permissions<W: Work>(
556 &self,
557 signature: Signature<'_, 's, S::Document, S::Generation>,
558 before: &Fields<'_, 's, S::Document, S::Generation>,
559 after: &Fields<'_, 's, S::Document, S::Generation>,
560 operation: &W,
561 ) -> Result<Assessment, AssessmentError<S::Error>> {
562 let work = self.session.query(operation);
563 let location = signature.location();
564 let first = before.graph;
565 let second = after.graph;
566 if second.revision < first.revision
567 || second.stamp != first.stamp
568 || !core::ptr::eq(second.session, self.session)
569 {
570 return Err(Error {
571 kind: ErrorKind::Foreign,
572 location,
573 }
574 .into());
575 }
576 let transforms = self.transforms(signature, first, &work).await?;
577 let placement = self.signature_placement(signature, first, before, &transforms, &work)?;
578 let mut outcome = if transforms.unsupported {
579 Outcome::Indeterminate
580 } else if transforms.permission.is_none() && transforms.locks.is_empty() {
581 Outcome::Unstated
582 } else {
583 Outcome::Permitted
584 };
585 if !self
586 .locks_preserved(before, after, &transforms, &work)
587 .await?
588 {
589 outcome = Outcome::Forbidden;
590 }
591 let mut changes = self.graph_changes(first, second, location, &work).await?;
592 let mut unclassified = false;
593 for definition in signature.catalogue().definitions() {
594 admit(&work, definition.location, 1)?;
595 if definition.revision <= first.revision
596 || definition.revision > second.revision
597 || definition.kind != crate::signature::DefinitionKind::Object
598 {
599 continue;
600 }
601 let key = crate::document::GraphKey {
602 number: definition.identity.0,
603 generation: definition.identity.1,
604 };
605 let current = second.index.get(&key, &work).map_err(|cause| Error {
606 kind: ErrorKind::Resource(cause),
607 location: definition.location,
608 })?;
609 let selected = current.and_then(|&index| second.nodes[index].reading.as_ref());
610 if selected
611 .is_some_and(|reading| reading.value.root().location() == definition.location)
612 {
613 continue;
614 }
615 unclassified = true;
618 changes
619 .try_push(Change {
620 identity: definition.identity,
621 location: definition.location,
622 })
623 .map_err(|cause| Error {
624 kind: ErrorKind::Resource(cause),
625 location: definition.location,
626 })?;
627 }
628 if !changes.is_empty()
629 && outcome != Outcome::Forbidden
630 && let Some(permission) = transforms.permission
631 {
632 outcome = self
633 .classify_changes(before, after, permission, &work)
634 .await?;
635 }
636 if !placement.all_requirements_met() {
637 outcome = Outcome::Forbidden;
638 }
639 if unclassified && outcome == Outcome::Permitted {
640 outcome = Outcome::Indeterminate;
641 }
642 self.check(&work, location)?;
643 Ok(Assessment {
644 transforms,
645 changes: changes.into_owned(),
646 outcome,
647 source_revision: first.revision,
648 compared_revision: second.revision,
649 })
650 }
651
652 #[expect(
657 clippy::result_large_err,
658 reason = "authenticated assessment propagates unchanged source-qualified inspection failures without allocating an envelope"
659 )]
660 pub async fn permissions<W: Work>(
661 &self,
662 mathematical: &crate::basic::Mathematical<
663 '_,
664 '_,
665 '_,
666 '_,
667 '_,
668 's,
669 S::Document,
670 S::Generation,
671 >,
672 before: &Fields<'_, 's, S::Document, S::Generation>,
673 after: &Fields<'_, 's, S::Document, S::Generation>,
674 operation: &W,
675 ) -> Result<Assessment, AssessmentError<S::Error>> {
676 let mut assessment = self
677 .inspect_permissions(mathematical.signature(), before, after, operation)
678 .await?;
679 if !mathematical.input_verified() && assessment.outcome == Outcome::Permitted {
680 assessment.outcome = Outcome::Indeterminate;
681 }
682 Ok(assessment)
683 }
684}