use crate::document::Document;
use crate::error::{Error, ErrorKind, ReadFailure, Syntax};
use crate::model::admit;
use crate::range::RangeSource;
use crate::signature::Signature;
use crate::source::Location;
use structured_asn1::{
Reader,
memory::{Memory, OwnedVec, Work},
};
use structured_cms::{ContentInfo, SignedData};
#[derive(Debug)]
pub enum ErrorOf<E> {
Pdf(ReadFailure<E>),
Pkix {
enclosure: Location,
cause: structured_pkix::Error,
},
Encoding {
enclosure: Location,
cause: structured_asn1::Error,
},
Cms {
enclosure: Location,
cause: structured_cms::Error,
},
}
impl<E> ErrorOf<E> {
#[must_use]
pub fn is_terminal(&self) -> bool {
match self {
Self::Pdf(cause) => cause.is_terminal(),
Self::Encoding { cause, .. } => cause.is_resource(),
Self::Cms { cause, .. } => cause.is_terminal(),
Self::Pkix { cause, .. } => {
cause.is_resource() || cause.code() == structured_pkix::ErrorCode::ForeignReader
}
}
}
}
pub struct Container<'a, 's, D, G> {
signature: Signature<'a, 's, D, G>,
bytes: OwnedVec<u8, Memory>,
der_length: usize,
session: &'s crate::Session,
}
impl<'a, 's, D: Copy + Eq, G: Copy + Eq> Container<'a, 's, D, G> {
pub(crate) fn require(
&self,
message: &SignedData<'_>,
parent: &Reader<'_>,
) -> crate::Result<()> {
let location = self.signature.contents();
self.session
.query(&parent.execution())
.charge_work(1)
.map_err(|cause| Error {
kind: ErrorKind::Resource(cause),
location,
})?;
let span = message.span();
let start = usize::try_from(span.start).map_err(|_| Error {
kind: ErrorKind::Foreign,
location,
})?;
let end = start.checked_add(message.as_bytes().len()).ok_or(Error {
kind: ErrorKind::Foreign,
location,
})?;
if !self.session.owns(parent)
|| self
.der()
.get(start..end)
.is_none_or(|bytes| !core::ptr::eq(bytes, message.as_bytes()))
{
return Err(Error {
kind: ErrorKind::Foreign,
location,
});
}
Ok(())
}
#[must_use]
pub fn signature(&self) -> Signature<'a, 's, D, G> {
self.signature
}
#[must_use]
pub fn contents(&self) -> &[u8] {
&self.bytes
}
#[must_use]
pub fn der(&self) -> &[u8] {
&self.bytes[..self.der_length]
}
#[expect(
clippy::result_large_err,
reason = "an unchanged timestamp grammar error retains its actual PDF enclosure without allocating an error envelope"
)]
pub fn read_timestamp<'q, W: Work + 'q>(
&'q self,
operation: &W,
) -> Result<
(structured_cms::timestamp::Token<'q>, Reader<'q>),
ErrorOf<core::convert::Infallible>,
> {
let enclosure = self.signature.contents();
let mut reader = self
.session
.carried(self.der(), operation)
.map_err(|cause| ErrorOf::Encoding { enclosure, cause })?;
let token = structured_cms::timestamp::Token::read_from(&mut reader)
.map_err(|cause| ErrorOf::Cms { enclosure, cause })?;
self.require(token.message(), &reader)
.map_err(|cause| ErrorOf::Pdf(cause.into()))?;
Ok((token, reader))
}
#[expect(
clippy::result_large_err,
reason = "the original CMS diagnostic and encoded PDF enclosure must survive the scoped reading without a fallible error allocation"
)]
pub fn read<'q, W: Work + 'q>(
&'q self,
operation: &W,
) -> Result<(SignedData<'q>, Reader<'q>), ErrorOf<core::convert::Infallible>> {
let enclosure = self.signature.contents();
let mut root = self
.session
.carried(self.der(), operation)
.map_err(|cause| ErrorOf::Encoding { enclosure, cause })?;
let (content, parent) =
ContentInfo::read(&mut root).map_err(|cause| ErrorOf::Cms { enclosure, cause })?;
let execution = parent.execution();
execution
.charge_work(content.kind().as_bytes().len() as u64)
.map_err(|cause| {
ErrorOf::Pdf(ReadFailure::Format(Error {
kind: ErrorKind::Resource(cause),
location: enclosure,
}))
})?;
if content.kind() != structured_cms::oid::SIGNED_DATA {
return Err(ErrorOf::Pdf(ReadFailure::Format(Error {
kind: ErrorKind::Syntax(Syntax::Container),
location: enclosure,
})));
}
let message = SignedData::read_element(&parent, content.content())
.map_err(|cause| ErrorOf::Cms { enclosure, cause })?;
Ok((message, parent))
}
}
impl<'s, S: RangeSource> Document<'s, S> {
#[expect(
clippy::result_large_err,
reason = "the original source/CMS refusal retains its original coordinate system rather than being boxed or flattened"
)]
pub async fn container<'a, W: Work>(
&self,
signature: Signature<'a, 's, S::Document, S::Generation>,
operation: &W,
) -> Result<Container<'a, 's, S::Document, S::Generation>, ErrorOf<S::Error>> {
let bytes = self
.contents(signature, operation)
.await
.map_err(ErrorOf::Pdf)?;
let enclosure = signature.contents();
let work = self.session.query(operation);
let mut reader = self
.session
.carried(&bytes, &work)
.map_err(|cause| ErrorOf::Encoding { enclosure, cause })?;
let element = reader
.next_element()
.map_err(|cause| ErrorOf::Encoding { enclosure, cause })?
.ok_or_else(|| {
ErrorOf::Pdf(ReadFailure::Format(Error {
kind: ErrorKind::Syntax(Syntax::String),
location: enclosure,
}))
})?;
let der_length = element.as_bytes().len();
admit(&work, enclosure, (bytes.len() - der_length) as u64)
.map_err(|cause| ErrorOf::Pdf(cause.into()))?;
if bytes[der_length..].iter().any(|&byte| byte != 0) {
return Err(ErrorOf::Pdf(
Error {
kind: ErrorKind::Syntax(Syntax::String),
location: enclosure,
}
.into(),
));
}
drop(reader);
Ok(Container {
signature,
bytes,
der_length,
session: self.session,
})
}
}