Skip to content
Merged
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ And nested ones, too.
| `date` | `2020-04-03` |
| `time` | `09:11:08` |

**Note**: In the case of a string formatted Date and not Date Object, there will be no manipulation on it. It should be properly formatted.
**Note**: In the case of a string formatted Date and not Date Object, the value is not reformatted nor validated: it should already be properly formatted. It is only escaped, so that the resulting document is always valid JSON.

Example with a Date object:

Expand Down
78 changes: 41 additions & 37 deletions lib/serializer.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,43 @@
// eslint-disable-next-line
const STR_ESCAPE = /[\u0000-\u001f\u0022\u005c\ud800-\udfff]/

function asString (str) {
const len = str.length
if (len === 0) {
return '""'
} else if (len < 42) {
// magically escape strings for json
// relying on their charCodeAt
// everything below 32 needs JSON.stringify()
// every string that contain surrogate needs JSON.stringify()
// 34 and 92 happens all the time, so we
// have a fast case for them
let result = ''
let last = -1
let point = 255
for (let i = 0; i < len; i++) {
point = str.charCodeAt(i)
if (
point === 0x22 || // '"'
point === 0x5c // '\'
) {
last === -1 && (last = 0)
result += str.slice(last, i) + '\\'
last = i
} else if (point < 32 || (point >= 0xD800 && point <= 0xDFFF)) {
// The current character is non-printable characters or a surrogate.
return JSON.stringify(str)
}
}
return (last === -1 && ('"' + str + '"')) || ('"' + result + str.slice(last) + '"')
} else if (len < 5000 && STR_ESCAPE.test(str) === false) {
// Only use the regular expression for shorter input. The overhead is otherwise too much.
return '"' + str + '"'
} else {
return JSON.stringify(str)
}
}

module.exports = class Serializer {
constructor (options) {
switch (options && options.rounding) {
Expand Down Expand Up @@ -63,7 +100,7 @@ module.exports = class Serializer {
return '"' + date.toISOString() + '"'
}
if (typeof date === 'string') {
return '"' + date + '"'
return asString(date)
}
throw new Error(`The value "${date}" cannot be converted to a date-time.`)
}
Expand All @@ -74,7 +111,7 @@ module.exports = class Serializer {
return '"' + new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().slice(0, 10) + '"'
}
if (typeof date === 'string') {
return '"' + date + '"'
return asString(date)
}
throw new Error(`The value "${date}" cannot be converted to a date.`)
}
Expand All @@ -85,46 +122,13 @@ module.exports = class Serializer {
return '"' + new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().slice(11, 19) + '"'
}
if (typeof date === 'string') {
return '"' + date + '"'
return asString(date)
}
throw new Error(`The value "${date}" cannot be converted to a time.`)
}

asString (str) {
const len = str.length
if (len === 0) {
return '""'
} else if (len < 42) {
// magically escape strings for json
// relying on their charCodeAt
// everything below 32 needs JSON.stringify()
// every string that contain surrogate needs JSON.stringify()
// 34 and 92 happens all the time, so we
// have a fast case for them
let result = ''
let last = -1
let point = 255
for (let i = 0; i < len; i++) {
point = str.charCodeAt(i)
if (
point === 0x22 || // '"'
point === 0x5c // '\'
) {
last === -1 && (last = 0)
result += str.slice(last, i) + '\\'
last = i
} else if (point < 32 || (point >= 0xD800 && point <= 0xDFFF)) {
// The current character is non-printable characters or a surrogate.
return JSON.stringify(str)
}
}
return (last === -1 && ('"' + str + '"')) || ('"' + result + str.slice(last) + '"')
} else if (len < 5000 && STR_ESCAPE.test(str) === false) {
// Only use the regular expression for shorter input. The overhead is otherwise too much.
return '"' + str + '"'
} else {
return JSON.stringify(str)
}
return asString(str)
}

asUnsafeString (str) {
Expand Down
27 changes: 27 additions & 0 deletions test/date.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -637,3 +637,30 @@ test('should serialize also an invalid string value, even if it is not a valid t
t.assert.equal(output, JSON.stringify(toStringify))
t.assert.equal(validate(JSON.parse(output)), false, 'valid schema')
})

test('should escape strings that are not valid dates', (t) => {
const formats = ['date-time', 'date', 'time']
const values = [
'2026-01-01T00:00:00Z","admin":true,"x":"',
'back\\slash',
'new\nline',
'lone \ud800 surrogate'
]

t.plan(formats.length * values.length * 2)

for (const format of formats) {
const stringify = build({
type: 'object',
properties: {
ts: { type: 'string', format }
}
})

for (const value of values) {
const output = stringify({ ts: value })
t.assert.equal(output, `{"ts":${JSON.stringify(value)}}`)
t.assert.deepStrictEqual(JSON.parse(output), { ts: value })
}
}
})
Loading