Why this crate
Schematron is a small standard with a large dependency footprint everywhere else. This page is the case for not having one.
Dependencies
No C, no XSLT, no JVM
The conventional way to run Schematron is to compile the schema into XSLT
using the ISO reference stylesheets, then run that XSLT. That works, and it
means a Rust project acquires either a libxml2 build, an FFI
boundary and a system package to install, or a Saxon process and a JVM to
start.
This crate implements the standard directly. The XML parser, the XPath 1.0
engine, the schema compiler, and the validator are all Rust, in this crate,
with no unsafe. The build story is cargo add, on
every platform Rust targets — musl static binaries and
cross-compiles included.
Interpreting rather than transpiling has a second benefit: error messages
point at your schema, not at generated XSLT. A mistake in a @context is reported as a mistake in a @context.
Security
XXE is structurally impossible
The XML parser never resolves an external entity and never processes a DTD's entity declarations. That is not a switch that defaults to off — the code path does not exist. A reference to a DTD-declared entity is an error.
The default resolver reads local files and refuses http: and https: URIs. Network access is something an application opts
into by supplying its own Resolver, never something the library
does behind your back. The command line tool has no network flag at all.
Parse depth, include depth, and expression nesting are all bounded, and
exceeding a bound returns an error rather than exhausting the stack. Four cargo-fuzz targets exist to keep that true.
Performance
Compile once, validate many
Compiling is the expensive step: it resolves includes, expands abstractions, and parses every XPath expression in the schema. It happens once. The compiled schema is then reused across documents and across threads.
Rule contexts are evaluated once per document rather than tested node by node, so matching is linear rather than quadratic in document size.
Schema is immutable and Send + Sync:
use std::sync::Arc;
let schema = Arc::new(Schema::from_path("rules.sch")?);
for path in paths {
let schema = Arc::clone(&schema);
std::thread::spawn(move || {
let document = Document::from_path(path)?;
schema.validate(&document)
});
}Patterns are independent, so a single schema with several of them can evaluate them on separate threads. The report is identical either way — this is a performance switch, not a behaviour switch:
let options = ValidateOptions::new().with_parallel_patterns(true);
let report = schema.validate_with(&document, &options)?;Measure before turning it on: on a small document the threads cost more
than they save. Indicative figures and the benchmarks that produce them
live in spec/testing/; run cargo bench yourself, because the numbers in any README are somebody else's hardware.
Stability
What you can depend on
- MSRV of 1.96, under a stated policy: current stable minus two. Not a number that moves when a dependency feels like it.
- A normative specification. If the code and
spec/disagree, that is a defect in one of them — and the test suite checks the docs against the code, so it is usually caught. - Divergences stated up front. Every place this crate differs from the ISO reference implementation is written down, with the measurement that established it.
- Unsupported constructs error by name. Outside the implemented XPath 2.0 and 3.0 subsets you get a hard error naming the construct, never a plausible wrong answer.