mod declaration;
mod types;
use crate::container::{Layout, XADES};
use crate::pki::CounterPaths;
use crate::prepare::Prepared;
use crate::reference::{LexicalSource, VisitError};
use crate::scalar::{self, Datatype, QNameFact, QNameXml, ScalarXml};
use crate::timestamp::XADES_141;
use crate::xml::{Access, AccessError, Name, Node, Snapshot, Xml};
use structured_asn1::memory::{MemoryError, Work};
pub trait AttributeXml: Xml + QNameXml {
fn visit_attributes<VE, W: Work>(
&self,
snapshot: Snapshot<Self::Document, Self::Generation>,
node: Self::Id,
execution: &W,
visitor: impl for<'view> FnMut(Name<'view>) -> Result<(), VE>,
) -> Result<(), VisitError<Self::Error, VE>>;
}
pub fn visit<X: AttributeXml, W: Work, E>(
access: &Access<'_, X, W>,
node: X::Id,
visitor: impl for<'view> FnMut(Name<'view>) -> Result<(), E>,
) -> crate::xml::VisitResult<(), X, E> {
access.query_fallible(node, |xml, snapshot, work| {
xml.visit_attributes(snapshot, node, work, visitor)
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InstanceAttribute {
Type,
SchemaLocation,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TypeProblem {
Absent,
ComplexContent,
Malformed,
UnboundPrefix,
}
#[derive(Debug)]
pub enum Error<E, N> {
Foreign,
Access(AccessError<E, N>),
Scalar(scalar::Error<E, N>),
Unexpected {
node: N,
attribute: usize,
},
Missing {
node: N,
local: &'static str,
},
InstanceUnassessed {
node: N,
kind: InstanceAttribute,
},
Type {
node: N,
problem: TypeProblem,
},
Unassessed {
node: N,
},
}
pub type AttributeResult<X> = Result<(), Error<<X as Xml>::Error, <X as Xml>::Id>>;
#[derive(Clone, Copy)]
enum Rule {
None,
Id,
Target,
Encoding,
Uri,
IdUri,
RequiredUri,
Include,
Format,
Identifier,
Order,
Open,
}
#[derive(Clone, Copy)]
enum Context {
Ordinary,
Renewed,
CertifiedV2,
CertV2,
}
struct Declaration {
rule: Rule,
descend: bool,
context: Context,
schema_type: Name<'static>,
}
#[derive(Clone, Copy)]
enum Field {
Typed(Datatype),
Required(Datatype),
Declared,
}
fn field(rule: Rule, local: &str) -> Option<Field> {
Some(match (rule, local) {
(Rule::Id | Rule::Target | Rule::Encoding | Rule::IdUri | Rule::RequiredUri, "Id") => {
Field::Typed(Datatype::Id)
}
(Rule::Target, "Target")
| (Rule::Format, "ObjectReference")
| (Rule::Include | Rule::RequiredUri, "URI") => Field::Required(Datatype::AnyUri),
(Rule::Encoding, "Encoding") | (Rule::Uri | Rule::IdUri, "URI") => {
Field::Typed(Datatype::AnyUri)
}
(Rule::Order, "Order") => Field::Required(Datatype::Integer),
(Rule::Identifier, "Qualifier") | (Rule::Include, "referencedData") => Field::Declared,
_ => return None,
})
}
use self::declaration::declaration;
pub fn check_prepared<X: AttributeXml + ScalarXml, W: Work>(
prepared: &Prepared<X>,
access: &Access<'_, X, W>,
) -> AttributeResult<X> {
let layout = prepared.bound().containers();
if layout.snapshot() != access.snapshot() {
return Err(Error::Foreign);
}
check_layout(layout, access)?;
access
.query(layout.signature(), |_, _, _| Ok(()))
.map_err(Error::Access)
}
pub fn check<X: AttributeXml + ScalarXml, W: Work>(
prepared: &Prepared<X>,
counters: &CounterPaths<'_, '_, X::Id, X::Document, X::Generation>,
access: &Access<'_, X, W>,
) -> AttributeResult<X> {
require_participants(prepared, counters, access)?;
check_prepared(prepared, access)?;
check_counter_nodes(prepared, counters, access)
}
fn require_participants<X: AttributeXml + ScalarXml, W: Work>(
prepared: &Prepared<X>,
counters: &CounterPaths<'_, '_, X::Id, X::Document, X::Generation>,
access: &Access<'_, X, W>,
) -> AttributeResult<X> {
let layout = prepared.bound().containers();
if layout.snapshot() != access.snapshot()
|| counters.snapshot() != access.snapshot()
|| !access
.equal_id(layout.signature(), counters.signature())
.map_err(|cause| resource(layout.signature(), cause))?
{
return Err(Error::Foreign);
}
Ok(())
}
pub(crate) fn check_counters<X: AttributeXml + ScalarXml, W: Work>(
prepared: &Prepared<X>,
counters: &CounterPaths<'_, '_, X::Id, X::Document, X::Generation>,
access: &Access<'_, X, W>,
) -> AttributeResult<X> {
require_participants(prepared, counters, access)?;
check_counter_nodes(prepared, counters, access)
}
fn check_counter_nodes<X: AttributeXml + ScalarXml, W: Work>(
prepared: &Prepared<X>,
counters: &CounterPaths<'_, '_, X::Id, X::Document, X::Generation>,
access: &Access<'_, X, W>,
) -> AttributeResult<X> {
let layout = prepared.bound().containers();
for actor in counters.actors() {
access
.execution()
.charge_work(1)
.map_err(|cause| resource(layout.signature(), cause))?;
if let Some(layout) = actor.containers() {
check_layout(layout, access)?;
}
}
access
.query(layout.signature(), |_, _, _| Ok(()))
.map_err(Error::Access)
}
struct Frame<N> {
parent: N,
next: Option<N>,
context: Context,
}
fn check_layout<X: AttributeXml + ScalarXml, W: Work>(
layout: &Layout<X>,
access: &Access<'_, X, W>,
) -> AttributeResult<X> {
let mut frames = access.execution().vector();
let mut next = Some((layout.qualifying(), Context::Ordinary));
loop {
if let Some((node, parent)) = next.take() {
let selected = access
.inspect(node, |value| {
let Node::Element(name) = value else {
return Ok(None);
};
let length = name
.namespace
.len()
.checked_add(name.local.len())
.and_then(|cost| u64::try_from(cost).ok())
.ok_or(MemoryError::WorkExceeded)?;
access.execution().charge_work(length)?;
if name.namespace != XADES && name.namespace != XADES_141 {
return Ok(None);
}
Ok(Some(declaration(name, parent)))
})
.map_err(Error::Access)?
.map_err(|cause| resource(node, cause))?;
if let Some(selected) = selected {
let selected = selected.ok_or(Error::Unassessed { node })?;
check_node(node, selected.rule, selected.schema_type, access)?;
if selected.descend {
let child = access.first_child(node).map_err(Error::Access)?;
frames
.try_push(Frame {
parent: node,
next: child,
context: selected.context,
})
.map_err(|cause| resource(node, cause))?;
}
}
}
access
.execution()
.charge_work(1)
.map_err(|cause| resource(layout.qualifying(), cause))?;
let Some(frame) = frames.last_mut() else {
break;
};
if let Some(child) = frame.next {
frame.next = access
.next_sibling(frame.parent, child)
.map_err(Error::Access)?;
next = Some((child, frame.context));
} else {
frames.pop();
}
}
Ok(())
}
fn resource<E, N>(node: N, cause: MemoryError) -> Error<E, N> {
Error::Access(AccessError::Resource { node, cause })
}
fn check_node<X: AttributeXml + ScalarXml, W: Work>(
node: X::Id,
rule: Rule,
schema_type: Name<'static>,
access: &Access<'_, X, W>,
) -> AttributeResult<X> {
let mut ordinal = 0usize;
let mut required = false;
let visited = access.query_fallible(node, |xml, snapshot, work| {
xml.visit_attributes(snapshot, node, work, |name| {
let cost = u64::try_from(name.local.len())
.ok()
.and_then(|length| length.checked_add(1))
.ok_or_else(|| resource(node, MemoryError::WorkExceeded))?;
work.charge_work(cost)
.map_err(|cause| resource(node, cause))?;
let current = ordinal;
ordinal = ordinal
.checked_add(1)
.ok_or_else(|| resource(node, MemoryError::WorkExceeded))?;
if access
.equal(name.namespace, "http://www.w3.org/2001/XMLSchema-instance")
.map_err(|cause| resource(node, cause))?
{
if name.local == "nil" {
return Err(Error::Unexpected {
node,
attribute: current,
});
}
if name.local == "noNamespaceSchemaLocation" {
return scalar::check(
access,
node,
LexicalSource::Attribute(name),
Datatype::AnyUri,
)
.map_err(Error::Scalar);
}
let kind = match name.local {
"type" => Some(InstanceAttribute::Type),
"schemaLocation" => Some(InstanceAttribute::SchemaLocation),
_ => None,
};
if let Some(kind) = kind {
if matches!(kind, InstanceAttribute::Type)
&& same_declared_type(node, name, schema_type, access)?
{
return Ok(());
}
return Err(Error::InstanceUnassessed { node, kind });
}
}
if matches!(rule, Rule::Open) {
return Ok(());
}
if !name.namespace.is_empty() {
return Err(Error::Unexpected {
node,
attribute: current,
});
}
let selected = field(rule, name.local).ok_or(Error::Unexpected {
node,
attribute: current,
})?;
if matches!(selected, Field::Required(_)) {
required = true;
}
if let Field::Typed(datatype) | Field::Required(datatype) = selected {
scalar::check(access, node, LexicalSource::Attribute(name), datatype)
.map_err(Error::Scalar)?;
}
Ok(())
})
});
match visited {
Ok(()) => (),
Err(AccessError::Xml(VisitError::Processing(cause))) => {
return Err(Error::Access(AccessError::Xml(cause)));
}
Err(AccessError::Xml(VisitError::Visitor(cause))) => return Err(cause),
Err(AccessError::Changed { node }) => {
return Err(Error::Access(AccessError::Changed { node }));
}
Err(AccessError::Resource { node, cause }) => return Err(resource(node, cause)),
}
if !required {
let local = match rule {
Rule::Target => Some("Target"),
Rule::Include | Rule::RequiredUri => Some("URI"),
Rule::Format => Some("ObjectReference"),
Rule::Order => Some("Order"),
_ => None,
};
if let Some(local) = local {
return Err(Error::Missing { node, local });
}
}
Ok(())
}
fn same_declared_type<X: AttributeXml, W: Work>(
node: X::Id,
attribute: Name<'_>,
expected: Name<'static>,
access: &Access<'_, X, W>,
) -> Result<bool, Error<X::Error, X::Id>> {
enum Reading {
Compared(bool),
Problem(TypeProblem),
}
let reading = access.query_fallible(node, |xml, snapshot, work| {
xml.with_qname(
snapshot,
node,
LexicalSource::Attribute(attribute),
work,
|read| {
let value = match read.fact {
QNameFact::Value(value) => value,
QNameFact::Unsupported => return Ok(Reading::Compared(false)),
QNameFact::Absent => return Ok(Reading::Problem(TypeProblem::Absent)),
QNameFact::ComplexContent => {
return Ok(Reading::Problem(TypeProblem::ComplexContent));
}
QNameFact::Malformed => return Ok(Reading::Problem(TypeProblem::Malformed)),
QNameFact::UnboundPrefix(_) => {
return Ok(Reading::Problem(TypeProblem::UnboundPrefix));
}
};
let Some(namespace) = value.namespace else {
return Ok(Reading::Compared(false));
};
if !access.equal(namespace, expected.namespace)? {
return Ok(Reading::Compared(false));
}
access
.equal(value.local_name, expected.local)
.map(Reading::Compared)
},
)
});
match reading {
Ok(Reading::Compared(value)) => Ok(value),
Ok(Reading::Problem(problem)) => Err(Error::Type { node, problem }),
Err(AccessError::Xml(VisitError::Processing(cause))) => {
Err(Error::Access(AccessError::Xml(cause)))
}
Err(AccessError::Xml(VisitError::Visitor(cause))) => Err(resource(node, cause)),
Err(AccessError::Changed { node }) => Err(Error::Access(AccessError::Changed { node })),
Err(AccessError::Resource { node, cause }) => Err(resource(node, cause)),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn certified_role_attribute_type_depends_on_its_original_parent_declaration() {
let parent = declaration::declaration(
Name {
namespace: XADES,
local: "CertifiedRolesV2",
},
Context::Ordinary,
)
.unwrap();
let wrapper = declaration::declaration(
Name {
namespace: XADES,
local: "CertifiedRole",
},
parent.context,
)
.unwrap();
assert!(matches!(wrapper.rule, Rule::None));
assert!(wrapper.descend);
let legacy = declaration::declaration(
Name {
namespace: XADES,
local: "CertifiedRole",
},
Context::Ordinary,
)
.unwrap();
assert!(matches!(legacy.rule, Rule::Encoding));
assert!(!legacy.descend);
}
}