Summary
FlightPlanUtils.processFlightPlan iterates pairs from index 1 with i += 2, assuming data always has an odd length (header + N pairs). When data.length is even — typical for a truncated H1 FPN/POS route segment where the final fpei key has no value — the last iteration reads value = data[i + 1] = undefined. Several case branches then call methods on it and throw:
'FP' → ResultFormatter.flightPlan(decodeResult, value.trim()) — undefined.trim() throws.
'D' / 'A' / 'AP' → FlightPlanUtils.addProcedure(...) → value.split('.') throws.
'CR' → addCompanyRoute(...) → value.split('.') throws.
'F' → addRoute(...) → value.split('.') throws.
'R' → addRunway(...) → value.length throws.
The dispatch loop in MessageDecoder.decode does not wrap individual plugins in try/catch (same dispatch shape called out in #459), so the exception propagates out of the plugin and aborts decoding of the entire message — not just the bad fpei.
For the silent-corruption branches ('AA', 'DA' → pass undefined into ResultFormatter.arrivalAirport / departureAirport), the function happily produces a "decoded" result with the airport ICAO stored as the literal string undefined.
This issue is a split-out of the second "Related observation" in #453, per the issue author's note ("Happy to split those out if helpful"). #453 itself remains scoped to processDateCode; the fix here belongs in a different function and is independent of that one.
Affected code
lib/utils/flight_plan_utils.ts:16-65
public static processFlightPlan(
decodeResult: DecodeResult,
data: string[],
): boolean {
let allKnownFields = FlightPlanUtils.parseHeader(decodeResult, data[0]);
for (let i = 1; i < data.length; i += 2) {
const fpei = data[i];
const value = data[i + 1]; // ← undefined when data.length is even
switch (fpei) {
case 'A': FlightPlanUtils.addProcedure(decodeResult, value, 'arrival'); break;
case 'AA': addArrivalAirport(decodeResult, value); break;
case 'AP': FlightPlanUtils.addProcedure(decodeResult, value, 'approach'); break;
case 'CR': addCompanyRoute(decodeResult, value); break;
case 'D': FlightPlanUtils.addProcedure(decodeResult, value, 'departure'); break;
case 'DA': addDepartureAirport(decodeResult, value); break;
case 'F': addRoute(decodeResult, value); break;
case 'FP': ResultFormatter.flightPlan(decodeResult, value.trim()); break; // throws
case 'R': addRunway(decodeResult, value); break; // throws
default: ...
}
}
return allKnownFields;
}
processFlightPlan is invoked from arinc_702_helper.ts:134 with fields[i].split(':') — any H1 FPN/POS message whose route segment ends mid-key (truncated stream, dropped tail byte, etc.) hits this path.
Reproduction
const r: DecodeResult = ...;
// 1. 'FP' with no value — throws TypeError.
FlightPlanUtils.processFlightPlan(r, ['RP', 'DA', 'KPHL', 'AA', 'KTPA', 'FP']);
// → TypeError: Cannot read properties of undefined (reading 'trim')
// 2. 'CR' with no value — throws TypeError (addCompanyRoute → split('.')).
FlightPlanUtils.processFlightPlan(r, ['RP', 'DA', 'KPHL', 'AA', 'KTPA', 'CR']);
// → TypeError: Cannot read properties of undefined (reading 'split')
// 3. 'R' with no value — throws TypeError (addRunway → value.length).
FlightPlanUtils.processFlightPlan(r, ['RP', 'DA', 'KPHL', 'AA', 'KTPA', 'R']);
// → TypeError: Cannot read properties of undefined (reading 'length')
// 4. 'AA' with no value — silently corrupts departure/arrival airport ICAO.
FlightPlanUtils.processFlightPlan(r, ['RP', 'DA', 'KPHL', 'AA']);
// → no throw, but r.raw.arrival_icao === undefined and
// the formatted items list contains a "Destination: undefined" entry.
Even-length data arrays arise naturally any time the upstream .split(':') produces an odd number of fields after the header — i.e., the final :KEY had no trailing :VALUE. Real-world ACARS streams contain truncated messages of exactly this shape.
Why it matters
A single truncated H1 FPN/POS message currently aborts the whole decode (because MessageDecoder.decode does not isolate plugin throws), so a pipeline processing a stream of real ACARS messages stops on the first malformed packet of this shape — not just for the bad segment. Same failure mode as #459, in a different file.
The silent-corruption branches (AA, DA) are arguably worse: they produce a "decoded" result with airport ICAOs written as the literal string undefined, which then flows into downstream airport lookups, route displays, and storage.
Suggested fix
Bail out of the iteration when the pair is incomplete — either skip the trailing orphan key or capture it as unknown:
for (let i = 1; i < data.length; i += 2) {
const fpei = data[i];
if (i + 1 >= data.length) {
// Trailing key with no value — preserve as unknown rather than throwing
// or silently writing `undefined` into typed fields.
if (allKnownFields) {
decodeResult.remaining.text = '';
allKnownFields = false;
}
decodeResult.remaining.text += `:${fpei}`;
decodeResult.decoder.decodeLevel = 'partial';
break;
}
const value = data[i + 1];
// ...existing switch...
}
Wrapping the dispatch loop body in try { ... } catch (e) { /* mark partial */ } (or wrapping entry.plugin.decode in MessageDecoder.decode — same shape as the alternative noted in #459) would also stop malformed input from killing the whole decode and would harden every fpei branch at once.
Test coverage
lib/utils/flight_plan_utils.test.ts does not exist. A handful of truncated-input cases asserting the call does not throw, and that the orphan key is captured into remaining.text rather than silently written into raw.arrival_icao, would lock the fix in.
Related
Summary
FlightPlanUtils.processFlightPlaniterates pairs from index 1 withi += 2, assumingdataalways has an odd length (header + N pairs). Whendata.lengthis even — typical for a truncated H1 FPN/POS route segment where the finalfpeikey has no value — the last iteration readsvalue = data[i + 1] = undefined. Severalcasebranches then call methods on it and throw:'FP'→ResultFormatter.flightPlan(decodeResult, value.trim())—undefined.trim()throws.'D'/'A'/'AP'→FlightPlanUtils.addProcedure(...)→value.split('.')throws.'CR'→addCompanyRoute(...)→value.split('.')throws.'F'→addRoute(...)→value.split('.')throws.'R'→addRunway(...)→value.lengththrows.The dispatch loop in
MessageDecoder.decodedoes not wrap individual plugins intry/catch(same dispatch shape called out in #459), so the exception propagates out of the plugin and aborts decoding of the entire message — not just the bad fpei.For the silent-corruption branches (
'AA','DA'→ passundefinedintoResultFormatter.arrivalAirport/departureAirport), the function happily produces a "decoded" result with the airport ICAO stored as the literal stringundefined.This issue is a split-out of the second "Related observation" in #453, per the issue author's note ("Happy to split those out if helpful"). #453 itself remains scoped to
processDateCode; the fix here belongs in a different function and is independent of that one.Affected code
lib/utils/flight_plan_utils.ts:16-65processFlightPlanis invoked fromarinc_702_helper.ts:134withfields[i].split(':')— any H1 FPN/POS message whose route segment ends mid-key (truncated stream, dropped tail byte, etc.) hits this path.Reproduction
Even-length
dataarrays arise naturally any time the upstream.split(':')produces an odd number of fields after the header — i.e., the final:KEYhad no trailing:VALUE. Real-world ACARS streams contain truncated messages of exactly this shape.Why it matters
A single truncated H1 FPN/POS message currently aborts the whole decode (because
MessageDecoder.decodedoes not isolate plugin throws), so a pipeline processing a stream of real ACARS messages stops on the first malformed packet of this shape — not just for the bad segment. Same failure mode as #459, in a different file.The silent-corruption branches (
AA,DA) are arguably worse: they produce a "decoded" result with airport ICAOs written as the literal stringundefined, which then flows into downstream airport lookups, route displays, and storage.Suggested fix
Bail out of the iteration when the pair is incomplete — either skip the trailing orphan key or capture it as unknown:
Wrapping the dispatch loop body in
try { ... } catch (e) { /* mark partial */ }(or wrappingentry.plugin.decodeinMessageDecoder.decode— same shape as the alternative noted in #459) would also stop malformed input from killing the whole decode and would harden every fpei branch at once.Test coverage
lib/utils/flight_plan_utils.test.tsdoes not exist. A handful of truncated-input cases asserting the call does not throw, and that the orphan key is captured intoremaining.textrather than silently written intoraw.arrival_icao, would lock the fix in.Related
Arinc702Helper.