This file provides guidance to coding agents working in this repository.
validate-xml is a high-performance CLI tool for validating XML files against XML Schemas (XSD), built with the latest stable Rust toolchain (edition 2024). Hardened libxml2 is the single authoritative validator. The crate uses concurrent async processing and two-tier caching (memory + disk).
cargo build --release # Build release binary
cargo test # Run all tests (unit + integration)
cargo test <test_name> # Run a single test by name (substring match)
cargo test --lib # Run only library unit tests (skip integration)
cargo test --test integration_tests # Run only integration tests
cargo bench # Run benchmarks (divan harness)
cargo fmt # Format code
cargo clippy # Lint
cargo install --path . # Install binary to ~/.cargo/binIntegration tests (tests/integration_tests.rs) invoke the binary Cargo builds for the active test profile through CARGO_BIN_EXE_validate-xml; no separate release build is required. Use cargo test --lib for a faster unit-test-only loop.
The samples/pom.xml file exercises the remote schema fetch path (Apache Maven POM schema) and is useful for smoke-testing HTTP + disk cache behavior end-to-end.
The validation pipeline flows: CLI parsing → file discovery → schema extraction → schema loading (with caching) → parallel validation → output formatting.
- main.rs — Entry point, sets up tokio runtime, progress bars (indicatif), exit codes (0=success, 1=config error, 2=system error, 3=validation failures)
- lib.rs — Re-exports the public library surface (see
src/lib.rs); the crate builds as both a binary and a library. - validator.rs — Core engine. Hybrid async/sync: async I/O for file/network ops, while backend compilation/validation runs through bounded
tokio::task::spawn_blockingcalls. Timeout reports a deadline failure but cannot interrupt an active FFI call; its permit remains held until return. Fail-fast stops scheduling and drains admitted work. - backend.rs —
CompiledSchematypestate boundary and hardened libxml2 validation. The FFI serializes initialization/schema compilation and creates a distinct validation context for every call. - cache.rs — Two-tier caching: L1 in-memory (
moka::CacheforCompiledSchema, with thundering-herd protection, bounded capacity, and TTL) and L2 disk (cacachewith TTL and size eviction). - schema_loader.rs — Extracts schema URLs from XML and loads the referenced XSDs. Several non-obvious design points:
- Streaming extraction: uses
quick-xml's namespace-awareNsReaderto walk only the prolog + root element's start tag, then stops. The read runs on a tokio blocking thread over a syncstd::io::BufReader, so the rest of the XML body never enters memory regardless of file size. - Schema-URL sources:
xsi:schemaLocation(whitespace-separated(namespace, location)+pairs retained for root-namespace matching),xsi:noNamespaceSchemaLocation, and<?xml-model href=... ?>processing instructions. - Namespace-URI attribute resolution: xsi attributes are matched by namespace URI (
http://www.w3.org/2001/XMLSchema-instance), not by literalxsi:prefix, so documents that bind xsi to a non-standard prefix still work. - Odd-token
schemaLocationsurfaces asValidationError::XmlParsing, not silently dropped. SchemaReferenceis an enum (Local(PathBuf)/Remote(String)). It was previously a struct with aurl: Stringfield that, forLocalrefs, held the unresolved raw reference and was used as the L1 cache key — two XML files in different directories both referencing"schema.xsd"would silently share a cache entry.SchemaReference::cache_key()provides a namespaced location identity;SchemaLoader::cache_key()additionally versions local schemas with the complete graph digest used by both cache tiers.- Loading paths: local files via
tokio::fs, remote viaAsyncHttpClient, both routed through the two-tier cache. - Root-element probe: fetched XSD bytes are checked (root element must be
{http://www.w3.org/2001/XMLSchema}schema) before being cached, so HTML/404 responses don't poison the disk cache.
- Streaming extraction: uses
- http_client.rs — Async HTTP client (reqwest) with exponential backoff retries.
- file_discovery.rs — Recursive
ignoretraversal on Tokio's blocking pool withglobset-based pattern matching. - cli.rs — Clap derive-based argument parsing. Key flags:
--schema(override XSD),--threads,--cache-ttl,--extensions.--schemapopulatesValidationConfig.schema_overrideand short-circuitsschema_loader.rs's extraction path entirely — useful to remember when a validation issue might lie in extraction vs. validation itself. - error.rs — Error hierarchy using
thiserror. Main types:ValidationError,ConfigError,CacheError,NetworkError. - output.rs — Color-coded TTY-aware output formatting with verbosity levels.
- Compiled-schema boundary keeps fetched bytes distinct from schemas accepted by libxml2. Local schemas receive a filesystem base URI for relative includes/imports; remote schemas remain memory-only so libxml2 cannot bypass the HTTP client. The external thread-safety stress repository supports the shared-schema/per-call-context pattern but is not a general security proof.
- Hybrid async/sync separates I/O-bound work (Tokio) from CPU-bound validation.
spawn_blockingprevents synchronous parsing and validation from occupying Tokio worker threads. - Globset replaced regex for file pattern matching (more intuitive, better performance).
- Two-tier cache separation: L1 stores
CompiledSchema(work reuse, moka single-flight); L2 stores raw schema bytes on disk (cacache, survives process restarts). A canonical path plus SHA-256 content digest versions local keys.
Errors flow through ValidationError (src/error.rs), a thiserror enum that distinguishes configuration, network, cache, schema-parsing (XSD side), XML-parsing (input-document side), and validation-failure cases. SchemaParsing { url, details } is for XSD content; XmlParsing { file, details } is for the XML input file being validated — don't conflate them (the old code stuffed XML file paths into SchemaParsing.url, which was a leaky abstraction). Exit-code mapping in main.rs depends on aggregate counters (error_files, invalid_files), not on the variant itself — add new error categories by extending the enum rather than stringly-typed messages, so the exit-code contract in the README stays stable.