140 lines
5.5 KiB
Markdown
140 lines
5.5 KiB
Markdown
# packet.js — Packet parsing
|
|
|
|
This project exposes lightweight types (`FieldType`, `Field`, `Segment`, `Dissected`, `BitField`)
|
|
and a `Reader`/`Writer` API to read/write binary packet data sequentially.
|
|
|
|
Below is a quick guide showing how to use the types together with `Reader` to
|
|
parse a packet and store parsed fields as a `Dissected` structure.
|
|
|
|
## Concepts
|
|
|
|
- `FieldType`: string constants describing how bytes should be interpreted (`UINT8`,
|
|
`UINT16_LE`, `BYTES`, `UTF8_STRING`, `BITS`, etc.).
|
|
- Field: describes a single named datum in a `Segment` (type, name, optional length
|
|
or bit definitions).
|
|
- `Segment`: a named collection of fields; segments form the `Dissected` result.
|
|
- `Dissected`: an array of `Segment` objects that represent the parsed packet.
|
|
- `Reader`: sequential reader that advances an offset and exposes methods
|
|
(`uint8`, `uint16`, `uint64`, `bytes`, `utf8String`, `cString`, `words`,
|
|
`dwords`, `qwords`, etc.) and performs bounds checking.
|
|
|
|
## Example: APRS (AX.25 UI-frame) parsing
|
|
|
|
APRS (Automatic Packet Reporting System) commonly rides on AX.25 UI-frames used
|
|
by amateur radio. AX.25 address blocks are fixed-width (7 bytes per address)
|
|
and the frame typically contains:
|
|
|
|
- Destination address (7 bytes)
|
|
- Source address (7 bytes)
|
|
- Optional digipeaters (n * 7 bytes)
|
|
- Control (1 byte, usually 0x03 for UI)
|
|
- PID (1 byte, usually 0xF0)
|
|
- Information field (UTF-8 text, remaining bytes) — APRS payload
|
|
|
|
Below is a minimal TypeScript example showing how to parse an AX.25 UI-frame
|
|
binary into a Dissected structure using `Reader`. The example assumes the
|
|
frame has a single destination, single source, no digipeaters, and a UTF-8 info
|
|
field for simplicity.
|
|
|
|
```ts
|
|
// Example segment description (informational only)
|
|
const ax25Segment = {
|
|
name: 'ax25_ui_frame',
|
|
fields: [
|
|
{ type: FieldType.BYTES, name: 'dst_addr', length: 7 },
|
|
{ type: FieldType.BYTES, name: 'src_addr', length: 7 },
|
|
{ type: FieldType.UINT8, name: 'control' },
|
|
{ type: FieldType.UINT8, name: 'pid' },
|
|
{ type: FieldType.UTF8_STRING, name: 'info', length: undefined } // rest of buffer
|
|
]
|
|
};
|
|
```
|
|
|
|
Parsing function (using `Reader`):
|
|
|
|
```ts
|
|
import { Reader } from './src/index';
|
|
import { FieldType, Dissected, Segment } from './src/types';
|
|
|
|
/**
|
|
* Parse a raw AX.25 UI-frame ArrayBuffer into a Dissected structure.
|
|
*/
|
|
function parseAX25UIFrame(buf: ArrayBuffer): Dissected {
|
|
const r = new Reader(buf); // defaults to LittleEndian where applicable
|
|
const seg: Segment = { name: 'ax25_ui_frame', fields: [] };
|
|
|
|
// destination address (7 bytes)
|
|
const dst = r.bytes(7);
|
|
seg.fields.push({ name: 'dst_addr', type: FieldType.BYTES, length: 7 });
|
|
// store raw data for convenience (optional) — store a slice of the
|
|
// underlying buffer for just this field rather than copying the entire buffer.
|
|
seg.data = dst.buffer.slice(dst.byteOffset, dst.byteOffset + dst.byteLength);
|
|
|
|
// source address (7 bytes)
|
|
const src = r.bytes(7);
|
|
seg.fields.push({ name: 'src_addr', type: FieldType.BYTES, length: 7 });
|
|
|
|
// control (1 byte)
|
|
const control = r.uint8();
|
|
seg.fields.push({ name: 'control', type: FieldType.UINT8 });
|
|
|
|
// pid (1 byte)
|
|
const pid = r.uint8();
|
|
seg.fields.push({ name: 'pid', type: FieldType.UINT8 });
|
|
|
|
// info: remaining bytes decoded as UTF-8
|
|
const info = r.utf8String(); // reads to end by default
|
|
seg.fields.push({ name: 'info', type: FieldType.UTF8_STRING });
|
|
|
|
// return a Dissected array containing the single segment
|
|
return [seg];
|
|
}
|
|
```
|
|
|
|
Notes on storing parsed values:
|
|
- The `Segment` and `Field` types describe the structure. You can store actual
|
|
parsed values by using the `Field.value` property or by keeping a parallel
|
|
`Map`/`Object` for the parsed values.
|
|
- In the example above, `seg.fields` declare the shape. To keep parsed values,
|
|
add a `values` property:
|
|
|
|
```ts
|
|
seg.values = {
|
|
dst_addr: new Uint8Array(dst),
|
|
src_addr: new Uint8Array(src),
|
|
control,
|
|
pid,
|
|
info
|
|
};
|
|
```
|
|
|
|
## Handling bit fields and arrays
|
|
- Use `FieldType.BITS` plus `Field.bits` for bit-level definitions. Read underlying
|
|
bytes with `bytes()` or `uint8()` and extract bits per `BitField` definitions.
|
|
-- For arrays (`WORDS`, `DWORDS`, `QWORDS`, `BYTES`) set `Field.length` and use the
|
|
corresponding `Reader` method (`words`, `dwords`, `qwords`, `bytes`). Note: the
|
|
`words`/`dwords`/`qwords` helpers create aligned copies and return typed arrays
|
|
that are safe to use on all platforms.
|
|
|
|
Helpers and test tips:
|
|
- Use `Reader.fromBytes()` / `Reader.fromString()` to construct readers from
|
|
convenience formats.
|
|
- Use `Writer.toBytes()` to obtain the written bytes; tests should prefer this
|
|
method instead of reaching into `Writer` internals.
|
|
|
|
## Best practices for radio parsing
|
|
- Always perform bounds checks (`Reader` does this internally via `checkBounds`).
|
|
- Validate control/PID bytes (AX.25 UI typically uses `0x03` and `0xF0`).
|
|
- When parsing addresses in AX.25, remember each address byte is 7-bit ASCII
|
|
shifted and the last address byte has the "end" flag; real-world parsing needs
|
|
address unpacking logic (not shown in the minimal example).
|
|
|
|
## Summary
|
|
- The provided types describe packet structure; `Reader` reads primitives
|
|
and advances offset safely.
|
|
- Compose `Segment` arrays (`Dissected`) while parsing to represent packet structure
|
|
and store parsed values next to their field descriptors for debugging,
|
|
display, or serialization.
|
|
- The APRS/AX.25 example demonstrates how a short radio protocol payload can be
|
|
parsed into a structured `Dissected` result suitable for display or further processing.
|