@monochromatic-dev/module-logger 0.1.0 → 0.3.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 +58 -12
- package/dist/final/neutral/browser.d.mts +60 -0
- package/dist/final/neutral/browser.mjs +1 -0
- package/dist/final/neutral/index.d.mts +366 -572
- 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 +366 -572
- 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 -5
- package/src/artifact-platform-split.unit.test.ts +140 -0
- package/src/browser.ts +14 -0
- package/src/create-logger.ts +249 -151
- package/src/create-logger.unit.test.ts +527 -75
- package/src/default-sinks.neutral.ts +34 -0
- package/src/default-sinks.node.ts +32 -0
- package/src/error-format.ts +23 -23
- package/src/index.ts +2 -0
- package/src/logger.ts +23 -50
- package/src/node.ts +23 -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
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createConsoleSink, } from './sink/console.ts';
|
|
2
|
+
import { createIndexedDbSink, } from './sink/indexed-db.ts';
|
|
3
|
+
import { createLocalStorageSink, } from './sink/local-storage.ts';
|
|
4
|
+
import { createSessionStorageSink, } from './sink/session-storage.ts';
|
|
5
|
+
|
|
6
|
+
import type { Sink, } from './types.ts';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
Default sink backends the platform-neutral artifact attempts, in priority
|
|
10
|
+
order. Chosen at bundle time: `package.json` maps `#default-sinks` to this
|
|
11
|
+
module under the `default` condition, so every non-Node resolution
|
|
12
|
+
(browsers, Deno, Bun, workers) inlines this list without a runtime
|
|
13
|
+
platform probe. Each runtime keeps only the sinks whose `verify` confirms
|
|
14
|
+
its backend: {@link createConsoleSink} everywhere,
|
|
15
|
+
{@link createIndexedDbSink} in browsers, {@link createSessionStorageSink}
|
|
16
|
+
wherever web storage round-trips (browsers, Deno),
|
|
17
|
+
{@link createLocalStorageSink} wherever `localStorage` round-trips
|
|
18
|
+
(browsers, Deno). The noop sink is intentionally absent: the console sink
|
|
19
|
+
verifies wherever `console` and `queueMicrotask` exist, so the default
|
|
20
|
+
logger has a backend in every supported runtime, and a custom
|
|
21
|
+
`createLogger` whose sinks all fail verification surfaces the "No logging
|
|
22
|
+
backends available" error instead of silently discarding. The file sink
|
|
23
|
+
is absent because its static `node:fs/promises` import cannot load outside
|
|
24
|
+
Node; it ships through the `./node` subpath and the Node default list
|
|
25
|
+
instead. The OPFS sink is exported from `./browser` but not a default:
|
|
26
|
+
its stream stages writes until a close that a crash never performs, so
|
|
27
|
+
IndexedDB holds the persistent-browser slot; see `DECISIONS.md`.
|
|
28
|
+
*/
|
|
29
|
+
export const defaultSinks: readonly Sink[] = [
|
|
30
|
+
createConsoleSink(),
|
|
31
|
+
createIndexedDbSink(),
|
|
32
|
+
createSessionStorageSink(),
|
|
33
|
+
createLocalStorageSink(),
|
|
34
|
+
];
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createConsoleSink, } from './sink/console.ts';
|
|
2
|
+
import { createFileSink, } from './sink/file.ts';
|
|
3
|
+
import { createLocalStorageSink, } from './sink/local-storage.ts';
|
|
4
|
+
import { createSessionStorageSink, } from './sink/session-storage.ts';
|
|
5
|
+
|
|
6
|
+
import type { Sink, } from './types.ts';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
Default sink backends the Node artifact attempts, in priority order.
|
|
10
|
+
Chosen at bundle time: `package.json` maps `#default-sinks` to this module
|
|
11
|
+
under the `node` condition, so `logger.ts` inlines this list without a
|
|
12
|
+
runtime platform probe. Each runtime keeps only the sinks whose `verify`
|
|
13
|
+
confirms its backend: {@link createConsoleSink} everywhere,
|
|
14
|
+
{@link createSessionStorageSink} wherever web storage round-trips (Node
|
|
15
|
+
22+, Deno), {@link createLocalStorageSink} wherever `localStorage`
|
|
16
|
+
round-trips (Deno, Node launched with `--localstorage-file`), and
|
|
17
|
+
{@link createFileSink} wherever an ancestor `node_modules` exists. The
|
|
18
|
+
noop sink is intentionally absent: the console sink verifies wherever
|
|
19
|
+
`console` and `queueMicrotask` exist, so the default logger has a backend
|
|
20
|
+
in every supported runtime, and a custom `createLogger` whose sinks all
|
|
21
|
+
fail verification surfaces the "No logging backends available" error
|
|
22
|
+
instead of silently discarding. The IndexedDB and OPFS sinks are absent
|
|
23
|
+
because Node exposes neither `indexedDB` nor `navigator.storage`; they
|
|
24
|
+
ship through the `./browser` subpath instead, keeping their probes and
|
|
25
|
+
code out of this artifact.
|
|
26
|
+
*/
|
|
27
|
+
export const defaultSinks: readonly Sink[] = [
|
|
28
|
+
createConsoleSink(),
|
|
29
|
+
createSessionStorageSink(),
|
|
30
|
+
createLocalStorageSink(),
|
|
31
|
+
createFileSink(),
|
|
32
|
+
];
|
package/src/error-format.ts
CHANGED
|
@@ -1,32 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
Internal logger error reporting helpers.
|
|
3
|
+
|
|
4
|
+
Logger internals cannot report failures through the logger itself without
|
|
5
|
+
risking recursion, so these helpers format caught values and write directly
|
|
6
|
+
to the host console.
|
|
7
|
+
|
|
8
|
+
@module
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { caughtValueText, } from '@monochromatic-dev/module-caught-value/ts';
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
14
|
+
Reports a logger-internal caught value without going back through logger
|
|
15
|
+
sinks, formatting it via {@link caughtValueText}.
|
|
16
|
+
|
|
17
|
+
@param context - Human-readable operation that caught the value.
|
|
18
|
+
|
|
19
|
+
@param error - Caught value to include in the diagnostic.
|
|
20
|
+
|
|
21
|
+
@mutates error - `caughtValueText` may invoke string-conversion hooks.
|
|
22
|
+
|
|
23
|
+
@example
|
|
24
|
+
```ts
|
|
25
|
+
reportLoggerInternalError({
|
|
26
|
+
context: 'console sink verify failed',
|
|
27
|
+
error: new Error('blocked'),
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
30
|
*/
|
|
31
31
|
export function reportLoggerInternalError(
|
|
32
32
|
{
|
package/src/index.ts
CHANGED
package/src/logger.ts
CHANGED
|
@@ -1,42 +1,15 @@
|
|
|
1
|
+
import { defaultSinks, } from '#default-sinks';
|
|
2
|
+
|
|
1
3
|
import { createLogger, } from './create-logger.ts';
|
|
2
|
-
import { createConsoleSink, } from './sink/console.ts';
|
|
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';
|
|
7
|
-
import type {
|
|
8
|
-
Logger,
|
|
9
|
-
Sink,
|
|
10
|
-
} from './types.ts';
|
|
11
4
|
|
|
12
|
-
|
|
13
|
-
* Default sink backends to attempt, in priority order. Each runtime keeps
|
|
14
|
-
* only the sinks whose `verify` confirms its backend: {@link createConsoleSink}
|
|
15
|
-
* everywhere, {@link createIndexedDbSink} in browsers,
|
|
16
|
-
* {@link createSessionStorageSink} wherever web storage round-trips (browsers,
|
|
17
|
-
* Node 22+, Deno), {@link createLocalStorageSink} wherever `localStorage`
|
|
18
|
-
* round-trips (browsers, Deno, Node launched with `--localstorage-file`),
|
|
19
|
-
* {@link createFileSink} under Node. The noop sink is intentionally absent:
|
|
20
|
-
* the console sink verifies wherever `console` and `queueMicrotask` exist,
|
|
21
|
-
* so the default logger has a backend in every supported runtime, and a
|
|
22
|
-
* custom `createLogger` whose sinks all fail verification surfaces the
|
|
23
|
-
* "No logging backends available" error instead of silently discarding.
|
|
24
|
-
* The OPFS sink is exported
|
|
25
|
-
* but no longer a default: its stream stages writes until a close that a
|
|
26
|
-
* crash never performs, so IndexedDB holds the persistent-browser slot; see
|
|
27
|
-
* `DECISIONS.md`.
|
|
28
|
-
*/
|
|
29
|
-
const defaultSinks: readonly Sink[] = [
|
|
30
|
-
createConsoleSink(),
|
|
31
|
-
createIndexedDbSink(),
|
|
32
|
-
createSessionStorageSink(),
|
|
33
|
-
createLocalStorageSink(),
|
|
34
|
-
createFileSink(),
|
|
35
|
-
];
|
|
5
|
+
import type { Logger, } from './types.ts';
|
|
36
6
|
|
|
37
7
|
/**
|
|
38
|
-
|
|
39
|
-
|
|
8
|
+
Default multi-sink logger plus its eager readiness promise, built by
|
|
9
|
+
applying {@link createLogger} to the platform-selected `defaultSinks`
|
|
10
|
+
(`default-sinks.node.ts` under the `node` condition,
|
|
11
|
+
`default-sinks.neutral.ts` otherwise; see the `imports` map in
|
|
12
|
+
`package.json`).
|
|
40
13
|
*/
|
|
41
14
|
const {
|
|
42
15
|
initPromise: defaultInitPromise,
|
|
@@ -44,24 +17,24 @@ const {
|
|
|
44
17
|
} = createLogger({ sinks: defaultSinks, },);
|
|
45
18
|
|
|
46
19
|
/**
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
20
|
+
Eager readiness promise. Consumers do not need to await this before logging;
|
|
21
|
+
{@link Logger.flush} awaits it internally, and startup records replay to
|
|
22
|
+
async sinks as they become available.
|
|
50
23
|
*/
|
|
51
24
|
export const initPromise: Promise<void> = defaultInitPromise;
|
|
52
25
|
|
|
53
26
|
/**
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
27
|
+
Multi-sink logger that writes to all available backends.
|
|
28
|
+
Startup records replay to async sinks that verify after the log call.
|
|
29
|
+
Log calls throw only when initialization proves no backend is available,
|
|
30
|
+
which the console sink prevents in every supported runtime.
|
|
31
|
+
|
|
32
|
+
@example
|
|
33
|
+
```ts
|
|
34
|
+
import { logger, } from '\@monochromatic-dev/module-logger/logger';
|
|
35
|
+
|
|
36
|
+
logger.error('unexpected shutdown',);
|
|
37
|
+
await logger.flush();
|
|
38
|
+
```
|
|
66
39
|
*/
|
|
67
40
|
export const logger: Logger = defaultLogger;
|
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
|
|
@@ -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) {
|