@prairielearn/sanitize 1.0.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.
File without changes
package/README.md ADDED
@@ -0,0 +1,26 @@
1
+ # `@prairielearn/sanitize`
2
+
3
+ A collection of functions for sanitizing and escaping various values.
4
+
5
+ ## Usage
6
+
7
+ ```ts
8
+ import { sanitizeObject, escapeRegExp, recursivelyTruncateStrings } from '@prairielearn/sanitize';
9
+
10
+ sanitizeObject({
11
+ value: 'null \u0000 byte',
12
+ });
13
+
14
+ escapeRegExp('foo*(bar)');
15
+
16
+ recursivelyTruncateStrings(
17
+ {
18
+ foo: {
19
+ bar: {
20
+ baz: 'biz'.repeat(10000),
21
+ },
22
+ },
23
+ },
24
+ 100
25
+ );
26
+ ```
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Recursively traverse an object and replace null bytes (\u0000) with the
3
+ * literal string "\u0000". This produces a new object and does not modify the
4
+ * provided object.
5
+ *
6
+ * @param value The object to be sanitized.
7
+ * @return The sanitized object.
8
+ */
9
+ export declare function sanitizeObject<T>(value: T): T;
10
+ /**
11
+ * Escape special characters in a RegExp string
12
+ * Source: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Using_special_characters
13
+ *
14
+ * @param str A literal string to act as a match for RegExp objects
15
+ * @return A string literal ready to match
16
+ */
17
+ export declare function escapeRegExp(str: string): string;
18
+ /**
19
+ * Recursively truncates all strings in a value to a maximum length.
20
+ */
21
+ export declare function recursivelyTruncateStrings<T>(value: T, maxLength: number): T;
package/dist/index.js ADDED
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.recursivelyTruncateStrings = exports.escapeRegExp = exports.sanitizeObject = void 0;
4
+ /**
5
+ * Recursively traverse an object and replace null bytes (\u0000) with the
6
+ * literal string "\u0000". This produces a new object and does not modify the
7
+ * provided object.
8
+ *
9
+ * @param value The object to be sanitized.
10
+ * @return The sanitized object.
11
+ */
12
+ function sanitizeObject(value) {
13
+ if (value === null) {
14
+ return null;
15
+ }
16
+ else if (Array.isArray(value)) {
17
+ return value.map(sanitizeObject);
18
+ }
19
+ else if (typeof value === 'string') {
20
+ return value.replace('\u0000', '\\u0000');
21
+ }
22
+ else if (typeof value === 'object') {
23
+ const sanitized = Object.entries(value).map(([key, value]) => {
24
+ return [key, sanitizeObject(value)];
25
+ });
26
+ return sanitized.reduce((acc, [key, value]) => {
27
+ acc[key] = value;
28
+ return acc;
29
+ }, {});
30
+ }
31
+ else {
32
+ return value;
33
+ }
34
+ }
35
+ exports.sanitizeObject = sanitizeObject;
36
+ /**
37
+ * Escape special characters in a RegExp string
38
+ * Source: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Using_special_characters
39
+ *
40
+ * @param str A literal string to act as a match for RegExp objects
41
+ * @return A string literal ready to match
42
+ */
43
+ function escapeRegExp(str) {
44
+ return str.replace(/[.*+\-?^${}()|[\]\\/]/g, '\\$&');
45
+ }
46
+ exports.escapeRegExp = escapeRegExp;
47
+ /**
48
+ * Recursively truncates all strings in a value to a maximum length.
49
+ */
50
+ function recursivelyTruncateStrings(value, maxLength) {
51
+ if (value === null) {
52
+ return null;
53
+ }
54
+ else if (typeof value === 'string') {
55
+ if (value.length <= maxLength) {
56
+ return value;
57
+ }
58
+ return (value.substring(0, maxLength) + '...[truncated]');
59
+ }
60
+ else if (Array.isArray(value)) {
61
+ return value.map((value) => recursivelyTruncateStrings(value, maxLength));
62
+ }
63
+ else if (typeof value === 'object') {
64
+ return Object.entries(value).reduce((acc, [key, value]) => {
65
+ acc[key] = recursivelyTruncateStrings(value, maxLength);
66
+ return acc;
67
+ }, {});
68
+ }
69
+ else {
70
+ return value;
71
+ }
72
+ }
73
+ exports.recursivelyTruncateStrings = recursivelyTruncateStrings;
74
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA;;;;;;;GAOG;AACH,SAAgB,cAAc,CAAI,KAAQ;IACxC,IAAI,KAAK,KAAK,IAAI,EAAE;QAClB,OAAO,IAAS,CAAC;KAClB;SAAM,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE;QAC/B,OAAO,KAAK,CAAC,GAAG,CAAC,cAAc,CAAM,CAAC;KACvC;SAAM,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE;QACpC,OAAO,KAAK,CAAC,OAAO,CAAC,QAAQ,EAAE,SAAS,CAAM,CAAC;KAChD;SAAM,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE;QACpC,MAAM,SAAS,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE;YAC3D,OAAO,CAAC,GAAG,EAAE,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC;QACtC,CAAC,CAAC,CAAC;QACH,OAAO,SAAS,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE;YAC5C,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;YACjB,OAAO,GAAG,CAAC;QACb,CAAC,EAAE,EAAS,CAAM,CAAC;KACpB;SAAM;QACL,OAAO,KAAK,CAAC;KACd;AACH,CAAC;AAlBD,wCAkBC;AAED;;;;;;GAMG;AACH,SAAgB,YAAY,CAAC,GAAW;IACtC,OAAO,GAAG,CAAC,OAAO,CAAC,wBAAwB,EAAE,MAAM,CAAC,CAAC;AACvD,CAAC;AAFD,oCAEC;AAED;;GAEG;AACH,SAAgB,0BAA0B,CAAI,KAAQ,EAAE,SAAiB;IACvE,IAAI,KAAK,KAAK,IAAI,EAAE;QAClB,OAAO,IAAS,CAAC;KAClB;SAAM,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE;QACpC,IAAI,KAAK,CAAC,MAAM,IAAI,SAAS,EAAE;YAC7B,OAAO,KAAK,CAAC;SACd;QACD,OAAO,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,EAAE,SAAS,CAAC,GAAG,gBAAgB,CAAM,CAAC;KAChE;SAAM,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE;QAC/B,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,0BAA0B,CAAC,KAAK,EAAE,SAAS,CAAC,CAAM,CAAC;KAChF;SAAM,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE;QACpC,OAAO,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE;YACxD,GAAG,CAAC,GAAG,CAAC,GAAG,0BAA0B,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;YACxD,OAAO,GAAG,CAAC;QACb,CAAC,EAAE,EAAS,CAAM,CAAC;KACpB;SAAM;QACL,OAAO,KAAK,CAAC;KACd;AACH,CAAC;AAlBD,gEAkBC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const chai_1 = require("chai");
4
+ const index_1 = require("./index");
5
+ describe('sanitizeObject', () => {
6
+ it('sanitizes an empty object', () => {
7
+ const input = {};
8
+ const expected = {};
9
+ chai_1.assert.deepEqual(expected, (0, index_1.sanitizeObject)(input));
10
+ });
11
+ it('handles null byte in top-level string', () => {
12
+ const input = { test: 'test\u0000ing' };
13
+ const expected = { test: 'test\\u0000ing' };
14
+ chai_1.assert.deepEqual(expected, (0, index_1.sanitizeObject)(input));
15
+ });
16
+ it('handles null byte in nested string', () => {
17
+ const input = {
18
+ test: {
19
+ nestedTest: 'test\u0000ing',
20
+ },
21
+ };
22
+ const expected = {
23
+ test: {
24
+ nestedTest: 'test\\u0000ing',
25
+ },
26
+ };
27
+ chai_1.assert.deepEqual(expected, (0, index_1.sanitizeObject)(input));
28
+ });
29
+ it('handles null byte in top-level array', () => {
30
+ const input = {
31
+ test: ['testing', 'test\u0000ing'],
32
+ };
33
+ const expected = {
34
+ test: ['testing', 'test\\u0000ing'],
35
+ };
36
+ chai_1.assert.deepEqual(expected, (0, index_1.sanitizeObject)(input));
37
+ });
38
+ it('handles null byte in nested array', () => {
39
+ const input = {
40
+ test: {
41
+ test2: ['testing', 'test\u0000ing'],
42
+ },
43
+ };
44
+ const expected = {
45
+ test: {
46
+ test2: ['testing', 'test\\u0000ing'],
47
+ },
48
+ };
49
+ chai_1.assert.deepEqual(expected, (0, index_1.sanitizeObject)(input));
50
+ });
51
+ it('handles numbers correctly', () => {
52
+ const input = {
53
+ test: 'test\u0000ing',
54
+ a: 1,
55
+ b: 2.45,
56
+ };
57
+ const expected = {
58
+ test: 'test\\u0000ing',
59
+ a: 1,
60
+ b: 2.45,
61
+ };
62
+ chai_1.assert.deepEqual(expected, (0, index_1.sanitizeObject)(input));
63
+ });
64
+ it('handles null values correctly', () => {
65
+ const input = {
66
+ test: 'test\u0000ing',
67
+ a: null,
68
+ };
69
+ const expected = {
70
+ test: 'test\\u0000ing',
71
+ a: null,
72
+ };
73
+ chai_1.assert.deepEqual(expected, (0, index_1.sanitizeObject)(input));
74
+ });
75
+ });
76
+ describe('recursivelyTruncateStrings', () => {
77
+ it('handles empty object', () => {
78
+ chai_1.assert.deepEqual((0, index_1.recursivelyTruncateStrings)({}, 10), {});
79
+ });
80
+ it('handles null and undefined', () => {
81
+ chai_1.assert.deepEqual((0, index_1.recursivelyTruncateStrings)({ test: null }, 10), { test: null });
82
+ chai_1.assert.deepEqual((0, index_1.recursivelyTruncateStrings)({ test: undefined }, 10), { test: undefined });
83
+ });
84
+ it('handles legal string', () => {
85
+ chai_1.assert.deepEqual((0, index_1.recursivelyTruncateStrings)({ test: 'test' }, 10), { test: 'test' });
86
+ });
87
+ it('handles long string', () => {
88
+ chai_1.assert.deepEqual((0, index_1.recursivelyTruncateStrings)({ test: 'testtest' }, 4), {
89
+ test: 'test...[truncated]',
90
+ });
91
+ });
92
+ it('handles long string in array', () => {
93
+ chai_1.assert.deepEqual((0, index_1.recursivelyTruncateStrings)({ test: ['testtest'] }, 4), {
94
+ test: ['test...[truncated]'],
95
+ });
96
+ });
97
+ it('handles long string in object in array', () => {
98
+ chai_1.assert.deepEqual((0, index_1.recursivelyTruncateStrings)({ test: [{ test: 'testtest' }] }, 4), {
99
+ test: [{ test: 'test...[truncated]' }],
100
+ });
101
+ });
102
+ });
103
+ //# sourceMappingURL=index.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.test.js","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@prairielearn/sanitize",
3
+ "version": "1.0.0",
4
+ "main": "./dist/index.js",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/PrairieLearn/PrairieLearn.git",
8
+ "directory": "packages/sanitize"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "dev": "tsc --watch --preserveWatchOutput",
13
+ "test": "mocha --no-config --require ts-node/register src/*.test.ts"
14
+ },
15
+ "devDependencies": {
16
+ "@prairielearn/tsconfig": "^0.0.0",
17
+ "@types/node": "^18.15.11",
18
+ "mocha": "^10.2.0",
19
+ "ts-node": "^10.9.1",
20
+ "typescript": "^4.9.4"
21
+ }
22
+ }
@@ -0,0 +1,114 @@
1
+ import { assert } from 'chai';
2
+
3
+ import { sanitizeObject, recursivelyTruncateStrings } from './index';
4
+
5
+ describe('sanitizeObject', () => {
6
+ it('sanitizes an empty object', () => {
7
+ const input = {};
8
+ const expected = {};
9
+ assert.deepEqual(expected, sanitizeObject(input));
10
+ });
11
+
12
+ it('handles null byte in top-level string', () => {
13
+ const input = { test: 'test\u0000ing' };
14
+ const expected = { test: 'test\\u0000ing' };
15
+ assert.deepEqual(expected, sanitizeObject(input));
16
+ });
17
+
18
+ it('handles null byte in nested string', () => {
19
+ const input = {
20
+ test: {
21
+ nestedTest: 'test\u0000ing',
22
+ },
23
+ };
24
+ const expected = {
25
+ test: {
26
+ nestedTest: 'test\\u0000ing',
27
+ },
28
+ };
29
+ assert.deepEqual(expected, sanitizeObject(input));
30
+ });
31
+
32
+ it('handles null byte in top-level array', () => {
33
+ const input = {
34
+ test: ['testing', 'test\u0000ing'],
35
+ };
36
+ const expected = {
37
+ test: ['testing', 'test\\u0000ing'],
38
+ };
39
+ assert.deepEqual(expected, sanitizeObject(input));
40
+ });
41
+
42
+ it('handles null byte in nested array', () => {
43
+ const input = {
44
+ test: {
45
+ test2: ['testing', 'test\u0000ing'],
46
+ },
47
+ };
48
+ const expected = {
49
+ test: {
50
+ test2: ['testing', 'test\\u0000ing'],
51
+ },
52
+ };
53
+ assert.deepEqual(expected, sanitizeObject(input));
54
+ });
55
+
56
+ it('handles numbers correctly', () => {
57
+ const input = {
58
+ test: 'test\u0000ing',
59
+ a: 1,
60
+ b: 2.45,
61
+ };
62
+ const expected = {
63
+ test: 'test\\u0000ing',
64
+ a: 1,
65
+ b: 2.45,
66
+ };
67
+ assert.deepEqual(expected, sanitizeObject(input));
68
+ });
69
+
70
+ it('handles null values correctly', () => {
71
+ const input = {
72
+ test: 'test\u0000ing',
73
+ a: null,
74
+ };
75
+ const expected = {
76
+ test: 'test\\u0000ing',
77
+ a: null,
78
+ };
79
+ assert.deepEqual(expected, sanitizeObject(input));
80
+ });
81
+ });
82
+
83
+ describe('recursivelyTruncateStrings', () => {
84
+ it('handles empty object', () => {
85
+ assert.deepEqual(recursivelyTruncateStrings({}, 10), {});
86
+ });
87
+
88
+ it('handles null and undefined', () => {
89
+ assert.deepEqual(recursivelyTruncateStrings({ test: null }, 10), { test: null });
90
+ assert.deepEqual(recursivelyTruncateStrings({ test: undefined }, 10), { test: undefined });
91
+ });
92
+
93
+ it('handles legal string', () => {
94
+ assert.deepEqual(recursivelyTruncateStrings({ test: 'test' }, 10), { test: 'test' });
95
+ });
96
+
97
+ it('handles long string', () => {
98
+ assert.deepEqual(recursivelyTruncateStrings({ test: 'testtest' }, 4), {
99
+ test: 'test...[truncated]',
100
+ });
101
+ });
102
+
103
+ it('handles long string in array', () => {
104
+ assert.deepEqual(recursivelyTruncateStrings({ test: ['testtest'] }, 4), {
105
+ test: ['test...[truncated]'],
106
+ });
107
+ });
108
+
109
+ it('handles long string in object in array', () => {
110
+ assert.deepEqual(recursivelyTruncateStrings({ test: [{ test: 'testtest' }] }, 4), {
111
+ test: [{ test: 'test...[truncated]' }],
112
+ });
113
+ });
114
+ });
package/src/index.ts ADDED
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Recursively traverse an object and replace null bytes (\u0000) with the
3
+ * literal string "\u0000". This produces a new object and does not modify the
4
+ * provided object.
5
+ *
6
+ * @param value The object to be sanitized.
7
+ * @return The sanitized object.
8
+ */
9
+ export function sanitizeObject<T>(value: T): T {
10
+ if (value === null) {
11
+ return null as T;
12
+ } else if (Array.isArray(value)) {
13
+ return value.map(sanitizeObject) as T;
14
+ } else if (typeof value === 'string') {
15
+ return value.replace('\u0000', '\\u0000') as T;
16
+ } else if (typeof value === 'object') {
17
+ const sanitized = Object.entries(value).map(([key, value]) => {
18
+ return [key, sanitizeObject(value)];
19
+ });
20
+ return sanitized.reduce((acc, [key, value]) => {
21
+ acc[key] = value;
22
+ return acc;
23
+ }, {} as any) as T;
24
+ } else {
25
+ return value;
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Escape special characters in a RegExp string
31
+ * Source: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Using_special_characters
32
+ *
33
+ * @param str A literal string to act as a match for RegExp objects
34
+ * @return A string literal ready to match
35
+ */
36
+ export function escapeRegExp(str: string) {
37
+ return str.replace(/[.*+\-?^${}()|[\]\\/]/g, '\\$&');
38
+ }
39
+
40
+ /**
41
+ * Recursively truncates all strings in a value to a maximum length.
42
+ */
43
+ export function recursivelyTruncateStrings<T>(value: T, maxLength: number): T {
44
+ if (value === null) {
45
+ return null as T;
46
+ } else if (typeof value === 'string') {
47
+ if (value.length <= maxLength) {
48
+ return value;
49
+ }
50
+ return (value.substring(0, maxLength) + '...[truncated]') as T;
51
+ } else if (Array.isArray(value)) {
52
+ return value.map((value) => recursivelyTruncateStrings(value, maxLength)) as T;
53
+ } else if (typeof value === 'object') {
54
+ return Object.entries(value).reduce((acc, [key, value]) => {
55
+ acc[key] = recursivelyTruncateStrings(value, maxLength);
56
+ return acc;
57
+ }, {} as any) as T;
58
+ } else {
59
+ return value;
60
+ }
61
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "@prairielearn/tsconfig",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src",
6
+ "types": ["mocha", "node"],
7
+ }
8
+ }