validate-xml validates an XML file or directory tree against XML Schema (XSD) using the system libxml2 library. It can discover schema hints from documents, fetch and cache standalone remote schemas, reuse compiled schemas, and validate files concurrently.
- Rust 1.98 or newer.
rust-toolchain.tomltracks the latest stable toolchain. - libxml2 development files.
pkg-configon Linux and macOS.
Install the native dependencies before building:
# Debian or Ubuntu
sudo apt-get install libxml2-dev pkg-config
# macOS with Homebrew
brew install libxml2 pkg-config
export PKG_CONFIG_PATH="$(brew --prefix libxml2)/lib/pkgconfig"
# Windows with vcpkg
vcpkg install libxml2:x64-windowsThe Windows build also needs the vcpkg x64-windows library directory in LIB and its binary directory in PATH; see the CI workflow for the exact setup.
git clone https://github.com/FranklinChen/validate-xml-rust.git
cd validate-xml-rust
cargo install --locked --path .Validate every .xml file below a directory:
validate-xml /path/to/documentsValidate one file against an explicit local schema:
validate-xml --schema /path/to/schema.xsd /path/to/document.xmlSelect more extensions and filesystem paths with globs:
validate-xml \
--extensions xml,cmdi \
--include '**/records/**' \
--exclude '**/fixtures/**' \
/path/to/documentsShow individual invalid/error results and performance information:
validate-xml --verbose /path/to/documentssamples/pom.xml exercises the standalone remote-schema path:
validate-xml samples/pom.xmlThat smoke test requires network access on the first run; subsequent runs can use the disk cache until its TTL expires.
Unless --schema is supplied, the validator reads schema hints from the XML prolog and document element:
xsi:schemaLocationnamespace/location pairs are retained, and the location matching the document element's namespace is selected.xsi:noNamespaceSchemaLocationis used for a document element without a namespace.<?xml-model href="…"?>is a fallback when no applicablexsihint exists.
Relative local schema paths are resolved against the XML document. Local XSDs receive a filesystem base URI, so relative xs:include and xs:import references work. The complete local composition graph contributes to the cache identity, so changing an included schema invalidates the compiled entry.
HTTP and HTTPS schema hints are normalized and downloaded by the application's HTTP client. A remote schema is compiled from memory without an HTTP base URI. Remote xs:include, xs:import, xs:redefine, or xs:override composition is therefore rejected instead of allowing libxml2 to fetch nested resources outside the application's network and cache policy.
validate-xml [OPTIONS] <PATH>
| Option | Default | Meaning |
|---|---|---|
<PATH> |
required | XML file or directory tree to validate |
-e, --extensions <LIST> |
xml |
Comma-separated file extensions |
-t, --threads <N> |
available parallelism | Maximum concurrent validations and blocking FFI operations |
-v, --verbose |
off | Include individual failures and performance information |
-q, --quiet |
off | Print only nonzero error/invalid counts |
--cache-dir <PATH> |
platform cache directory | Raw-schema disk-cache directory |
--cache-ttl <HOURS> |
24 |
Raw disk-cache TTL |
--timeout <SECONDS> |
30 |
Per-file validation deadline and HTTP request timeout |
--retry-attempts <N> |
3 |
Retry attempts for failed schema downloads |
--include <GLOB> |
none | Include matching paths; repeat for multiple patterns |
--exclude <GLOB> |
none | Exclude matching paths; repeat for multiple patterns |
--progress |
automatic on an interactive terminal | Show a progress indicator |
--fail-fast |
off | Stop scheduling after the first invalid/error result and drain admitted work |
--max-cache-size <MB> |
100 |
Indexed raw-schema disk budget; evict oldest entries first |
--schema <PATH> |
none | Use one local XSD for every input and skip hint extraction |
--verbose and --quiet are mutually exclusive. Zero threads, timeout, cache TTL, or cache size are rejected.
| Code | Meaning |
|---|---|
0 |
Every processed file is valid, or no invalid/error result occurred |
1 |
CLI, configuration, initialization, or top-level workflow error |
2 |
At least one file encountered a system, network, parsing, cache, or deadline error |
3 |
At least one file was well-formed but violated its schema |
Files without an applicable schema hint are reported as skipped and do not by themselves produce a nonzero exit code.
The pipeline is:
CLI → file discovery → schema-hint extraction → schema loading → compilation → validation → reporting
- Directory traversal uses
ignoreon Tokio's blocking pool and appliesglobsetinclude/exclude patterns. - File, HTTP, and cache I/O is asynchronous where appropriate. XML parsing and libxml2 work run on blocking workers.
- A semaphore bounds actual blocking schema-compilation and validation operations to
--threads. - A deadline cannot interrupt a libxml2 call that has already started. A timed-out call retains its semaphore permit until it returns, preventing later work from exceeding the configured FFI concurrency.
- Fail-fast stops admitting new files after the first invalid/error result but drains work already admitted.
- A
CompiledSchemacan only be constructed after libxml2 accepts the XSD. This keeps untrusted raw bytes distinct from compiled schemas in the type system.
Caching has three layers of responsibility:
- A bounded, TTL-aware
mokacache stores compiledCompiledSchemavalues and single-flights concurrent loads of the same key. - A second bounded, TTL-aware
mokacache stores raw schema bytes in memory. cacachestores raw schema bytes across processes. Application metadata provides expiry and oldest-first indexed-data eviction.
Local cache keys contain the canonical path plus a SHA-256 digest of the root XSD and every recursively referenced local schema. Remote cache keys use the normalized URL. Cache removal and clearing invalidate compiled and raw entries.
The FFI is confined to src/backend.rs. It uses checked buffer lengths, RAII wrappers, per-context structured error callbacks, and a distinct validation context per call. Schema compilation is serialized; successfully compiled schemas are immutable and shared. Caller-owned local schema documents remain alive for the compiled schema's lifetime and are freed after the schema.
Input documents are parsed with XML_PARSE_NONET and without entity substitution. The validator never calls libxml2's global cleanup function. Unix builds select libxml2 through pkg-config; macOS CI additionally verifies that the release binary links Homebrew's library instead of Apple's system copy.
These controls narrow the unsafe and I/O boundaries; they do not make arbitrary hostile XML or XSD risk-free. Keep libxml2 patched and use the tool only with documents and schemas from sources appropriate to your threat model. The separate libxml2-thread-safety-test stress-tests the shared-schema/per-call-context pattern, but is not a general security proof.
cargo fmt --all -- --check
cargo clippy --locked --all-targets --all-features -- -D warnings
cargo test --locked --all-features
RUSTDOCFLAGS='-D warnings' cargo doc --locked --all-features --no-deps
cargo build --locked --release --all-features
cargo bench --locked --all-features --no-runThe integration tests use Cargo's profile-correct CARGO_BIN_EXE_validate-xml binary. Run cargo test --lib for a faster unit-only loop. Divan benchmarks exercise the same libxml2 compile/validate path as the application; this repository does not publish historical microbenchmark numbers as current guarantees.
See AGENTS.md for repository architecture notes and CHANGELOG.md for release history.