oa_gateway_uci/xsd/assemble.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143
use roxmltree::Document;
use crate::schema::{GlobalElement, Schema};
use crate::UciError;
use super::node_util::{definitions, is_xs, required, type_ref};
use super::parse::{complex_type, simple_type};
use super::references::check_references;
/// Compile XSD documents into a single [`Schema`].
///
/// Pass every document the schema spans; `xs:include` and `xs:import`
/// directives are not followed, because resolving them would mean reading paths
/// out of the schema text. Anything left dangling is reported by name, so a
/// forgotten document surfaces as a clear error rather than a missing type at
/// conversion time.
///
/// # Errors
///
/// Returns [`UciError::Xsd`] if a document is not well-formed, uses a construct
/// outside the supported subset, defines the same name twice, or leaves a type
/// reference unresolved.
///
/// # Examples
///
/// ```
/// # use oa_gateway_uci::xsd;
/// let schema = xsd::compile(&[r#"
/// <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
/// xmlns:uci="urn:example"
/// targetNamespace="urn:example">
/// <xs:element name="Ping" type="uci:PingType"/>
/// <xs:complexType name="PingType">
/// <xs:sequence>
/// <xs:element name="n" type="xs:int" minOccurs="0"/>
/// </xs:sequence>
/// </xs:complexType>
/// </xs:schema>
/// "#])?;
///
/// assert_eq!(schema.global_type("Ping"), Some("PingType"));
/// # Ok::<(), oa_gateway_uci::UciError>(())
/// ```
pub fn compile(documents: &[&str]) -> Result<Schema, UciError> {
let mut schema = Schema::new();
for (index, text) in documents.iter().enumerate() {
// A schema is operator-supplied rather than peer-supplied, so this is
// about a truncated download or the wrong file, not an attacker. It
// still has to fail as an error: the parser would otherwise exhaust the
// stack, and a gateway that aborts at startup says nothing about why.
if crate::xml::nesting_exceeds(text, crate::MAX_DEPTH) {
return Err(UciError::Xsd(format!(
"document {index} nests deeper than {} elements",
crate::MAX_DEPTH
)));
}
let doc = Document::parse(text)
.map_err(|e| UciError::Xsd(format!("document {index} is not well-formed XML: {e}")))?;
add_document(&doc, &mut schema)?;
}
check_references(&schema)?;
Ok(schema)
}
/// Merges one `<xs:schema>` into `schema`. `include` / `import` are
/// ignored; the caller must pass those documents separately.
fn add_document(doc: &Document<'_>, schema: &mut Schema) -> Result<(), UciError> {
let root = doc.root_element();
if !is_xs(root) || root.tag_name().name() != "schema" {
return Err(UciError::Xsd(format!(
"root element is <{}>, expected <xs:schema>",
root.tag_name().name()
)));
}
for child in definitions(root)? {
match child.tag_name().name() {
"element" => {
let name = required(child, "name")?.to_owned();
let type_name = type_ref(child, required(child, "type").map_err(|_| {
UciError::Xsd(format!(
"global element '{name}' has no type=; anonymous inline types are not supported"
))
})?);
claim(schema, &name, Definition::Element)?;
schema
.global_elements
.insert(name, GlobalElement { type_name });
}
"complexType" => {
let ct = complex_type(child)?;
claim(schema, &ct.name, Definition::Complex)?;
schema.complex_types.insert(ct.name.clone(), ct);
}
"simpleType" => {
let (name, simple) = simple_type(child)?;
claim(schema, &name, Definition::Simple)?;
schema.simple_types.insert(name, simple);
}
// Resolved by the caller supplying every document, not by us
// reading paths out of the schema text.
"include" | "import" => {}
other => {
return Err(UciError::Xsd(format!(
"top-level 'xs:{other}' is outside the supported subset"
)))
}
}
}
Ok(())
}
/// Which map a name would land in, used to report redefinitions precisely.
#[derive(Clone, Copy)]
enum Definition {
Element,
Complex,
Simple,
}
/// Reject a redefinition rather than letting the last document silently win.
///
/// The most likely cause is passing the same document twice — the standard ships
/// more than one copy of the message definitions — which would otherwise look
/// like it worked.
fn claim(schema: &Schema, name: &str, kind: Definition) -> Result<(), UciError> {
let taken = match kind {
Definition::Element => schema.global_elements.contains_key(name),
Definition::Complex => schema.complex_types.contains_key(name),
Definition::Simple => schema.simple_types.contains_key(name),
};
if taken {
let what = match kind {
Definition::Element => "global element",
Definition::Complex => "complexType",
Definition::Simple => "simpleType",
};
return Err(UciError::Xsd(format!(
"{what} '{name}' is defined twice; is the same document passed more than once?"
)));
}
Ok(())
}