@oh-my-pi/pi-utils 18.10.4 → 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.
@@ -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. */
@@ -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.10.4",
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.10.4"
60
+ "@oh-my-pi/pi-natives": "18.11.1"
61
61
  },
62
62
  "devDependencies": {
63
63
  "@types/bun": "^1.3.14"
@@ -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
  }