From 003b92f64a60af2f4681863f974a777bdc89ea2e Mon Sep 17 00:00:00 2001 From: VG31OP Date: Tue, 13 Oct 2026 16:57:55 +0530 Subject: [PATCH] fix(satellites): reject TLE pairs with mismatched catalog numbers (#906, #936) --- CHANGELOG.md | 7 ++++--- src/sources/tle.js | 15 +++++++++++---- src/tools/queries/space.test.mjs | 17 +++++++++++++++++ 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b24ac95ed..83b32baaa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -95,9 +95,10 @@ ## [Unreleased] -- Keep later satellites when a TLE catalog entry is missing an orbital line. - The parser skips the incomplete set and resumes at the next complete named - set, instead of losing the rest of the group (#906). +- Keep later satellites when a TLE catalog entry is missing an orbital line or + pairs lines with mismatched NORAD catalog numbers. The parser skips corrupt sets + and resumes at the next complete named set, instead of losing the rest of the + group (#906, #936). - Add an experimental ChatGPT/Codex OAuth option for cloud voice in Provider Settings. API-key voice remains the default. Local sign-in can start from diff --git a/src/sources/tle.js b/src/sources/tle.js index ac7aedc2c..8f97e4eda 100644 --- a/src/sources/tle.js +++ b/src/sources/tle.js @@ -13,7 +13,14 @@ export function parseTleText(text) { const name = lines[i]; const line1 = lines[i + 1]; const line2 = lines[i + 2]; - if (line1.startsWith('1 ') && line2.startsWith('2 ')) { + const cat1 = tleCatalogNumber(line1); + const cat2 = tleCatalogNumber(line2); + if ( + line1.startsWith('1 ') && + line2.startsWith('2 ') && + cat1 !== null && + cat1 === cat2 + ) { result.push({ name, line1, line2 }); i += 2; } @@ -21,8 +28,8 @@ export function parseTleText(text) { return result; } -/** The NORAD catalog number from TLE line 1, or null. */ -export function tleCatalogNumber(line1) { - const number = Number.parseInt(String(line1).slice(2, 7), 10); +/** The NORAD catalog number from a TLE line, or null. */ +export function tleCatalogNumber(line) { + const number = Number.parseInt(String(line).slice(2, 7), 10); return Number.isInteger(number) ? number : null; } diff --git a/src/tools/queries/space.test.mjs b/src/tools/queries/space.test.mjs index f6c2a8df8..2b2586b45 100644 --- a/src/tools/queries/space.test.mjs +++ b/src/tools/queries/space.test.mjs @@ -51,6 +51,23 @@ for (const missingLine of [1, 2]) { }); } +test('TLE parsing rejects pairs with mismatched catalog numbers and resumes', () => { + const text = `SAT A +1 25544U 98067A 24001.50000000 .00016717 00000-0 30270-3 0 9994 +2 48274 41.4697 175.8913 0005643 312.8370 141.6893 15.61287398150000 +SAT B +1 48274U 21035A 24001.50000000 .00020137 00000-0 24481-3 0 9993 +2 48274 41.4697 175.8913 0005643 312.8370 141.6893 15.61287398150000 +`; + const entries = parseTleText(text); + assert.deepEqual( + entries.map((entry) => entry.name), + ['SAT B'], + ); + assert.equal(tleCatalogNumber(entries[0].line1), 48274); + assert.equal(tleCatalogNumber(entries[0].line2), 48274); +}); + test('the next pass defaults to the ISS and reports times, peak and direction', async () => { const catalog = composeCatalog({ tools: coreTools,