Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions lib/plugins/Label_H1_OHMA.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { DecoderPlugin } from '../DecoderPlugin';
import { DecodeResult, Message, Options } from '../DecoderPluginInterface';
import { ResultFormatter } from '../utils/result_formatter';
import { base64ToUint8Array, inflateData } from '../utils/compression';

import * as zlib from 'minizlib';
import { Buffer } from 'node:buffer';
const textDecoder = new TextDecoder();

export class Label_H1_OHMA extends DecoderPlugin {
name = 'label-h1-ohma';
Expand All @@ -23,12 +23,12 @@ export class Label_H1_OHMA extends DecoderPlugin {

const data = message.text.split('OHMA')[1]; // throw out '/RTNOCR.' - even though it means something
try {
const compressedBuffer = Buffer.from(data, 'base64');
const decompress = new zlib.Inflate({});
decompress.write(compressedBuffer);
decompress.flush(zlib.constants.Z_SYNC_FLUSH);
const result = decompress.read();
const jsonText = result?.toString() || '';
const compressedBuffer = base64ToUint8Array(data);
const result = inflateData(compressedBuffer, false);
if (!result || result.length === 0) {
throw new Error('Decompression produced no output');
}
const jsonText = textDecoder.decode(result);

let formattedMsg;
let jsonMessage;
Expand Down
51 changes: 51 additions & 0 deletions lib/utils/ascii85.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* Pure JavaScript ASCII85 (Adobe variant) decoder.
* Replaces the `base85` npm package to eliminate Node.js Buffer dependency.
* Works in browsers, Node.js 18+, Deno, Bun, and edge runtimes.
*/
export function ascii85Decode(input: string): Uint8Array | null {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can probably be it's own lib, but i guess works for now. I won't even pretend to understand it

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed it could be its own lib. For now it's a single function (~40 lines) with no dependencies, so keeping it in-tree seemed simplest. Happy to extract it if it ends up being useful elsewhere.

let str = input;
if (str.startsWith('<~')) str = str.slice(2);
if (str.endsWith('~>')) str = str.slice(0, -2);
str = str.replace(/\s/g, '');
if (str.length === 0) return new Uint8Array(0);

const output: number[] = [];
let i = 0;

while (i < str.length) {
if (str[i] === 'z') {
output.push(0, 0, 0, 0);
i++;
continue;
}

const remaining = str.length - i;
const chunkLen = Math.min(5, remaining);
if (chunkLen === 1) break; // trailing single char from truncated input; ignore it

let padded = str.slice(i, i + chunkLen);
while (padded.length < 5) padded += 'u';

let value = 0;
for (let j = 0; j < 5; j++) {
const digit = padded.charCodeAt(j) - 33;
if (digit < 0 || digit > 84) return null;
value = value * 85 + digit;
}

if (chunkLen === 5 && value > 0xffffffff) return null;

// Use >>> 0 to simulate uint32 overflow for padded groups
const v = value >>> 0;
const numBytes = chunkLen === 5 ? 4 : chunkLen - 1;
if (numBytes >= 1) output.push((v >>> 24) & 0xff);
if (numBytes >= 2) output.push((v >>> 16) & 0xff);
if (numBytes >= 3) output.push((v >>> 8) & 0xff);
if (numBytes >= 4) output.push(v & 0xff);

i += chunkLen;
}

return new Uint8Array(output);
}
56 changes: 56 additions & 0 deletions lib/utils/compression.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import * as pako from 'pako';

/**
* Inflate compressed data with support for partial/truncated streams.
* Uses Z_SYNC_FLUSH to extract as much data as possible, even from
* incomplete deflate streams. Captures output via onData callback.
*
* @param data - The compressed input bytes
* @param raw - If true, use raw deflate (no zlib header); if false, expect zlib header
* @returns The decompressed bytes, or undefined if no output was produced
*/
export function inflateData(
data: Uint8Array,
raw: boolean,
): Uint8Array | undefined {
const chunks: Uint8Array[] = [];
const inflator = new pako.Inflate({ windowBits: raw ? -15 : 15 });
inflator.onData = (chunk: Uint8Array) => {
chunks.push(chunk);
};
inflator.push(data, 2); // Z_SYNC_FLUSH

if (chunks.length === 0) return undefined;
if (chunks.length === 1) return chunks[0];

const totalLen = chunks.reduce((sum, c) => sum + c.length, 0);
const result = new Uint8Array(totalLen);
let offset = 0;
for (const chunk of chunks) {
result.set(chunk, offset);
offset += chunk.length;
}
return result;
}

/**
* Decode a base64 or base64url string to Uint8Array.
* Handles missing padding and converts base64url chars (- and _) to
* standard base64 chars (+ and /), matching Buffer.from(str, 'base64') behavior.
*
* @param base64 - The base64 or base64url encoded string
* @returns The decoded bytes
*/
export function base64ToUint8Array(base64: string): Uint8Array {
const cleaned = base64
.replace(/-/g, '+')
.replace(/_/g, '/')
.replace(/[^A-Za-z0-9+/]/g, '');
const padded = cleaned + '='.repeat((4 - (cleaned.length % 4)) % 4);
const binary = atob(padded);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes;
}
98 changes: 52 additions & 46 deletions lib/utils/miam.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import * as Base85 from 'base85';
import * as zlib from 'minizlib';
import { Buffer } from 'node:buffer';
import { ascii85Decode } from './ascii85';
import { inflateData } from './compression';

const textDecoder = new TextDecoder();
const textEncoder = new TextEncoder();

enum MIAMVersion {
V1 = 1,
Expand Down Expand Up @@ -155,34 +157,41 @@ export class MIAMCoreUtils {
};
}

let hdr = Base85.decode('<~' + rawHdr + '~>', 'ascii85');
let hdr = ascii85Decode('<~' + rawHdr + '~>');
if (!hdr || hdr.length < hpad) {
return {
decoded: false,
error: 'Ascii85 decode failed for MIAM message header',
};
}

let body: Buffer | undefined = undefined;
let body: Uint8Array | undefined = undefined;

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

body = Base85.decode('<~' + rawBody + '~>', 'ascii85') || undefined;
if (body && body.length >= bpadValue) {
const decoded = ascii85Decode('<~' + rawBody + '~>');
if (decoded === null) {
return {
decoded: false,
error: 'Ascii85 decode failed for MIAM message body',
};
}
body = decoded;
if (body.length >= bpadValue) {
body = body.subarray(0, body.length - bpadValue);
}
} else if (bpad === '-') {
body = Buffer.from(rawBody);
body = textEncoder.encode(rawBody);
}
}

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

const version = hdr.readUInt8(0) & 0xf;
const pduType = (hdr.readUInt8(0) >> 4) & 0xf;
const version = hdr[0] & 0xf;
const pduType = (hdr[0] >> 4) & 0xf;

if (isMIAMVersion(version) && isMIAMCorePdu(pduType)) {
const versionPduHandler =
Expand Down Expand Up @@ -225,7 +234,7 @@ export class MIAMCoreUtils {
},
};

private static arincCrc16(buf: Buffer, seed?: number) {
private static arincCrc16(buf: Uint8Array, seed?: number) {
const crctable = [
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, 0x8108,
0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, 0x1231, 0x0210,
Expand Down Expand Up @@ -263,14 +272,14 @@ export class MIAMCoreUtils {
for (let i = 0; i < buf.length; i++) {
crc =
(((crc << 8) >>> 0) ^
crctable[(((crc >>> 8) ^ buf.readUInt8(i)) >>> 0) & 0xff]) >>>
crctable[(((crc >>> 8) ^ buf[i]) >>> 0) & 0xff]) >>>
0;
}

return crc & 0xffff;
}

private static arinc665Crc32(buf: Buffer, seed?: number) {
private static arinc665Crc32(buf: Uint8Array, seed?: number) {
const crctable = [
0x00000000, 0x04c11db7, 0x09823b6e, 0x0d4326d9, 0x130476dc, 0x17c56b6b,
0x1a864db2, 0x1e475005, 0x2608edb8, 0x22c9f00f, 0x2f8ad6d6, 0x2b4bcb61,
Expand Down Expand Up @@ -321,9 +330,7 @@ export class MIAMCoreUtils {

for (let i = 0; i < buf.length; i++) {
crc =
(((crc << 8) >>> 0) ^
crctable[((crc >>> 24) ^ buf.readUInt8(i)) >>> 0]) >>>
0;
(((crc << 8) >>> 0) ^ crctable[((crc >>> 24) ^ buf[i]) >>> 0]) >>> 0;
}

return crc;
Expand All @@ -347,8 +354,8 @@ export class MIAMCoreUtils {
version: MIAMVersion,
minHdrSize: number,
crcLen: number,
hdr: Buffer,
body?: Buffer,
hdr: Uint8Array,
body?: Uint8Array,
): PduDecodingResult {
if (hdr.length < minHdrSize) {
return {
Expand All @@ -369,7 +376,7 @@ export class MIAMCoreUtils {
let pduAppType: number = 0;
let pduAppId: string = '';
let pduCrc: number = 0;
let pduData: Buffer | null = null;
let pduData: Uint8Array | null = null;
let pduCrcIsOk: boolean = false;
let pduIsComplete: boolean = true;

Expand All @@ -380,8 +387,7 @@ export class MIAMCoreUtils {
let ackOptions: number = 0;

if (version === MIAMVersion.V1) {
pduSize =
(hdr.readUInt8(1) << 16) | (hdr.readUInt8(2) << 8) | hdr.readUInt8(3);
pduSize = (hdr[1] << 16) | (hdr[2] << 8) | hdr[3];

const msgSize = hdr.length + (body === undefined ? 0 : body.length);
if (pduSize > msgSize) {
Expand All @@ -392,20 +398,19 @@ export class MIAMCoreUtils {
}
hdr = hdr.subarray(4);

tail = hdr.subarray(0, 7).toString('ascii');
tail = textDecoder.decode(hdr.subarray(0, 7));
hdr = hdr.subarray(7);
} else if (version === MIAMVersion.V2) {
hdr = hdr.subarray(1);
}

msgNum = (hdr.readUInt8(0) >> 1) & 0x7f;
ackOptions = hdr.readUInt8(0) & 1;
msgNum = (hdr[0] >> 1) & 0x7f;
ackOptions = hdr[0] & 1;
hdr = hdr.subarray(1);

pduCompression =
((hdr.readUInt8(0) << 2) | ((hdr.readUInt8(1) >> 6) & 0x3)) & 0x7;
pduEncoding = (hdr.readUInt8(1) >> 4) & 0x3;
pduAppType = hdr.readUInt8(1) & 0xf;
pduCompression = ((hdr[0] << 2) | ((hdr[1] >> 6) & 0x3)) & 0x7;
pduEncoding = (hdr[1] >> 4) & 0x3;
pduAppType = hdr[1] & 0xf;
hdr = hdr.subarray(2);

let appIdLen;
Expand Down Expand Up @@ -440,17 +445,13 @@ export class MIAMCoreUtils {
};
}

pduAppId = hdr.subarray(0, appIdLen).toString('ascii');
pduAppId = textDecoder.decode(hdr.subarray(0, appIdLen));
hdr = hdr.subarray(appIdLen);

if (crcLen === 4) {
pduCrc =
(hdr.readUInt8(0) << 24) |
(hdr.readUInt8(1) << 16) |
(hdr.readUInt8(2) << 8) |
hdr.readUInt8(3); // crc
pduCrc = (hdr[0] << 24) | (hdr[1] << 16) | (hdr[2] << 8) | hdr[3]; // crc
} else if (crcLen === 2) {
pduCrc = (hdr.readUInt8(0) << 8) | hdr.readUInt8(1); // crc
pduCrc = (hdr[0] << 8) | hdr[1]; // crc
}
hdr = hdr.subarray(crcLen);

Expand All @@ -461,10 +462,12 @@ export class MIAMCoreUtils {
) >= 0
) {
try {
const decompress = new zlib.InflateRaw({});
decompress.write(body);
decompress.flush(zlib.constants.Z_SYNC_FLUSH);
pduData = decompress.read();
const inflated = inflateData(body, true);
if (inflated === undefined) {
pduErrors.push('Inflation produced no output for body');
} else {
pduData = inflated;
}
} catch (e) {
pduErrors.push('Inflation failed for body: ' + e);
}
Expand All @@ -483,9 +486,9 @@ export class MIAMCoreUtils {
if (pduData !== null) {
const crcAlgoHandlerByVersion: Record<
MIAMVersion,
(buf: Buffer, seed?: number) => number
(buf: Uint8Array, seed?: number) => number
> = {
[MIAMVersion.V1]: (buf: Buffer, seed?: number) => {
[MIAMVersion.V1]: (buf: Uint8Array, seed?: number) => {
return ~this.arinc665Crc32(buf, seed);
},
[MIAMVersion.V2]: this.arincCrc16,
Expand Down Expand Up @@ -537,12 +540,12 @@ export class MIAMCoreUtils {
label,
...(sublabel ? { sublabel } : {}),
...(mfi ? { mfi } : {}),
...(pduData ? { text: pduData.toString('ascii') } : {}),
...(pduData ? { text: textDecoder.decode(pduData) } : {}),
};
} else {
pdu.non_acars = {
appId: pduAppId,
...(pduData ? { text: pduData.toString('ascii') } : {}),
...(pduData ? { text: textDecoder.decode(pduData) } : {}),
};
}

Expand All @@ -556,10 +559,13 @@ export class MIAMCoreUtils {

static VersionPduHandlerTable: Record<
MIAMVersion,
Record<MIAMCorePdu, (hdr: Buffer, body?: Buffer) => PduDecodingResult>
Record<
MIAMCorePdu,
(hdr: Uint8Array, body?: Uint8Array) => PduDecodingResult
>
> = {
[MIAMVersion.V1]: {
[MIAMCorePdu.Data]: (hdr: Buffer, body?: Buffer) => {
[MIAMCorePdu.Data]: (hdr: Uint8Array, body?: Uint8Array) => {
return this.corePduDataHandler(
MIAMVersion.V1,
20,
Expand All @@ -579,7 +585,7 @@ export class MIAMCoreUtils {
},
},
[MIAMVersion.V2]: {
[MIAMCorePdu.Data]: (hdr: Buffer, body?: Buffer) => {
[MIAMCorePdu.Data]: (hdr: Uint8Array, body?: Uint8Array) => {
return this.corePduDataHandler(
MIAMVersion.V2,
7,
Expand Down
Loading
Loading