Skip to main content

structured_pades/
permissions.rs

1//! Original signed transforms and historical permission evaluation.
2
3use 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/// Structural acquisition and interpreted validation-material failures remain
18/// separate phases. The PDF reader itself does not carry CMS parser state.
19#[derive(Debug)]
20pub enum AssessmentError<E> {
21    /// Original PDF syntax, source, or execution cause.
22    Pdf(ReadFailure<E>),
23    /// Original interpreted material cause and its actual source enclosure.
24    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/// Original `DocMDP` permission, including its normative default P=2.
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum Permission {
46    /// No ordinary document modification; validation updates remain separate.
47    NoChanges,
48    /// Form filling, template instantiation and signing are permitted.
49    FormFill,
50    /// Form/signing permissions also permit annotation changes.
51    Annotations,
52}
53
54/// Original `FieldMDP` field-selection operator.
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum Action {
57    /// Every original field is protected.
58    All,
59    /// Only the named fields and their descendants are protected.
60    Include,
61    /// Every field except the named fields and their descendants is protected.
62    Exclude,
63}
64
65/// One original `FieldMDP` selection, without writable replacement field names.
66pub struct Lock {
67    action: Action,
68    fields: OwnedVec<OwnedText, Memory>,
69    location: Location,
70}
71
72impl Lock {
73    /// Original selection operator.
74    #[must_use]
75    pub fn action(&self) -> Action {
76        self.action
77    }
78    /// Original fully qualified text names, decoded without normalizing them.
79    #[must_use]
80    pub fn fields(&self) -> &[OwnedText] {
81        &self.fields
82    }
83    /// Original transform-parameter dictionary.
84    #[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
112/// Format-derived permissions from the ORIGINAL signed dictionary.
113pub struct Transforms {
114    permission: Option<Permission>,
115    locks: OwnedVec<Lock, Memory>,
116    unsupported: bool,
117    location: Location,
118}
119
120impl Transforms {
121    /// Original signature dictionary carrying these transform selections.
122    #[must_use]
123    pub fn location(&self) -> Location {
124        self.location
125    }
126
127    /// The original `DocMDP` permission, if this is a certification signature.
128    #[must_use]
129    pub fn permission(&self) -> Option<Permission> {
130        self.permission
131    }
132    /// Every original `FieldMDP` transform, without choosing a successful subset.
133    #[must_use]
134    pub fn locks(&self) -> &[Lock] {
135        &self.locks
136    }
137    /// A different transform requires its own normative mechanism.
138    #[must_use]
139    pub fn has_unsupported_transform(&self) -> bool {
140        self.unsupported
141    }
142}
143
144/// Completed modification result, independent of signature arithmetic and trust.
145#[derive(Debug, Clone, Copy, PartialEq, Eq)]
146pub enum Outcome {
147    /// All actual observed changes satisfy the original transform permissions.
148    Permitted,
149    /// At least one source-derived change violates a selected permission.
150    Forbidden,
151    /// A change's required semantics are unavailable; no positive result.
152    Indeterminate,
153    /// No original `DocMDP` or `FieldMDP` restriction was supplied by this signature.
154    Unstated,
155}
156
157/// One original changed indirect definition. Names are facts of the source,
158/// never caller-supplied modification categories.
159#[derive(Debug, Clone, Copy)]
160pub struct Change {
161    /// Original indirect object identity.
162    pub identity: (u64, u16),
163    /// Definition in the compared revision, or its prior definition on deletion.
164    pub location: Location,
165}
166
167/// Complete historical structural comparison with its actual original rules.
168/// A signature's byte-range math must be considered independently by callers.
169pub 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    /// Original signed transform selections used for this comparison.
179    #[must_use]
180    pub fn transforms(&self) -> &Transforms {
181        &self.transforms
182    }
183    /// Every observed original indirect-object change, including indeterminate ones.
184    #[must_use]
185    pub fn changes(&self) -> &[Change] {
186        &self.changes
187    }
188    /// Completed normative answer, never a math or application trust verdict.
189    #[must_use]
190    pub fn outcome(&self) -> Outcome {
191        self.outcome
192    }
193    /// Exact signed and compared chronological revision ordinals.
194    #[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    /// Reads ISO32000-1 §12.8.2/Tables253,254,256 from the original signature.
328    /// Parameters are direct signed objects; `FieldMDP` Data is the specified
329    /// indirect catalog identity, not a caller-selected replacement graph.
330    /// <https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf>
331    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                    // ISO32000-1 Table254 declares a number: integral real
384                    // spellings are valid, fractional values cannot truncate.
385                    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    /// Compares the complete actual historical catalog closures and every
546    /// selected field value. Unavailable change semantics stay indeterminate;
547    /// they cannot become permitted by an empty caller-supplied change list.
548    ///
549    /// # Panics
550    /// Panics if an internally selected original graph node loses its owner.
551    #[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            // Superseded intermediate and unindexed definitions remain facts.
616            // Comparing only the final graph cannot authorize their semantics.
617            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    /// ISO32000-1 §12.8.2.2.2 first requires the byte-range digest to be
653    /// verified. This closed route accepts only this crate's actual range pass;
654    /// failed arithmetic/input facts cannot produce a positive permission.
655    /// Use inspection separately when reporting unauthenticated change facts.
656    #[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}