@monochromatic-dev/module-logger 0.2.0 → 0.4.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/CHANGELOG.md +27 -0
- package/README.md +53 -13
- package/dist/final/neutral/browser.d.mts +60 -0
- package/dist/final/neutral/browser.mjs +1 -0
- package/dist/final/neutral/index.d.mts +359 -596
- package/dist/final/neutral/index.mjs +2 -3
- package/dist/final/neutral/indexed-db-hsIfv7Cv.mjs +2 -0
- package/dist/final/neutral/types-BkkBXgY3.d.mts +76 -0
- package/dist/final/node/file-CRGb1hDK.mjs +1 -0
- package/dist/final/node/index.d.mts +359 -596
- package/dist/final/node/index.mjs +3 -3
- package/dist/final/node/node.d.mts +103 -0
- package/dist/final/node/node.mjs +1 -0
- package/dist/final/node/types-BkkBXgY3.d.mts +76 -0
- package/package.json +19 -4
- package/src/artifact-platform-split.unit.test.ts +140 -0
- package/src/browser.ts +14 -0
- package/src/create-logger.ts +183 -183
- package/src/create-logger.unit.test.ts +112 -112
- package/src/default-sinks.neutral.ts +47 -0
- package/src/default-sinks.node.ts +43 -0
- package/src/error-format.ts +23 -23
- package/src/index.ts +1 -4
- package/src/logger.ts +76 -51
- package/src/node.ts +23 -0
- package/src/restricted-global-scope.unit.test.ts +101 -0
- package/src/sink/console-control-chars.ts +64 -64
- package/src/sink/console-control-chars.unit.test.ts +14 -14
- package/src/sink/console.ts +194 -194
- package/src/sink/console.unit.test.ts +18 -18
- package/src/sink/file.ts +136 -140
- package/src/sink/file.unit.test.ts +19 -26
- package/src/sink/index.ts +4 -7
- package/src/sink/indexed-db-util.ts +42 -42
- package/src/sink/indexed-db.browser.test.ts +7 -7
- package/src/sink/indexed-db.ts +109 -109
- package/src/sink/indexed-db.unit.test.ts +5 -13
- package/src/sink/local-storage-key.ts +73 -73
- package/src/sink/local-storage-key.unit.test.ts +8 -8
- package/src/sink/local-storage-quota.ts +37 -37
- package/src/sink/local-storage-quota.unit.test.ts +8 -8
- package/src/sink/local-storage-store.ts +113 -113
- package/src/sink/local-storage-store.unit.test.ts +35 -35
- package/src/sink/local-storage.ts +72 -72
- package/src/sink/local-storage.unit.test.ts +27 -27
- package/src/sink/noop.ts +20 -20
- package/src/sink/noop.unit.test.ts +1 -1
- package/src/sink/opfs.browser.test.ts +7 -7
- package/src/sink/opfs.ts +62 -62
- package/src/sink/opfs.unit.test.ts +5 -13
- package/src/sink/record-buffer.ts +84 -84
- package/src/sink/record-buffer.unit.test.ts +20 -20
- package/src/sink/session-storage-quota.ts +34 -34
- package/src/sink/session-storage-quota.unit.test.ts +8 -8
- package/src/sink/session-storage-store.ts +72 -72
- package/src/sink/session-storage.ts +48 -48
- package/src/sink/session-storage.unit.test.ts +39 -39
- package/src/sink/web-storage-quota-error.ts +22 -22
- package/src/sink/web-storage-quota-error.unit.test.ts +2 -2
- package/src/sink/web-storage-runtime.ts +24 -24
- package/src/startup.unit.test.ts +18 -18
- package/src/tagged.ts +35 -35
- package/src/tagged.unit.test.ts +8 -8
- package/src/types.ts +39 -39
package/src/logger.ts
CHANGED
|
@@ -1,67 +1,92 @@
|
|
|
1
|
+
import { createDefaultSinks, } from '#default-sinks';
|
|
2
|
+
|
|
1
3
|
import { createLogger, } from './create-logger.ts';
|
|
2
|
-
|
|
3
|
-
import { createFileSink, } from './sink/file.ts';
|
|
4
|
-
import { createIndexedDbSink, } from './sink/indexed-db.ts';
|
|
5
|
-
import { createLocalStorageSink, } from './sink/local-storage.ts';
|
|
6
|
-
import { createSessionStorageSink, } from './sink/session-storage.ts';
|
|
4
|
+
|
|
7
5
|
import type {
|
|
6
|
+
Level,
|
|
8
7
|
Logger,
|
|
9
|
-
Sink,
|
|
10
8
|
} from './types.ts';
|
|
11
9
|
|
|
12
10
|
/**
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
11
|
+
Default logger instance and its readiness promise, built on first use.
|
|
12
|
+
*/
|
|
13
|
+
type DefaultInstance = {
|
|
14
|
+
readonly initPromise: Promise<void>;
|
|
15
|
+
readonly logger: Logger;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
Memo for the default instance. Empty until the first log or flush call, so
|
|
20
|
+
importing this module (or `tagged`, which reaches it) runs no sink
|
|
21
|
+
discovery: no timers, no I/O, no storage probes. Runtimes that forbid those
|
|
22
|
+
in global scope (Cloudflare Workers, issue #493) therefore pay nothing at
|
|
23
|
+
import and verify their sinks inside whatever handler logs first.
|
|
24
|
+
*/
|
|
25
|
+
const memo: { current?: DefaultInstance; } = {};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
Builds the default instance on first use and returns it afterwards.
|
|
29
|
+
|
|
30
|
+
@returns Default logger and its readiness promise.
|
|
31
|
+
|
|
32
|
+
@example
|
|
33
|
+
```ts
|
|
34
|
+
const { logger } = defaultInstance();
|
|
35
|
+
```
|
|
28
36
|
*/
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
createLocalStorageSink(),
|
|
34
|
-
createFileSink(),
|
|
35
|
-
];
|
|
37
|
+
function defaultInstance(): DefaultInstance {
|
|
38
|
+
memo.current ??= createLogger({ sinks: createDefaultSinks(), },);
|
|
39
|
+
return memo.current;
|
|
40
|
+
}
|
|
36
41
|
|
|
37
42
|
/**
|
|
38
|
-
|
|
39
|
-
|
|
43
|
+
Builds one level method that forwards to the default instance, creating it
|
|
44
|
+
on the first call.
|
|
45
|
+
|
|
46
|
+
@param level - Severity the method logs at.
|
|
47
|
+
|
|
48
|
+
@returns Forwarding level method.
|
|
40
49
|
*/
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
50
|
+
function forward(level: Level,): (message: string,) => void {
|
|
51
|
+
return function logAtLevel(message: string,): void {
|
|
52
|
+
defaultInstance()
|
|
53
|
+
.logger[level](message,);
|
|
54
|
+
};
|
|
55
|
+
}
|
|
45
56
|
|
|
46
57
|
/**
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
* async sinks as they become available.
|
|
58
|
+
Awaits the default instance's own `flush`, creating the instance first so a
|
|
59
|
+
flush before any log still verifies the sinks and drains them.
|
|
50
60
|
*/
|
|
51
|
-
|
|
61
|
+
async function flush(): Promise<void> {
|
|
62
|
+
await defaultInstance()
|
|
63
|
+
.logger
|
|
64
|
+
.flush();
|
|
65
|
+
}
|
|
52
66
|
|
|
53
67
|
/**
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
68
|
+
Multi-sink logger that writes to all available backends, built lazily on
|
|
69
|
+
the first call. Startup records replay to async sinks that verify after
|
|
70
|
+
the log call. Log calls throw only when initialization proves no backend is
|
|
71
|
+
available, which the console sink prevents in every supported runtime.
|
|
72
|
+
`flush()` awaits verification internally, so no readiness promise is
|
|
73
|
+
exported: awaiting one at module top level was the mistake this design
|
|
74
|
+
removes.
|
|
75
|
+
|
|
76
|
+
@example
|
|
77
|
+
```ts
|
|
78
|
+
import { logger, } from '\@monochromatic-dev/module-logger';
|
|
79
|
+
|
|
80
|
+
logger.error('unexpected shutdown',);
|
|
81
|
+
await logger.flush();
|
|
82
|
+
```
|
|
66
83
|
*/
|
|
67
|
-
export const logger: Logger =
|
|
84
|
+
export const logger: Logger = {
|
|
85
|
+
debug: forward('debug',),
|
|
86
|
+
error: forward('error',),
|
|
87
|
+
fatal: forward('fatal',),
|
|
88
|
+
flush,
|
|
89
|
+
info: forward('info',),
|
|
90
|
+
trace: forward('trace',),
|
|
91
|
+
warn: forward('warn',),
|
|
92
|
+
};
|
package/src/node.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
Node-only entry (`\@monochromatic-dev/module-logger/node`).
|
|
3
|
+
|
|
4
|
+
Ships the file sink, whose static `node:fs/promises` and `node:path`
|
|
5
|
+
imports must never reach the platform-neutral root entry. Built only by
|
|
6
|
+
`rolldown.node.config.ts`, so the neutral artifact carries no `node:`
|
|
7
|
+
specifier at all.
|
|
8
|
+
|
|
9
|
+
@module
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export { createFileSink, } from './sink/file.ts';
|
|
13
|
+
|
|
14
|
+
//region Internal seams
|
|
15
|
+
// Underscore-prefixed re-exports let `file.unit.test.ts` exercise the ancestor
|
|
16
|
+
// search through the built artifact (the `require-eventual-artifact` rule)
|
|
17
|
+
// without widening the documented API; they are not part of the public
|
|
18
|
+
// contract.
|
|
19
|
+
export {
|
|
20
|
+
findNodeModulesUp as _findNodeModulesUp,
|
|
21
|
+
NO_NODE_MODULES_FOUND as _NO_NODE_MODULES_FOUND,
|
|
22
|
+
} from './sink/file.ts';
|
|
23
|
+
//endregion Internal seams
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
Guards issue #493: evaluating the root entry in a runtime that forbids
|
|
3
|
+
timers in global scope (Cloudflare Workers throw from `setTimeout` there)
|
|
4
|
+
must produce no `logger internal error` output, because the default logger
|
|
5
|
+
is built on first use, not at import. Once a handler runs, the first log
|
|
6
|
+
call builds it and the console sink verifies normally.
|
|
7
|
+
|
|
8
|
+
The built artifact is imported dynamically inside the test so the throwing
|
|
9
|
+
`setTimeout` is in place during module evaluation; this file therefore
|
|
10
|
+
imports nothing from the logger statically.
|
|
11
|
+
|
|
12
|
+
@module
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
describe,
|
|
17
|
+
expect,
|
|
18
|
+
it,
|
|
19
|
+
} from '@monochromatic-dev/module-test/ts';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
Message the first log call inside the "handler" sends, distinct enough to
|
|
23
|
+
find in the console stub's calls.
|
|
24
|
+
*/
|
|
25
|
+
const HANDLER_MESSAGE = 'first log inside a handler after a restricted import';
|
|
26
|
+
|
|
27
|
+
await describe({
|
|
28
|
+
name: 'default logger under a global-scope-restricted runtime',
|
|
29
|
+
// Both tests stub console methods, so they run one at a time.
|
|
30
|
+
concurrency: 1,
|
|
31
|
+
children: [
|
|
32
|
+
it({
|
|
33
|
+
name: 'importing the root entry while setTimeout throws writes no breadcrumb, and the first log inside a handler works',
|
|
34
|
+
fn: async ({ sinon, },) => {
|
|
35
|
+
const warn = sinon.stub(
|
|
36
|
+
console,
|
|
37
|
+
'warn',
|
|
38
|
+
);
|
|
39
|
+
const info = sinon.stub(
|
|
40
|
+
console,
|
|
41
|
+
'info',
|
|
42
|
+
);
|
|
43
|
+
/**
|
|
44
|
+
Timer stub that behaves like a Workers global scope: any timer is a
|
|
45
|
+
disallowed operation.
|
|
46
|
+
*/
|
|
47
|
+
const forbidTimers = sinon.stub(
|
|
48
|
+
globalThis,
|
|
49
|
+
'setTimeout',
|
|
50
|
+
)
|
|
51
|
+
.throws(new Error('Disallowed operation called within global scope',),);
|
|
52
|
+
/**
|
|
53
|
+
Root entry evaluated with timers forbidden, as a Worker isolate does.
|
|
54
|
+
*/
|
|
55
|
+
const entry = await import('@monochromatic-dev/module-logger');
|
|
56
|
+
/**
|
|
57
|
+
`tagged` reaches the singleton through its default parameter; wrapping
|
|
58
|
+
must not build it either.
|
|
59
|
+
*/
|
|
60
|
+
const l = entry.tagged({ tag: 'restricted', },);
|
|
61
|
+
expect(warn.callCount,)
|
|
62
|
+
.toBe(0,);
|
|
63
|
+
forbidTimers.restore();
|
|
64
|
+
|
|
65
|
+
// Inside a handler, timers are allowed again: the first log builds the
|
|
66
|
+
// default logger, its sinks verify, and the console sink writes.
|
|
67
|
+
l.info(HANDLER_MESSAGE,);
|
|
68
|
+
await entry.logger.flush();
|
|
69
|
+
expect(warn.callCount,)
|
|
70
|
+
.toBe(0,);
|
|
71
|
+
/**
|
|
72
|
+
Console lines that carried the handler message.
|
|
73
|
+
*/
|
|
74
|
+
const landed = info.getCalls()
|
|
75
|
+
.filter(function carriesMessage(call,) {
|
|
76
|
+
return call.args
|
|
77
|
+
.some(function mentions(argument,) {
|
|
78
|
+
return String(argument,)
|
|
79
|
+
.includes(HANDLER_MESSAGE,);
|
|
80
|
+
},);
|
|
81
|
+
},);
|
|
82
|
+
expect(landed.length,)
|
|
83
|
+
.toBe(1,);
|
|
84
|
+
},
|
|
85
|
+
},),
|
|
86
|
+
|
|
87
|
+
it({
|
|
88
|
+
name: 'flush before any log still builds the default logger and resolves',
|
|
89
|
+
fn: async ({ sinon, },) => {
|
|
90
|
+
const warn = sinon.stub(
|
|
91
|
+
console,
|
|
92
|
+
'warn',
|
|
93
|
+
);
|
|
94
|
+
const entry = await import('@monochromatic-dev/module-logger');
|
|
95
|
+
await entry.logger.flush();
|
|
96
|
+
expect(warn.callCount,)
|
|
97
|
+
.toBe(0,);
|
|
98
|
+
},
|
|
99
|
+
},),
|
|
100
|
+
],
|
|
101
|
+
},);
|
|
@@ -1,68 +1,68 @@
|
|
|
1
1
|
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
Console-bound text crosses a syntax boundary: a terminal interprets C0 and
|
|
3
|
+
C1 control characters as commands (clear screen, set title, move cursor,
|
|
4
|
+
write clipboard). Log messages can carry attacker-influenced text, so the
|
|
5
|
+
console sink neutralizes every control character except newline and tab
|
|
6
|
+
before the text reaches `console.*` or `process.stderr`.
|
|
7
|
+
|
|
8
|
+
@module
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
First code unit above the C0 control range; everything below it except
|
|
13
|
+
newline and tab is neutralized.
|
|
14
14
|
*/
|
|
15
15
|
const C0_CONTROL_LIMIT = 0x20;
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
|
|
18
|
+
Newline stays literal: multi-line messages (stack traces) are a core use.
|
|
19
19
|
*/
|
|
20
20
|
const NEWLINE_CODE_UNIT = 0x0A;
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
|
-
|
|
23
|
+
Tab stays literal: indentation in multi-line messages is harmless.
|
|
24
24
|
*/
|
|
25
25
|
const TAB_CODE_UNIT = 0x09;
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
|
|
28
|
+
DEL sits alone above the printable ASCII range and is a control character.
|
|
29
29
|
*/
|
|
30
30
|
const DELETE_CODE_UNIT = 0x7F;
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
|
-
|
|
33
|
+
First code unit of the C1 control range (8-bit CSI, OSC, and friends).
|
|
34
34
|
*/
|
|
35
35
|
const C1_CONTROL_START = 0x80;
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
|
-
|
|
38
|
+
Last code unit of the C1 control range.
|
|
39
39
|
*/
|
|
40
40
|
const C1_CONTROL_END = 0x9F;
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
|
-
|
|
43
|
+
Radix for the hexadecimal digits inside a `\uXXXX` escape.
|
|
44
44
|
*/
|
|
45
45
|
const HEX_RADIX = 16;
|
|
46
46
|
|
|
47
47
|
/**
|
|
48
|
-
|
|
48
|
+
Digit count of a `\uXXXX` escape, zero-padded on the left.
|
|
49
49
|
*/
|
|
50
50
|
const UNICODE_ESCAPE_WIDTH = 4;
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
53
|
+
Reports whether one UTF-16 code unit is a control character the console
|
|
54
|
+
sink must neutralize.
|
|
55
|
+
|
|
56
|
+
@param codeUnit - UTF-16 code unit read from the message.
|
|
57
|
+
|
|
58
|
+
@returns Whether the code unit is a C0 control other than newline and tab,
|
|
59
|
+
DEL, or a C1 control.
|
|
60
|
+
|
|
61
|
+
@example
|
|
62
|
+
```ts
|
|
63
|
+
isNeutralizedControl(0x1B); // true (ESC)
|
|
64
|
+
isNeutralizedControl(0x0A); // false (newline stays)
|
|
65
|
+
```
|
|
66
66
|
*/
|
|
67
67
|
function isNeutralizedControl(codeUnit: number,): boolean {
|
|
68
68
|
if (codeUnit < C0_CONTROL_LIMIT)
|
|
@@ -75,18 +75,18 @@ function isNeutralizedControl(codeUnit: number,): boolean {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
/**
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
78
|
+
Renders one code unit as a `\uXXXX` escape with uppercase hex digits (the
|
|
79
|
+
repository's escape-case convention) so the attempted control stays
|
|
80
|
+
visible for forensics instead of vanishing.
|
|
81
|
+
|
|
82
|
+
@param codeUnit - UTF-16 code unit to escape.
|
|
83
|
+
|
|
84
|
+
@returns Six-character escape such as `\u001B`.
|
|
85
|
+
|
|
86
|
+
@example
|
|
87
|
+
```ts
|
|
88
|
+
escapeCodeUnit(0x1B); // '\\u001B'
|
|
89
|
+
```
|
|
90
90
|
*/
|
|
91
91
|
function escapeCodeUnit(codeUnit: number,): string {
|
|
92
92
|
return `\\u${
|
|
@@ -101,36 +101,36 @@ function escapeCodeUnit(codeUnit: number,): string {
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
/**
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
104
|
+
Neutralizes terminal control characters in console-bound text. One linear
|
|
105
|
+
pass over the code points: each neutralized control becomes a `\uXXXX`
|
|
106
|
+
escape, everything else is copied through, and newline and tab pass
|
|
107
|
+
untouched. Well-formed and malformed escape sequences get no
|
|
108
|
+
special treatment because the introducer byte itself is neutralized, so a
|
|
109
|
+
trailing lone ESC, an unterminated OSC, and a nested ESC all lose their
|
|
110
|
+
teeth the same way.
|
|
111
|
+
|
|
112
|
+
@param text - Message text destined for `console.*` or `process.stderr`.
|
|
113
|
+
|
|
114
|
+
@returns Text with every neutralized control rendered as `\uXXXX`.
|
|
115
|
+
|
|
116
|
+
@example
|
|
117
|
+
```ts
|
|
118
|
+
neutralizeControlCharacters('title:\u001B]0;x\u0007 ok\n\tnext');
|
|
119
|
+
// => 'title:\\u001B]0;x\\u0007 ok\n\tnext'
|
|
120
|
+
```
|
|
121
121
|
*/
|
|
122
122
|
export function neutralizeControlCharacters(text: string,): string {
|
|
123
123
|
/**
|
|
124
|
-
|
|
125
|
-
|
|
124
|
+
Output pieces in input order: each code point either verbatim or as its
|
|
125
|
+
escape, joined once at the end so no per-character string rebuild occurs.
|
|
126
126
|
*/
|
|
127
127
|
const pieces: string[] = [];
|
|
128
128
|
for (const character of text) {
|
|
129
129
|
/**
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
130
|
+
Leading code unit of this iteration element. String iteration walks
|
|
131
|
+
code points, so a surrogate pair arrives as one two-unit string whose
|
|
132
|
+
lead surrogate is never a control, and a lone surrogate passes the
|
|
133
|
+
same way.
|
|
134
134
|
*/
|
|
135
135
|
// oxlint-disable-next-line unicorn/prefer-code-point -- Classifier reads the lead code unit on purpose; controls below U+00A0 never sit inside a surrogate pair, so code-point decoding adds nothing.
|
|
136
136
|
const codeUnit = character.charCodeAt(0,);
|
|
@@ -8,9 +8,9 @@ import {
|
|
|
8
8
|
} from '@monochromatic-dev/module-logger';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
Adversarial inputs at the terminal boundary paired with the exact output
|
|
12
|
+
the neutralizer must produce. Each case names the attack or the malformed
|
|
13
|
+
shape it pins.
|
|
14
14
|
*/
|
|
15
15
|
const BOUNDARY_CASES: readonly {
|
|
16
16
|
readonly name: string;
|
|
@@ -80,7 +80,7 @@ const BOUNDARY_CASES: readonly {
|
|
|
80
80
|
];
|
|
81
81
|
|
|
82
82
|
/**
|
|
83
|
-
|
|
83
|
+
Inputs the neutralizer must return unchanged.
|
|
84
84
|
*/
|
|
85
85
|
const PASSTHROUGH_CASES: readonly {
|
|
86
86
|
readonly name: string;
|
|
@@ -117,17 +117,17 @@ const PASSTHROUGH_CASES: readonly {
|
|
|
117
117
|
];
|
|
118
118
|
|
|
119
119
|
/**
|
|
120
|
-
|
|
121
|
-
|
|
120
|
+
One past the last C1 code unit; the exhaustive sweep covers every code unit
|
|
121
|
+
below it.
|
|
122
122
|
*/
|
|
123
123
|
const CONTROL_SWEEP_LENGTH = 0xA0;
|
|
124
124
|
|
|
125
125
|
/**
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
126
|
+
Reports whether an output code unit is still a control the boundary forbids.
|
|
127
|
+
|
|
128
|
+
@param codeUnit - UTF-16 code unit read from neutralizer output.
|
|
129
|
+
|
|
130
|
+
@returns Whether the code unit should have been neutralized.
|
|
131
131
|
*/
|
|
132
132
|
function isForbiddenControl(codeUnit: number,): boolean {
|
|
133
133
|
if (codeUnit < 0x20)
|
|
@@ -156,7 +156,7 @@ await describe({
|
|
|
156
156
|
name: 'output never contains a neutralized control after the pass',
|
|
157
157
|
fn: async () => {
|
|
158
158
|
/**
|
|
159
|
-
|
|
159
|
+
Every code unit from NUL through U+009F, in one string.
|
|
160
160
|
*/
|
|
161
161
|
const allControls = Array.from(
|
|
162
162
|
{ length: CONTROL_SWEEP_LENGTH, },
|
|
@@ -169,11 +169,11 @@ await describe({
|
|
|
169
169
|
)
|
|
170
170
|
.join('',);
|
|
171
171
|
/**
|
|
172
|
-
|
|
172
|
+
Neutralized sweep; only newline and tab may survive as controls.
|
|
173
173
|
*/
|
|
174
174
|
const output = neutralizeControlCharacters(allControls,);
|
|
175
175
|
/**
|
|
176
|
-
|
|
176
|
+
Output code units that are still forbidden controls; must stay empty.
|
|
177
177
|
*/
|
|
178
178
|
const leaked: number[] = [];
|
|
179
179
|
for (const character of output) {
|