@kikuchan/hexdump 0.1.0-alpha.4 → 0.1.0-alpha.5
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 +46 -1
- package/package.json +3 -15
- package/src/index.ts +278 -0
- package/tests/hexdump.spec.ts +388 -0
- package/tsconfig.json +1 -0
- package/main.d.ts +0 -44
- package/main.js +0 -2
package/README.md
CHANGED
|
@@ -1,8 +1,53 @@
|
|
|
1
1
|
# @kikuchan/hexdump
|
|
2
2
|
|
|
3
|
+
Tiny, configurable hex dump utility for Node.js and browsers.
|
|
4
|
+
|
|
3
5
|
## Install
|
|
4
6
|
|
|
5
7
|
```bash
|
|
6
|
-
|
|
8
|
+
npm install -D @kikuchan/hexdump
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { hexdump } from '@kikuchan/hexdump';
|
|
15
|
+
|
|
16
|
+
const bytes = Uint8Array.from([
|
|
17
|
+
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
|
|
18
|
+
0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
hexdump.log(bytes);
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
output:
|
|
25
|
+
|
|
7
26
|
```
|
|
27
|
+
00000000: 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................|
|
|
28
|
+
00000010: | |
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## API
|
|
32
|
+
|
|
33
|
+
### Basic Functions
|
|
34
|
+
|
|
35
|
+
- `hexdump.log|warn|error(data, options?)`: outputs to `console.log|warn|error` respectively.
|
|
36
|
+
- `hexdump(data, options?)`: returns the full string instead of printing.
|
|
37
|
+
- `hexdump.create(printer)`: returns a hexdump function that prints to `printer` (a function that takes a string).
|
|
38
|
+
|
|
39
|
+
### `data`
|
|
40
|
+
- Binary-like data: `Uint8Array`, `Uint8ClampedArray`, `ArrayBufferLike` or `DataView`;
|
|
8
41
|
|
|
42
|
+
### `options?`
|
|
43
|
+
- `addrOffset: number` — Starting address for the first byte. Default `0`.
|
|
44
|
+
- `addrLength: number` — Hex digits shown for the address. Default `8 - prefix.length` (clamped to `>= 0`).
|
|
45
|
+
- `prefix: string` — Printed before the address (e.g. `"0x"`). Default `""`.
|
|
46
|
+
- `foldSize: number` — Bytes per row. Default `16`.
|
|
47
|
+
- `printChars: boolean` — Show the ASCII gutter at right. Default `true`.
|
|
48
|
+
- `footer: boolean` — Print a trailing line showing the next address. Default `true`.
|
|
49
|
+
- `color: boolean | 'simple' | 'html' | ((s: string, ctx: Context) => ColorizerOperation | undefined)` —
|
|
50
|
+
- `true` or `'simple'`: add ANSI colors in terminals.
|
|
51
|
+
- `'html'`: wrap parts with semantic spans like `hexdump-address`, `hexdump-ascii`, etc., and escape content.
|
|
52
|
+
- `printer?: (line: string) => void | null` — Provide a per-line sink directly in options. Set to `null` to disable printing even if a default printer is configured.
|
|
53
|
+
- `formatter: (s: string, ctx: Context) => string | undefined` — Transform each fragment before it is appended. Return `undefined` to drop that fragment.
|
package/package.json
CHANGED
|
@@ -4,25 +4,13 @@
|
|
|
4
4
|
"keywords": [
|
|
5
5
|
"hexdump"
|
|
6
6
|
],
|
|
7
|
-
"version": "0.1.0-alpha.
|
|
7
|
+
"version": "0.1.0-alpha.5",
|
|
8
8
|
"type": "module",
|
|
9
|
-
"main": "
|
|
10
|
-
"types": "main.d.ts",
|
|
11
|
-
"files": [
|
|
12
|
-
"main.js",
|
|
13
|
-
"main.d.ts"
|
|
14
|
-
],
|
|
9
|
+
"main": "./src/index.ts",
|
|
15
10
|
"author": "kikuchan <kikuchan98@gmail.com>",
|
|
16
11
|
"homepage": "https://github.com/kikuchan/utils-on-npm#readme",
|
|
17
12
|
"license": "MIT",
|
|
18
13
|
"scripts": {
|
|
19
|
-
"
|
|
20
|
-
"build:types": "tsc",
|
|
21
|
-
"build": "pnpm run build:esm && pnpm run build:types",
|
|
22
|
-
"build:esm": "esbuild main.ts --minify --outfile=main.js",
|
|
23
|
-
"lint": "biome lint .",
|
|
24
|
-
"lint:fix": "biome lint . --write",
|
|
25
|
-
"format": "biome format .",
|
|
26
|
-
"format:fix": "biome format . --write"
|
|
14
|
+
"build": "tsdown"
|
|
27
15
|
}
|
|
28
16
|
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
export type BinaryLike = Uint8Array | Uint8ClampedArray | ArrayBufferLike | DataView;
|
|
2
|
+
|
|
3
|
+
function hex(v: number, c: number) {
|
|
4
|
+
return Number(v).toString(16).padStart(c, '0');
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
type Context =
|
|
8
|
+
| {
|
|
9
|
+
type: 'hex-value' | 'hex-value-prefix' | 'hex-value-suffix' | 'character-value';
|
|
10
|
+
address: number;
|
|
11
|
+
value: number;
|
|
12
|
+
}
|
|
13
|
+
| {
|
|
14
|
+
type: 'address';
|
|
15
|
+
address: number;
|
|
16
|
+
}
|
|
17
|
+
| {
|
|
18
|
+
type:
|
|
19
|
+
| 'line-prefix'
|
|
20
|
+
| 'address-prefix'
|
|
21
|
+
| 'address-suffix'
|
|
22
|
+
| 'hex-dump-prefix'
|
|
23
|
+
| 'hex-group-prefix'
|
|
24
|
+
| 'hex-value-no-data'
|
|
25
|
+
| 'character-value-no-data'
|
|
26
|
+
| 'hex-gap'
|
|
27
|
+
| 'hex-group-gap'
|
|
28
|
+
| 'hex-group-suffix'
|
|
29
|
+
| 'hex-dump-suffix'
|
|
30
|
+
| 'character-prefix'
|
|
31
|
+
| 'character-suffix'
|
|
32
|
+
| 'line-suffix'
|
|
33
|
+
| 'flush';
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type ColorizerOperation = { enter: string; leave: string; escape?: (s: string) => string } | null;
|
|
37
|
+
|
|
38
|
+
type Colorizer =
|
|
39
|
+
| boolean
|
|
40
|
+
| undefined
|
|
41
|
+
| 'simple'
|
|
42
|
+
| 'html'
|
|
43
|
+
| ((s: string, ctx: Context) => ColorizerOperation | undefined);
|
|
44
|
+
type Formatter = undefined | ((s: string, ctx: Context) => string | undefined);
|
|
45
|
+
|
|
46
|
+
type Options = {
|
|
47
|
+
addrOffset?: number;
|
|
48
|
+
addrLength?: number;
|
|
49
|
+
printer?: null | ((s: string) => void) /* for each line */;
|
|
50
|
+
formatter?: Formatter /* for each item */;
|
|
51
|
+
color?: Colorizer;
|
|
52
|
+
prefix?: string;
|
|
53
|
+
printChars?: boolean;
|
|
54
|
+
foldSize?: number;
|
|
55
|
+
footer?: boolean;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
interface Hexdumper {
|
|
59
|
+
(buf: BinaryLike, options?: Options): string;
|
|
60
|
+
(buf: BinaryLike, len: number, options?: Options): string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface Hexdump extends Hexdumper {
|
|
64
|
+
log: Hexdumper;
|
|
65
|
+
warn: Hexdumper;
|
|
66
|
+
error: Hexdumper;
|
|
67
|
+
string: Hexdumper;
|
|
68
|
+
|
|
69
|
+
create: (printer: (s: string) => void) => Hexdumper;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const identity = <T>(s: T) => s;
|
|
73
|
+
|
|
74
|
+
const htmlEscaper = (s: string) =>
|
|
75
|
+
s.replace(
|
|
76
|
+
/[&<>]/g,
|
|
77
|
+
(x) =>
|
|
78
|
+
({
|
|
79
|
+
'&': '&',
|
|
80
|
+
'<': '<',
|
|
81
|
+
'>': '>',
|
|
82
|
+
})[x]!,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const simpleColorizer = {
|
|
86
|
+
simple: {
|
|
87
|
+
address: { enter: '\x1b[38;5;238m', leave: '\x1b[m' },
|
|
88
|
+
separator: { enter: '\x1b[38;5;238m', leave: '\x1b[m' },
|
|
89
|
+
control: { enter: '\x1b[38;5;178m', leave: '\x1b[m' },
|
|
90
|
+
ascii: { enter: '\x1b[m', leave: '\x1b[m' },
|
|
91
|
+
exascii: { enter: '\x1b[38;5;209m', leave: '\x1b[m' },
|
|
92
|
+
null: { enter: '\x1b[38;5;244m', leave: '\x1b[m' },
|
|
93
|
+
normal: null,
|
|
94
|
+
},
|
|
95
|
+
html: {
|
|
96
|
+
address: { enter: '<span class="hexdump-address">', leave: '</span>', escape: htmlEscaper },
|
|
97
|
+
separator: { enter: '<span class="hexdump-separator">', leave: '</span>', escape: htmlEscaper },
|
|
98
|
+
control: { enter: '<span class="hexdump-control">', leave: '</span>', escape: htmlEscaper },
|
|
99
|
+
ascii: { enter: '<span class="hexdump-ascii">', leave: '</span>', escape: htmlEscaper },
|
|
100
|
+
exascii: { enter: '<span class="hexdump-exascii">', leave: '</span>', escape: htmlEscaper },
|
|
101
|
+
null: { enter: '<span class="hexdump-null">', leave: '</span>', escape: htmlEscaper },
|
|
102
|
+
normal: { enter: '', leave: '', escape: htmlEscaper },
|
|
103
|
+
},
|
|
104
|
+
} as Record<
|
|
105
|
+
string,
|
|
106
|
+
{
|
|
107
|
+
address?: ColorizerOperation;
|
|
108
|
+
separator?: ColorizerOperation;
|
|
109
|
+
control?: ColorizerOperation;
|
|
110
|
+
ascii?: ColorizerOperation;
|
|
111
|
+
exascii?: ColorizerOperation;
|
|
112
|
+
null?: ColorizerOperation;
|
|
113
|
+
normal: ColorizerOperation;
|
|
114
|
+
}
|
|
115
|
+
>;
|
|
116
|
+
|
|
117
|
+
const create_colorizer = (colorizer: Colorizer) => {
|
|
118
|
+
let lastColor: ColorizerOperation | undefined = undefined;
|
|
119
|
+
|
|
120
|
+
if (!colorizer) return identity;
|
|
121
|
+
if (colorizer === true) colorizer = 'simple';
|
|
122
|
+
|
|
123
|
+
if (typeof colorizer === 'string') {
|
|
124
|
+
const defs = simpleColorizer[colorizer];
|
|
125
|
+
const separators = ['address-prefix', 'address-suffix', 'character-prefix', 'character-suffix'];
|
|
126
|
+
|
|
127
|
+
colorizer = function (s, ctx) {
|
|
128
|
+
if (!s.trim()) return undefined; // keep the last color context on empty
|
|
129
|
+
|
|
130
|
+
if (defs.address && ctx.type === 'address') return defs.address;
|
|
131
|
+
if (defs.separator && separators.includes(ctx.type)) return defs.separator;
|
|
132
|
+
|
|
133
|
+
if ((ctx.type === 'hex-value' || ctx.type === 'character-value') && typeof ctx.value === 'number') {
|
|
134
|
+
if (defs.null !== undefined && ctx.value === 0) return defs.null;
|
|
135
|
+
if (defs.control !== undefined && ctx.value < 0x20) return defs.control;
|
|
136
|
+
if (defs.ascii !== undefined && 0x20 <= ctx.value && ctx.value < 0x7f) return defs.ascii;
|
|
137
|
+
if (defs.exascii !== undefined && 0x80 <= ctx.value && ctx.value <= 0xff) return defs.exascii;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return defs.normal;
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return (s: string, ctx: Context) => {
|
|
145
|
+
const color = ctx.type === 'flush' ? null : colorizer(s, ctx);
|
|
146
|
+
|
|
147
|
+
// If color changes, emit leave/enter tokens and escape content.
|
|
148
|
+
if (
|
|
149
|
+
color !== undefined &&
|
|
150
|
+
(lastColor?.enter !== color?.enter || lastColor?.leave !== color?.leave || lastColor?.escape !== color?.escape)
|
|
151
|
+
) {
|
|
152
|
+
s = (lastColor?.leave || '') + (color?.enter || '') + (color?.escape || identity)(s);
|
|
153
|
+
lastColor = color;
|
|
154
|
+
return s;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// If color stays the same and provides an escaper, still escape content.
|
|
158
|
+
if (color && color.escape) {
|
|
159
|
+
return color.escape(s);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return s;
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const create_formatter = (formatter: Formatter) => {
|
|
167
|
+
if (!formatter) formatter = identity;
|
|
168
|
+
|
|
169
|
+
return (s: string, ctx: Context) => {
|
|
170
|
+
return formatter(s, ctx) || '';
|
|
171
|
+
};
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
function create_hexdumper(printer: ((s: string) => void) | null): Hexdumper {
|
|
175
|
+
return (buf: BinaryLike, len?: number | Options, options?: Options) => {
|
|
176
|
+
const u8 = ArrayBuffer.isView(buf)
|
|
177
|
+
? new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength)
|
|
178
|
+
: new Uint8Array(buf);
|
|
179
|
+
if (typeof len !== 'number') {
|
|
180
|
+
options = len;
|
|
181
|
+
len = u8.length;
|
|
182
|
+
}
|
|
183
|
+
options = { ...(options ?? {}) };
|
|
184
|
+
if (len === undefined || len < 0) len = u8.length;
|
|
185
|
+
|
|
186
|
+
printer = options.printer === null ? null : (options.printer ?? printer);
|
|
187
|
+
const formatter = create_formatter(options.formatter);
|
|
188
|
+
const colorize = create_colorizer(options.color);
|
|
189
|
+
|
|
190
|
+
const foldSize = options.foldSize || 16;
|
|
191
|
+
const printChars = options.printChars !== false;
|
|
192
|
+
|
|
193
|
+
let address = options.addrOffset || 0;
|
|
194
|
+
const offset = address % foldSize;
|
|
195
|
+
const rows = (len ? Math.ceil(((offset % foldSize) + len) / foldSize) : 0) + (options.footer !== false ? 1 : 0);
|
|
196
|
+
|
|
197
|
+
const result: string[] = [];
|
|
198
|
+
let line = '';
|
|
199
|
+
const print = function (s: string, ctx: Context) {
|
|
200
|
+
line += colorize(formatter(s, ctx), ctx);
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const prefix = options?.prefix || '';
|
|
204
|
+
const addrLength = Math.max(options?.addrLength ?? 8 - prefix.length, 0);
|
|
205
|
+
|
|
206
|
+
for (let i = 0; i < rows; i++) {
|
|
207
|
+
const addressBase = address;
|
|
208
|
+
|
|
209
|
+
print('', { type: 'line-prefix' });
|
|
210
|
+
print(prefix, { type: 'address-prefix' });
|
|
211
|
+
if (addrLength >= 1) print(hex(address, addrLength), { type: 'address', address });
|
|
212
|
+
print(': ', { type: 'address-suffix' });
|
|
213
|
+
|
|
214
|
+
print(' ', { type: 'hex-dump-prefix' });
|
|
215
|
+
print('', { type: 'hex-group-prefix' });
|
|
216
|
+
for (let j = 0; j < foldSize; j++) {
|
|
217
|
+
const idx = i * foldSize + j - offset;
|
|
218
|
+
if (j && j % 8 == 0) {
|
|
219
|
+
print('', { type: 'hex-group-suffix' });
|
|
220
|
+
print(' ', { type: 'hex-group-gap' });
|
|
221
|
+
print('', { type: 'hex-group-prefix' });
|
|
222
|
+
} else if (j) {
|
|
223
|
+
print(' ', { type: 'hex-gap' });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (0 <= idx && idx < len) {
|
|
227
|
+
print('', { type: 'hex-value-prefix', address, value: u8[idx] });
|
|
228
|
+
print(hex(u8[idx], 2), { type: 'hex-value', address, value: u8[idx] });
|
|
229
|
+
print('', { type: 'hex-value-suffix', address, value: u8[idx] });
|
|
230
|
+
address++;
|
|
231
|
+
} else {
|
|
232
|
+
print(' ', { type: 'hex-value-no-data' });
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
print('', { type: 'hex-group-suffix' });
|
|
236
|
+
print(' ', { type: 'hex-dump-suffix' });
|
|
237
|
+
|
|
238
|
+
if (printChars) {
|
|
239
|
+
let address = addressBase;
|
|
240
|
+
print(' |', { type: 'character-prefix' });
|
|
241
|
+
for (let j = 0; j < foldSize; j++) {
|
|
242
|
+
const idx = i * foldSize + j - offset;
|
|
243
|
+
|
|
244
|
+
if (0 <= idx && idx < len) {
|
|
245
|
+
print(u8[idx] >= 0x20 && u8[idx] < 0x7f ? String.fromCharCode(u8[idx]) : '.', {
|
|
246
|
+
type: 'character-value',
|
|
247
|
+
address,
|
|
248
|
+
value: u8[idx],
|
|
249
|
+
});
|
|
250
|
+
address++;
|
|
251
|
+
} else {
|
|
252
|
+
print(' ', { type: 'character-value-no-data' });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
print('|', { type: 'character-suffix' });
|
|
256
|
+
}
|
|
257
|
+
print('', { type: 'line-suffix' });
|
|
258
|
+
print('', { type: 'flush' });
|
|
259
|
+
|
|
260
|
+
printer?.(line);
|
|
261
|
+
result.push(line);
|
|
262
|
+
line = '';
|
|
263
|
+
}
|
|
264
|
+
return result.join('\n');
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export const hexdump: Hexdump = Object.assign(create_hexdumper(null), {
|
|
269
|
+
create: create_hexdumper,
|
|
270
|
+
|
|
271
|
+
log: create_hexdumper((s) => console.log(s)),
|
|
272
|
+
warn: create_hexdumper((s) => console.warn(s)),
|
|
273
|
+
error: create_hexdumper((s) => console.error(s)),
|
|
274
|
+
|
|
275
|
+
string: create_hexdumper(null),
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
export default hexdump;
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import hexdumpDefault, { hexdump } from '@kikuchan/hexdump';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
const u8 = (arr: number[]) => new Uint8Array(arr);
|
|
5
|
+
|
|
6
|
+
describe('hexdump basics', () => {
|
|
7
|
+
it('prints a basic hexdump', () => {
|
|
8
|
+
const out = hexdump(u8([0x00, 0x20, 0x41, 0x7e, 0x7f, 0x80, 0xff]));
|
|
9
|
+
expect(out).toBe(
|
|
10
|
+
[
|
|
11
|
+
`00000000: 00 20 41 7e 7f 80 ff |. A~... |`,
|
|
12
|
+
`00000007: | |`,
|
|
13
|
+
].join('\n'),
|
|
14
|
+
);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('prints a single footer line for empty input', () => {
|
|
18
|
+
const out = hexdump(u8([]), {
|
|
19
|
+
foldSize: 8,
|
|
20
|
+
printChars: false,
|
|
21
|
+
addrLength: 4,
|
|
22
|
+
});
|
|
23
|
+
expect(out).toBe(['0000: '].join('\n'));
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('prints bytes and a footer line', () => {
|
|
27
|
+
const out = hexdump(u8([0x41, 0x42, 0x43]), {
|
|
28
|
+
foldSize: 4,
|
|
29
|
+
printChars: false,
|
|
30
|
+
addrLength: 4,
|
|
31
|
+
});
|
|
32
|
+
expect(out).toBe(['0000: 41 42 43 ', '0003: '].join('\n'));
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('hexdump printer integration', () => {
|
|
37
|
+
it('invokes custom printer for each line', () => {
|
|
38
|
+
const lines: string[] = [];
|
|
39
|
+
const dump = hexdump.create((s) => lines.push(s));
|
|
40
|
+
const out = dump(new Uint8Array([0x41, 0x42, 0x43]), {
|
|
41
|
+
foldSize: 2,
|
|
42
|
+
printChars: false,
|
|
43
|
+
addrLength: 4,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const outLines = out.split('\n');
|
|
47
|
+
expect(lines.join('\n')).toBe(out);
|
|
48
|
+
expect(lines.length).toBe(outLines.length);
|
|
49
|
+
expect(lines[0]).toContain('0000:');
|
|
50
|
+
expect(lines[0]).toContain('41 42');
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('hexdump HTML colorizer escaping', () => {
|
|
55
|
+
it('escapes <, >, & in ascii region', () => {
|
|
56
|
+
const out = hexdump(new TextEncoder().encode('<&>'), {
|
|
57
|
+
foldSize: 8,
|
|
58
|
+
printChars: true,
|
|
59
|
+
addrLength: 2,
|
|
60
|
+
color: 'html',
|
|
61
|
+
});
|
|
62
|
+
expect(out).toContain('<');
|
|
63
|
+
expect(out).toContain('>');
|
|
64
|
+
expect(out).toContain('&');
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('hexdump hex value prefix/suffix', () => {
|
|
69
|
+
it('wraps each hex byte with custom markers', () => {
|
|
70
|
+
const bytes = new Uint8Array([0x41, 0x42]);
|
|
71
|
+
const out = hexdump(bytes, {
|
|
72
|
+
foldSize: 2,
|
|
73
|
+
printChars: false,
|
|
74
|
+
addrLength: 2,
|
|
75
|
+
color: (_s, ctx) => {
|
|
76
|
+
if (ctx.type === 'hex-value-prefix') return { enter: '{', leave: '' };
|
|
77
|
+
if (ctx.type === 'hex-value-suffix') return { enter: '', leave: '}' };
|
|
78
|
+
return undefined;
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
const firstLine = out.split('\n')[0] || '';
|
|
82
|
+
const opens = (firstLine.match(/\{/g) || []).length;
|
|
83
|
+
const closes = (firstLine.match(/\}/g) || []).length;
|
|
84
|
+
expect(opens).toBe(2);
|
|
85
|
+
expect(closes).toBe(2);
|
|
86
|
+
expect(firstLine).toContain('{41');
|
|
87
|
+
expect(firstLine).toContain('{42');
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('hexdump flush handling', () => {
|
|
92
|
+
it('appends leave token at end of line on flush', () => {
|
|
93
|
+
const out = hexdump(new Uint8Array([0x00]), {
|
|
94
|
+
foldSize: 1,
|
|
95
|
+
printChars: false,
|
|
96
|
+
addrLength: 2,
|
|
97
|
+
color: (_s, ctx) => {
|
|
98
|
+
if (ctx.type === 'line-prefix') return { enter: '<', leave: '>' };
|
|
99
|
+
return undefined;
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
const first = out.split('\n')[0] || '';
|
|
103
|
+
expect(first.endsWith('>')).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('hexdump options', () => {
|
|
108
|
+
it('respects addrOffset and computes footer address', () => {
|
|
109
|
+
const out = hexdump(u8([0xaa, 0xbb]), {
|
|
110
|
+
addrOffset: 1,
|
|
111
|
+
foldSize: 4,
|
|
112
|
+
printChars: false,
|
|
113
|
+
addrLength: 4,
|
|
114
|
+
});
|
|
115
|
+
expect(out).toBe(['0001: aa bb ', '0003: '].join('\n'));
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('applies a custom formatter to hex values', () => {
|
|
119
|
+
const out = hexdump(u8([0x00, 0x20, 0x41]), {
|
|
120
|
+
foldSize: 4,
|
|
121
|
+
printChars: false,
|
|
122
|
+
addrLength: 4,
|
|
123
|
+
formatter: (s, ctx) => (ctx.type === 'hex-value' ? `[${s}]` : s),
|
|
124
|
+
});
|
|
125
|
+
expect(out).toBe(['0000: [00] [20] [41] ', '0003: '].join('\n'));
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("adds ANSI codes when color is 'simple'", () => {
|
|
129
|
+
const out = hexdump(u8([0x00]), {
|
|
130
|
+
foldSize: 1,
|
|
131
|
+
printChars: false,
|
|
132
|
+
addrLength: 2,
|
|
133
|
+
color: 'simple',
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
expect(/\x1b\[[0-9;]*m/.test(out)).toBe(true);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("applies 'control' color for <0x20 (simple)", () => {
|
|
140
|
+
const out = hexdump(u8([0x01]), {
|
|
141
|
+
foldSize: 1,
|
|
142
|
+
printChars: false,
|
|
143
|
+
addrLength: 2,
|
|
144
|
+
color: 'simple',
|
|
145
|
+
});
|
|
146
|
+
expect(/\x1b\[38;5;178m/.test(out)).toBe(true);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("escapes ASCII with color:'html' and emits span", () => {
|
|
150
|
+
const out = hexdump(u8([0x3c, 0x20]), {
|
|
151
|
+
foldSize: 2,
|
|
152
|
+
printChars: true,
|
|
153
|
+
addrLength: 2,
|
|
154
|
+
color: 'html',
|
|
155
|
+
});
|
|
156
|
+
expect(out).toContain('hexdump-ascii');
|
|
157
|
+
expect(out).toContain('<');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("assigns exascii class for >=0x80 with color:'html'", () => {
|
|
161
|
+
const out = hexdump(u8([0x80]), {
|
|
162
|
+
foldSize: 1,
|
|
163
|
+
printChars: true,
|
|
164
|
+
addrLength: 2,
|
|
165
|
+
color: 'html',
|
|
166
|
+
});
|
|
167
|
+
expect(out).toContain('hexdump-exascii');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("uses 'normal' branch for DEL (0x7f) with color:'html'", () => {
|
|
171
|
+
const out = hexdump(u8([0x7f]), {
|
|
172
|
+
foldSize: 1,
|
|
173
|
+
printChars: true,
|
|
174
|
+
addrLength: 2,
|
|
175
|
+
color: 'html',
|
|
176
|
+
});
|
|
177
|
+
expect(out).not.toContain('hexdump-ascii');
|
|
178
|
+
expect(out).not.toContain('hexdump-control');
|
|
179
|
+
expect(out).not.toContain('hexdump-exascii');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('respects footer:false (no footer line)', () => {
|
|
183
|
+
const out = hexdump(u8([0x41, 0x42, 0x43]), {
|
|
184
|
+
foldSize: 8,
|
|
185
|
+
printChars: false,
|
|
186
|
+
addrLength: 4,
|
|
187
|
+
footer: false,
|
|
188
|
+
});
|
|
189
|
+
const lines = out.split(/\n/);
|
|
190
|
+
expect(lines.length).toBe(1);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('derives addrLength from prefix when not set', () => {
|
|
194
|
+
const out = hexdump(u8([]), {
|
|
195
|
+
foldSize: 8,
|
|
196
|
+
printChars: false,
|
|
197
|
+
prefix: '0x',
|
|
198
|
+
});
|
|
199
|
+
expect(out).toBe(['0x000000: '].join('\n'));
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('clamps addrLength to 0 when prefix is long (address hidden)', () => {
|
|
203
|
+
const out = hexdump(u8([]), {
|
|
204
|
+
foldSize: 8,
|
|
205
|
+
printChars: false,
|
|
206
|
+
prefix: '012345678',
|
|
207
|
+
});
|
|
208
|
+
expect(out).toBe(['012345678: '].join('\n'));
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('color:true behaves like simple (ANSI on)', () => {
|
|
212
|
+
const out = hexdump(u8([0x41]), {
|
|
213
|
+
foldSize: 1,
|
|
214
|
+
printChars: false,
|
|
215
|
+
addrLength: 2,
|
|
216
|
+
color: true,
|
|
217
|
+
});
|
|
218
|
+
expect(/\x1b\[[0-9;]*m/.test(out)).toBe(true);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('custom colorizer wraps line with markers and flush closes it', () => {
|
|
222
|
+
const out = hexdump(u8([0x41, 0x42]), {
|
|
223
|
+
foldSize: 2,
|
|
224
|
+
printChars: false,
|
|
225
|
+
addrLength: 2,
|
|
226
|
+
color: (s) => {
|
|
227
|
+
if (!s.trim()) return undefined;
|
|
228
|
+
return { enter: '[', leave: ']' };
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
expect(out).toBe(['[00: 41 42 ]', '[02: ]'].join('\n'));
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe('printer callbacks', () => {
|
|
236
|
+
it('calls the printer once per rendered line', () => {
|
|
237
|
+
const lines: string[] = [];
|
|
238
|
+
const dump = hexdump.create((s) => lines.push(s));
|
|
239
|
+
|
|
240
|
+
const out = dump(u8([1, 2, 3, 4, 5]), {
|
|
241
|
+
foldSize: 4,
|
|
242
|
+
printChars: false,
|
|
243
|
+
addrLength: 4,
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const split = out.split(/\n/);
|
|
247
|
+
expect(lines.length).toBe(split.length);
|
|
248
|
+
expect(lines[0]).toBe(split[0]);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('disables printer when options.printer is null', () => {
|
|
252
|
+
const dump = hexdump.create((s) => {
|
|
253
|
+
throw new Error('should not be called');
|
|
254
|
+
});
|
|
255
|
+
const out = dump(u8([0x41]), { foldSize: 1, printChars: false, addrLength: 2, printer: null });
|
|
256
|
+
expect(out.includes('41')).toBe(true);
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
describe('characters view', () => {
|
|
261
|
+
it('shows ASCII and dots for non-printables', () => {
|
|
262
|
+
const out = hexdump(u8([0x00, 0x41, 0x7f, 0x80]), {
|
|
263
|
+
foldSize: 4,
|
|
264
|
+
printChars: true,
|
|
265
|
+
addrLength: 2,
|
|
266
|
+
});
|
|
267
|
+
expect(out).toBe(['00: 00 41 7f 80 |.A..|', '04: | |'].join('\n'));
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
describe('overloads and inputs', () => {
|
|
272
|
+
it('accepts len overload to limit bytes', () => {
|
|
273
|
+
const out = hexdump(u8([0, 1, 2, 3]), 2, {
|
|
274
|
+
foldSize: 8,
|
|
275
|
+
printChars: false,
|
|
276
|
+
addrLength: 4,
|
|
277
|
+
});
|
|
278
|
+
expect(out).toBe(['0000: 00 01 ', '0002: '].join('\n'));
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('treats negative len as full length', () => {
|
|
282
|
+
const out = hexdump(u8([1, 2, 3]), -1, {
|
|
283
|
+
foldSize: 8,
|
|
284
|
+
printChars: false,
|
|
285
|
+
addrLength: 4,
|
|
286
|
+
});
|
|
287
|
+
expect(out).toBe(['0000: 01 02 03 ', '0003: '].join('\n'));
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('accepts DataView input', () => {
|
|
291
|
+
const arr = u8([0xde, 0xad, 0xbe, 0xef]);
|
|
292
|
+
const view = new DataView(arr.buffer);
|
|
293
|
+
const out = hexdump(view, {
|
|
294
|
+
foldSize: 8,
|
|
295
|
+
printChars: false,
|
|
296
|
+
addrLength: 4,
|
|
297
|
+
});
|
|
298
|
+
expect(out).toBe(['0000: de ad be ef ', '0004: '].join('\n'));
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('accepts ArrayBuffer input', () => {
|
|
302
|
+
const arr = u8([0xde, 0xad]);
|
|
303
|
+
const out = hexdump(arr.buffer, {
|
|
304
|
+
foldSize: 8,
|
|
305
|
+
printChars: false,
|
|
306
|
+
addrLength: 4,
|
|
307
|
+
});
|
|
308
|
+
expect(out).toBe(['0000: de ad ', '0002: '].join('\n'));
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
describe('grouping and gaps', () => {
|
|
313
|
+
it('inserts a double-space between groups of 8', () => {
|
|
314
|
+
const bytes = Array.from({ length: 10 }, (_, i) => i);
|
|
315
|
+
const out = hexdump(u8(bytes), {
|
|
316
|
+
foldSize: 10,
|
|
317
|
+
printChars: false,
|
|
318
|
+
addrLength: 4,
|
|
319
|
+
});
|
|
320
|
+
expect(out).toBe(['0000: 00 01 02 03 04 05 06 07 08 09 ', '000a: '].join('\n'));
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
describe('predefined printers', () => {
|
|
325
|
+
it('warn printer emits to console and returns string', () => {
|
|
326
|
+
const orig = console.warn;
|
|
327
|
+
const captured: string[] = [];
|
|
328
|
+
console.warn = (s: string) => void captured.push(s);
|
|
329
|
+
try {
|
|
330
|
+
const out = hexdump.warn(u8([0x11, 0x22]), {
|
|
331
|
+
foldSize: 8,
|
|
332
|
+
printChars: false,
|
|
333
|
+
addrLength: 4,
|
|
334
|
+
});
|
|
335
|
+
expect(out).toBe(['0000: 11 22 ', '0002: '].join('\n'));
|
|
336
|
+
expect(captured.length).toBeGreaterThan(0);
|
|
337
|
+
expect(captured[0]).toBe(out.split(/\n/)[0]);
|
|
338
|
+
} finally {
|
|
339
|
+
console.warn = orig;
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('log printer emits to console.log', () => {
|
|
344
|
+
const orig = console.log;
|
|
345
|
+
const captured: string[] = [];
|
|
346
|
+
console.log = (s: string) => void captured.push(s);
|
|
347
|
+
try {
|
|
348
|
+
const out = hexdump.log(u8([0xaa]), { foldSize: 8, printChars: false, addrLength: 4 });
|
|
349
|
+
expect(out).toBe(['0000: aa ', '0001: '].join('\n'));
|
|
350
|
+
expect(captured.length).toBeGreaterThan(0);
|
|
351
|
+
expect(captured[0]).toBe(out.split(/\n/)[0]);
|
|
352
|
+
} finally {
|
|
353
|
+
console.log = orig;
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it('error printer emits to console.error', () => {
|
|
358
|
+
const orig = console.error;
|
|
359
|
+
const captured: string[] = [];
|
|
360
|
+
console.error = (s: string) => void captured.push(s);
|
|
361
|
+
try {
|
|
362
|
+
const out = hexdump.error(u8([0xbb]), { foldSize: 8, printChars: false, addrLength: 4 });
|
|
363
|
+
expect(out).toBe(['0000: bb ', '0001: '].join('\n'));
|
|
364
|
+
expect(captured.length).toBeGreaterThan(0);
|
|
365
|
+
expect(captured[0]).toBe(out.split(/\n/)[0]);
|
|
366
|
+
} finally {
|
|
367
|
+
console.error = orig;
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it('string and default produce identical output', () => {
|
|
372
|
+
const buf = u8([0, 1, 2]);
|
|
373
|
+
const opts = { foldSize: 8, printChars: false, addrLength: 4 } as const;
|
|
374
|
+
const a = hexdump(buf, opts);
|
|
375
|
+
const b = hexdump.string(buf, opts);
|
|
376
|
+
expect(a).toBe(b);
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
describe('default export', () => {
|
|
381
|
+
it('exposes hexdump equivalent to named export', () => {
|
|
382
|
+
const buf = u8([0x01, 0x02]);
|
|
383
|
+
const opts = { foldSize: 8, printChars: false, addrLength: 4 } as const;
|
|
384
|
+
const a = hexdump(buf, opts);
|
|
385
|
+
const b = hexdumpDefault(buf, opts);
|
|
386
|
+
expect(a).toBe(b);
|
|
387
|
+
});
|
|
388
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "extends": "../../tsconfig.common.json" }
|
package/main.d.ts
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
type Context = {
|
|
2
|
-
type: 'hex-value' | 'hex-value-prefix' | 'hex-value-suffix' | 'character-value';
|
|
3
|
-
address: number;
|
|
4
|
-
value: number;
|
|
5
|
-
} | {
|
|
6
|
-
type: 'address';
|
|
7
|
-
address: number;
|
|
8
|
-
} | {
|
|
9
|
-
type: 'line-prefix' | 'address-prefix' | 'address-suffix' | 'hex-dump-prefix' | 'hex-group-prefix' | 'hex-value-no-data' | 'character-value-no-data' | 'hex-gap' | 'hex-group-gap' | 'hex-group-suffix' | 'hex-dump-suffix' | 'character-prefix' | 'character-suffix' | 'line-suffix' | 'flush';
|
|
10
|
-
};
|
|
11
|
-
type ColorizerOperation = {
|
|
12
|
-
enter: string;
|
|
13
|
-
leave: string;
|
|
14
|
-
escape?: (s: string) => string;
|
|
15
|
-
} | null;
|
|
16
|
-
type Colorizer = boolean | undefined | 'simple' | 'html' | ((s: string, ctx: Context) => ColorizerOperation | undefined);
|
|
17
|
-
type Formatter = undefined | ((s: string, ctx: Context) => string | undefined);
|
|
18
|
-
type Options = {
|
|
19
|
-
addrOffset?: number;
|
|
20
|
-
addrLength?: number;
|
|
21
|
-
printer?: false | ((s: string) => void);
|
|
22
|
-
formatter?: Formatter;
|
|
23
|
-
color?: Colorizer;
|
|
24
|
-
prefix?: string;
|
|
25
|
-
printChars?: boolean;
|
|
26
|
-
foldSize?: number;
|
|
27
|
-
footer?: boolean;
|
|
28
|
-
};
|
|
29
|
-
interface Hexdumper {
|
|
30
|
-
(buf: ArrayLike<number> | ArrayBuffer | DataView, options?: Options): string;
|
|
31
|
-
(buf: ArrayLike<number> | ArrayBuffer | DataView, len: number, options?: Options): string;
|
|
32
|
-
}
|
|
33
|
-
interface Hexdump extends Hexdumper {
|
|
34
|
-
log: Hexdumper;
|
|
35
|
-
warn: Hexdumper;
|
|
36
|
-
error: Hexdumper;
|
|
37
|
-
string: Hexdumper;
|
|
38
|
-
create: (printer: (s: string) => void) => Hexdumper;
|
|
39
|
-
}
|
|
40
|
-
export declare const hexdump: Hexdump;
|
|
41
|
-
declare const _default: {
|
|
42
|
-
hexdump: Hexdump;
|
|
43
|
-
};
|
|
44
|
-
export default _default;
|
package/main.js
DELETED
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
"use strict";function C(t,n){return Number(t).toString(16).padStart(n,"0")}const g=t=>t,f=t=>t.replace(/[&<>]/g,n=>({"&":"&","<":"<",">":">"})[n]),A={simple:{address:{enter:"\x1B[38;5;238m",leave:"\x1B[m"},separator:{enter:"\x1B[38;5;238m",leave:"\x1B[m"},control:{enter:"\x1B[38;5;178m",leave:"\x1B[m"},ascii:{enter:"\x1B[m",leave:"\x1B[m"},exascii:{enter:"\x1B[38;5;209m",leave:"\x1B[m"},null:{enter:"\x1B[38;5;244m",leave:"\x1B[m"},normal:null},html:{address:{enter:'<span class="hexdump-address">',leave:"</span>",escape:f},separator:{enter:'<span class="hexdump-separator">',leave:"</span>",escape:f},control:{enter:'<span class="hexdump-control">',leave:"</span>",escape:f},ascii:{enter:'<span class="hexdump-ascii">',leave:"</span>",escape:f},exascii:{enter:'<span class="hexdump-exascii">',leave:"</span>",escape:f},null:{enter:'<span class="hexdump-null">',leave:"</span>",escape:f},normal:{enter:"",leave:"",escape:f}}},L=t=>{let n;if(!t)return g;if(t===!0&&(t="simple"),typeof t=="string"){const e=A[t],s=["address-prefix","address-suffix","character-prefix","character-suffix"];t=function(a,i){if(a.trim())return e.address&&i.type==="address"?e.address:e.separator&&s.includes(i.type)?e.separator:(i.type==="hex-value"||i.type==="character-value")&&typeof i.value=="number"?e.null!==void 0&&i.value===0?e.null:e.control!==void 0&&i.value<32?e.control:e.ascii!==void 0&&32<=i.value&&i.value<127?e.ascii:e.exascii!==void 0&&128<=i.value&&i.value<=255?e.exascii:e.normal:e.normal}}return(e,s)=>{const a=s.type==="flush"?null:t(e,s);return a!==void 0&&(n?.enter!==a?.enter||n?.leave!==a?.leave||n?.escape!==a?.escape)&&(e=(n?.leave||"")+(a?.enter||"")+(a?.escape||g)(e),n=a),e}},S=t=>(t||(t=g),(n,e)=>t(n,e)||"");function c(t){return(n,e,s)=>{const a=ArrayBuffer.isView(n)?new Uint8Array(n.buffer,n.byteOffset,n.byteLength):new Uint8Array(n);typeof e!="number"&&(s=e,e=a.length),s={...s??{}},(e===void 0||e<0)&&(e=a.length),t=s.printer||t;const i=S(s.formatter),O=L(s.color),l=s.foldSize||16,z=s.printChars!==!1;let p=s.addrOffset||0;const y=p%l,w=(e?Math.ceil((y%l+e)/l):0)+(s.footer!==!1?1:0),v=[];let m="";const r=function(d,h){m+=O(i(d,h),h)},b=s?.prefix||"",H=Math.max(s?.addrLength??8-b.length,0);for(let d=0;d<w;d++){const h=p;r("",{type:"line-prefix"}),r(b,{type:"address-prefix"}),r(C(p,H),{type:"address",address:p}),r(": ",{type:"address-suffix"}),r(" ",{type:"hex-dump-prefix"}),r("",{type:"hex-group-prefix"});for(let o=0;o<l;o++){const u=d*l+o-y;o&&o%8==0?(r("",{type:"hex-group-suffix"}),r(" ",{type:"hex-group-gap"}),r("",{type:"hex-group-prefix"})):o&&r(" ",{type:"hex-gap"}),0<=u&&u<e?(r("",{type:"hex-value-prefix",address:p,value:a[u]}),r(C(a[u],2),{type:"hex-value",address:p,value:a[u]}),r("",{type:"hex-value-suffix",address:p,value:a[u]}),p++):r(" ",{type:"hex-value-no-data"})}if(r("",{type:"hex-group-suffix"}),r(" ",{type:"hex-dump-suffix"}),z){let o=h;r(" |",{type:"character-prefix"});for(let u=0;u<l;u++){const x=d*l+u-y;0<=x&&x<e?(r(a[x]>=32&&a[x]<127?String.fromCharCode(a[x]):".",{type:"character-value",address:o,value:a[x]}),o++):r(" ",{type:"character-value-no-data"})}r("|",{type:"character-suffix"})}r("",{type:"line-suffix"}),r("",{type:"flush"}),t?.(m),v.push(m),m=""}return v.join(`
|
|
2
|
-
`)}}export const hexdump=Object.assign(c(null),{create:c,log:c(console.log),warn:c(console.warn),error:c(console.error),string:c(null)});export default{hexdump};
|