@aeroswarm/codec 0.1.11 → 0.1.12

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/dist/index.js CHANGED
@@ -116,11 +116,15 @@ function parseBits(s) {
116
116
  }
117
117
  return out;
118
118
  }
119
+ function repetitionBytes(body, fallback) {
120
+ if (/<BDS[\s/>]/.test(body)) return 8;
121
+ const m = body.match(/<Fixed\s+length="(\d+)"/);
122
+ return m ? parseInt(m[1]) : fallback;
123
+ }
119
124
  function parseRepetitive(s) {
120
125
  const inner = firstContent(s, "Repetitive");
121
126
  const repAttr = inner.match(/<Repetitive\s+rep="(\d+)"/);
122
- const m = inner.match(/<Fixed\s+length="(\d+)"/);
123
- return { type: "repetitive", repBytes: repAttr ? parseInt(repAttr[1]) : 1, itemBytes: m ? parseInt(m[1]) : 0 };
127
+ return { type: "repetitive", repBytes: repAttr ? parseInt(repAttr[1]) : 1, itemBytes: repetitionBytes(inner, 0) };
124
128
  }
125
129
  function parseCompound(s) {
126
130
  const inner = firstContent(s, "Compound");
@@ -139,7 +143,7 @@ function parseCompound(s) {
139
143
  subs.push({ type: "variable", chunkBytes: first ? parseInt(first[1]) : 1 });
140
144
  } else if (child.tag === "Repetitive") {
141
145
  const repAttr = child.open.match(/rep="(\d+)"/);
142
- subs.push({ type: "repetitive", repBytes: repAttr ? parseInt(repAttr[1]) : 1, itemBytes: first ? parseInt(first[1]) : 1 });
146
+ subs.push({ type: "repetitive", repBytes: repAttr ? parseInt(repAttr[1]) : 1, itemBytes: repetitionBytes(child.body, 1) });
143
147
  } else {
144
148
  subs.push({ type: "explicit" });
145
149
  }
@@ -188,23 +192,23 @@ function parseFspec(buf, start = 0) {
188
192
  return { bits, end: i };
189
193
  }
190
194
  function skipItem(buf, off, def) {
191
- if (off >= buf.length) return buf.length;
195
+ if (off >= buf.length) return buf.length + 1;
192
196
  switch (def.type) {
193
197
  case "fixed":
194
- return Math.min(off + def.bytes, buf.length);
198
+ return off + def.bytes;
195
199
  case "variable": {
196
200
  while (off + def.chunkBytes <= buf.length) {
197
201
  const last = buf[off + def.chunkBytes - 1];
198
202
  off += def.chunkBytes;
199
203
  if ((last & 1) === 0) break;
200
204
  }
201
- return Math.min(off, buf.length);
205
+ return off;
202
206
  }
203
207
  case "repetitive": {
204
- if (off + def.repBytes > buf.length) return buf.length;
208
+ if (off + def.repBytes > buf.length) return buf.length + 1;
205
209
  let rep = 0;
206
210
  for (let i = 0; i < def.repBytes; i++) rep = rep << 8 | buf[off + i];
207
- return Math.min(off + def.repBytes + rep * def.itemBytes, buf.length);
211
+ return off + def.repBytes + rep * def.itemBytes;
208
212
  }
209
213
  case "compound": {
210
214
  const presentIdxs = [];
@@ -222,13 +226,12 @@ function skipItem(buf, off, def) {
222
226
  if (idx >= def.subs.length) break;
223
227
  off = skipItem(buf, off, def.subs[idx]);
224
228
  }
225
- return Math.min(off, buf.length);
229
+ return off;
226
230
  }
227
231
  case "explicit": {
228
- if (off >= buf.length) return off;
232
+ if (off >= buf.length) return buf.length + 1;
229
233
  const len = buf[off];
230
- off++;
231
- return Math.min(off + len - 1, buf.length);
234
+ return off + Math.max(len, 1);
232
235
  }
233
236
  }
234
237
  }
@@ -238,17 +241,18 @@ function decodeBlock(block, def, onField) {
238
241
  const uapMap = new Map(def.uap.map((e) => [e.bit, e.id]));
239
242
  for (let i = 0; i < bits.length; i++) {
240
243
  if (!bits[i]) continue;
241
- if (off >= block.length) return { end: block.length, complete: true };
244
+ if (off >= block.length) return { end: block.length, complete: true, overrun: true };
242
245
  const uapPos = i + Math.floor(i / 7);
243
246
  const id = uapMap.get(uapPos);
244
- if (id === void 0) return { end: off, complete: false };
247
+ if (id === void 0) return { end: off, complete: false, overrun: false };
245
248
  const itemDef = def.items.get(id);
246
- if (!itemDef) return { end: off, complete: false };
249
+ if (!itemDef) return { end: off, complete: false, overrun: false };
247
250
  const start = off;
248
251
  off = skipItem(block, off, itemDef);
249
- onField(id, block.slice(start, off), itemDef);
252
+ onField(id, block.slice(start, Math.min(off, block.length)), itemDef);
253
+ if (off > block.length) return { end: block.length, complete: true, overrun: true };
250
254
  }
251
- return { end: off, complete: true };
255
+ return { end: off, complete: true, overrun: false };
252
256
  }
253
257
 
254
258
  // src/surveillance/asterix/fields.ts
@@ -936,12 +940,12 @@ function parseAsterixBlocks(buf, defaultEdition) {
936
940
  for (const edition of toTry) {
937
941
  const def = loadDef(cat, edition);
938
942
  if (!def) continue;
939
- const { all, consumed } = decodeRecords(block, def, cat);
943
+ const { all, consumed, full } = decodeRecords(block, def, cat);
940
944
  const first = all[0];
941
945
  if (!first) continue;
942
946
  const score = plausibilityScore(first);
943
- if (!best || score > best.score) {
944
- best = { edition, all, consumed, score };
947
+ if (!best || full && !best.full || full === best.full && score > best.score) {
948
+ best = { edition, all, consumed, score, full };
945
949
  }
946
950
  }
947
951
  blocks.push({
@@ -949,15 +953,16 @@ function parseAsterixBlocks(buf, defaultEdition) {
949
953
  edition: best?.edition ?? null,
950
954
  length: block.length,
951
955
  consumed: best?.consumed ?? 0,
956
+ clean: best?.full ?? false,
952
957
  records: best?.all ?? []
953
958
  });
954
959
  }
955
960
  return blocks;
956
961
  }
957
962
  function decodeRecords(block, def, cat) {
958
- const out = [];
963
+ const records = [];
959
964
  let pos = 0;
960
- let consumed = 0;
965
+ let clean = false;
961
966
  while (pos < block.length) {
962
967
  const fields = {};
963
968
  let result;
@@ -972,13 +977,19 @@ function decodeRecords(block, def, cat) {
972
977
  break;
973
978
  }
974
979
  if (!isPlausible(fields)) break;
975
- if (out.length > 0 && Object.keys(fields).length === 0) break;
976
- out.push(fields);
977
- if (!result.complete || result.end <= 0) break;
980
+ if (result.overrun && records.length > 0) break;
981
+ records.push({ fields, end: pos + result.end });
982
+ if (!result.complete || result.end <= 0 || result.overrun) break;
978
983
  pos += result.end;
979
- consumed = pos;
984
+ clean = pos >= block.length;
985
+ }
986
+ if (clean) {
987
+ const kept = records.filter((r, i) => i === 0 || Object.keys(r.fields).length > 0).map((r) => r.fields);
988
+ return { all: kept, consumed: block.length, full: true };
980
989
  }
981
- return { all: out, consumed };
990
+ const firstEmpty = records.findIndex((r, i) => i > 0 && Object.keys(r.fields).length === 0);
991
+ const usable = firstEmpty === -1 ? records : records.slice(0, firstEmpty);
992
+ return { all: usable.map((r) => r.fields), consumed: usable.length > 0 ? usable[usable.length - 1].end : 0, full: false };
982
993
  }
983
994
  function isPlausible(f) {
984
995
  if (f.lat !== void 0 && (f.lat < -90 || f.lat > 90)) return false;
@@ -1333,6 +1344,7 @@ export {
1333
1344
  decodeMlatFrame,
1334
1345
  parseAftn,
1335
1346
  parseAsterix,
1347
+ parseAsterixBlocks,
1336
1348
  resolveAsterixAltitudeFt,
1337
1349
  resolveAsterixPosition,
1338
1350
  splitAftnStream
@@ -4,16 +4,21 @@ export declare function parseFspec(buf: Buffer, start?: number): {
4
4
  bits: boolean[];
5
5
  end: number;
6
6
  };
7
- /** Advance offset past one DataItem, returning new offset. */
7
+ /**
8
+ * Advance offset past one DataItem, returning new offset. The result is NOT clamped to the buffer: an offset past
9
+ * `buf.length` means the item needs more octets than there are (an overrun), which `decodeBlock` reports.
10
+ */
8
11
  export declare function skipItem(buf: Buffer, off: number, def: ItemDef): number;
9
12
  /**
10
13
  * Decode ONE record at the start of `block` (a data block can carry many records back to back, each with its own
11
14
  * FSPEC). Calls `onField(id, bytes, itemDef)` for each present DataItem. Returns where the record ended, so the caller
12
15
  * can go on with the next one, and whether that position can be trusted: `complete` is false when an item the
13
- * definition does not know stopped the decode (the length of what follows is then unknown).
16
+ * definition does not know stopped the decode (the length of what follows is then unknown); `overrun` is true when an
17
+ * item needed more octets than the block has (the record is cut short or an item was sized wrongly).
14
18
  */
15
19
  export declare function decodeBlock(block: Buffer, def: AsterixDef, onField: (id: string, data: Buffer, itemDef: ItemDef) => void): {
16
20
  end: number;
17
21
  complete: boolean;
22
+ overrun: boolean;
18
23
  };
19
24
  //# sourceMappingURL=decoder.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"decoder.d.ts","sourceRoot":"","sources":["../../../src/surveillance/asterix/decoder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAI3D,sFAAsF;AACtF,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,SAAI,GAAG;IAAE,IAAI,EAAE,OAAO,EAAE,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAUnF;AAID,8DAA8D;AAC9D,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,GAAG,MAAM,CAiDvE;AAID;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,UAAU,EACf,OAAO,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,KAAK,IAAI,GAC5D;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,CA0BpC"}
1
+ {"version":3,"file":"decoder.d.ts","sourceRoot":"","sources":["../../../src/surveillance/asterix/decoder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAI3D,sFAAsF;AACtF,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,SAAI,GAAG;IAAE,IAAI,EAAE,OAAO,EAAE,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAUnF;AAID;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,GAAG,MAAM,CAiDvE;AAID;;;;;;GAMG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,UAAU,EACf,OAAO,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,KAAK,IAAI,GAC5D;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CA2BtD"}
@@ -1,4 +1,5 @@
1
- export { parseAsterix } from './parser.js';
1
+ export { parseAsterix, parseAsterixBlocks } from './parser.js';
2
+ export type { AsterixBlockInfo } from './parser.js';
2
3
  export { resolveAsterixPosition, resolveAsterixAltitudeFt } from './resolvers.js';
3
4
  export type { RadarSite } from './resolvers.js';
4
5
  export type { AsterixRecord, AsterixFields } from './types.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/surveillance/asterix/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,sBAAsB,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAC;AAClF,YAAY,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/surveillance/asterix/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAC/D,YAAY,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EAAE,sBAAsB,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAC;AAClF,YAAY,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC"}
@@ -19,8 +19,10 @@ export interface AsterixBlockInfo {
19
19
  edition: string | null;
20
20
  /** Octets of the block after its 3-octet header. */
21
21
  length: number;
22
- /** Octets taken up by the decoded records. Less than `length` means an item was sized wrongly or the tail was not understood. */
22
+ /** Octets taken up by the decoded records. */
23
23
  consumed: number;
24
+ /** The records tile the block exactly: no octets left over, none missing, no item that ran past the end. False points at an item sized wrongly or a tail that was not understood. */
25
+ clean: boolean;
24
26
  records: AsterixFields[];
25
27
  }
26
28
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../../src/surveillance/asterix/parser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAsB/D;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAC1B,GAAG,EAAE,MAAM,EACX,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACtC,aAAa,EAAE,CAOjB;AAED,iJAAiJ;AACjJ,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,oDAAoD;IACpD,MAAM,EAAE,MAAM,CAAC;IACf,iIAAiI;IACjI,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,aAAa,EAAE,CAAC;CAC1B;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,MAAM,EACX,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACtC,gBAAgB,EAAE,CAwCpB"}
1
+ {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../../src/surveillance/asterix/parser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAsB/D;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAC1B,GAAG,EAAE,MAAM,EACX,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACtC,aAAa,EAAE,CAOjB;AAED,iJAAiJ;AACjJ,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,oDAAoD;IACpD,MAAM,EAAE,MAAM,CAAC;IACf,8CAA8C;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,qLAAqL;IACrL,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,EAAE,aAAa,EAAE,CAAC;CAC1B;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,MAAM,EACX,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACtC,gBAAgB,EAAE,CA2CpB"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=repetitive.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repetitive.test.d.ts","sourceRoot":"","sources":["../../../src/surveillance/asterix/repetitive.test.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aeroswarm/codec",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",