diff --git a/lib/saxes.js b/lib/saxes.js index 0478808f..954d0c47 100644 --- a/lib/saxes.js +++ b/lib/saxes.js @@ -66,6 +66,13 @@ const S_ATTRIB_VALUE_ENTITY_Q = S_INDEX++; // +const S_XML_DECL_NAME_START = S_INDEX++; // ") { + if (this.piIsXMLDecl) { + if (c === ">") { + if (this.piTarget !== "xml") { + this.fail("processing instructions are not allowed before root."); + } + else if (this.xmlDeclState !== S_XML_DECL_NAME_START) { + this.fail("XML declaration is incomplete."); + } + else if (this.xmlDeclExpects.includes("version")) { + this.fail("XML declaration must contain a version."); + } + this.xmlDeclName = this.xmlDeclValue = ""; + this.requiredSeparator = undefined; + this.state = S_TEXT; + } + else { + // We got here because the previous character was a ?, but the + // question mark character is not valid inside any of the XML + // delcaration name/value pairs. + this.fail( + "The character ? is disallowed anywhere in XML declarations."); + } + } + else if (c === ">") { if (this.piTarget.trim() === "") { this.fail("processing instruction without a target."); } + else if (this.piTarget.trim().toLowerCase() === "xml") { + this.fail("the XML declaration must appear at the start of the document."); + } this.emitNode("onprocessinginstruction", { target: this.piTarget, body: this.piBody, @@ -566,6 +721,7 @@ class SAXParser { this.piBody += `?${c}`; this.state = S_PI_BODY; } + this.xmlDeclPossible = false; continue; case S_OPEN_TAG: diff --git a/test/end_empty_stream.js b/test/end_empty_stream.js index 54c13502..7a73b1b3 100644 --- a/test/end_empty_stream.js +++ b/test/end_empty_stream.js @@ -8,5 +8,5 @@ it("end empty stream", () => { // It musn't throw. expect(() => saxesStream.end()).to.throw( Error, - /^undefined:1:0: document must contain a root element.$/); + /^undefined:1:0: document must contain a root element.$/); }); diff --git a/test/issue-84.js b/test/issue-84.js index f5653521..b7320268 100644 --- a/test/issue-84.js +++ b/test/issue-84.js @@ -3,11 +3,11 @@ // https://github.com/isaacs/sax-js/issues/84 require(".").test({ name: "issue 84 (unbalanced quotes in pi)", - xml: "body", + xml: "body", expect: [ - ["processinginstruction", { target: "has", body: "unbalanced \"quotes" }], ["opentagstart", { name: "xml", attributes: {} }], ["opentag", { name: "xml", attributes: {}, isSelfClosing: false }], + ["processinginstruction", { target: "has", body: "unbalanced \"quotes" }], ["text", "body"], ["closetag", "xml"], ], diff --git a/test/xml-declaration.js b/test/xml-declaration.js new file mode 100644 index 00000000..193858c7 --- /dev/null +++ b/test/xml-declaration.js @@ -0,0 +1,175 @@ +"use strict"; + +const { expect } = require("chai"); +const saxes = require("../lib/saxes"); +const { test } = require("."); + +describe("xml declaration", () => { + test({ + name: "empty declaration", + xml: "", + expect: [ + ["error", "undefined:1:7: XML declaration must contain a version."], + ["opentagstart", { name: "root", attributes: {}, ns: {} }], + [ + "opentag", + { + name: "root", + prefix: "", + local: "root", + uri: "", + attributes: {}, + ns: {}, + isSelfClosing: true, + }, + ], + ["closetag", "root"], + ], + opt: { + xmlns: true, + }, + }); + + test({ + name: "version without value", + xml: "", + expect: [ + ["error", "undefined:1:15: XML declaration is incomplete."], + ["opentagstart", { name: "root", attributes: {}, ns: {} }], + [ + "opentag", + { + name: "root", + prefix: "", + local: "root", + uri: "", + attributes: {}, + ns: {}, + isSelfClosing: true, + }, + ], + ["closetag", "root"], + ], + opt: { + xmlns: true, + }, + }); + + test({ + name: "version without value", + xml: "", + expect: [ + ["error", "undefined:1:16: XML declaration is incomplete."], + ["opentagstart", { name: "root", attributes: {}, ns: {} }], + [ + "opentag", + { + name: "root", + prefix: "", + local: "root", + uri: "", + attributes: {}, + ns: {}, + isSelfClosing: true, + }, + ], + ["closetag", "root"], + ], + opt: { + xmlns: true, + }, + }); + + test({ + name: "unquoted value", + xml: "", + expect: [ + ["error", "undefined:1:15: value must be quoted."], + ["error", "undefined:1:17: XML declaration is incomplete."], + ["opentagstart", { name: "root", attributes: {}, ns: {} }], + [ + "opentag", + { + name: "root", + prefix: "", + local: "root", + uri: "", + attributes: {}, + ns: {}, + isSelfClosing: true, + }, + ], + ["closetag", "root"], + ], + opt: { + xmlns: true, + }, + }); + + test({ + name: "unterminated value", + xml: "", + expect: [ + ["error", "undefined:1:18: XML declaration is incomplete."], + ["opentagstart", { name: "root", attributes: {}, ns: {} }], + [ + "opentag", + { + name: "root", + prefix: "", + local: "root", + uri: "", + attributes: {}, + ns: {}, + isSelfClosing: true, + }, + ], + ["closetag", "root"], + ], + opt: { + xmlns: true, + }, + }); + + test({ + name: "bad version", + xml: "", + expect: [ + ["error", "undefined:1:17: version number must match /^1\\.[0-9]+$/."], + ["opentagstart", { name: "root", attributes: {}, ns: {} }], + [ + "opentag", + { + name: "root", + prefix: "", + local: "root", + uri: "", + attributes: {}, + ns: {}, + isSelfClosing: true, + }, + ], + ["closetag", "root"], + ], + opt: { + xmlns: true, + }, + }); + + it("well-formed", () => { + const parser = saxes.parser(); + let seen = false; + parser.onopentagstart = () => { + expect(parser.xmlDecl).to.deep.equal({ + version: "1.1", + encoding: "utf-8", + standalone: "yes", + }); + seen = true; + }; + parser.write( + ""); + parser.close(); + expect(seen).to.be.true; + }); +});