@vbarbarosh/node-helpers 3.77.0 → 3.78.1
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 +1 -1
- package/dist/browser.js +1 -1
- package/dist/index.html +8 -16
- package/package.json +5 -5
- package/src/crypto_decrypt_aes256.js +43 -7
- package/src/crypto_decrypt_aes256.md +15 -10
- package/src/crypto_encrypt_aes256.js +23 -9
- package/src/crypto_encrypt_aes256.md +20 -10
- package/src/escape_content_disposition.js +8 -2
- package/src/escape_content_disposition.md +10 -2
- package/src/escape_content_disposition_utf8.js +11 -0
- package/src/escape_content_disposition_utf8.md +11 -0
- package/src/fastdl.js +7 -1
- package/src/fastdl.md +5 -0
- package/src/format_date_fs.js +8 -0
- package/src/format_error_report.js +102 -4
- package/src/format_error_report.md +47 -0
- package/src/http_get_stream_range.js +5 -1
- package/src/http_get_stream_range.md +2 -1
- package/src/http_stream_range.js +14 -1
- package/src/http_stream_range.md +3 -1
- package/src/make_progress.js +16 -9
- package/src/make_progress.md +7 -4
- package/src/pgid_exists.js +5 -0
- package/src/pgid_exists.md +4 -0
- package/src/pgid_kill_grace.js +5 -0
- package/src/pgid_kill_grace.md +3 -0
- package/src/pid_exists.js +5 -0
- package/src/pid_exists.md +4 -0
- package/src/pid_kill_grace.js +5 -0
- package/src/pid_kill_grace.md +3 -0
- package/src/redis_poll_zshift_callback_rpush.js +4 -0
- package/src/redis_poll_zshift_callback_rpush.md +4 -3
- package/src/redis_poll_zshift_spawn_rpush.js +4 -0
- package/src/redis_poll_zshift_spawn_rpush.md +4 -3
- package/src/shell.js +1 -1
- package/src/shell.md +2 -1
- package/src/shell_json.js +1 -1
- package/src/shell_json.md +2 -1
- package/src/stream_strpbrk.js +9 -1
- package/src/stream_xml_analyze.js +6 -1
- package/src/stream_xml_parse.js +11 -3
- package/src/urlmod.md +3 -1
- package/src/urlparts.js +111 -15
- package/src/urlparts.md +44 -12
|
@@ -10,3 +10,50 @@ errors get special treatment; everything else becomes indented JSON.
|
|
|
10
10
|
split into an array of lines (circular-safe via `json_stringify_safe`).
|
|
11
11
|
- Falsy error — `{"error":null}`-style JSON; `undefined` is reported as the
|
|
12
12
|
string `'---undefined---'`.
|
|
13
|
+
|
|
14
|
+
**Masking:** credentials are replaced by `***` in the REQUEST
|
|
15
|
+
section so the report can be logged or sent to error tracking.
|
|
16
|
+
|
|
17
|
+
Header values are masked when the name, matched case-insensitively, is one
|
|
18
|
+
of:
|
|
19
|
+
|
|
20
|
+
- `api-key`
|
|
21
|
+
- `authorization`
|
|
22
|
+
- `cookie`
|
|
23
|
+
- `proxy-authorization`
|
|
24
|
+
- `set-cookie`
|
|
25
|
+
- `x-api-key`
|
|
26
|
+
- `x-auth-token`
|
|
27
|
+
|
|
28
|
+
Headers nested per method the Axios way (`headers.common`, `headers.get`,
|
|
29
|
+
...) and `AxiosHeaders` instances (via `toJSON`) are handled; the error
|
|
30
|
+
object itself is not modified.
|
|
31
|
+
|
|
32
|
+
URL userinfo is masked (`https://user:pass@host` becomes
|
|
33
|
+
`https://***@host`), and so is the value of every query parameter with a
|
|
34
|
+
secret-looking name (`?token=abc&page=2` becomes `?token=***&page=2`). A
|
|
35
|
+
name is secret-looking when, split on `_`/`-`/`.` and camelCase boundaries
|
|
36
|
+
and lower-cased, any segment is one of the words below, or when the whole
|
|
37
|
+
name with separators removed is one of them:
|
|
38
|
+
|
|
39
|
+
- `accesstoken`
|
|
40
|
+
- `apikey`
|
|
41
|
+
- `auth`
|
|
42
|
+
- `authorization`
|
|
43
|
+
- `key`
|
|
44
|
+
- `pass`
|
|
45
|
+
- `passwd`
|
|
46
|
+
- `password`
|
|
47
|
+
- `pwd`
|
|
48
|
+
- `secret`
|
|
49
|
+
- `sig`
|
|
50
|
+
- `signature`
|
|
51
|
+
- `token`
|
|
52
|
+
|
|
53
|
+
So `api_key`, `apiKey`, `access_token`, `x-auth-token`, `client_secret`,
|
|
54
|
+
`sig` and `key` are masked while `keyword`, `design`, `author`,
|
|
55
|
+
`keyboard`, `signal` and `client_id` are not. The rest of the URL is
|
|
56
|
+
reported verbatim.
|
|
57
|
+
|
|
58
|
+
The response body is **not** sanitized — only truncated to 10240
|
|
59
|
+
characters — and the request body is never included.
|
|
@@ -46,7 +46,11 @@ async function http_get_stream_range(url, first, last, options)
|
|
|
46
46
|
else if (b !== '') {
|
|
47
47
|
const actual_length = out.content_range.last - out.content_range.first + 1;
|
|
48
48
|
const expected_length = out.content_range.total === null ? Math.min(actual_length, b) : Math.min(out.content_range.total, b);
|
|
49
|
-
if (
|
|
49
|
+
if (out.content_range.last === null) {
|
|
50
|
+
// Chunked 200 with no Content-Range: the server ignored the suffix range
|
|
51
|
+
out.destroy(new Error(`Server ignored the requested suffix range of ${fmt(b)} bytes and returned a response of unknown length`));
|
|
52
|
+
}
|
|
53
|
+
else if (actual_length !== expected_length) {
|
|
50
54
|
out.destroy(new Error(`Length of a returned range (${fmt(actual_length)}) is not as expected: [${fmt(expected_length)}]`));
|
|
51
55
|
}
|
|
52
56
|
else if (out.content_range.total !== null && out.content_range.last !== out.content_range.total - 1) {
|
|
@@ -14,7 +14,8 @@ chunked 200 response (neither header) is accepted as
|
|
|
14
14
|
The returned range is validated according to the requested form. Closed ranges
|
|
15
15
|
accept a `last` clamped to the end of the resource; open-ended ranges must end
|
|
16
16
|
at the resource end when its size is known; suffix ranges validate their
|
|
17
|
-
length and resource end
|
|
17
|
+
length and resource end, and a chunked 200 (unknown length) in reply to a
|
|
18
|
+
suffix request is rejected. A server that ignores a range and replies 200 is
|
|
18
19
|
rejected.
|
|
19
20
|
|
|
20
21
|
```js
|
package/src/http_stream_range.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const escape_content_disposition = require('./escape_content_disposition');
|
|
2
|
+
const escape_content_disposition_utf8 = require('./escape_content_disposition_utf8');
|
|
2
3
|
const fs_fclose = require('./fs_fclose');
|
|
3
4
|
const fs_fopen = require('./fs_fopen');
|
|
4
5
|
const fs_fread = require('./fs_fread');
|
|
@@ -30,7 +31,7 @@ async function http_stream_range(req, res, file)
|
|
|
30
31
|
res.header('Accept-Ranges', 'bytes');
|
|
31
32
|
res.header('Content-Type', mime);
|
|
32
33
|
res.header('Content-Length', total);
|
|
33
|
-
res.header('Content-Disposition',
|
|
34
|
+
res.header('Content-Disposition', content_disposition(fs_path_basename(file)));
|
|
34
35
|
res.status(200);
|
|
35
36
|
res.end();
|
|
36
37
|
return;
|
|
@@ -122,4 +123,16 @@ async function http_stream_range(req, res, file)
|
|
|
122
123
|
}
|
|
123
124
|
}
|
|
124
125
|
|
|
126
|
+
function content_disposition(name)
|
|
127
|
+
{
|
|
128
|
+
// The quoted `filename` degrades anything outside ISO-8859-1 to '?', so a
|
|
129
|
+
// non-ASCII name also gets the RFC 5987 `filename*` that browsers prefer.
|
|
130
|
+
let out = `inline; filename=${escape_content_disposition(name)}`;
|
|
131
|
+
// eslint-disable-next-line no-control-regex
|
|
132
|
+
if (/[^\x00-\x7f]/.test(name)) {
|
|
133
|
+
out += `; filename*=${escape_content_disposition_utf8(name)}`;
|
|
134
|
+
}
|
|
135
|
+
return out + ';';
|
|
136
|
+
}
|
|
137
|
+
|
|
125
138
|
module.exports = http_stream_range;
|
package/src/http_stream_range.md
CHANGED
|
@@ -11,7 +11,9 @@ http_stream_range(req, res, file) → Promise
|
|
|
11
11
|
|
|
12
12
|
- `HEAD` → 200 with `Content-Type`, `Content-Length`, and an
|
|
13
13
|
`inline; filename=...` `Content-Disposition` (escaped via
|
|
14
|
-
`escape_content_disposition
|
|
14
|
+
`escape_content_disposition`, so control characters are encoded and a
|
|
15
|
+
non-ASCII basename is also sent as an RFC 5987 `filename*=UTF-8''...`
|
|
16
|
+
parameter); no body.
|
|
15
17
|
- `GET` with `Range` → 206 with `Content-Range: bytes first-last/total` and a
|
|
16
18
|
matching `Content-Length`.
|
|
17
19
|
- `GET` without `Range` → 200 with the whole file.
|
package/src/make_progress.js
CHANGED
|
@@ -21,10 +21,16 @@ function make_progress(total)
|
|
|
21
21
|
progress: null,
|
|
22
22
|
percents: null,
|
|
23
23
|
add: function (delta = 0) {
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
const now = Date.now();
|
|
25
|
+
// The oldest sample is the anchor the rate is measured from, so
|
|
26
|
+
// the newest sample older than 10s is kept: the window then
|
|
27
|
+
// stays close to 10s instead of shrinking after each eviction
|
|
28
|
+
const expired = history.findLastIndex(v => v.time < now - 10000);
|
|
29
|
+
if (expired > 0) {
|
|
30
|
+
history.splice(0, expired);
|
|
31
|
+
}
|
|
27
32
|
out.done += delta;
|
|
33
|
+
history.push({time: now, done: out.done});
|
|
28
34
|
out.refresh();
|
|
29
35
|
},
|
|
30
36
|
update: function (done) {
|
|
@@ -32,16 +38,17 @@ function make_progress(total)
|
|
|
32
38
|
},
|
|
33
39
|
refresh: function () {
|
|
34
40
|
out.duration = (Date.now() - time0)/1000;
|
|
35
|
-
// `rate` and `eta` are null while unknown
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
41
|
+
// `rate` and `eta` are null while unknown: a single sample says
|
|
42
|
+
// nothing about how long its delta took to accumulate (counting
|
|
43
|
+
// it doubled the rate early on), a zero time window would give
|
|
44
|
+
// rate = Infinity, and a zero rate would give eta = Infinity.
|
|
45
|
+
if (history.length > 1) {
|
|
46
|
+
const delta = out.done - history[0].done;
|
|
40
47
|
const time_sec = (Date.now() - history[0].time)/1000;
|
|
41
48
|
out.rate = time_sec ? delta/time_sec : null;
|
|
42
49
|
}
|
|
43
50
|
else {
|
|
44
|
-
out.rate =
|
|
51
|
+
out.rate = null;
|
|
45
52
|
}
|
|
46
53
|
out.percents = !out.total ? null : out.done/out.total;
|
|
47
54
|
out.eta = (!out.total || !out.rate) ? null : (out.total - out.done)/out.rate;
|
package/src/make_progress.md
CHANGED
|
@@ -3,10 +3,13 @@ rows created). Pass the snapshot to `format_progress_bytes` /
|
|
|
3
3
|
`format_progress_kilo` for display.
|
|
4
4
|
|
|
5
5
|
**Fields:** `done`, `total`, `duration` (seconds since creation), `rate`
|
|
6
|
-
(units/second over a sliding 10-second history window), `eta` (seconds),
|
|
7
|
-
`percents` (a fraction, `done/total`).
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
(units/second over a sliding ~10-second history window), `eta` (seconds),
|
|
7
|
+
`percents` (a fraction, `done/total`). The rate is measured between the
|
|
8
|
+
oldest sample in the window and now, so the first sample's own delta is
|
|
9
|
+
not counted (how long it took to accumulate is unknown). `rate` and `eta`
|
|
10
|
+
are `null` while unknown — until two `add` calls exist, or when they would
|
|
11
|
+
be `Infinity` (zero time window, zero rate); `percents` is `null` when
|
|
12
|
+
`total` is falsy.
|
|
10
13
|
|
|
11
14
|
**Methods:**
|
|
12
15
|
- `add(delta)` — add `delta` to `done` and recompute
|
package/src/pgid_exists.js
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
// keep in sync with pid_exists
|
|
3
3
|
function pgid_exists(pgid)
|
|
4
4
|
{
|
|
5
|
+
// kill(-0, ...) targets the caller's own group and kill(-1, ...) every
|
|
6
|
+
// process the user may signal: never let such ids reach the kernel
|
|
7
|
+
if (!Number.isInteger(pgid) || pgid <= 1) {
|
|
8
|
+
throw new TypeError(`pgid_exists: invalid pgid: ${pgid}`);
|
|
9
|
+
}
|
|
5
10
|
try {
|
|
6
11
|
process.kill(-pgid, 0);
|
|
7
12
|
return true;
|
package/src/pgid_exists.md
CHANGED
|
@@ -3,4 +3,8 @@ Checks whether a process group is alive by sending signal `0` to the group via
|
|
|
3
3
|
at least one member, `false` on any error (no such group, or no permission).
|
|
4
4
|
A group id equals the pid of the group leader.
|
|
5
5
|
|
|
6
|
+
Throws `TypeError` when `pgid` is not an integer greater than `1`: `0` would
|
|
7
|
+
probe the caller's own process group and `1` every process the user may
|
|
8
|
+
signal (`kill(-1, ...)`), so such values never reach `kill(2)`.
|
|
9
|
+
|
|
6
10
|
See `pid_exists` for the single-process variant.
|
package/src/pgid_kill_grace.js
CHANGED
|
@@ -7,6 +7,11 @@ const pgid_exists = require('./pgid_exists');
|
|
|
7
7
|
// keep in sync with pid_kill_grace
|
|
8
8
|
async function pgid_kill_grace(pgid, {grace_timeout_ms = 5000, log = ignore} = {})
|
|
9
9
|
{
|
|
10
|
+
// kill(-0, ...) targets the caller's own group and kill(-1, ...) every
|
|
11
|
+
// process the user may signal: never let such ids reach the kernel
|
|
12
|
+
if (!Number.isInteger(pgid) || pgid <= 1) {
|
|
13
|
+
throw new TypeError(`pgid_kill_grace: invalid pgid: ${pgid}`);
|
|
14
|
+
}
|
|
10
15
|
log(`Terminating process group ${pgid}: sending SIGTERM`);
|
|
11
16
|
try {
|
|
12
17
|
process.kill(-pgid, 'SIGTERM');
|
package/src/pgid_kill_grace.md
CHANGED
|
@@ -5,6 +5,9 @@ to deliver it. A group id equals the pid of the group leader.
|
|
|
5
5
|
|
|
6
6
|
- Options: `grace_timeout_ms` (default `5000`) and `log` (progress callback,
|
|
7
7
|
default no-op).
|
|
8
|
+
- Throws `TypeError` before sending anything when `pgid` is not an integer
|
|
9
|
+
greater than `1` (`0` would signal the caller's own group, `1` every
|
|
10
|
+
process the user may signal via `kill(-1, ...)`).
|
|
8
11
|
- Throws `Failed to send SIGTERM to process group <pgid>: ...` when the
|
|
9
12
|
initial signal cannot be sent.
|
|
10
13
|
- A group that dies on its own during the grace period skips the `SIGKILL`;
|
package/src/pid_exists.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
// keep in sync with pgid_exists
|
|
2
2
|
function pid_exists(pid)
|
|
3
3
|
{
|
|
4
|
+
// kill(0, ...) targets the caller's own group and kill(-1, ...) every
|
|
5
|
+
// process the user may signal: never let such ids reach the kernel
|
|
6
|
+
if (!Number.isInteger(pid) || pid <= 1) {
|
|
7
|
+
throw new TypeError(`pid_exists: invalid pid: ${pid}`);
|
|
8
|
+
}
|
|
4
9
|
try {
|
|
5
10
|
process.kill(pid, 0);
|
|
6
11
|
return true;
|
package/src/pid_exists.md
CHANGED
|
@@ -2,4 +2,8 @@ Checks whether a process is alive by sending signal `0` via
|
|
|
2
2
|
`process.kill(pid, 0)`. Returns `true` when the signal can be delivered,
|
|
3
3
|
`false` on any error (no such process, or no permission to signal it).
|
|
4
4
|
|
|
5
|
+
Throws `TypeError` when `pid` is not an integer greater than `1`: `0` would
|
|
6
|
+
probe the caller's own process group and `-1` every process the user may
|
|
7
|
+
signal, so such values never reach `kill(2)`.
|
|
8
|
+
|
|
5
9
|
See `pgid_exists` for the process-group variant.
|
package/src/pid_kill_grace.js
CHANGED
|
@@ -6,6 +6,11 @@ const pid_exists = require('./pid_exists');
|
|
|
6
6
|
// keep in sync with pgid_kill_grace
|
|
7
7
|
async function pid_kill_grace(pid, {grace_timeout_ms = 5000, log = ignore} = {})
|
|
8
8
|
{
|
|
9
|
+
// kill(0, ...) targets the caller's own group and kill(-1, ...) every
|
|
10
|
+
// process the user may signal: never let such ids reach the kernel
|
|
11
|
+
if (!Number.isInteger(pid) || pid <= 1) {
|
|
12
|
+
throw new TypeError(`pid_kill_grace: invalid pid: ${pid}`);
|
|
13
|
+
}
|
|
9
14
|
log(`Terminating process ${pid}: sending SIGTERM`);
|
|
10
15
|
try {
|
|
11
16
|
process.kill(pid, 'SIGTERM');
|
package/src/pid_kill_grace.md
CHANGED
|
@@ -4,6 +4,9 @@ waits up to 200ms more for the kernel to deliver it.
|
|
|
4
4
|
|
|
5
5
|
- Options: `grace_timeout_ms` (default `5000`) and `log` (progress callback,
|
|
6
6
|
default no-op).
|
|
7
|
+
- Throws `TypeError` before sending anything when `pid` is not an integer
|
|
8
|
+
greater than `1` (`0` would signal the caller's own group, `-1` every
|
|
9
|
+
process the user may signal).
|
|
7
10
|
- Throws `Failed to send SIGTERM to process <pid>: ...` when the initial
|
|
8
11
|
signal cannot be sent (e.g. the process does not exist).
|
|
9
12
|
- A process that dies on its own during the grace period skips the `SIGKILL`
|
|
@@ -64,6 +64,10 @@ async function redis_poll_zshift_callback_rpush(options)
|
|
|
64
64
|
log(`[${log_waiter_error_parse}] ${error.message}`);
|
|
65
65
|
continue;
|
|
66
66
|
}
|
|
67
|
+
if (!message || typeof message !== 'object') {
|
|
68
|
+
log(`[${log_waiter_error_parse}] message is not an object; skipped`);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
67
71
|
|
|
68
72
|
try {
|
|
69
73
|
const uid = message.uid;
|
|
@@ -5,9 +5,10 @@ type, value}` to `redis_output_queue` — `type: 'resolve'` with the callback's
|
|
|
5
5
|
return value, or `'reject'` with the error stack/message. An empty poll
|
|
6
6
|
sleeps a random 500–1500ms; the loop runs at most 86400 iterations.
|
|
7
7
|
|
|
8
|
-
- Unparseable messages
|
|
9
|
-
|
|
10
|
-
limited
|
|
8
|
+
- Unparseable messages, valid JSON that is not an object (`null`, numbers,
|
|
9
|
+
strings), and messages without `uid` are skipped; a missing or past
|
|
10
|
+
`expires_at` rejects the message (payloads carry signed urls with limited
|
|
11
|
+
lifetime).
|
|
11
12
|
- `user_friendly_status(value)` rpushes `{uid, version, type:
|
|
12
13
|
'user_friendly_status', value}` to the output queue; a generic
|
|
13
14
|
`'Started...'` status is always pushed first so the consumer starts
|
|
@@ -73,6 +73,10 @@ async function redis_poll_zshift_spawn_rpush(options)
|
|
|
73
73
|
log(`[${log_waiter_error_parse}] ${error.message}`);
|
|
74
74
|
continue;
|
|
75
75
|
}
|
|
76
|
+
if (!message || typeof message !== 'object') {
|
|
77
|
+
log(`[${log_waiter_error_parse}] message is not an object; skipped`);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
76
80
|
|
|
77
81
|
try {
|
|
78
82
|
const uid = message.uid;
|
|
@@ -12,9 +12,10 @@ iterations.
|
|
|
12
12
|
`'Started...'` status is always pushed first. stdout/stderr are logged
|
|
13
13
|
line by line under the `log_worker_stdout`/`log_worker_stderr` tags.
|
|
14
14
|
- Only exit code 0 is a success — a signal-killed child (code `null`) fails.
|
|
15
|
-
- Unparseable messages
|
|
16
|
-
|
|
17
|
-
limited
|
|
15
|
+
- Unparseable messages, valid JSON that is not an object (`null`, numbers,
|
|
16
|
+
strings), and messages without `uid` are skipped; a missing or past
|
|
17
|
+
`expires_at` fails the message (payloads carry signed urls with limited
|
|
18
|
+
lifetime).
|
|
18
19
|
- `options.log` must provide `spawn()`; queue access goes through
|
|
19
20
|
`redis_zshift`/`redis_rpush`, so any redis client from redis@1 to redis@6
|
|
20
21
|
works. See `redis_poll_zshift_callback_rpush` for the in-process
|
package/src/shell.js
CHANGED
package/src/shell.md
CHANGED
|
@@ -2,7 +2,8 @@ Basic helper for running external commands via `execFile`.
|
|
|
2
2
|
|
|
3
3
|
- Fails if the command exits with non-zero code.
|
|
4
4
|
- Fails if anything is printed to `stderr`.
|
|
5
|
-
- Resolves with `stdout` (string), exactly as produced
|
|
5
|
+
- Resolves with `stdout` (string), exactly as produced; a `Buffer` with
|
|
6
|
+
`{encoding: 'buffer'}`.
|
|
6
7
|
|
|
7
8
|
## Signature
|
|
8
9
|
|
package/src/shell_json.js
CHANGED
package/src/shell_json.md
CHANGED
|
@@ -3,7 +3,8 @@ Basic helper for running external commands via `execFile`, and parse result by J
|
|
|
3
3
|
- Fails if the command exits with non-zero code.
|
|
4
4
|
- Fails if anything is printed to `stderr`.
|
|
5
5
|
- Fails if stdout is not valid JSON.
|
|
6
|
-
- Resolves with parsed JSON value
|
|
6
|
+
- Resolves with parsed JSON value (`{encoding: 'buffer'}` is accepted;
|
|
7
|
+
`JSON.parse` stringifies the `Buffer`).
|
|
7
8
|
|
|
8
9
|
## Signature
|
|
9
10
|
|
package/src/stream_strpbrk.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const stream = require('stream');
|
|
2
|
+
const {StringDecoder} = require('string_decoder');
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Split the input stream by consecutive delimiters in `chars`
|
|
@@ -6,10 +7,13 @@ const stream = require('stream');
|
|
|
6
7
|
function stream_strpbrk(chars = '\r\n')
|
|
7
8
|
{
|
|
8
9
|
const pending = [];
|
|
10
|
+
// StringDecoder buffers a multi-byte character split across chunk
|
|
11
|
+
// boundaries; plain buf.toString() would corrupt it into �.
|
|
12
|
+
const decoder = new StringDecoder('utf8');
|
|
9
13
|
return new stream.Transform({
|
|
10
14
|
objectMode: true,
|
|
11
15
|
transform: function (str, encoding, callback) {
|
|
12
|
-
str = Buffer.isBuffer(str) ?
|
|
16
|
+
str = Buffer.isBuffer(str) ? decoder.write(str) : str;
|
|
13
17
|
|
|
14
18
|
let off = 0;
|
|
15
19
|
// leading delimiters
|
|
@@ -67,6 +71,10 @@ function stream_strpbrk(chars = '\r\n')
|
|
|
67
71
|
callback();
|
|
68
72
|
},
|
|
69
73
|
flush: function (callback) {
|
|
74
|
+
const tail = decoder.end();
|
|
75
|
+
if (tail) {
|
|
76
|
+
pending.push(tail);
|
|
77
|
+
}
|
|
70
78
|
if (pending.length) {
|
|
71
79
|
this.push(''.concat(...pending.splice(0)));
|
|
72
80
|
}
|
|
@@ -17,7 +17,12 @@ function stream_xml_analyze()
|
|
|
17
17
|
onopentag: function (name, attrs) {
|
|
18
18
|
path.push(name);
|
|
19
19
|
path_str = path.join(' > ');
|
|
20
|
-
|
|
20
|
+
// Element names come from the outside world: `??=` on a plain
|
|
21
|
+
// object would see Object.prototype.constructor and produce NaN
|
|
22
|
+
// for a root element named `constructor`.
|
|
23
|
+
if (!Object.hasOwn(analytics, path_str)) {
|
|
24
|
+
Object.defineProperty(analytics, path_str, {value: 0, enumerable: true, writable: true, configurable: true});
|
|
25
|
+
}
|
|
21
26
|
analytics[path_str]++;
|
|
22
27
|
},
|
|
23
28
|
onclosetag: function (name) {
|
package/src/stream_xml_parse.js
CHANGED
|
@@ -99,22 +99,30 @@ function guess(node)
|
|
|
99
99
|
|
|
100
100
|
node.children.forEach(function (child) {
|
|
101
101
|
const tmp = stream_xml_parse.guess(child);
|
|
102
|
-
|
|
102
|
+
// Element names come from the outside world: `in` and plain
|
|
103
|
+
// assignment would let `__proto__`, `constructor`, `toString`...
|
|
104
|
+
// hit Object.prototype instead of becoming own data properties.
|
|
105
|
+
if (Object.hasOwn(out, child.name)) {
|
|
103
106
|
if (Array.isArray(out[child.name])) {
|
|
104
107
|
out[child.name].push(tmp);
|
|
105
108
|
}
|
|
106
109
|
else {
|
|
107
|
-
out
|
|
110
|
+
define(out, child.name, [out[child.name], tmp]);
|
|
108
111
|
}
|
|
109
112
|
}
|
|
110
113
|
else {
|
|
111
|
-
out
|
|
114
|
+
define(out, child.name, tmp);
|
|
112
115
|
}
|
|
113
116
|
});
|
|
114
117
|
|
|
115
118
|
return out;
|
|
116
119
|
}
|
|
117
120
|
|
|
121
|
+
function define(obj, name, value)
|
|
122
|
+
{
|
|
123
|
+
Object.defineProperty(obj, name, {value, enumerable: true, writable: true, configurable: true});
|
|
124
|
+
}
|
|
125
|
+
|
|
118
126
|
function ashtml(node)
|
|
119
127
|
{
|
|
120
128
|
return node.raw.map(fn).filter(v => v).join('').trim();
|
package/src/urlmod.md
CHANGED
|
@@ -7,7 +7,9 @@ parameters, or an empty parameters object, returns the input unchanged.
|
|
|
7
7
|
|
|
8
8
|
`null` and `undefined` remove a parameter. Boolean values are encoded as `1`
|
|
9
9
|
and `0`; other values use the standard `URLSearchParams` conversion and
|
|
10
|
-
encoding rules.
|
|
10
|
+
encoding rules. Special names such as `__proto__`, `constructor`, and
|
|
11
|
+
`hasOwnProperty` are handled as ordinary query parameter keys when provided as
|
|
12
|
+
enumerable own properties.
|
|
11
13
|
|
|
12
14
|
```js
|
|
13
15
|
urlmod('', {a: 1}) // '?a=1'
|
package/src/urlparts.js
CHANGED
|
@@ -1,20 +1,116 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Parse an absolute or relative URL-like string without rejecting malformed
|
|
3
|
+
* URL text. Missing components are returned as null.
|
|
4
|
+
*/
|
|
5
|
+
function urlparts(input)
|
|
2
6
|
{
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
:
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
path: tmp.pathname,
|
|
15
|
-
search: tmp.search,
|
|
16
|
-
hash: tmp.hash,
|
|
7
|
+
const out = {
|
|
8
|
+
protocol: null,
|
|
9
|
+
username: null,
|
|
10
|
+
password: null,
|
|
11
|
+
host: null,
|
|
12
|
+
port: null,
|
|
13
|
+
path: null,
|
|
14
|
+
search: null,
|
|
15
|
+
hash: null,
|
|
16
|
+
query: {},
|
|
17
|
+
fragment: {},
|
|
17
18
|
};
|
|
19
|
+
|
|
20
|
+
if (typeof input !== 'string') {
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let resource = input;
|
|
25
|
+
const hash_pos = resource.indexOf('#');
|
|
26
|
+
if (hash_pos !== -1) {
|
|
27
|
+
out.hash = resource.slice(hash_pos);
|
|
28
|
+
out.fragment = parse_params(resource.slice(hash_pos + 1));
|
|
29
|
+
resource = resource.slice(0, hash_pos);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const query_pos = resource.indexOf('?');
|
|
33
|
+
if (query_pos !== -1) {
|
|
34
|
+
out.search = resource.slice(query_pos);
|
|
35
|
+
out.query = parse_params(resource.slice(query_pos + 1));
|
|
36
|
+
resource = resource.slice(0, query_pos);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const protocol_match = resource.match(/^([A-Za-z][A-Za-z0-9+.-]*):/);
|
|
40
|
+
if (protocol_match) {
|
|
41
|
+
out.protocol = protocol_match[1].toLowerCase();
|
|
42
|
+
resource = resource.slice(protocol_match[0].length);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (resource.startsWith('//')) {
|
|
46
|
+
// WHATWG parsers treat `\` as `/` for special schemes, so the
|
|
47
|
+
// authority ends at the first of either.
|
|
48
|
+
const slash_offset = resource.slice(2).search(/[/\\]/);
|
|
49
|
+
const slash_pos = slash_offset === -1 ? -1 : slash_offset + 2;
|
|
50
|
+
const authority = slash_pos === -1
|
|
51
|
+
? resource.slice(2)
|
|
52
|
+
: resource.slice(2, slash_pos);
|
|
53
|
+
|
|
54
|
+
out.path = slash_pos === -1 ? null : resource.slice(slash_pos) || null;
|
|
55
|
+
parse_authority(authority, out);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
out.path = resource || null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function parse_params(value)
|
|
65
|
+
{
|
|
66
|
+
const out = {};
|
|
67
|
+
for (const field of value.split('&')) {
|
|
68
|
+
const item = new URLSearchParams(field).entries().next();
|
|
69
|
+
if (!item.done) {
|
|
70
|
+
const [key, value] = item.value;
|
|
71
|
+
Object.defineProperty(out, key, {
|
|
72
|
+
configurable: true,
|
|
73
|
+
enumerable: true,
|
|
74
|
+
value,
|
|
75
|
+
writable: true,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function parse_authority(authority, parts)
|
|
83
|
+
{
|
|
84
|
+
const at_pos = authority.lastIndexOf('@');
|
|
85
|
+
let host = authority;
|
|
86
|
+
|
|
87
|
+
if (at_pos !== -1) {
|
|
88
|
+
const userinfo = authority.slice(0, at_pos);
|
|
89
|
+
const colon_pos = userinfo.indexOf(':');
|
|
90
|
+
|
|
91
|
+
parts.username = colon_pos === -1 ? userinfo : userinfo.slice(0, colon_pos);
|
|
92
|
+
parts.password = colon_pos === -1 ? null : userinfo.slice(colon_pos + 1);
|
|
93
|
+
host = authority.slice(at_pos + 1);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (host.startsWith('[')) {
|
|
97
|
+
const bracket_pos = host.indexOf(']');
|
|
98
|
+
if (bracket_pos !== -1 && host[bracket_pos + 1] === ':') {
|
|
99
|
+
parts.host = host.slice(0, bracket_pos + 1) || null;
|
|
100
|
+
parts.port = host.slice(bracket_pos + 2);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const first_colon_pos = host.indexOf(':');
|
|
106
|
+
const last_colon_pos = host.lastIndexOf(':');
|
|
107
|
+
if (first_colon_pos !== -1 && first_colon_pos === last_colon_pos) {
|
|
108
|
+
parts.host = host.slice(0, first_colon_pos) || null;
|
|
109
|
+
parts.port = host.slice(first_colon_pos + 1);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
parts.host = host || null;
|
|
18
114
|
}
|
|
19
115
|
|
|
20
116
|
module.exports = urlparts;
|