From c355daa00eaee2b4718f64874581b5355a59d64d Mon Sep 17 00:00:00 2001 From: Mihaela Mihaljevic Date: Wed, 24 Jun 2026 03:04:45 +0200 Subject: [PATCH] test(xslt): decode non-UTF-8 conformance sources via the library's encoding detection (#261) The Xalan harness read source and stylesheet files as UTF-8, so a UTF-16 source (output80) or other non-UTF-8 input (attribvaltemplate08) was refused as unreadable before the transform ran. Read them through PureXML's own ByteDecoder (BOM or XML-declaration detection, UTF-8 fallback), as the harness already does for gold files. The library round-trips them correctly, so both cases now pass. Closes Apache Xalan conformance cases output80 and attribvaltemplate08. --- CHANGELOG.md | 2 ++ Tests/Fixtures/xalan-baseline.txt | 2 -- Tests/XalanConfSuiteTests.swift | 19 ++++++++++++++----- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 970a267..1f571d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- The XSLT conformance harness now decodes source and stylesheet files through the library's own encoding detection (a BOM or the XML declaration) rather than assuming UTF-8, so a non-UTF-8 corpus input is fed to the transform instead of refused. This confirms the library round-trips a UTF-16 source correctly and closes Apache Xalan conformance cases `output80` (UTF-16) and `attribvaltemplate08`, which were unreadable by the harness, not library failures. + - The `html` output method now serializes an element whose expanded-name has a non-null namespace URI as XML rather than HTML (XSLT 1.0 16.2). When the result's document element is namespaced, the tree is serialized through the XML path (empty elements self-close, the namespace is declared once and inherited) while the html method still omits the XML declaration; previously such a tree was emitted as HTML, leaving empty namespaced elements unclosed and repeating the `xmlns` declaration on every element, which is not well-formed XML. A null-namespace tree is unchanged. The rule is applied at document-element granularity rather than per element, so a tree mixing null- and non-null-namespace elements follows its document element's path (the realistic result is all-null or all-namespaced). (Apache Xalan `output63` exercises this rule but remains baselined: its gold differs further only in attribute order, which is not normative.) - The `html` output method now names the document type declaration `HTML` rather than the document element name (XSLT 1.0 16.2: "The name following the `` regardless of the result's root element; the `xml` method still uses the document element name (16.1). Closes Apache Xalan conformance cases `output40`, `output48`, and `output60`. diff --git a/Tests/Fixtures/xalan-baseline.txt b/Tests/Fixtures/xalan-baseline.txt index 478ce94..ad4f890 100644 --- a/Tests/Fixtures/xalan-baseline.txt +++ b/Tests/Fixtures/xalan-baseline.txt @@ -2,7 +2,6 @@ # one category/name per line, exact in both directions: a fixed case # must leave this list and a regression shows up as a new failure. attribset/attribset44 -attribvaltemplate/attribvaltemplate08 axes/axes116 conflictres/conflictres29 conflictres/conflictres30 @@ -34,7 +33,6 @@ output/output70 output/output72 output/output73 output/output75 -output/output80 output/output86 position/position106 processorinfo/processorinfo03 diff --git a/Tests/XalanConfSuiteTests.swift b/Tests/XalanConfSuiteTests.swift index 5b90fdf..5127d8f 100644 --- a/Tests/XalanConfSuiteTests.swift +++ b/Tests/XalanConfSuiteTests.swift @@ -74,10 +74,21 @@ struct XalanConfSuiteTests { #expect(fixed.isEmpty, "\(fixed.count) baselined cases now pass (remove from xalan-baseline.txt): \(fixed.prefix(40))") } + /// Reads a file and decodes it through PureXML's own encoding detection (a + /// BOM or the XML declaration), falling back to UTF-8, so a non-UTF-8 source + /// is decoded rather than refused. nil when the file is missing. + private static func decodeFile(_ path: String) -> String? { + guard let data = FileManager.default.contents(atPath: path) else { return nil } + return (try? PureXML.Parsing.ByteDecoder.decode([UInt8](data))) ?? String(data: data, encoding: .utf8) + } + private func runCase(category: String, name: String, root: String, goldPath: String) -> String? { let directory = root + "/conf/" + category - guard let stylesheet = try? String(contentsOfFile: directory + "/" + name + ".xsl", encoding: .utf8), - let source = try? String(contentsOfFile: directory + "/" + name + ".xml", encoding: .utf8) + // Decode inputs the way PureXML decodes any document, by detecting the + // encoding (a BOM or the XML declaration) rather than assuming UTF-8, so + // a UTF-16 or other non-UTF-8 source is fed to the transform, not refused. + guard let stylesheet = Self.decodeFile(directory + "/" + name + ".xsl"), + let source = Self.decodeFile(directory + "/" + name + ".xml") else { return "unreadable inputs" } @@ -86,9 +97,7 @@ struct XalanConfSuiteTests { else { return "unreadable gold" } - let loader: (String) -> String? = { uri in - (try? String(contentsOfFile: directory + "/" + uri, encoding: .utf8)) - } + let loader: (String) -> String? = { uri in Self.decodeFile(directory + "/" + uri) } let actual: String do { actual = try PureXML.XSLT.transform(