@oh-my-pi/pi-utils 18.11.0 → 18.11.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/dist/types/incoming-json.d.ts +12 -0
- package/dist/types/mime.d.ts +21 -0
- package/package.json +2 -2
- package/src/incoming-json.ts +15 -0
- package/src/mime.ts +44 -0
|
@@ -48,6 +48,18 @@
|
|
|
48
48
|
* await args.key("path").value<string>(); // "a.ts"
|
|
49
49
|
* ```
|
|
50
50
|
*/
|
|
51
|
+
/**
|
|
52
|
+
* @internal exported for tests — monotonic count of members re-lexed by
|
|
53
|
+
* selecting scans: one per object member or array element a scan walked over
|
|
54
|
+
* on the way to the member it was asked for.
|
|
55
|
+
*
|
|
56
|
+
* Cursor resumption (see {@link ContainerIndex}) is what keeps a full
|
|
57
|
+
* traversal linear in the document instead of quadratic, and losing it
|
|
58
|
+
* changes nothing about the pulled values — only how many members get
|
|
59
|
+
* re-lexed. Read the live binding before and after a traversal to measure
|
|
60
|
+
* exactly that.
|
|
61
|
+
*/
|
|
62
|
+
export declare let rescannedMembers: number;
|
|
51
63
|
/** Location component in a pulled JSON path: an object member name or an array index. */
|
|
52
64
|
export type PullPathSegment = string | number;
|
|
53
65
|
/** JSON shape observed by a started pull. */
|
package/dist/types/mime.d.ts
CHANGED
|
@@ -26,5 +26,26 @@ export type ImageMetadata = {
|
|
|
26
26
|
hasAlpha?: boolean;
|
|
27
27
|
};
|
|
28
28
|
export declare function parseImageMetadata(header: Uint8Array): ImageMetadata | null;
|
|
29
|
+
/** Why base64 image data is unusable; `undefined` when it is canonical and non-empty. */
|
|
30
|
+
export type Base64ImageDataFailure = "invalid base64 image data" | "empty image data";
|
|
31
|
+
/**
|
|
32
|
+
* Non-empty decoded bytes on success, or a validation failure reason, never both.
|
|
33
|
+
* Check `reason` before using `bytes`; successful base64 decoding does not establish
|
|
34
|
+
* that the bytes represent a supported or decodable image.
|
|
35
|
+
*/
|
|
36
|
+
export type Base64ImageDecodeResult = {
|
|
37
|
+
bytes: Buffer;
|
|
38
|
+
reason?: never;
|
|
39
|
+
} | {
|
|
40
|
+
bytes?: never;
|
|
41
|
+
reason: Base64ImageDataFailure;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Validate standard base64 image data without decoding it. Padding may be omitted, but
|
|
45
|
+
* present padding must be canonical, the trailing bits must be zero, and the data non-empty.
|
|
46
|
+
*/
|
|
47
|
+
export declare function base64ImageDataFailureReason(data: string): Base64ImageDataFailure | undefined;
|
|
48
|
+
/** Decode standard base64 image data under the {@link base64ImageDataFailureReason} rules. */
|
|
49
|
+
export declare function decodeBase64ImageData(data: string): Base64ImageDecodeResult;
|
|
29
50
|
export declare function readImageMetadataSync(filePath: string, maxBytes?: number): ImageMetadata | null;
|
|
30
51
|
export declare function readImageMetadata(filePath: string, maxBytes?: number): Promise<ImageMetadata | null>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-utils",
|
|
3
|
-
"version": "18.11.
|
|
3
|
+
"version": "18.11.1",
|
|
4
4
|
"description": "Shared utilities for pi packages",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"@oh-my-pi/pi-natives": "18.11.
|
|
60
|
+
"@oh-my-pi/pi-natives": "18.11.1"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@types/bun": "^1.3.14"
|
package/src/incoming-json.ts
CHANGED
|
@@ -67,6 +67,19 @@ import { parseJsonWithRepair } from "./json-parse";
|
|
|
67
67
|
/** Maximum container nesting before a scan reports the value as pending forever. */
|
|
68
68
|
const MAX_DEPTH = 128;
|
|
69
69
|
|
|
70
|
+
/**
|
|
71
|
+
* @internal exported for tests — monotonic count of members re-lexed by
|
|
72
|
+
* selecting scans: one per object member or array element a scan walked over
|
|
73
|
+
* on the way to the member it was asked for.
|
|
74
|
+
*
|
|
75
|
+
* Cursor resumption (see {@link ContainerIndex}) is what keeps a full
|
|
76
|
+
* traversal linear in the document instead of quadratic, and losing it
|
|
77
|
+
* changes nothing about the pulled values — only how many members get
|
|
78
|
+
* re-lexed. Read the live binding before and after a traversal to measure
|
|
79
|
+
* exactly that.
|
|
80
|
+
*/
|
|
81
|
+
export let rescannedMembers = 0;
|
|
82
|
+
|
|
70
83
|
/** Location component in a pulled JSON path: an object member name or an array index. */
|
|
71
84
|
export type PullPathSegment = string | number;
|
|
72
85
|
|
|
@@ -325,6 +338,7 @@ function selectKey(
|
|
|
325
338
|
lex.ws();
|
|
326
339
|
if (lex.atEnd) return PENDING;
|
|
327
340
|
if (key === wanted) return selectValue(lex, path, at, ended, depth + 1, slot);
|
|
341
|
+
rescannedMembers++;
|
|
328
342
|
const skipped = scanValue(lex, ended, depth + 1);
|
|
329
343
|
if (skipped.tag !== "located" || skipped.end === undefined) return PENDING;
|
|
330
344
|
lex.ws();
|
|
@@ -370,6 +384,7 @@ function selectIndex(
|
|
|
370
384
|
}
|
|
371
385
|
const slot = index.elements[element] ?? (index.tail ??= { offset: lex.pos });
|
|
372
386
|
if (element === wanted) return selectValue(lex, path, at, ended, depth + 1, slot);
|
|
387
|
+
rescannedMembers++;
|
|
373
388
|
const skipped = scanValue(lex, ended, depth + 1);
|
|
374
389
|
if (skipped.tag !== "located" || skipped.end === undefined) return PENDING;
|
|
375
390
|
if (element === index.elements.length) {
|
package/src/mime.ts
CHANGED
|
@@ -143,6 +143,50 @@ export function parseImageMetadata(header: Uint8Array): ImageMetadata | null {
|
|
|
143
143
|
);
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
/** Why base64 image data is unusable; `undefined` when it is canonical and non-empty. */
|
|
147
|
+
export type Base64ImageDataFailure = "invalid base64 image data" | "empty image data";
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Non-empty decoded bytes on success, or a validation failure reason, never both.
|
|
151
|
+
* Check `reason` before using `bytes`; successful base64 decoding does not establish
|
|
152
|
+
* that the bytes represent a supported or decodable image.
|
|
153
|
+
*/
|
|
154
|
+
export type Base64ImageDecodeResult =
|
|
155
|
+
| { bytes: Buffer; reason?: never }
|
|
156
|
+
| { bytes?: never; reason: Base64ImageDataFailure };
|
|
157
|
+
|
|
158
|
+
const BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Validate standard base64 image data without decoding it. Padding may be omitted, but
|
|
162
|
+
* present padding must be canonical, the trailing bits must be zero, and the data non-empty.
|
|
163
|
+
*/
|
|
164
|
+
export function base64ImageDataFailureReason(data: string): Base64ImageDataFailure | undefined {
|
|
165
|
+
if (data.length === 0) return "empty image data";
|
|
166
|
+
|
|
167
|
+
const match = /^([A-Za-z0-9+/]+)(={0,2})$/.exec(data);
|
|
168
|
+
if (!match) return "invalid base64 image data";
|
|
169
|
+
const encoded = match[1];
|
|
170
|
+
const paddingLength = match[2].length;
|
|
171
|
+
const remainder = encoded.length % 4;
|
|
172
|
+
if (remainder === 1 || (paddingLength === 1 && remainder !== 3) || (paddingLength === 2 && remainder !== 2)) {
|
|
173
|
+
return "invalid base64 image data";
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const lastSextet = BASE64_ALPHABET.indexOf(encoded[encoded.length - 1]);
|
|
177
|
+
if ((remainder === 2 && (lastSextet & 0b1111) !== 0) || (remainder === 3 && (lastSextet & 0b11) !== 0)) {
|
|
178
|
+
return "invalid base64 image data";
|
|
179
|
+
}
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Decode standard base64 image data under the {@link base64ImageDataFailureReason} rules. */
|
|
184
|
+
export function decodeBase64ImageData(data: string): Base64ImageDecodeResult {
|
|
185
|
+
const reason = base64ImageDataFailureReason(data);
|
|
186
|
+
if (reason) return { reason };
|
|
187
|
+
return { bytes: Buffer.from(data, "base64") };
|
|
188
|
+
}
|
|
189
|
+
|
|
146
190
|
export function readImageMetadataSync(filePath: string, maxBytes = IMAGE_METADATA_HEADER_BYTES): ImageMetadata | null {
|
|
147
191
|
return peekFileSync(filePath, maxBytes, parseImageMetadata);
|
|
148
192
|
}
|