Skip to content

Commit c78b720

Browse files
kevinelliottclaude
andcommitted
Replace Node.js built-ins with Web APIs for browser compatibility
Migrate from Node.js-specific `Buffer`, `minizlib`, and `base85` packages to cross-platform alternatives (`Uint8Array`, `pako`, pure JS ASCII85 decoder). This eliminates the need for `vite-plugin-node-polyfills` or equivalent in browser environments and removes all transitive production vulnerabilities from `base85` → `crypto-browserify` → `elliptic`. Changes: - Replace `Buffer` with `Uint8Array` + `TextEncoder`/`TextDecoder` - Replace `minizlib` with `pako` (pure JS zlib, works everywhere) - Replace `base85` npm package with pure JS ASCII85 decoder - Replace `Buffer.from(data, 'base64')` with `atob()`-based utility Closes #379 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 4af8cfa commit c78b720

5 files changed

Lines changed: 178 additions & 178 deletions

File tree

lib/plugins/Label_H1_OHMA.ts

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,45 @@ import { DecoderPlugin } from '../DecoderPlugin';
22
import { DecodeResult, Message, Options } from '../DecoderPluginInterface';
33
import { ResultFormatter } from '../utils/result_formatter';
44

5-
import * as zlib from 'minizlib';
6-
import { Buffer } from 'node:buffer';
5+
import * as pako from 'pako';
6+
7+
const textDecoder = new TextDecoder();
8+
9+
function base64ToUint8Array(base64: string): Uint8Array {
10+
// Match Buffer.from(str, 'base64') behavior: strip non-base64 chars, handle missing padding
11+
const cleaned = base64.replace(/[^A-Za-z0-9+/]/g, '');
12+
const padded = cleaned + '='.repeat((4 - (cleaned.length % 4)) % 4);
13+
const binary = atob(padded);
14+
const bytes = new Uint8Array(binary.length);
15+
for (let i = 0; i < binary.length; i++) {
16+
bytes[i] = binary.charCodeAt(i);
17+
}
18+
return bytes;
19+
}
20+
21+
/**
22+
* Inflate compressed data with support for partial/truncated streams.
23+
*/
24+
function inflateData(data: Uint8Array): Uint8Array | undefined {
25+
const chunks: Uint8Array[] = [];
26+
const inflator = new pako.Inflate({ windowBits: 15 });
27+
inflator.onData = (chunk: Uint8Array) => {
28+
chunks.push(chunk);
29+
};
30+
inflator.push(data, 2); // Z_SYNC_FLUSH
31+
32+
if (chunks.length === 0) return undefined;
33+
if (chunks.length === 1) return chunks[0];
34+
35+
const totalLen = chunks.reduce((sum, c) => sum + c.length, 0);
36+
const result = new Uint8Array(totalLen);
37+
let offset = 0;
38+
for (const chunk of chunks) {
39+
result.set(chunk, offset);
40+
offset += chunk.length;
41+
}
42+
return result;
43+
}
744

845
export class Label_H1_OHMA extends DecoderPlugin {
946
name = 'label-h1-ohma';
@@ -23,12 +60,12 @@ export class Label_H1_OHMA extends DecoderPlugin {
2360

2461
const data = message.text.split('OHMA')[1]; // throw out '/RTNOCR.' - even though it means something
2562
try {
26-
const compressedBuffer = Buffer.from(data, 'base64');
27-
const decompress = new zlib.Inflate({});
28-
decompress.write(compressedBuffer);
29-
decompress.flush(zlib.constants.Z_SYNC_FLUSH);
30-
const result = decompress.read();
31-
const jsonText = result?.toString() || '';
63+
const compressedBuffer = base64ToUint8Array(data);
64+
const result = inflateData(compressedBuffer);
65+
if (!result || result.length === 0) {
66+
throw new Error('Decompression produced no output');
67+
}
68+
const jsonText = textDecoder.decode(result);
3269

3370
let formattedMsg;
3471
let jsonMessage;

lib/utils/ascii85.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/**
2+
* Pure JavaScript ASCII85 (Adobe variant) decoder.
3+
* Replaces the `base85` npm package to eliminate Node.js Buffer dependency.
4+
* Works in browsers, Node.js 18+, Deno, Bun, and edge runtimes.
5+
*/
6+
export function ascii85Decode(input: string): Uint8Array | null {
7+
let str = input;
8+
if (str.startsWith('<~')) str = str.slice(2);
9+
if (str.endsWith('~>')) str = str.slice(0, -2);
10+
str = str.replace(/\s/g, '');
11+
if (str.length === 0) return new Uint8Array(0);
12+
13+
const output: number[] = [];
14+
let i = 0;
15+
16+
while (i < str.length) {
17+
if (str[i] === 'z') {
18+
output.push(0, 0, 0, 0);
19+
i++;
20+
continue;
21+
}
22+
23+
const remaining = str.length - i;
24+
const chunkLen = Math.min(5, remaining);
25+
if (chunkLen === 1) break; // trailing single char from truncated input; ignore it
26+
27+
let padded = str.slice(i, i + chunkLen);
28+
while (padded.length < 5) padded += 'u';
29+
30+
let value = 0;
31+
for (let j = 0; j < 5; j++) {
32+
const digit = padded.charCodeAt(j) - 33;
33+
if (digit < 0 || digit > 84) return null;
34+
value = value * 85 + digit;
35+
}
36+
37+
if (chunkLen === 5 && value > 0xffffffff) return null;
38+
39+
// Use >>> 0 to simulate uint32 overflow for padded groups
40+
const v = value >>> 0;
41+
const numBytes = chunkLen === 5 ? 4 : chunkLen - 1;
42+
if (numBytes >= 1) output.push((v >>> 24) & 0xff);
43+
if (numBytes >= 2) output.push((v >>> 16) & 0xff);
44+
if (numBytes >= 3) output.push((v >>> 8) & 0xff);
45+
if (numBytes >= 4) output.push(v & 0xff);
46+
47+
i += chunkLen;
48+
}
49+
50+
return new Uint8Array(output);
51+
}

lib/utils/miam.ts

Lines changed: 64 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,33 @@
1-
import * as Base85 from 'base85';
2-
import * as zlib from 'minizlib';
3-
import { Buffer } from 'node:buffer';
1+
import { ascii85Decode } from './ascii85';
2+
import * as pako from 'pako';
3+
4+
const textDecoder = new TextDecoder();
5+
const textEncoder = new TextEncoder();
6+
7+
/**
8+
* Inflate compressed data with support for partial/truncated streams.
9+
* Captures output chunks via onData to handle Z_SYNC_FLUSH correctly.
10+
*/
11+
function inflateData(data: Uint8Array, raw: boolean): Uint8Array | undefined {
12+
const chunks: Uint8Array[] = [];
13+
const inflator = new pako.Inflate({ windowBits: raw ? -15 : 15 });
14+
inflator.onData = (chunk: Uint8Array) => {
15+
chunks.push(chunk);
16+
};
17+
inflator.push(data, 2); // Z_SYNC_FLUSH
18+
19+
if (chunks.length === 0) return undefined;
20+
if (chunks.length === 1) return chunks[0];
21+
22+
const totalLen = chunks.reduce((sum, c) => sum + c.length, 0);
23+
const result = new Uint8Array(totalLen);
24+
let offset = 0;
25+
for (const chunk of chunks) {
26+
result.set(chunk, offset);
27+
offset += chunk.length;
28+
}
29+
return result;
30+
}
431

532
enum MIAMVersion {
633
V1 = 1,
@@ -155,34 +182,34 @@ export class MIAMCoreUtils {
155182
};
156183
}
157184

158-
let hdr = Base85.decode('<~' + rawHdr + '~>', 'ascii85');
185+
let hdr = ascii85Decode('<~' + rawHdr + '~>');
159186
if (!hdr || hdr.length < hpad) {
160187
return {
161188
decoded: false,
162189
error: 'Ascii85 decode failed for MIAM message header',
163190
};
164191
}
165192

166-
let body: Buffer | undefined = undefined;
193+
let body: Uint8Array | undefined = undefined;
167194

168195
const rawBody = txt.substring(delimIdx + 1);
169196
if (rawBody.length > 0) {
170197
if ('0123'.indexOf(bpad) >= 0) {
171198
const bpadValue = parseInt(bpad);
172199

173-
body = Base85.decode('<~' + rawBody + '~>', 'ascii85') || undefined;
200+
body = ascii85Decode('<~' + rawBody + '~>') || undefined;
174201
if (body && body.length >= bpadValue) {
175202
body = body.subarray(0, body.length - bpadValue);
176203
}
177204
} else if (bpad === '-') {
178-
body = Buffer.from(rawBody);
205+
body = textEncoder.encode(rawBody);
179206
}
180207
}
181208

182209
hdr = hdr.subarray(0, hdr.length - hpad);
183210

184-
const version = hdr.readUInt8(0) & 0xf;
185-
const pduType = (hdr.readUInt8(0) >> 4) & 0xf;
211+
const version = hdr[0] & 0xf;
212+
const pduType = (hdr[0] >> 4) & 0xf;
186213

187214
if (isMIAMVersion(version) && isMIAMCorePdu(pduType)) {
188215
const versionPduHandler =
@@ -225,7 +252,7 @@ export class MIAMCoreUtils {
225252
},
226253
};
227254

228-
private static arincCrc16(buf: Buffer, seed?: number) {
255+
private static arincCrc16(buf: Uint8Array, seed?: number) {
229256
const crctable = [
230257
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, 0x8108,
231258
0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, 0x1231, 0x0210,
@@ -263,14 +290,14 @@ export class MIAMCoreUtils {
263290
for (let i = 0; i < buf.length; i++) {
264291
crc =
265292
(((crc << 8) >>> 0) ^
266-
crctable[(((crc >>> 8) ^ buf.readUInt8(i)) >>> 0) & 0xff]) >>>
293+
crctable[(((crc >>> 8) ^ buf[i]) >>> 0) & 0xff]) >>>
267294
0;
268295
}
269296

270297
return crc & 0xffff;
271298
}
272299

273-
private static arinc665Crc32(buf: Buffer, seed?: number) {
300+
private static arinc665Crc32(buf: Uint8Array, seed?: number) {
274301
const crctable = [
275302
0x00000000, 0x04c11db7, 0x09823b6e, 0x0d4326d9, 0x130476dc, 0x17c56b6b,
276303
0x1a864db2, 0x1e475005, 0x2608edb8, 0x22c9f00f, 0x2f8ad6d6, 0x2b4bcb61,
@@ -321,9 +348,7 @@ export class MIAMCoreUtils {
321348

322349
for (let i = 0; i < buf.length; i++) {
323350
crc =
324-
(((crc << 8) >>> 0) ^
325-
crctable[((crc >>> 24) ^ buf.readUInt8(i)) >>> 0]) >>>
326-
0;
351+
(((crc << 8) >>> 0) ^ crctable[((crc >>> 24) ^ buf[i]) >>> 0]) >>> 0;
327352
}
328353

329354
return crc;
@@ -347,8 +372,8 @@ export class MIAMCoreUtils {
347372
version: MIAMVersion,
348373
minHdrSize: number,
349374
crcLen: number,
350-
hdr: Buffer,
351-
body?: Buffer,
375+
hdr: Uint8Array,
376+
body?: Uint8Array,
352377
): PduDecodingResult {
353378
if (hdr.length < minHdrSize) {
354379
return {
@@ -369,7 +394,7 @@ export class MIAMCoreUtils {
369394
let pduAppType: number = 0;
370395
let pduAppId: string = '';
371396
let pduCrc: number = 0;
372-
let pduData: Buffer | null = null;
397+
let pduData: Uint8Array | null = null;
373398
let pduCrcIsOk: boolean = false;
374399
let pduIsComplete: boolean = true;
375400

@@ -380,8 +405,7 @@ export class MIAMCoreUtils {
380405
let ackOptions: number = 0;
381406

382407
if (version === MIAMVersion.V1) {
383-
pduSize =
384-
(hdr.readUInt8(1) << 16) | (hdr.readUInt8(2) << 8) | hdr.readUInt8(3);
408+
pduSize = (hdr[1] << 16) | (hdr[2] << 8) | hdr[3];
385409

386410
const msgSize = hdr.length + (body === undefined ? 0 : body.length);
387411
if (pduSize > msgSize) {
@@ -392,20 +416,19 @@ export class MIAMCoreUtils {
392416
}
393417
hdr = hdr.subarray(4);
394418

395-
tail = hdr.subarray(0, 7).toString('ascii');
419+
tail = textDecoder.decode(hdr.subarray(0, 7));
396420
hdr = hdr.subarray(7);
397421
} else if (version === MIAMVersion.V2) {
398422
hdr = hdr.subarray(1);
399423
}
400424

401-
msgNum = (hdr.readUInt8(0) >> 1) & 0x7f;
402-
ackOptions = hdr.readUInt8(0) & 1;
425+
msgNum = (hdr[0] >> 1) & 0x7f;
426+
ackOptions = hdr[0] & 1;
403427
hdr = hdr.subarray(1);
404428

405-
pduCompression =
406-
((hdr.readUInt8(0) << 2) | ((hdr.readUInt8(1) >> 6) & 0x3)) & 0x7;
407-
pduEncoding = (hdr.readUInt8(1) >> 4) & 0x3;
408-
pduAppType = hdr.readUInt8(1) & 0xf;
429+
pduCompression = ((hdr[0] << 2) | ((hdr[1] >> 6) & 0x3)) & 0x7;
430+
pduEncoding = (hdr[1] >> 4) & 0x3;
431+
pduAppType = hdr[1] & 0xf;
409432
hdr = hdr.subarray(2);
410433

411434
let appIdLen;
@@ -440,17 +463,13 @@ export class MIAMCoreUtils {
440463
};
441464
}
442465

443-
pduAppId = hdr.subarray(0, appIdLen).toString('ascii');
466+
pduAppId = textDecoder.decode(hdr.subarray(0, appIdLen));
444467
hdr = hdr.subarray(appIdLen);
445468

446469
if (crcLen === 4) {
447-
pduCrc =
448-
(hdr.readUInt8(0) << 24) |
449-
(hdr.readUInt8(1) << 16) |
450-
(hdr.readUInt8(2) << 8) |
451-
hdr.readUInt8(3); // crc
470+
pduCrc = (hdr[0] << 24) | (hdr[1] << 16) | (hdr[2] << 8) | hdr[3]; // crc
452471
} else if (crcLen === 2) {
453-
pduCrc = (hdr.readUInt8(0) << 8) | hdr.readUInt8(1); // crc
472+
pduCrc = (hdr[0] << 8) | hdr[1]; // crc
454473
}
455474
hdr = hdr.subarray(crcLen);
456475

@@ -461,10 +480,7 @@ export class MIAMCoreUtils {
461480
) >= 0
462481
) {
463482
try {
464-
const decompress = new zlib.InflateRaw({});
465-
decompress.write(body);
466-
decompress.flush(zlib.constants.Z_SYNC_FLUSH);
467-
pduData = decompress.read();
483+
pduData = inflateData(body, true) || null;
468484
} catch (e) {
469485
pduErrors.push('Inflation failed for body: ' + e);
470486
}
@@ -483,9 +499,9 @@ export class MIAMCoreUtils {
483499
if (pduData !== null) {
484500
const crcAlgoHandlerByVersion: Record<
485501
MIAMVersion,
486-
(buf: Buffer, seed?: number) => number
502+
(buf: Uint8Array, seed?: number) => number
487503
> = {
488-
[MIAMVersion.V1]: (buf: Buffer, seed?: number) => {
504+
[MIAMVersion.V1]: (buf: Uint8Array, seed?: number) => {
489505
return ~this.arinc665Crc32(buf, seed);
490506
},
491507
[MIAMVersion.V2]: this.arincCrc16,
@@ -537,12 +553,12 @@ export class MIAMCoreUtils {
537553
label,
538554
...(sublabel ? { sublabel } : {}),
539555
...(mfi ? { mfi } : {}),
540-
...(pduData ? { text: pduData.toString('ascii') } : {}),
556+
...(pduData ? { text: textDecoder.decode(pduData) } : {}),
541557
};
542558
} else {
543559
pdu.non_acars = {
544560
appId: pduAppId,
545-
...(pduData ? { text: pduData.toString('ascii') } : {}),
561+
...(pduData ? { text: textDecoder.decode(pduData) } : {}),
546562
};
547563
}
548564

@@ -556,10 +572,13 @@ export class MIAMCoreUtils {
556572

557573
static VersionPduHandlerTable: Record<
558574
MIAMVersion,
559-
Record<MIAMCorePdu, (hdr: Buffer, body?: Buffer) => PduDecodingResult>
575+
Record<
576+
MIAMCorePdu,
577+
(hdr: Uint8Array, body?: Uint8Array) => PduDecodingResult
578+
>
560579
> = {
561580
[MIAMVersion.V1]: {
562-
[MIAMCorePdu.Data]: (hdr: Buffer, body?: Buffer) => {
581+
[MIAMCorePdu.Data]: (hdr: Uint8Array, body?: Uint8Array) => {
563582
return this.corePduDataHandler(
564583
MIAMVersion.V1,
565584
20,
@@ -579,7 +598,7 @@ export class MIAMCoreUtils {
579598
},
580599
},
581600
[MIAMVersion.V2]: {
582-
[MIAMCorePdu.Data]: (hdr: Buffer, body?: Buffer) => {
601+
[MIAMCorePdu.Data]: (hdr: Uint8Array, body?: Uint8Array) => {
583602
return this.corePduDataHandler(
584603
MIAMVersion.V2,
585604
7,

0 commit comments

Comments
 (0)