use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::borrow::Cow;
use sup_xml::{encoding, parse_bytes, EntityResolver, ParseOptions, ResolveError};
const KNOWN_FAILING_IDS: &[&str] = &[];
#[derive(Debug)]
struct FixtureResolver {
root_dir: PathBuf,
test_dir: PathBuf,
}
impl EntityResolver for FixtureResolver {
fn resolve(
&self,
_public_id: Option<&str>,
system_id: &str,
_base_uri: Option<&str>,
) -> Result<Vec<u8>, ResolveError> {
let stripped = system_id.strip_prefix("file://").unwrap_or(system_id);
if stripped.contains("://") {
return Err(ResolveError::Refused(format!(
"FixtureResolver only handles file:// URIs, got: {system_id}"
)));
}
let raw = if Path::new(stripped).is_absolute() {
PathBuf::from(stripped)
} else {
self.test_dir.join(stripped)
};
let canonical = raw.canonicalize().map_err(|_| ResolveError::Refused(
format!("path {} not found", raw.display())
))?;
let root_canonical = self.root_dir.canonicalize().map_err(|_| ResolveError::Refused(
format!("root {} not canonicalisable", self.root_dir.display())
))?;
if !canonical.starts_with(&root_canonical) {
return Err(ResolveError::Refused(format!(
"path {} escapes catalog root {}",
canonical.display(), root_canonical.display()
)));
}
std::fs::read(&canonical).map_err(|e| ResolveError::Io(
format!("reading {}: {e}", canonical.display())
))
}
}
fn w3c_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../tests/w3c")
.canonicalize()
.expect("tests/w3c assets directory must exist")
}
const CATALOGS: &[&str] = &[
"xmltest/xmltest.xml",
"sun/sun-valid.xml",
"sun/sun-not-wf.xml",
"sun/sun-invalid.xml",
"oasis/oasis.xml",
"ibm/ibm_oasis_valid.xml",
"ibm/ibm_oasis_not-wf.xml",
"ibm/ibm_oasis_invalid.xml",
"eduni/errata-2e/errata2e.xml",
"eduni/errata-3e/errata3e.xml",
"eduni/errata-4e/errata4e.xml",
"eduni/namespaces/1.0/rmt-ns10.xml",
"eduni/misc/ht-bh.xml",
];
#[derive(Debug, PartialEq)]
enum TestType {
Valid,
Invalid,
NotWf,
Error,
}
struct TestCase {
id: String,
ty: TestType,
path: PathBuf,
needs_external_entities: bool,
fourth_edition: bool,
namespace_aware: bool,
}
fn attr<'a>(line: &'a str, key: &str) -> Option<&'a str> {
let needle_dq = format!("{key}=\"");
let needle_sq = format!("{key}='");
if let Some(start) = line.find(&needle_dq) {
let rest = &line[start + needle_dq.len()..];
rest.find('"').map(|end| &rest[..end])
} else if let Some(start) = line.find(&needle_sq) {
let rest = &line[start + needle_sq.len()..];
rest.find('\'').map(|end| &rest[..end])
} else {
None
}
}
fn load_catalog(catalog_path: &Path) -> Vec<TestCase> {
let base_dir = catalog_path.parent().unwrap().to_path_buf();
let text = match std::fs::read_to_string(catalog_path) {
Ok(t) => t,
Err(e) => {
eprintln!("WARN: could not read catalog {:?}: {e}", catalog_path);
return Vec::new();
}
};
let mut cases = Vec::new();
let mut current = String::new();
let mut in_test = false;
for line in text.lines() {
let trimmed = line.trim();
if trimmed.starts_with("<TEST ") || (in_test && !trimmed.is_empty()) {
current.push(' ');
current.push_str(trimmed);
in_test = true;
}
if in_test && (trimmed.contains('>') || trimmed.ends_with("/>")) {
let elem = ¤t;
if let (Some(id), Some(ty_str), Some(uri)) = (
attr(elem, "ID"),
attr(elem, "TYPE"),
attr(elem, "URI"),
) {
if attr(elem, "VERSION") == Some("1.1") {
current.clear();
in_test = false;
continue;
}
let ty = match ty_str {
"valid" => TestType::Valid,
"invalid" => TestType::Invalid,
"not-wf" => TestType::NotWf,
"error" => TestType::Error,
_ => { current.clear(); in_test = false; continue; }
};
let entities = attr(elem, "ENTITIES").unwrap_or("none");
let needs_external = matches!(entities, "general" | "parameter" | "both");
let fourth_edition = id.starts_with("ibm-not-wf-P85-")
|| id.starts_with("ibm-not-wf-P86-")
|| id.starts_with("ibm-not-wf-P87-")
|| id.starts_with("ibm-not-wf-P88-")
|| id.starts_with("ibm-not-wf-P89-")
|| id == "not-wf-sa-140"
|| id == "not-wf-sa-141";
let recommendation = attr(elem, "RECOMMENDATION").unwrap_or("");
let namespace_attr = attr(elem, "NAMESPACE").unwrap_or("");
let namespace_aware = recommendation == "NS1.0" && namespace_attr != "no";
cases.push(TestCase {
id: id.to_string(),
ty,
path: base_dir.join(uri),
needs_external_entities: needs_external,
fourth_edition,
namespace_aware,
});
}
current.clear();
in_test = false;
}
}
cases
}
#[test]
fn w3c_xml_conformance() {
let root = w3c_root();
if !root.exists() {
eprintln!("SKIP: W3C test suite not found at {root:?}");
return;
}
let mut total = 0usize;
let mut passed = 0usize;
let mut failed = 0usize;
let mut skipped = 0usize;
let mut xfail = 0usize; let mut unexpected_pass = 0usize; let mut failures: Vec<String> = Vec::new();
let mut unexpected_passes: Vec<String> = Vec::new();
for catalog_rel in CATALOGS {
let catalog_path = root.join(catalog_rel);
if !catalog_path.exists() {
eprintln!("WARN: catalog not found: {catalog_path:?}");
continue;
}
for case in load_catalog(&catalog_path) {
total += 1;
if case.ty == TestType::Error {
skipped += 1;
continue;
}
let bytes = match std::fs::read(&case.path) {
Ok(b) => b,
Err(e) => {
skipped += 1;
eprintln!("WARN: could not read {:?}: {e}", case.path);
continue;
}
};
let external_resolver = if case.needs_external_entities {
case.path.parent().map(|test_dir| {
Arc::new(FixtureResolver {
root_dir: root.clone(),
test_dir: test_dir.to_path_buf(),
}) as Arc<dyn EntityResolver>
})
} else {
None
};
let base_url = format!("file://{}", case.path.display());
let opts = ParseOptions {
xml10_fourth_edition: case.fourth_edition,
namespace_aware: case.namespace_aware,
auto_transcode: false,
external_resolver,
load_external_dtd: case.needs_external_entities,
base_url: Some(base_url),
..ParseOptions::default()
};
let utf8: Cow<[u8]> = match encoding::transcode_to_utf8_strict(&bytes) {
Ok(c) => c,
Err(e) => {
match case.ty {
TestType::NotWf => {
passed += 1;
continue;
}
_ => {
failed += 1;
failures.push(format!(
"FAIL {} {}: encoding rejection unexpected, got: {}",
match case.ty { TestType::Valid => "valid", TestType::Invalid => "invalid", _ => "?" },
case.id, e.message,
));
continue;
}
}
}
};
let result = parse_bytes(&utf8, &opts);
let known_failing = KNOWN_FAILING_IDS.contains(&case.id.as_str());
let test_passed = match case.ty {
TestType::NotWf => result.is_err(),
TestType::Valid | TestType::Invalid => result.is_ok(),
TestType::Error => unreachable!(),
};
match (test_passed, known_failing) {
(true, false) => passed += 1,
(false, true) => xfail += 1,
(false, false) => {
failed += 1;
failures.push(match case.ty {
TestType::NotWf => format!(
"FAIL not-wf {}: should have been rejected but parsed OK",
case.id),
TestType::Valid | TestType::Invalid => format!(
"FAIL {} {}: should have parsed OK, got: {}",
if case.ty == TestType::Valid { "valid" } else { "invalid" },
case.id,
result.unwrap_err().message),
TestType::Error => unreachable!(),
});
}
(true, true) => {
unexpected_pass += 1;
unexpected_passes.push(format!(
"UNEXPECTED PASS {} {}: in KNOWN_FAILING_IDS but now passes — remove the allowlist entry",
match case.ty {
TestType::NotWf => "not-wf",
TestType::Valid => "valid",
TestType::Invalid => "invalid",
TestType::Error => "?",
},
case.id,
));
}
}
}
}
println!(
"\nW3C XML Conformance: {passed} passed, {failed} failed, \
{xfail} xfail (allow-listed), {skipped} skipped (of {total} total)"
);
let limit = std::env::var("W3C_FAILURES").ok()
.and_then(|s| s.parse::<usize>().ok())
.unwrap_or(40);
for f in failures.iter().take(limit) {
println!(" {f}");
}
if failures.len() > limit {
println!(" ... and {} more failures", failures.len() - limit);
}
for u in &unexpected_passes {
println!(" {u}");
}
assert_eq!(
failed, 0,
"{failed} W3C conformance tests failed (see the FAIL lines above)"
);
assert_eq!(
unexpected_pass, 0,
"{unexpected_pass} test(s) on the KNOWN_FAILING_IDS allowlist now pass — \
remove them from the allowlist in crates/api/tests/w3c.rs"
);
}