Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions src/builder/XMLBuilderCBImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ import {
} from "../interfaces"
import { applyDefaults, isString, isObject } from "@oozcitak/util"
import { fragment, create } from "./BuilderFunctions"
import { sanitizeInput } from "./dom"
import {
xml_isName, xml_isLegalChar, xml_isPubidChar
} from "@oozcitak/dom/lib/algorithm"
import { namespace as infraNamespace } from "@oozcitak/infra"
import { NamespacePrefixMap } from "@oozcitak/dom/lib/serializer/NamespacePrefixMap"
import {
Comment, Text, ProcessingInstruction, CDATASection, DocumentType, Element, Node
Comment, Text, ProcessingInstruction, DocumentType, Element, Node
} from "@oozcitak/dom/lib/dom/interfaces"
import { LocalNameSet } from "@oozcitak/dom/lib/serializer/LocalNameSet"
import { Guard } from "@oozcitak/dom/lib/util"
Expand Down Expand Up @@ -266,15 +267,28 @@ export class XMLBuilderCBImpl extends EventEmitter implements XMLBuilderCB {
dat(content: string): this {
this._serializeOpenTag(true)

let node: CDATASection
if (content === null || content === undefined) {
if (this._options.keepNullNodes) {
content = ""
} else {
return this
}
}

let data: string
try {
node = fragment(this._builderOptions).dat(content).first().node as CDATASection
data = sanitizeInput(content, this._options.invalidCharReplacement)
} catch (err) {
this.emit("error", err)
return this
}

this._push(this._writer.cdata(node.data))
if (this._options.wellFormed && !xml_isLegalChar(data)) {
this.emit("error", new Error("CDATA contains invalid characters (well-formed required)."))
return this
}

this._push(this._writer.cdata(data))
return this
}

Expand Down
10 changes: 6 additions & 4 deletions src/builder/XMLBuilderImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { Guard } from "@oozcitak/dom/lib/util"
import {
namespace_extractQName, tree_index, create_element
} from "@oozcitak/dom/lib/algorithm"
import { sanitizeInput } from "./dom"
import { sanitizeInput, splitCDATA } from "./dom"
import { namespace as infraNamespace } from "@oozcitak/infra"
import { ObjectReader, JSONReader, XMLReader, YAMLReader } from "../readers"

Expand Down Expand Up @@ -297,9 +297,11 @@ export class XMLBuilderImpl implements XMLBuilder {
}
}

const child = this._doc.createCDATASection(
sanitizeInput(content, this._options.invalidCharReplacement))
this.node.appendChild(child)
const data = sanitizeInput(content, this._options.invalidCharReplacement)
for (const section of splitCDATA(data)) {
const child = this._doc.createCDATASection(section)
this.node.appendChild(child)
}

return this
}
Expand Down
13 changes: 13 additions & 0 deletions src/builder/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,16 @@ export function sanitizeInput(str: any,
return result
}
}

/**
* Splits CDATA content at its forbidden closing sequence while preserving the
* original text across adjacent CDATA sections.
*
* @param str - CDATA content
*/
export function splitCDATA(str: string): string[] {
const parts = str.split("]]>")
return parts.map((part, index) =>
(index === 0 ? "" : ">") + part +
(index === parts.length - 1 ? "" : "]]"))
}
3 changes: 2 additions & 1 deletion src/writers/XMLCBWriter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ export class XMLCBWriter extends BaseCBWriter<XMLCBWriterOptions> {
}
/** @inheritdoc */
cdata(data: string): string {
return this._beginLine() + "<![CDATA[" + data + "]]>"
return this._beginLine() + "<![CDATA[" +
data.replace(/]]>/g, "]]]]><![CDATA[>") + "]]>"
}
/** @inheritdoc */
openTagBegin(name: string): string {
Expand Down
20 changes: 17 additions & 3 deletions src/writers/XMLWriter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { XMLWriterOptions, XMLBuilderOptions } from "../interfaces"
import { applyDefaults } from "@oozcitak/util"
import { Node, NodeType } from "@oozcitak/dom/lib/dom/interfaces"
import { CDATASection, Node, NodeType } from "@oozcitak/dom/lib/dom/interfaces"
import { BaseWriter } from "./BaseWriter"
import { Guard } from "@oozcitak/dom/lib/util"

Expand Down Expand Up @@ -112,14 +112,23 @@ export class XMLWriter extends BaseWriter<XMLWriterOptions, string> {
let childNode = this.currentNode.firstChild
let cdataCount = 0
let textCount = 0
let splitCDATASequence = true
let previousCData: CDATASection | undefined
while (childNode) {
if (Guard.isExclusiveTextNode(childNode)) {
textCount++
splitCDATASequence = false
} else if (Guard.isCDATASectionNode(childNode)) {
if (previousCData !== undefined &&
(!previousCData.data.endsWith("]]") || !childNode.data.startsWith(">"))) {
splitCDATASequence = false
}
previousCData = childNode
cdataCount++
} else {
textOnlyNode = false
emptyNode = false
splitCDATASequence = false
break
}

Expand All @@ -129,7 +138,12 @@ export class XMLWriter extends BaseWriter<XMLWriterOptions, string> {

childNode = childNode.nextSibling
}
this._refs.suppressPretty = !this._writerOptions.indentTextOnlyNodes && textOnlyNode && ((cdataCount <= 1 && textCount === 0) || cdataCount === 0)
// Adjacent CDATA sections created by splitCDATA must remain adjacent so
// pretty-printing does not change their combined text content.
const escapedCDATA = cdataCount > 1 && textCount === 0 && splitCDATASequence
this._refs.suppressPretty = escapedCDATA ||
(!this._writerOptions.indentTextOnlyNodes && textOnlyNode &&
((cdataCount <= 1 && textCount === 0) || cdataCount === 0))
this._refs.emptyNode = emptyNode
}

Expand Down Expand Up @@ -256,4 +270,4 @@ type StringWriterRefs = {
* The string representing the serialized document.
*/
markup: string
}
}
6 changes: 3 additions & 3 deletions test/callback/wellformed.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,10 +178,10 @@ $$.suite('well-formed checks', () => {
await $$.expectCBError(xmlStream, () => xmlStream.ins('name', '\0'))
})

$$.test('invalid cdata node', async () => {
$$.test('escaped cdata terminator', async () => {
const xmlStream = $$.createCB({ wellFormed: true })
xmlStream.ele('ns', 'root')
await $$.expectCBError(xmlStream, () => xmlStream.dat(']]>'))
xmlStream.ele('ns', 'root').dat(']]>').end()
await $$.expectCBResult(xmlStream, '<root xmlns="ns"><![CDATA[]]]]><![CDATA[>]]></root>')
})

$$.test('same attribute', async () => {
Expand Down
14 changes: 13 additions & 1 deletion test/dom/sanitizeInput.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import $$ from '../TestHelpers'
import { sanitizeInput } from '../../src/builder/dom'
import { sanitizeInput, splitCDATA } from '../../src/builder/dom'

$$.suite('sanitizeInput', () => {

Expand Down Expand Up @@ -32,3 +32,15 @@ $$.suite('sanitizeInput', () => {
})

})

$$.suite('splitCDATA', () => {
$$.test('preserves text across forbidden closing sequences', () => {
$$.deepEqual(splitCDATA('before]]>after'), ['before]]', '>after'])
$$.deepEqual(splitCDATA('before]]>middle]]>after'), [
'before]]',
'>middle]]',
'>after'
])
$$.deepEqual(splitCDATA('unchanged'), ['unchanged'])
})
})
41 changes: 41 additions & 0 deletions test/issues/issue-230.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import $$ from '../TestHelpers'

$$.suite('Replicate issue', () => {
const content = 'before]]>after'
const expected = '<root><![CDATA[before]]]]><![CDATA[>after]]></root>'

$$.test('#230 - escapes CDATA terminators', () => {
const doc = $$.create()
.ele('root')
.dat(content)
.doc()

$$.deepEqual(doc.root().node.textContent, content)
$$.deepEqual(doc.end({ headless: true }), expected)
$$.deepEqual(doc.end({ headless: true, prettyPrint: true }), expected)
$$.deepEqual(doc.end({
headless: true,
prettyPrint: true,
indentTextOnlyNodes: true
}), expected)
})

$$.test('#230 - escapes CDATA terminators with the callback API', async () => {
const xmlStream = $$.createCB()
.ele('root')
.dat(content)
.end()

await $$.expectCBResult(xmlStream, expected)
})

$$.test('#230 - escapes CDATA terminators from an object', () => {
const xml = $$.create(
{ convert: { text: '#text', cdata: '#cdata' } },
{ root: { '#cdata': content } }
)
.end({ headless: true, prettyPrint: true })

$$.deepEqual(xml, expected)
})
})