bytepeel 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pinta365
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,63 @@
1
- # Temporary Holding Version
1
+ # bytepeel (WebAssembly)
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Peel layers of encoding off binary data, and edit it in place, from
4
+ JavaScript: hex, xxd/hexdump dumps, base64, PEM (incl. bundles), JWT,
5
+ gzip/zlib/zstd, DER/ASN.1, CBOR, MessagePack and schemaless protobuf.
6
+
7
+ One `bytepeel.wasm` with no imports and no JS dependencies; runs in Node
8
+ (≥18), Deno, Bun and browsers.
9
+
10
+ ```sh
11
+ npm install bytepeel # npm (Node, Bun, bundlers)
12
+ deno add jsr:@pinta365/bytepeel # JSR (Deno; also npx jsr add for Node)
13
+ ```
14
+
15
+ The JSR package imports from `"@pinta365/bytepeel"` and has the same API.
16
+
17
+ ```js
18
+ import { load, raw, BytepeelError } from "bytepeel";
19
+
20
+ const bp = await load();
21
+
22
+ bp.peel("ChRTR1ZzYkc4c0lIZHZjbXhrSVNFPRAq"); // the decoded tree (object)
23
+ bp.tree("1a03089601"); // the text outline
24
+ bp.get(pem, "0.0.5.2.0.1").value; // "ISRG Root X2"
25
+ bp.getRaw("1a03089601", "3.1"); // Uint8Array [0x96, 0x01]
26
+
27
+ // Edits return the data in its original outer format (hex stays hex, PEM
28
+ // stays PEM, ...), with every other byte preserved. Each edit is verified.
29
+ const out = bp.edit("08 96 01 12 03 66 6f 6f", [
30
+ { add: "3", value: 42 },
31
+ { del: "2" },
32
+ ]);
33
+ new TextDecoder().decode(out); // "08 96 01 18 2a"
34
+ ```
35
+
36
+ - **Options** (all methods): `{ in: "auto"|"hex"|"base64"|"raw", as:
37
+ "protobuf"|"der"|"cbor"|"msgpack"|"text"|"bytes", depth, alts, max_items }`.
38
+ - **Input:** a string (UTF-8) or `Uint8Array`/`ArrayBuffer`/typed array.
39
+ - **Values** are typed: a string is text (never parsed, so `"42"` is the
40
+ text `42`); an integer `number` or a `bigint` is an integer; other numbers
41
+ are floats; `true`/`false`/`null`; a `Uint8Array` is bytes. Use
42
+ `raw("sint:-2")`, `raw("float:1")` for the CLI value syntax.
43
+ - **64-bit integers are JSON strings** in `peel`/`get` output, so they never
44
+ lose precision.
45
+ - **Errors** throw `BytepeelError` with the CLI's stable `code` (e.g.
46
+ `PATH_NOT_FOUND`) and `offset`.
47
+ - **Loading:** from npm, `load()` reads `bytepeel.wasm` next to `index.js`
48
+ (a file read in Node/Deno/Bun, `fetch` over HTTP). The JSR package embeds
49
+ the module, so `load()` needs no file or network access (no Deno
50
+ permissions). Pass the bytes, a URL, a `Response`, or a compiled
51
+ `WebAssembly.Module` to load it yourself.
52
+
53
+ The JSON schema, paths and edit rules are in
54
+ [docs/DESIGN.md](https://github.com/pinta365/bytepeel/blob/main/docs/DESIGN.md).
55
+
56
+ ## Building
57
+
58
+ ```sh
59
+ rustup target add wasm32-unknown-unknown
60
+ npm run build # cargo build --profile wasm ...; copies bytepeel.wasm here and
61
+ # writes bytepeel_wasm.js (the module as base64, for JSR)
62
+ npm test # node --test; also: deno test --allow-read test/, bun test test/
63
+ ```
package/bytepeel.wasm ADDED
Binary file
package/index.d.ts ADDED
@@ -0,0 +1,48 @@
1
+ export interface Options {
2
+ /** How to read the top-level input (default "auto"). */
3
+ in?: "auto" | "hex" | "base64" | "raw";
4
+ /** Force the interpretation of the first decoded byte layer. */
5
+ as?: "protobuf" | "der" | "cbor" | "msgpack" | "text" | "bytes";
6
+ /** Max nesting of layers and messages (default 16). */
7
+ depth?: number;
8
+ /** Max interpretations kept per protobuf LEN field (default 3). */
9
+ alts?: number;
10
+ /** Max protobuf fields, CBOR/MessagePack values and DER elements in total (default 1000000). */
11
+ max_items?: number;
12
+ }
13
+
14
+ /**
15
+ * A value for an edit. Strings are text (never parsed); numbers that are
16
+ * integers are integers, other numbers are floats; Uint8Array is bytes.
17
+ * Use raw("sint:-2"), raw("float:1") etc. for the CLI value syntax.
18
+ */
19
+ export type Value = string | number | bigint | boolean | null | Uint8Array | { syntax: string };
20
+
21
+ export type Edit =
22
+ | { set: string; value: Value }
23
+ | { add: string; value: Value }
24
+ | { del: string };
25
+
26
+ /** The decoded tree. See docs/DESIGN.md for the full schema. */
27
+ export type Node = { codec: string; range: [number, number]; confidence: number; [key: string]: unknown };
28
+
29
+ export interface Bytepeel {
30
+ peel(input: string | Uint8Array | ArrayBuffer | ArrayBufferView, opts?: Options): Node;
31
+ tree(input: string | Uint8Array | ArrayBuffer | ArrayBufferView, opts?: Options): string;
32
+ get(input: string | Uint8Array | ArrayBuffer | ArrayBufferView, path: string, opts?: Options): Record<string, unknown>;
33
+ getRaw(input: string | Uint8Array | ArrayBuffer | ArrayBufferView, path: string, opts?: Options): Uint8Array;
34
+ edit(input: string | Uint8Array | ArrayBuffer | ArrayBufferView, edits: Edit[], opts?: Options): Uint8Array;
35
+ }
36
+
37
+ export class BytepeelError extends Error {
38
+ /** Stable SCREAMING_SNAKE identifier, e.g. "PATH_NOT_FOUND". */
39
+ readonly code: string;
40
+ /** Byte offset into the buffer being decoded, or null. */
41
+ readonly offset: number | null;
42
+ readonly detail: string;
43
+ }
44
+
45
+ /** Pass a value in the CLI value syntax verbatim. */
46
+ export function raw(syntax: string): { syntax: string };
47
+
48
+ export function load(source?: BufferSource | URL | string | Response | WebAssembly.Module): Promise<Bytepeel>;
package/index.js ADDED
@@ -0,0 +1,186 @@
1
+ // @ts-self-types="./index.d.ts"
2
+ // bytepeel for JavaScript: a thin wrapper over bytepeel.wasm (Node, Deno,
3
+ // Bun, browsers). See README.md. The JSON schema is docs/DESIGN.md.
4
+
5
+ const OP_PEEL = 0;
6
+ const OP_TREE = 1;
7
+ const OP_GET = 2;
8
+ const OP_GET_RAW = 3;
9
+ const OP_EDIT = 4;
10
+
11
+ const enc = new TextEncoder();
12
+ const dec = new TextDecoder();
13
+
14
+ /** An error from bytepeel, with the same stable `code` the CLI prints. */
15
+ export class BytepeelError extends Error {
16
+ constructor({ code, message, offset }) {
17
+ super(`${code}: ${message}`);
18
+ this.name = "BytepeelError";
19
+ /** Stable SCREAMING_SNAKE identifier, e.g. "PATH_NOT_FOUND". */
20
+ this.code = code;
21
+ /** Byte offset into the buffer being decoded, or null. */
22
+ this.offset = offset;
23
+ this.detail = message;
24
+ }
25
+ }
26
+
27
+ /** Pass a value in the CLI value syntax verbatim, e.g. raw("sint:-2"). */
28
+ export function raw(syntax) {
29
+ return { syntax: String(syntax) };
30
+ }
31
+
32
+ function toBytes(input) {
33
+ if (typeof input === "string") return enc.encode(input);
34
+ if (input instanceof Uint8Array) return input;
35
+ if (input instanceof ArrayBuffer) return new Uint8Array(input);
36
+ if (ArrayBuffer.isView(input)) {
37
+ return new Uint8Array(input.buffer, input.byteOffset, input.byteLength);
38
+ }
39
+ throw new TypeError("input must be a string, Uint8Array, ArrayBuffer or typed array");
40
+ }
41
+
42
+ function optsString(opts = {}) {
43
+ const lines = [];
44
+ for (const [k, v] of Object.entries(opts)) {
45
+ if (v === undefined) continue;
46
+ if (!["in", "as", "depth", "alts", "max_items"].includes(k)) {
47
+ throw new TypeError(`unknown option '${k}' (in, as, depth, alts, max_items)`);
48
+ }
49
+ lines.push(`${k}=${v}`);
50
+ }
51
+ return lines.join("\n");
52
+ }
53
+
54
+ const HEX = Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, "0"));
55
+
56
+ /** A JS value to the CLI value syntax. Strings are text, never parsed. */
57
+ function valueSyntax(v) {
58
+ if (v !== null && typeof v === "object" && "syntax" in v) return v.syntax;
59
+ if (v === null) return "null";
60
+ if (typeof v === "boolean") return v ? "true" : "false";
61
+ if (typeof v === "bigint") return v.toString();
62
+ if (typeof v === "number") {
63
+ if (Number.isInteger(v)) {
64
+ if (!Number.isSafeInteger(v)) {
65
+ throw new RangeError(`${v} is not a safe integer; pass a BigInt`);
66
+ }
67
+ return String(v);
68
+ }
69
+ if (Number.isNaN(v)) return "float:NaN";
70
+ if (v === Infinity) return "float:Infinity";
71
+ if (v === -Infinity) return "float:-Infinity";
72
+ return `float:${v}`;
73
+ }
74
+ if (typeof v === "string") return `str:${v}`;
75
+ if (v instanceof Uint8Array || ArrayBuffer.isView(v) || v instanceof ArrayBuffer) {
76
+ const b = toBytes(v);
77
+ let s = "hex:";
78
+ for (const x of b) s += HEX[x];
79
+ return s;
80
+ }
81
+ throw new TypeError(`unsupported value type: ${typeof v}`);
82
+ }
83
+
84
+ function editsString(edits) {
85
+ if (!Array.isArray(edits) || edits.length === 0) {
86
+ throw new TypeError("edits must be a non-empty array");
87
+ }
88
+ const parts = [];
89
+ for (const e of edits) {
90
+ if ("set" in e) parts.push("set", e.set, valueSyntax(e.value));
91
+ else if ("add" in e) parts.push("add", e.add, valueSyntax(e.value));
92
+ else if ("del" in e) parts.push("del", e.del, "");
93
+ else throw new TypeError("each edit is {set, value}, {add, value} or {del}");
94
+ }
95
+ for (const p of parts) {
96
+ if (p.includes("\0")) throw new TypeError("paths and values cannot contain NUL");
97
+ }
98
+ return parts.join("\0");
99
+ }
100
+
101
+ function wrap(instance) {
102
+ const { memory, bp_alloc, bp_free, bp_call } = instance.exports;
103
+
104
+ function put(bytes) {
105
+ const ptr = bp_alloc(bytes.length);
106
+ new Uint8Array(memory.buffer, ptr, bytes.length).set(bytes);
107
+ return [ptr, bytes.length];
108
+ }
109
+
110
+ function call(op, input, arg, opts) {
111
+ const bufs = [put(toBytes(input)), put(enc.encode(arg)), put(enc.encode(opts))];
112
+ try {
113
+ const res = bp_call(op, ...bufs[0], ...bufs[1], ...bufs[2]);
114
+ // Re-read the buffer: memory may have grown during the call.
115
+ const view = new DataView(memory.buffer);
116
+ const status = view.getUint8(res);
117
+ const len = view.getUint32(res + 1, true);
118
+ const body = new Uint8Array(memory.buffer, res + 5, len).slice();
119
+ bp_free(res, 5 + len);
120
+ if (status !== 0) throw new BytepeelError(JSON.parse(dec.decode(body)).error);
121
+ return body;
122
+ } finally {
123
+ for (const [p, n] of bufs) bp_free(p, n);
124
+ }
125
+ }
126
+
127
+ return {
128
+ /** Decode `input` into the node tree (DESIGN.md schema). */
129
+ peel(input, opts) {
130
+ return JSON.parse(dec.decode(call(OP_PEEL, input, "", optsString(opts))));
131
+ },
132
+ /** The human-readable outline (`--format tree`). */
133
+ tree(input, opts) {
134
+ return dec.decode(call(OP_TREE, input, "", optsString(opts)));
135
+ },
136
+ /** One protobuf field, DER element or CBOR/MessagePack value by path. */
137
+ get(input, path, opts) {
138
+ return JSON.parse(dec.decode(call(OP_GET, input, String(path), optsString(opts))));
139
+ },
140
+ /** The payload bytes at `path` (`--get PATH --format raw`). */
141
+ getRaw(input, path, opts) {
142
+ return call(OP_GET_RAW, input, String(path), optsString(opts));
143
+ },
144
+ /**
145
+ * Apply edits in order and return the re-encoded data, in the input's
146
+ * original outer format. Each edit is verified.
147
+ */
148
+ edit(input, edits, opts) {
149
+ return call(OP_EDIT, input, editsString(edits), optsString(opts));
150
+ },
151
+ };
152
+ }
153
+
154
+ async function readDefault() {
155
+ const url = new URL("./bytepeel.wasm", import.meta.url);
156
+ if (url.protocol === "file:") {
157
+ const { readFile } = await import("node:fs/promises");
158
+ return readFile(url);
159
+ }
160
+ const res = await fetch(url);
161
+ if (!res.ok) throw new Error(`failed to fetch ${url}: ${res.status}`);
162
+ return res.arrayBuffer();
163
+ }
164
+
165
+ /**
166
+ * Load bytepeel. `source` is optional: the wasm bytes, a URL, a Response,
167
+ * or a compiled WebAssembly.Module. By default bytepeel.wasm next to this
168
+ * file is used.
169
+ */
170
+ export async function load(source) {
171
+ let module;
172
+ if (source instanceof WebAssembly.Module) {
173
+ module = source;
174
+ } else {
175
+ let bytes = source;
176
+ if (source === undefined) bytes = await readDefault();
177
+ else if (source instanceof URL || typeof source === "string") {
178
+ bytes = await (await fetch(source)).arrayBuffer();
179
+ } else if (typeof Response !== "undefined" && source instanceof Response) {
180
+ bytes = await source.arrayBuffer();
181
+ }
182
+ module = await WebAssembly.compile(bytes);
183
+ }
184
+ const instance = await WebAssembly.instantiate(module, {});
185
+ return wrap(instance);
186
+ }
package/package.json CHANGED
@@ -1,6 +1,50 @@
1
1
  {
2
2
  "name": "bytepeel",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Peel layers of encoding off binary data (hex, base64, PEM, JWT, gzip, zstd, protobuf, DER/ASN.1, CBOR, MessagePack) and edit it in place. WebAssembly, no dependencies.",
5
+ "license": "MIT",
6
+ "author": "Pinta365",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/pinta365/bytepeel.git",
10
+ "directory": "npm"
11
+ },
12
+ "homepage": "https://github.com/pinta365/bytepeel#readme",
13
+ "bugs": "https://github.com/pinta365/bytepeel/issues",
14
+ "type": "module",
15
+ "main": "./index.js",
16
+ "types": "./index.d.ts",
17
+ "exports": {
18
+ ".": {
19
+ "types": "./index.d.ts",
20
+ "default": "./index.js"
21
+ },
22
+ "./bytepeel.wasm": "./bytepeel.wasm"
23
+ },
24
+ "files": [
25
+ "index.js",
26
+ "index.d.ts",
27
+ "bytepeel.wasm",
28
+ "README.md",
29
+ "LICENSE"
30
+ ],
31
+ "sideEffects": false,
32
+ "engines": {
33
+ "node": ">=18"
34
+ },
35
+ "scripts": {
36
+ "build": "sh build.sh",
37
+ "test": "node --test test/*.test.js"
38
+ },
39
+ "keywords": [
40
+ "protobuf",
41
+ "asn1",
42
+ "der",
43
+ "cbor",
44
+ "msgpack",
45
+ "base64",
46
+ "jwt",
47
+ "decoder",
48
+ "wasm"
49
+ ]
50
+ }