Skip to content

Commit 70fe59f

Browse files
Initialize the document locator before starting tokenization
Problem: When a caller supplies its own ContentHandler, the Document system identifier is null. Cause: tokenizer.initLocation() wasn’t getting called until after Driver.tokenize() called tokenizer.start(). So the tokenizer’s systemId/ publicId were still null when start() delivered the first SAX events. Fix: Call initLocation() before start(). The locator carries the input source’s public/system IDs before any downstream handler observes it. Fixes #123
1 parent 65a1ffa commit 70fe59f

2 files changed

Lines changed: 103 additions & 1 deletion

File tree

‎src/nu/validator/htmlparser/io/Driver.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,10 +194,10 @@ public void tokenize(InputSource is, int bufferSize)
194194
if (is == null) {
195195
throw new IllegalArgumentException("InputSource was null.");
196196
}
197+
tokenizer.initLocation(is.getPublicId(), is.getSystemId());
197198
tokenizer.start();
198199
confidence = Confidence.TENTATIVE;
199200
rewindableInputStream = null;
200-
tokenizer.initLocation(is.getPublicId(), is.getSystemId());
201201
this.reader = is.getCharacterStream();
202202
this.characterEncoding = encodingFromExternalDeclaration(is.getEncoding());
203203
if (this.reader == null) {
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* Copyright (c) 2026 Mozilla Foundation
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a
5+
* copy of this software and associated documentation files (the "Software"),
6+
* to deal in the Software without restriction, including without limitation
7+
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
8+
* and/or sell copies of the Software, and to permit persons to whom the
9+
* Software is furnished to do so, subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in
12+
* all copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
17+
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
19+
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
20+
* DEALINGS IN THE SOFTWARE.
21+
*/
22+
23+
package nu.validator.htmlparser.test;
24+
25+
import java.io.IOException;
26+
import java.io.StringReader;
27+
28+
import org.xml.sax.InputSource;
29+
import org.xml.sax.Locator;
30+
import org.xml.sax.SAXException;
31+
import org.xml.sax.helpers.DefaultHandler;
32+
33+
import nu.validator.htmlparser.common.XmlViolationPolicy;
34+
import nu.validator.htmlparser.sax.HtmlParser;
35+
36+
/**
37+
* Regression test for
38+
* <a href="https://github.com/validator/htmlparser/issues/123">issue&nbsp;#123</a>:
39+
* the document system identifier must be available to a caller-supplied
40+
* {@link org.xml.sax.ContentHandler} by the time {@code startDocument()} fires.
41+
*
42+
* <p>Both SAX configurations are exercised, because they surface the system
43+
* identifier by different mechanisms: the default buffered path snapshots the
44+
* locator when the root node is created, whereas the streaming path hands the
45+
* live tokenizer to the handler directly.
46+
*/
47+
public class SystemIdTester {
48+
49+
private static final String SYSTEM_ID = "http://example.org/doc.html";
50+
51+
private static final String DOCUMENT = "<!DOCTYPE html><html><head><title>Test</title></head><body><p>Hello</p></body></html>";
52+
53+
/**
54+
* Records the system identifier reported by the {@link Locator} at
55+
* {@code startDocument()} time, i.e. the earliest point at which a
56+
* content handler can reasonably read it.
57+
*/
58+
private static final class SystemIdRecorder extends DefaultHandler {
59+
60+
private Locator locator;
61+
62+
private String systemIdAtStartDocument = "<startDocument() not called>";
63+
64+
@Override public void setDocumentLocator(Locator locator) {
65+
this.locator = locator;
66+
}
67+
68+
@Override public void startDocument() throws SAXException {
69+
systemIdAtStartDocument = (locator == null) ? null
70+
: locator.getSystemId();
71+
}
72+
}
73+
74+
private static void check(XmlViolationPolicy streamabilityViolationPolicy,
75+
String label) throws SAXException, IOException {
76+
HtmlParser parser = new HtmlParser();
77+
parser.setStreamabilityViolationPolicy(streamabilityViolationPolicy);
78+
SystemIdRecorder recorder = new SystemIdRecorder();
79+
parser.setContentHandler(recorder);
80+
81+
InputSource in = new InputSource(new StringReader(DOCUMENT));
82+
in.setSystemId(SYSTEM_ID);
83+
parser.parse(in);
84+
85+
if (!SYSTEM_ID.equals(recorder.systemIdAtStartDocument)) {
86+
throw new AssertionError(label
87+
+ ": expected system identifier \"" + SYSTEM_ID
88+
+ "\" at startDocument() but got \""
89+
+ recorder.systemIdAtStartDocument + "\"");
90+
}
91+
System.out.println(label + ": OK");
92+
}
93+
94+
public static void main(String[] args) throws SAXException, IOException {
95+
// Default configuration: the SAXTreeBuilder path, which replays a
96+
// buffered tree to the content handler.
97+
check(XmlViolationPolicy.ALLOW, "buffered (SAXTreeBuilder)");
98+
// Any non-ALLOW policy selects the direct SAXStreamer path.
99+
check(XmlViolationPolicy.ALTER_INFOSET, "streaming (SAXStreamer)");
100+
System.out.println("issue #123 regression test passed");
101+
}
102+
}

0 commit comments

Comments
 (0)