fast-json-stringify 5.12.0 → 5.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -630,6 +630,24 @@ integer-like values, such as:
630
630
  - `'2e4'` - _note this will be converted to `2`, not `20000`_
631
631
  - `1.5` - _note this will be converted to `1`_
632
632
 
633
+ <a name="unsafe"></a>
634
+ #### Unsafe string
635
+ By default, the library escapes all strings. With the 'unsafe' format, the string isn't escaped. This has a potentially dangerous security issue. You can use it only if you are sure that your data doesn't need escaping. The advantage is a significant performance improvement.
636
+
637
+ Example:
638
+ ```javascript
639
+ const stringify = fastJson({
640
+ title: 'Example Schema',
641
+ type: 'object',
642
+ properties: {
643
+ 'code': {
644
+ type: 'string',
645
+ format 'unsafe'
646
+ }
647
+ }
648
+ })
649
+ ```
650
+
633
651
  ##### Benchmarks
634
652
 
635
653
  For reference, here goes some benchmarks for comparison over the three
@@ -47,6 +47,14 @@ const benchmarks = [
47
47
  },
48
48
  input: 'hello world'
49
49
  },
50
+ {
51
+ name: 'unsafe short string',
52
+ schema: {
53
+ type: 'string',
54
+ format: 'unsafe'
55
+ },
56
+ input: 'hello world'
57
+ },
50
58
  {
51
59
  name: 'short string with double quote',
52
60
  schema: {
@@ -61,6 +69,14 @@ const benchmarks = [
61
69
  },
62
70
  input: longSimpleString
63
71
  },
72
+ {
73
+ name: 'unsafe long string without double quotes',
74
+ schema: {
75
+ type: 'string',
76
+ format: 'unsafe'
77
+ },
78
+ input: longSimpleString
79
+ },
64
80
  {
65
81
  name: 'long string',
66
82
  schema: {
@@ -68,6 +84,14 @@ const benchmarks = [
68
84
  },
69
85
  input: longString
70
86
  },
87
+ {
88
+ name: 'unsafe long string',
89
+ schema: {
90
+ type: 'string',
91
+ format: 'unsafe'
92
+ },
93
+ input: longString
94
+ },
71
95
  {
72
96
  name: 'number',
73
97
  schema: {
package/index.js CHANGED
@@ -725,8 +725,24 @@ function buildSingleTypeSerializer (context, location, input) {
725
725
  return `json += serializer.asDate(${input})`
726
726
  } else if (schema.format === 'time') {
727
727
  return `json += serializer.asTime(${input})`
728
+ } else if (schema.format === 'unsafe') {
729
+ return `json += serializer.asUnsafeString(${input})`
728
730
  } else {
729
- return `json += serializer.asString(${input})`
731
+ return `
732
+ if (typeof ${input} !== 'string') {
733
+ if (${input} === null) {
734
+ json += '""'
735
+ } else if (${input} instanceof Date) {
736
+ json += '"' + ${input}.toISOString() + '"'
737
+ } else if (${input} instanceof RegExp) {
738
+ json += serializer.asString(${input}.source)
739
+ } else {
740
+ json += serializer.asString(${input}.toString())
741
+ }
742
+ } else {
743
+ json += serializer.asString(${input})
744
+ }
745
+ `
730
746
  }
731
747
  }
732
748
  case 'integer':
package/lib/serializer.js CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  // eslint-disable-next-line
4
- const STR_ESCAPE = /[\u0000-\u001f\u0022\u005c\ud800-\udfff]|[\ud800-\udbff](?![\udc00-\udfff])|(?:[^\ud800-\udbff]|^)[\udc00-\udfff]/
4
+ const STR_ESCAPE = /[\u0000-\u001f\u0022\u005c\ud800-\udfff]/
5
5
 
6
6
  module.exports = class Serializer {
7
7
  constructor (options) {
@@ -25,13 +25,12 @@ module.exports = class Serializer {
25
25
 
26
26
  asInteger (i) {
27
27
  if (typeof i === 'number') {
28
- if (i === Infinity || i === -Infinity) {
29
- throw new Error(`The value "${i}" cannot be converted to an integer.`)
30
- }
31
28
  if (Number.isInteger(i)) {
32
29
  return '' + i
33
30
  }
34
- if (Number.isNaN(i)) {
31
+ // check if number is Infinity or NaN
32
+ // eslint-disable-next-line no-self-compare
33
+ if (i === Infinity || i === -Infinity || i !== i) {
35
34
  throw new Error(`The value "${i}" cannot be converted to an integer.`)
36
35
  }
37
36
  return this.parseInteger(i)
@@ -52,7 +51,9 @@ module.exports = class Serializer {
52
51
 
53
52
  asNumber (i) {
54
53
  const num = Number(i)
55
- if (Number.isNaN(num)) {
54
+ // check if number is NaN
55
+ // eslint-disable-next-line no-self-compare
56
+ if (num !== num) {
56
57
  throw new Error(`The value "${i}" cannot be converted to a number.`)
57
58
  } else if (!Number.isFinite(num)) {
58
59
  return 'null'
@@ -99,29 +100,20 @@ module.exports = class Serializer {
99
100
  }
100
101
 
101
102
  asString (str) {
102
- if (typeof str !== 'string') {
103
- if (str === null) {
104
- return '""'
105
- }
106
- if (str instanceof Date) {
107
- return '"' + str.toISOString() + '"'
108
- }
109
- if (str instanceof RegExp) {
110
- str = str.source
111
- } else {
112
- str = str.toString()
113
- }
114
- }
115
-
116
103
  if (str.length < 42) {
117
104
  return this.asStringSmall(str)
118
- } else if (STR_ESCAPE.test(str) === false) {
105
+ } else if (str.length < 5000 && STR_ESCAPE.test(str) === false) {
106
+ // Only use the regular expression for shorter input. The overhead is otherwise too much.
119
107
  return '"' + str + '"'
120
108
  } else {
121
109
  return JSON.stringify(str)
122
110
  }
123
111
  }
124
112
 
113
+ asUnsafeString (str) {
114
+ return '"' + str + '"'
115
+ }
116
+
125
117
  // magically escape strings for json
126
118
  // relying on their charCodeAt
127
119
  // everything below 32 needs JSON.stringify()
@@ -137,13 +129,6 @@ module.exports = class Serializer {
137
129
  // eslint-disable-next-line
138
130
  for (var i = 0; i < len; i++) {
139
131
  point = str.charCodeAt(i)
140
- if (point < 32) {
141
- return JSON.stringify(str)
142
- }
143
- if (point >= 0xD800 && point <= 0xDFFF) {
144
- // The current character is a surrogate.
145
- return JSON.stringify(str)
146
- }
147
132
  if (
148
133
  point === 0x22 || // '"'
149
134
  point === 0x5c // '\'
@@ -151,6 +136,9 @@ module.exports = class Serializer {
151
136
  last === -1 && (last = 0)
152
137
  result += str.slice(last, i) + '\\'
153
138
  last = i
139
+ } else if (point < 32 || (point >= 0xD800 && point <= 0xDFFF)) {
140
+ // The current character is non-printable characters or a surrogate.
141
+ return JSON.stringify(str)
154
142
  }
155
143
  }
156
144
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fast-json-stringify",
3
- "version": "5.12.0",
3
+ "version": "5.14.0",
4
4
  "description": "Stringify your JSON at max speed",
5
5
  "main": "index.js",
6
6
  "type": "commonjs",
@@ -46,13 +46,13 @@
46
46
  "simple-git": "^3.7.1",
47
47
  "standard": "^17.0.0",
48
48
  "tap": "^16.0.1",
49
- "tsd": "^0.30.3",
49
+ "tsd": "^0.31.0",
50
50
  "webpack": "^5.40.0",
51
51
  "fast-json-stringify": "."
52
52
  },
53
53
  "dependencies": {
54
54
  "ajv": "^8.10.0",
55
- "ajv-formats": "^2.1.1",
55
+ "ajv-formats": "^3.0.1",
56
56
  "fast-deep-equal": "^3.1.3",
57
57
  "fast-uri": "^2.1.0",
58
58
  "rfdc": "^1.2.0",
@@ -18,6 +18,12 @@ function buildTest (schema, toStringify) {
18
18
  })
19
19
  }
20
20
 
21
+ buildTest({
22
+ title: 'string',
23
+ type: 'string',
24
+ format: 'unsafe'
25
+ }, 'hello world')
26
+
21
27
  buildTest({
22
28
  title: 'basic',
23
29
  type: 'object',
@@ -48,3 +48,37 @@ test('serialize long string', (t) => {
48
48
  t.equal(output, `"${new Array(2e4).fill('\\u0000').join('')}"`)
49
49
  t.equal(JSON.parse(output), input)
50
50
  })
51
+
52
+ test('unsafe string', (t) => {
53
+ t.plan(2)
54
+
55
+ const schema = {
56
+ type: 'string',
57
+ format: 'unsafe'
58
+ }
59
+
60
+ const input = 'abcd'
61
+ const stringify = build(schema)
62
+ const output = stringify(input)
63
+
64
+ t.equal(output, `"${input}"`)
65
+ t.equal(JSON.parse(output), input)
66
+ })
67
+
68
+ test('unsafe unescaped string', (t) => {
69
+ t.plan(2)
70
+
71
+ const schema = {
72
+ type: 'string',
73
+ format: 'unsafe'
74
+ }
75
+
76
+ const input = 'abcd "abcd"'
77
+ const stringify = build(schema)
78
+ const output = stringify(input)
79
+
80
+ t.equal(output, `"${input}"`)
81
+ t.throws(function () {
82
+ JSON.parse(output)
83
+ })
84
+ })
package/types/index.d.ts CHANGED
@@ -208,6 +208,9 @@ interface StandaloneOption extends build.Options {
208
208
  mode: 'standalone'
209
209
  }
210
210
 
211
+ type StringCoercible = string | Date | RegExp;
212
+ type IntegerCoercible = number | BigInt;
213
+
211
214
  /**
212
215
  * Build a stringify function using a schema of the documents that should be stringified
213
216
  * @param schema The schema used to stringify values
@@ -217,8 +220,8 @@ declare function build(schema: build.AnySchema, options: DebugOption): { code: s
217
220
  declare function build(schema: build.AnySchema, options: DeprecateDebugOption): { code: string, ajv: Ajv };
218
221
  declare function build(schema: build.AnySchema, options: StandaloneOption): string;
219
222
  declare function build(schema: build.AnySchema, options?: build.Options): <TDoc = any>(doc: TDoc) => any;
220
- declare function build(schema: build.StringSchema, options?: build.Options): <TDoc extends string = string>(doc: TDoc) => string;
221
- declare function build(schema: build.IntegerSchema | build.NumberSchema, options?: build.Options): <TDoc extends number = number>(doc: TDoc) => string;
223
+ declare function build(schema: build.StringSchema, options?: build.Options): <TDoc extends StringCoercible = StringCoercible>(doc: TDoc) => string;
224
+ declare function build(schema: build.IntegerSchema | build.NumberSchema, options?: build.Options): <TDoc extends IntegerCoercible = IntegerCoercible>(doc: TDoc) => string;
222
225
  declare function build(schema: build.NullSchema, options?: build.Options): <TDoc extends null = null>(doc: TDoc) => "null";
223
226
  declare function build(schema: build.BooleanSchema, options?: build.Options): <TDoc extends boolean = boolean>(doc: TDoc) => string;
224
227
  declare function build(schema: build.ArraySchema | build.TupleSchema, options?: build.Options): <TDoc extends any[]= any[]>(doc: TDoc) => string;
@@ -3,61 +3,62 @@ import build, { restore, Schema, validLargeArrayMechanisms } from '..'
3
3
  import { expectError, expectType } from 'tsd'
4
4
 
5
5
  // Number schemas
6
- const schema1: Schema = {
6
+ build({
7
7
  type: 'number'
8
- }
9
- const schema2: Schema = {
8
+ })(25)
9
+ build({
10
10
  type: 'integer'
11
- }
12
-
13
- build(schema1)(25)
14
- build(schema2)(-5)
11
+ })(-5)
12
+ build({
13
+ type: 'integer'
14
+ })(5n)
15
15
 
16
- build(schema2, { rounding: 'ceil' })
17
- build(schema2, { rounding: 'floor' })
18
- build(schema2, { rounding: 'round' })
19
- build(schema2, { rounding: 'trunc' })
20
- expectError(build(schema2, { rounding: 'invalid' }))
16
+ build({
17
+ type: 'number'
18
+ }, { rounding: 'ceil' })
19
+ build({
20
+ type: 'number'
21
+ }, { rounding: 'floor' })
22
+ build({
23
+ type: 'number'
24
+ }, { rounding: 'round' })
25
+ build({
26
+ type: 'number'
27
+ }, { rounding: 'trunc' })
28
+ expectError(build({
29
+ type: 'number'
30
+ }, { rounding: 'invalid' }))
21
31
 
22
32
  // String schema
23
- const schema3: Schema = {
24
- type: 'string'
25
- }
26
-
27
- build(schema3)('foobar')
33
+ build({
34
+ type: 'string'
35
+ })('foobar')
28
36
 
29
37
  // Boolean schema
30
- const schema4: Schema = {
38
+ build({
31
39
  type: 'boolean'
32
- }
33
-
34
- build(schema4)(true)
40
+ })(true)
35
41
 
36
42
  // Null schema
37
- const schema5: Schema = {
43
+ build({
38
44
  type: 'null'
39
- }
40
-
41
- build(schema5)(null)
45
+ })(null)
42
46
 
43
47
  // Array schemas
44
- const schema6: Schema = {
48
+ build({
45
49
  type: 'array',
46
50
  items: { type: 'number' }
47
- }
48
- const schema7: Schema = {
51
+ })([25])
52
+ build({
49
53
  type: 'array',
50
54
  items: [{ type: 'string'}, {type: 'integer'}]
51
- }
52
-
53
- build(schema6)([25])
54
- build(schema7)(['hello', 42])
55
+ })(['hello', 42])
55
56
 
56
57
  // Object schemas
57
- const schema8: Schema = {
58
+ build({
58
59
  type: 'object'
59
- }
60
- const schema9: Schema = {
60
+ })({})
61
+ build({
61
62
  type: 'object',
62
63
  properties: {
63
64
  foo: { type: 'string' },
@@ -70,14 +71,24 @@ const schema9: Schema = {
70
71
  additionalProperties: {
71
72
  type: 'boolean'
72
73
  }
73
- }
74
-
75
- build(schema8)({})
76
- build(schema9)({ foo: 'bar' })
77
- build(schema9, { rounding: 'floor' })({ foo: 'bar' })
74
+ })({ foo: 'bar' })
75
+ build({
76
+ type: 'object',
77
+ properties: {
78
+ foo: { type: 'string' },
79
+ bar: { type: 'integer' }
80
+ },
81
+ required: ['foo'],
82
+ patternProperties: {
83
+ 'baz*': { type: 'null' }
84
+ },
85
+ additionalProperties: {
86
+ type: 'boolean'
87
+ }
88
+ }, { rounding: 'floor' })({ foo: 'bar' })
78
89
 
79
90
  // Reference schemas
80
- const schema10: Schema = {
91
+ build({
81
92
  title: 'Example Schema',
82
93
  definitions: {
83
94
  num: {
@@ -109,12 +120,10 @@ const schema10: Schema = {
109
120
  additionalProperties: {
110
121
  $ref: '#/definitions/def'
111
122
  }
112
- }
113
-
114
- build(schema10)({ nickname: '', num: { int: 5 }, other: null })
123
+ })({ nickname: '', num: { int: 5 }, other: null })
115
124
 
116
125
  // Conditional/Combined schemas
117
- const schema11: Schema = {
126
+ build({
118
127
  title: 'Conditional/Combined Schema',
119
128
  type: 'object',
120
129
  properties: {
@@ -140,24 +149,37 @@ const schema11: Schema = {
140
149
  somethingElse: { type: 'null' }
141
150
  }
142
151
  }
143
- }
144
-
145
- build(schema11)({ something: 'a string', somethingElse: 42 })
152
+ })({ something: 'a string', somethingElse: 42 })
146
153
 
147
154
  // String schema with format
148
- const schema12: Schema = {
155
+
156
+ build({
149
157
  type: 'string',
150
158
  format: 'date-time'
151
- }
152
-
153
- build(schema12)(new Date())
159
+ })(new Date())
154
160
 
161
+ /*
162
+ This overload doesn't work yet -
163
+ TypeScript chooses the generic for the schema
164
+ before it chooses the overload for the options
165
+ parameter.
155
166
  let str: string, ajv: Ajv
156
- str = build(schema1, { debugMode: true }).code
157
- ajv = build(schema1, { debugMode: true }).ajv
158
- str = build(schema1, { mode: 'debug' }).code
159
- ajv = build(schema1, { mode: 'debug' }).ajv
160
- str = build(schema1, { mode: 'standalone' })
167
+ str = build({
168
+ type: 'number'
169
+ }, { debugMode: true }).code
170
+ ajv = build({
171
+ type: 'number'
172
+ }, { debugMode: true }).ajv
173
+ str = build({
174
+ type: 'number'
175
+ }, { mode: 'debug' }).code
176
+ ajv = build({
177
+ type: 'number'
178
+ }, { mode: 'debug' }).ajv
179
+ str = build({
180
+ type: 'number'
181
+ }, { mode: 'standalone' })
182
+ */
161
183
 
162
184
  const debugCompiled = build({
163
185
  title: 'default string',