Skip to content

Bug: FlightPlanUtils.processFlightPlan throws on trailing fpei with no value (split-out from #453 observation) #467

Description

@kevinelliott

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions