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
55 changes: 55 additions & 0 deletions lib/parser-normalize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
export type ParsedAttributeVisitor = (
attributeKey: string,
attributeValue: unknown,
attributes: Record<string, unknown>,
path: string[]
) => void;

export function visitElementAttributes(
input: unknown,
visitor: ParsedAttributeVisitor,
path: string[] = []
): void {
if (Array.isArray(input)) {
for (const [index, value] of input.entries()) {
visitElementAttributes(value, visitor, [...path, String(index)]);
}

return;
}

if (!input || typeof input !== 'object') {
return;
}

const record = input as Record<string, unknown>;
for (const [key, value] of Object.entries(record)) {
if (key === '_attributes' && value && typeof value === 'object' && !Array.isArray(value)) {
const attributes = value as Record<string, unknown>;

for (const [attributeKey, attributeValue] of Object.entries(attributes)) {
visitor(attributeKey, attributeValue, attributes, [...path, key]);
visitElementAttributes(attributes[attributeKey], visitor, [...path, key, attributeKey]);
}

continue;
}

visitElementAttributes(value, visitor, [...path, key]);
}
}

export function normalizeBooleanAttributeValues(input: unknown): void {
visitElementAttributes(input, (attributeKey, attributeValue, attributes) => {
if (typeof attributeValue !== 'string') {
return;
}

const normalized = attributeValue.toLowerCase();
if (normalized === 'true') {
attributes[attributeKey] = true;
} else if (normalized === 'false') {
attributes[attributeKey] = false;
}
});
}
7 changes: 6 additions & 1 deletion lib/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import {
InputFeature,
} from './types/feature.js';
import { normalizeBooleanAttributeValues } from './parser-normalize.js';
import JSONCoT, { Detail } from './types/types.js'
import CoT from './cot.js';
import type { CoTOptions } from './cot.js';
Expand Down Expand Up @@ -157,8 +158,11 @@
raw: Buffer | string,
opts: CoTOptions = {}
): CoT {
const parsed = xml2js(String(raw), { compact: true }) as Static<typeof JSONCoT>;
normalizeBooleanAttributeValues(parsed);

const cot = new CoT(
xml2js(String(raw), { compact: true }) as Static<typeof JSONCoT>,
parsed,
opts
);

Expand Down Expand Up @@ -193,7 +197,7 @@

// The spread operator is important to make sure the delete doesn't modify the underlying detail object
const detail = { ...cot.raw.event.detail };
const msg: any = {

Check warning on line 200 in lib/parser.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected any. Specify a different type

Check warning on line 200 in lib/parser.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected any. Specify a different type
cotEvent: {
...cot.raw.event._attributes,
sendTime: new Date(cot.raw.event._attributes.time).getTime(),
Expand Down Expand Up @@ -240,15 +244,16 @@
const ProtoMessage = RootMessage.lookupType(`atakmap.commoncommo.protobuf.v${version}.TakMessage`)

// TODO Type this
const msg: any = ProtoMessage.decode(raw);

Check warning on line 247 in lib/parser.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected any. Specify a different type

Check warning on line 247 in lib/parser.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected any. Specify a different type

if (!msg.cotEvent) throw new Err(400, null, 'No cotEvent Data');

const detail: Record<string, any> = {};

Check warning on line 251 in lib/parser.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected any. Specify a different type

Check warning on line 251 in lib/parser.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected any. Specify a different type
const metadata: Record<string, unknown> = {};
for (const key in msg.cotEvent.detail) {
if (key === 'xmlDetail') {
const parsed: any = xml2js(`<detail>${msg.cotEvent.detail.xmlDetail}</detail>`, { compact: true });

Check warning on line 255 in lib/parser.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected any. Specify a different type

Check warning on line 255 in lib/parser.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected any. Specify a different type
normalizeBooleanAttributeValues(parsed);
Object.assign(detail, parsed.detail);

if (detail.metadata) {
Expand Down
18 changes: 18 additions & 0 deletions test/validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,21 @@ test('await CoTParser.from_xml - Invalid', async (t) => {

t.end();
});

test('await CoTParser.from_xml - Case-insensitive boolean attributes', async (t) => {
const cot = await CoTParser.from_xml(`
<event version="2.0" uid="case-insensitive-bool" type="u-d-f" time="2026-03-26T00:00:00Z" start="2026-03-26T00:00:00Z" stale="2026-03-27T00:00:00Z" how="h-g-i-g-o">
<point lat="0" lon="0" hae="0" ce="9999999.0" le="9999999.0"/>
<detail>
<contact callsign="Test"/>
<labels_on value="False"/>
</detail>
</event>`);

t.equal(cot.raw.event.detail?.labels_on?._attributes?.value, false, 'boolean attributes are normalized before validation');

const feature = await CoTParser.to_geojson(cot);
t.equal(feature.properties.labels, false, 'normalized boolean attributes remain booleans in GeoJSON output');

t.end();
});
Loading