Skip to content

Commit f3e0234

Browse files
[MOD] Parsing, INTPARSE: Better DTD support (default values, etc.)
1 parent 88fef63 commit f3e0234

4 files changed

Lines changed: 169 additions & 44 deletions

File tree

basex-core/src/main/java/org/basex/build/xml/XMLParser.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,11 @@ public final void parse() throws IOException {
7070
while(true) {
7171
if(scanner.type == Type.TEXT) {
7272
final byte[] text = scanner.token.toArray();
73-
if(!elms.isEmpty() || fragment || !ws(text)) {
73+
// ignore whitespace outside the root element, and ignorable whitespace between the
74+
// children of an element with element-only content (matching the default parser)
75+
final boolean ignorable = ws(text) && (elms.isEmpty() ? !fragment :
76+
scanner.elementContent(elms.peek()));
77+
if(!ignorable) {
7478
if(strips.peek()) scanner.token.trim();
7579
builder.text(scanner.token.toArray());
7680
}
@@ -127,8 +131,8 @@ private boolean parseElement() throws IOException {
127131
nsp.reset();
128132

129133
// get element name
130-
byte[] en = consumeToken(Type.ELEMNAME);
131-
if(stripNS) en = local(en);
134+
final byte[] raw = consumeToken(Type.ELEMNAME);
135+
byte[] en = stripNS ? local(raw) : raw;
132136
skipSpace();
133137

134138
// parse optional attributes
@@ -161,6 +165,9 @@ private boolean parseElement() throws IOException {
161165
}
162166
}
163167

168+
// apply DTD-declared attribute defaults and tokenized-type value normalization
169+
scanner.attributes(raw, atts, stripNS);
170+
164171
// send empty element to builder
165172
if(scanner.type == Type.CLOSE_R_BR) {
166173
builder.emptyElem(en, atts, nsp);

basex-core/src/main/java/org/basex/build/xml/XMLScanner.java

Lines changed: 90 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,17 @@ private enum Scan {
4949
private final TokenObjectMap<byte[]> ents = new TokenObjectMap<>();
5050
/** Index for all PEReferences. */
5151
private final TokenObjectMap<byte[]> pents = new TokenObjectMap<>();
52+
/** Declared attributes per element name (for default values and value normalization). */
53+
private final TokenObjectMap<TokenObjectMap<AttDecl>> attDecls = new TokenObjectMap<>();
54+
/** Element names declared with element-only content. */
55+
private final TokenSet elemContent = new TokenSet();
56+
57+
/**
58+
* Declared attribute.
59+
* @param tokenized tokenized (non-CDATA) type flag
60+
* @param value default value (or {@code null})
61+
*/
62+
private record AttDecl(boolean tokenized, byte[] value) { }
5263
/** DTD flag. */
5364
private final boolean dtd;
5465
/** Parse fragment. */
@@ -802,7 +813,7 @@ private boolean markupDecl() throws IOException {
802813
pe = true;
803814
} else if(consume(ELEM)) { // [45]
804815
checkS();
805-
name(true);
816+
final byte[] elem = name(true);
806817
checkS();
807818
pe = true;
808819
if(!consume(EMP) && !consume(ANY)) { // [46]
@@ -814,7 +825,8 @@ private boolean markupDecl() throws IOException {
814825
while(consume('|')) { s(); name(true); s(); alt = true; }
815826
check(')');
816827
if(!consume('*') && alt) throw error(INVEND);
817-
} else { // [47] children
828+
} else { // [47] children (element-only content)
829+
elemContent.add(elem);
818830
cp();
819831
while(sep()) cp();
820832
s();
@@ -830,13 +842,14 @@ private boolean markupDecl() throws IOException {
830842
} else if(consume(ATTL)) { // [52]
831843
pe = true;
832844
checkS();
833-
name(true);
845+
final byte[] elem = name(true);
834846
s();
835-
while(name(false) != null) { // [53]
847+
for(byte[] att; (att = name(false)) != null;) { // [53]
836848
checkS();
837-
if(!consume(CD) && !consume(IDRS) && !consume(IDR) && !consume(ID) &&
838-
!consume(ENTS) && !consume(ENT1) && !consume(NMTS) &&
839-
!consume(NMT)) { // [56]
849+
// [56] AttType: CDATA is the only non-tokenized (StringType) type
850+
final boolean tokenized = !consume(CD);
851+
if(tokenized && !consume(IDRS) && !consume(IDR) && !consume(ID) &&
852+
!consume(ENTS) && !consume(ENT1) && !consume(NMTS) && !consume(NMT)) {
840853
if(consume(NOT)) { // [57,58]
841854
checkS(); check('(');
842855
do { s(); name(true); s(); } while(consume('|'));
@@ -847,14 +860,18 @@ private boolean markupDecl() throws IOException {
847860
check(')');
848861
}
849862

850-
// [54]
863+
// [54] DefaultDecl
851864
pe = true;
852865
checkS();
866+
byte[] value = null;
853867
if(!consume(REQ) && !consume(IMP)) { // [60]
854868
if(consume(FIX)) checkS();
855869
quote = qu();
870+
token.reset();
856871
attValue(consume());
872+
value = tokenized ? normalize(token.toArray()) : token.toArray();
857873
}
874+
if(tokenized || value != null) declareAtt(elem, att, tokenized, value);
858875
s();
859876
}
860877
check('>');
@@ -910,6 +927,71 @@ private void occ() throws IOException {
910927
if(!consume('+') && !consume('?')) consume('*');
911928
}
912929

930+
/**
931+
* Registers a declared attribute. The first declaration of an attribute is binding.
932+
* @param elem element name
933+
* @param att attribute name
934+
* @param tokenized non-CDATA (tokenized) type flag
935+
* @param value default value (or {@code null})
936+
*/
937+
private void declareAtt(final byte[] elem, final byte[] att, final boolean tokenized,
938+
final byte[] value) {
939+
attDecls.computeIfAbsent(elem, () -> new TokenObjectMap<>()).
940+
computeIfAbsent(att, () -> new AttDecl(tokenized, value));
941+
}
942+
943+
/**
944+
* Applies declared attribute defaults and normalization. [3.3.2, 3.3.3]
945+
* @param elem element name
946+
* @param atts attributes assembled from the start tag
947+
* @param stripNS strip namespaces
948+
*/
949+
void attributes(final byte[] elem, final Atts atts, final boolean stripNS) {
950+
final TokenObjectMap<AttDecl> decls = attDecls.get(elem);
951+
if(decls == null) return;
952+
// normalize specified values of tokenized-type attributes
953+
final int as = atts.size();
954+
for(int a = 0; a < as; a++) {
955+
final AttDecl decl = decls.get(atts.name(a));
956+
if(decl != null && decl.tokenized) atts.value(a, normalize(atts.value(a)));
957+
}
958+
// add default values for declared attributes that are absent (in declaration order)
959+
for(final byte[] att : decls) {
960+
final AttDecl decl = decls.get(att);
961+
if(decl.value != null && !atts.contains(att)) atts.add(att, decl.value, stripNS);
962+
}
963+
}
964+
965+
/**
966+
* Indicates whether the named element was declared with element-only content, so that
967+
* whitespace between its child elements is ignorable. [3.2.1]
968+
* @param elem element name
969+
* @return result of check
970+
*/
971+
boolean elementContent(final byte[] elem) {
972+
return elemContent.contains(elem);
973+
}
974+
975+
/**
976+
* Normalizes an attribute value of a tokenized type. [3.3.3]
977+
* @param value value
978+
* @return normalized value
979+
*/
980+
private static byte[] normalize(final byte[] value) {
981+
final TokenBuilder tb = new TokenBuilder();
982+
boolean space = false;
983+
for(final byte b : value) {
984+
if(b == ' ') {
985+
space = true;
986+
} else {
987+
if(space && !tb.isEmpty()) tb.add(' ');
988+
space = false;
989+
tb.add(b);
990+
}
991+
}
992+
return tb.finish();
993+
}
994+
913995
/**
914996
* Scans an entity value. [9]
915997
* @param p pe reference flag

basex-core/src/main/java/org/basex/util/Atts.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,17 @@ public byte[] value(final int index) {
138138
return list[(index << 1) + 1];
139139
}
140140

141+
/**
142+
* Sets the value at the specified index position.
143+
* @param index index
144+
* @param value value
145+
* @return self reference
146+
*/
147+
public Atts value(final int index, final byte[] value) {
148+
list[(index << 1) + 1] = value;
149+
return this;
150+
}
151+
141152
/**
142153
* Returns the value for the specified name or {@code null}.
143154
* @param name name to be found

basex-core/src/test/java/org/basex/build/XMLParserTest.java

Lines changed: 58 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -125,24 +125,53 @@ public final class XMLParserTest extends SandboxTest {
125125

126126
/** Internal and default parser must agree on DTD content models. */
127127
@Test public void dtdContentModelParsers() {
128-
set(MainOptions.DTD, true);
129-
130128
// well-formed documents with an internal subset and a matching element tree
131-
final String[] docs = {
129+
sameOnBothParsers(
132130
"<!DOCTYPE a [ <!ELEMENT a (b, c)> ]><a><b/><c/></a>",
133131
"<!DOCTYPE a [ <!ELEMENT a ((b)?, (c)*)> ]><a><c/><c/></a>",
134132
"<!DOCTYPE a [ <!ELEMENT a (b | c)> ]><a><b/></a>",
135133
"<!DOCTYPE a [ <!ELEMENT a (#PCDATA | b)*> ]><a>x<b/>y</a>",
136-
"<!DOCTYPE a [ <!ELEMENT a (b, (c, d)+, e)> ]><a><b/><c/><d/><e/></a>",
137-
};
138-
for(final String doc : docs) {
139-
set(MainOptions.INTPARSE, false);
140-
execute(new CreateDB(NAME, doc));
141-
final String def = query(".");
142-
set(MainOptions.INTPARSE, true);
143-
execute(new CreateDB(NAME, doc));
144-
assertEquals(def, query("."), "Parsers disagree on: " + doc);
145-
}
134+
"<!DOCTYPE a [ <!ELEMENT a (b, (c, d)+, e)> ]><a><b/><c/><d/><e/></a>");
135+
}
136+
137+
/**
138+
* Internal and default parser must agree on attribute defaults (incl. #FIXED and enumeration
139+
* defaults) and on tokenized-type attribute-value normalization.
140+
*/
141+
@Test public void dtdAttributes() {
142+
sameOnBothParsers(
143+
// default values, incl. empty and multiple defaults; specified values win
144+
"<!DOCTYPE a [ <!ATTLIST a b CDATA \"x\"> ]><a/>",
145+
"<!DOCTYPE a [ <!ATTLIST a b CDATA \"x\"> ]><a b=\"y\"/>",
146+
"<!DOCTYPE a [ <!ATTLIST a b CDATA \"\"> ]><a/>",
147+
"<!DOCTYPE a [ <!ATTLIST a b CDATA \"1\" c CDATA \"2\"> ]><a c=\"X\"/>",
148+
// #FIXED and enumeration defaults
149+
"<!DOCTYPE a [ <!ATTLIST a b CDATA #FIXED \"x\"> ]><a/>",
150+
"<!DOCTYPE a [ <!ATTLIST a b (l | r) \"l\"> ]><a/>",
151+
// entity reference in a default value
152+
"<!DOCTYPE a [ <!ATTLIST a b CDATA \"&#65;\"> ]><a/>",
153+
// tokenized-type normalization (collapse + trim); CDATA is left untouched
154+
"<!DOCTYPE a [ <!ATTLIST a b NMTOKEN #IMPLIED> ]><a b=\" x \"/>",
155+
"<!DOCTYPE a [ <!ATTLIST a b NMTOKENS #IMPLIED> ]><a b=\" x y \"/>",
156+
"<!DOCTYPE a [ <!ATTLIST a b (l | r) #IMPLIED> ]><a b=\" l \"/>",
157+
"<!DOCTYPE a [ <!ATTLIST a b CDATA #IMPLIED> ]><a b=\" x y \"/>",
158+
// #IMPLIED / #REQUIRED add nothing
159+
"<!DOCTYPE a [ <!ATTLIST a b CDATA #IMPLIED> ]><a/>",
160+
"<!DOCTYPE a [ <!ATTLIST a b CDATA #REQUIRED> ]><a b=\"z\"/>");
161+
}
162+
163+
/** Internal and default parser must agree on ignorable (element-content) whitespace. */
164+
@Test public void dtdElementContentWhitespace() {
165+
sameOnBothParsers(
166+
// element-only content: whitespace between children is ignorable and dropped
167+
"<!DOCTYPE a [ <!ELEMENT a (b, b)><!ELEMENT b EMPTY> ]><a>\n <b/>\n <b/>\n</a>",
168+
"<!DOCTYPE a [ <!ELEMENT a (b)><!ELEMENT b (c)><!ELEMENT c EMPTY> ]>" +
169+
"<a>\n <b>\n <c/>\n </b>\n</a>",
170+
// mixed content and ANY: whitespace is significant and kept
171+
"<!DOCTYPE a [ <!ELEMENT a (#PCDATA | b)*><!ELEMENT b EMPTY> ]><a>\n <b/>\n</a>",
172+
"<!DOCTYPE a [ <!ELEMENT a ANY><!ELEMENT b EMPTY> ]><a>\n <b/>\n</a>",
173+
// non-whitespace text in element content is kept by both (non-validating)
174+
"<!DOCTYPE a [ <!ELEMENT a (b)><!ELEMENT b EMPTY> ]><a> x <b/> y </a>");
146175
}
147176

148177
/** A malformed DTD must report its real cause, not a masked "empty document" error. */
@@ -161,26 +190,6 @@ public final class XMLParserTest extends SandboxTest {
161190
assertTrue(empty.contains("No input found"), "Unexpected error: " + empty);
162191
}
163192

164-
/** Internal parser: ATTLIST declarations, including enumerations and empty default values. */
165-
@Test public void dtdAttlist() {
166-
set(MainOptions.INTPARSE, true);
167-
set(MainOptions.DTD, true);
168-
169-
// attribute declarations that must be accepted; an empty default value ("") regressed the
170-
// scanner before the fix, overrunning the whole declaration
171-
final String[] attlists = {
172-
"<!ATTLIST a b CDATA \"\">",
173-
"<!ATTLIST a b CDATA '' c CDATA '50'>",
174-
"<!ATTLIST a b (x | y | z) \"x\">",
175-
"<!ATTLIST a b NMTOKEN #IMPLIED c CDATA #REQUIRED d CDATA #FIXED \"1\">",
176-
"<!ATTLIST a align (left | right | center | justify | char) \"left\" char CDATA \"\">",
177-
};
178-
for(final String attlist : attlists) {
179-
execute(new CreateDB(NAME, "<!DOCTYPE a [ " + attlist + " ]><a/>"));
180-
query(".", "<a/>");
181-
}
182-
}
183-
184193
/**
185194
* Internal parser: a complex external DTD subset (parameter entities inside declarations,
186195
* a parameter-entity-driven conditional section, enumerations and empty default values).
@@ -220,6 +229,22 @@ public final class XMLParserTest extends SandboxTest {
220229
query("name(*)", "tgroup");
221230
}
222231

232+
/**
233+
* Asserts that the internal and the default parser produce identical documents.
234+
* @param docs document strings (each with an internal DTD subset)
235+
*/
236+
private void sameOnBothParsers(final String... docs) {
237+
set(MainOptions.DTD, true);
238+
for(final String doc : docs) {
239+
set(MainOptions.INTPARSE, false);
240+
execute(new CreateDB(NAME, doc));
241+
final String def = query(".");
242+
set(MainOptions.INTPARSE, true);
243+
execute(new CreateDB(NAME, doc));
244+
assertEquals(def, query("."), "Parsers disagree on: " + doc);
245+
}
246+
}
247+
223248
/**
224249
* Creates a database and returns the resulting error message, or {@code null} on success.
225250
* @param doc document string

0 commit comments

Comments
 (0)