diff --git a/lib/serializer.js b/lib/serializer.js index 0f4dde76..d8ff4b87 100644 --- a/lib/serializer.js +++ b/lib/serializer.js @@ -1,5 +1,8 @@ 'use strict' +// eslint-disable-next-line +const STR_ESCAPE = /[\u0000-\u001f\u0022\u005c\ud800-\udfff]/ + module.exports = class Serializer { constructor (options) { switch (options && options.rounding) { @@ -116,7 +119,7 @@ module.exports = class Serializer { } } return (last === -1 && ('"' + str + '"')) || ('"' + result + str.slice(last) + '"') - } else if (len < 5000 && str.isWellFormed()) { + } 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 { diff --git a/test/issue-793.test.js b/test/issue-793.test.js new file mode 100644 index 00000000..0fd5681e --- /dev/null +++ b/test/issue-793.test.js @@ -0,0 +1,107 @@ +'use strict' + +const { test } = require('node:test') + +const build = require('..') + +test('serialize string with newlines - issue #793', (t) => { + t.plan(2) + + const schema = { + type: 'object', + properties: { + message: { + type: 'string' + } + } + } + + const input = { + message: `This is a string +with multiple +newlines in it +Foo` + } + + const stringify = build(schema) + const output = stringify(input) + + // The output should be valid JSON + t.assert.doesNotThrow(() => { + JSON.parse(output) + }, 'JSON output should be parseable') + + // The parsed output should match the input + const parsed = JSON.parse(output) + t.assert.equal(parsed.message, input.message) +}) + +test('serialize string with various newline characters - issue #793', (t) => { + t.plan(4) + + const schema = { + type: 'string' + } + + const stringify = build(schema) + + // Test \n (line feed) + const inputLF = 'line1\nline2' + const outputLF = stringify(inputLF) + t.assert.equal(JSON.parse(outputLF), inputLF) + + // Test \r (carriage return) + const inputCR = 'line1\rline2' + const outputCR = stringify(inputCR) + t.assert.equal(JSON.parse(outputCR), inputCR) + + // Test \r\n (CRLF) + const inputCRLF = 'line1\r\nline2' + const outputCRLF = stringify(inputCRLF) + t.assert.equal(JSON.parse(outputCRLF), inputCRLF) + + // Test mixed newlines + const inputMixed = 'line1\nline2\rline3\r\nline4' + const outputMixed = stringify(inputMixed) + t.assert.equal(JSON.parse(outputMixed), inputMixed) +}) + +test('serialize object with newlines in multiple properties - issue #793', (t) => { + t.plan(2) + + const schema = { + type: 'object', + properties: { + message: { + type: 'string' + }, + description: { + type: 'string' + }, + timestamp: { + type: 'string' + } + } + } + + const input = { + message: `This is a string +with multiple +newlines in it +Foo`, + description: 'This JSON response contains a field with newline characters', + timestamp: new Date().toISOString() + } + + const stringify = build(schema) + const output = stringify(input) + + // The output should be valid JSON + t.assert.doesNotThrow(() => { + JSON.parse(output) + }, 'JSON output should be parseable') + + // The parsed output should match the input + const parsed = JSON.parse(output) + t.assert.deepEqual(parsed, input) +}) diff --git a/test/issue-794.test.js b/test/issue-794.test.js new file mode 100644 index 00000000..dda9a6c7 --- /dev/null +++ b/test/issue-794.test.js @@ -0,0 +1,177 @@ +'use strict' + +const { test } = require('node:test') + +const build = require('..') + +test('serialize string with quotes - issue #794', (t) => { + t.plan(2) + + const schema = { + type: 'object', + properties: { + message: { + type: 'string' + } + } + } + + const input = { + message: 'Error: Property "name" is required' + } + + const stringify = build(schema) + const output = stringify(input) + + // The output should be valid JSON + t.assert.doesNotThrow(() => { + JSON.parse(output) + }, 'JSON output should be parseable') + + // The parsed output should match the input + const parsed = JSON.parse(output) + t.assert.equal(parsed.message, input.message) +}) + +test('serialize string with various quote types - issue #794', (t) => { + t.plan(6) + + const schema = { + type: 'string' + } + + const stringify = build(schema) + + // Test double quotes + const inputDoubleQuotes = 'Property "name" is required' + const outputDoubleQuotes = stringify(inputDoubleQuotes) + t.assert.doesNotThrow(() => JSON.parse(outputDoubleQuotes)) + t.assert.equal(JSON.parse(outputDoubleQuotes), inputDoubleQuotes) + + // Test single quotes (should be fine but test for completeness) + const inputSingleQuotes = "Property 'name' is required" + const outputSingleQuotes = stringify(inputSingleQuotes) + t.assert.doesNotThrow(() => JSON.parse(outputSingleQuotes)) + t.assert.equal(JSON.parse(outputSingleQuotes), inputSingleQuotes) + + // Test mixed quotes + const inputMixedQuotes = 'Error: "Property \'name\' is required"' + const outputMixedQuotes = stringify(inputMixedQuotes) + t.assert.doesNotThrow(() => JSON.parse(outputMixedQuotes)) + t.assert.equal(JSON.parse(outputMixedQuotes), inputMixedQuotes) +}) + +test('serialize error-like object with quotes in message - issue #794', (t) => { + t.plan(2) + + const schema = { + type: 'object', + properties: { + error: { + type: 'object', + properties: { + message: { + type: 'string' + }, + code: { + type: 'string' + } + } + } + } + } + + const input = { + error: { + message: 'Validation failed: Property "email" must be a valid email address', + code: 'VALIDATION_ERROR' + } + } + + const stringify = build(schema) + const output = stringify(input) + + // The output should be valid JSON + t.assert.doesNotThrow(() => { + JSON.parse(output) + }, 'JSON output should be parseable') + + // The parsed output should match the input + const parsed = JSON.parse(output) + t.assert.deepEqual(parsed, input) +}) + +test('serialize validation errors array with quotes - issue #794', (t) => { + t.plan(2) + + const schema = { + type: 'object', + properties: { + errors: { + type: 'array', + items: { + type: 'object', + properties: { + message: { + type: 'string' + }, + field: { + type: 'string' + } + } + } + } + } + } + + const input = { + errors: [ + { + message: 'Property "name" is required', + field: 'name' + }, + { + message: 'Property "email" must be a valid email address', + field: 'email' + }, + { + message: 'Value must be between "1" and "100"', + field: 'age' + } + ] + } + + const stringify = build(schema) + const output = stringify(input) + + // The output should be valid JSON + t.assert.doesNotThrow(() => { + JSON.parse(output) + }, 'JSON output should be parseable') + + // The parsed output should match the input + const parsed = JSON.parse(output) + t.assert.deepEqual(parsed, input) +}) + +test('serialize string with backslashes and quotes - issue #794', (t) => { + t.plan(4) + + const schema = { + type: 'string' + } + + const stringify = build(schema) + + // Test backslashes + const inputBackslash = 'Path: C:\\Users\\test\\file.json' + const outputBackslash = stringify(inputBackslash) + t.assert.doesNotThrow(() => JSON.parse(outputBackslash)) + t.assert.equal(JSON.parse(outputBackslash), inputBackslash) + + // Test combination of backslashes and quotes + const inputMixed = 'Error: Could not find file "C:\\Users\\test\\config.json"' + const outputMixed = stringify(inputMixed) + t.assert.doesNotThrow(() => JSON.parse(outputMixed)) + t.assert.equal(JSON.parse(outputMixed), inputMixed) +})