From 5b8324ed02477ea7314c9426a6b999c1ba0b43e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D1=82=D0=BE=D0=BD=20=D0=A2=D1=80=D0=BE=D1=88?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Fri, 2 Oct 2026 12:29:48 +0300 Subject: [PATCH] v3.13.1: expanded-registry pages try every command format The explorer's next-occurrence page (P2=03) is retried in the other command formats (ramPageFormats: the expanded codings first - the serving one first when it is expanded - and the compact command string last, because a compact page attempt can consume the page without delivering it) and the format that serves a page is kept for the following ones. Live 2026-10-02 (test-eSIM card): a compact page 2 answered 61B6 and the chained GET RESPONSE 6A86, after which the AA and AE 80 retries both saw the occurrence already gone; the AA-wrapped page returns 182 B with the remaining app instances' CE/CC/EA, while AE 80 is refused. A page that no format serves ends the listing. Only an explicit 6310 promise counts for the "(first page only)" flag (ramExpandedMore): a probe after a 61xx page that every format refuses is the card's final answer, so that card's ELF listing is no longer flagged (its expanded registry has three entries; the fourth package exists only in the compact listing - the ELF tag-list forms and per-AID queries are refused in every format). In Auto the explorer also queries entries the bulk listing left without expanded fields individually by AID (ramAppsMissingDetails, <= 6 packets, the compact command string carries the GET RESPONSE for 61xx). Docs: help EN/RU, repo AGENTS (v3.13.1 note). sw cache simple-v371. Tests: 667 Python + 794 frontend green. --- frontend/help-ru.html | 2 +- frontend/help.html | 2 +- frontend/index.html | 152 +++++++++++++++++++++++++++------- frontend/sw.js | 2 +- frontend/tests/ram.test.js | 28 ++++++- pyproject.toml | 2 +- pysim_simple_server/server.py | 2 +- 7 files changed, 153 insertions(+), 37 deletions(-) diff --git a/frontend/help-ru.html b/frontend/help-ru.html index 9d7763e..5c7a691 100644 --- a/frontend/help-ru.html +++ b/frontend/help-ru.html @@ -314,7 +314,7 @@

Кнопка Проверить в pySim сверяет собранный пакет с эталонной реализацией OtaDialectSms.encode_cmd. Кнопка Отправить на карту доставляет пакет через ENVELOPE SMS-PP-DOWNLOAD (при подключении к серверу). 2-байтовый префикс CPL передаётся, когда пакет зашифрован или несёт RC/CC/DS (входит в их входные данные), а также при конкатенации SMS; одиночное незащищённое SMS сохраняет форму pySim, начинающуюся с CHL (TS 31.115 Table 1 NOTE). Под полем пакета показывается его размер и число SMS; пакет, не помещающийся в одно SMS, отправляется конкатенированной загрузкой (TS 31.115 §4.3) — пакет делится на части SMS-пользовательских данных (первое SMS 132 октета, последующие по 134; первое дополнительно несёт информационные элементы конкатенации и CPI), и сегменты отправляются по порядку. Более 5 сегментов отклоняется: это практический предел буфера конкатенации карты. Полученный Proof of Receipt декодируется и показывается строкой статуса PoR (статус, TAR, счётчик, сырой PoR); статусное слово и данные ответа последней команды подставляются в подвкладку «Разбор R-APDU» (Remote APDU → «Разбор»), а успешный PoR увеличивает счётчик повторов (когда в SPI1 есть проверка счётчика — b5 b4 ≠ 00; без неё счётчик не увеличивается и не сохраняется) и очищает пакет.

3.2 RAM

-

Выполняет операции удалённого управления приложениями (Remote Application Management) как защищённые пакеты SCP80 через SMS-PP-DOWNLOAD ENVELOPE. Карта должна поддерживать SCP03 (AES или 3DES). Пресет карты со вкладки Карты обеспечивает ключи, таблицу TAR и счётчик (RAM использует ISD TAR пресета, по умолчанию 000000; SPI1 берётся из MSL этого TAR). Селектор Keyset рядом с выбором пресета указывает, из какого keyset берутся ключи и счётчик операции. Селектор недоступен, пока не выбран пресет карты. Кнопка запуска недоступна, пока не выбран пресет карты и один из его keysets. Селектор Формат фиксирует формат удалённой команды: Авто (определение) опрашивает read-only команду GET STATUS в компактном виде и в обоих expanded-кодированиях и оставляет первое, которое карта выполнила; Компактный отправляет голую C-APDU; Expanded (AA, определённая длина) / Expanded (AE 80, неопределённая длина) заворачивают её в шаблон Command Scripting из TS 102 226 §5.2.1 (на живой карте выполняются обе; шаблон, у которого заявленная длина не совпадает с телом, получает Bad format TLV — операция показывает это как ошибку шага). Запросы листингов и реестра следуют этому формату: компактные листинги несут сцепленный GET RESPONSE, а expanded-прогон отправляет те же запросы P2=00 обёрнутыми и без цепочки (форма эталонного загрузчика TCA; Le=’00’ возвращает листинг сразу), и запрос реестра, отклонённый картой в текущем формате, повторяется в остальных — отчёт называет тот, который сработал. Блок ресурсов карты также называет предполагаемый релиз ETSI карты (версия uicc.toolkit → REL-N, то же сопоставление, что и в анализе CAP; показывается, только если расширенный реестр вернул версии).

+

Выполняет операции удалённого управления приложениями (Remote Application Management) как защищённые пакеты SCP80 через SMS-PP-DOWNLOAD ENVELOPE. Карта должна поддерживать SCP03 (AES или 3DES). Пресет карты со вкладки Карты обеспечивает ключи, таблицу TAR и счётчик (RAM использует ISD TAR пресета, по умолчанию 000000; SPI1 берётся из MSL этого TAR). Селектор Keyset рядом с выбором пресета указывает, из какого keyset берутся ключи и счётчик операции. Селектор недоступен, пока не выбран пресет карты. Кнопка запуска недоступна, пока не выбран пресет карты и один из его keysets. Селектор Формат фиксирует формат удалённой команды: Авто (определение) опрашивает read-only команду GET STATUS в компактном виде и в обоих expanded-кодированиях и оставляет первое, которое карта выполнила; Компактный отправляет голую C-APDU; Expanded (AA, определённая длина) / Expanded (AE 80, неопределённая длина) заворачивают её в шаблон Command Scripting из TS 102 226 §5.2.1 (на живой карте выполняются обе; шаблон, у которого заявленная длина не совпадает с телом, получает Bad format TLV — операция показывает это как ошибку шага). Запросы листингов и реестра следуют этому формату: компактные листинги несут сцепленный GET RESPONSE, а expanded-прогон отправляет те же запросы P2=00 обёрнутыми и без цепочки (форма эталонного загрузчика TCA; Le=’00’ возвращает листинг сразу), и запрос реестра, отклонённый картой в текущем формате, повторяется в остальных — отчёт называет тот, который сработал. Страница следующего вхождения (P2=03), отклонённая форматом, тоже повторяется в остальных форматах — сначала expanded-кодирования (первым то, что обслужило предыдущую страницу, если оно expanded), компактная командная строка последней: компактная попытка страницы может израсходовать страницу, не доставив её (карта отвечает 61xx, а сцепленный GET RESPONSE отклоняется — все последующие форматы видят вхождение уже использованным) — некоторые карты отдают expanded-страницу только в шаблоне AA — и пометка (только первая страница) ставится, только если карта обещала продолжение (6310), а ни один формат не смог его получить; в режиме Авто проводник также опрашивает по AID записи, оставшиеся без деталей (несколько дополнительных пакетов). Блок ресурсов карты также называет предполагаемый релиз ETSI карты (версия uicc.toolkit → REL-N, то же сопоставление, что и в анализе CAP; показывается, только если расширенный реестр вернул версии).

Операции

diff --git a/frontend/help.html b/frontend/help.html index 73fe946..f872247 100644 --- a/frontend/help.html +++ b/frontend/help.html @@ -313,7 +313,7 @@

A “Verify vs pySim” button cross-checks the assembled packet against pySim’s reference OtaDialectSms.encode_cmd. A “Send to Card” button delivers it via SMS-PP-DOWNLOAD ENVELOPE (when connected to the server). The 2-byte CPL prefix is transmitted whenever the packet is ciphered or carries RC/CC/DS (it is part of their input) and whenever the packet is concatenated; a single unprotected SMS keeps pySim’s CHL-first form (TS 31.115 Table 1 NOTE). Under the packet field the app reports its size and how many SMS it takes; a packet that does not fit one SMS is sent as a concatenated download (TS 31.115 §4.3) — the packet is split into SMS user-data parts (first SM 132 octets, following ones 134; the first one additionally carries the concatenation and CPI information elements) and the segments are sent in order. More than 5 segments is refused, that being the practical limit of the card’s concatenation buffer. The returned Proof of Receipt is decoded and shown as a PoR status line (status, TAR, counter, raw PoR); the last command’s status word and response data are filled into the R-APDU parser panel, and a successful PoR advances the replay counter (when SPI1 carries a counter check — b5 b4 ≠ 00; without one no counter is advanced or saved) and clears the packet. A low-counter verdict (cntr_low, or a warning ENVELOPE answer that still carries such a PoR) is not recoverable by retrying: every SCP80/RAM operation stops immediately. The card never reports its counter (the PoR’s CNTR field is a copy of the packet’s), so the notice offers a bounded Sync counter probe — a read-only command with increasing counter values (doubling steps, a 32-bit ceiling, at most 40 packets) until the card accepts one, then accepted+1 is stored — and a Go to preset button for raising the counter by hand; nothing else is written automatically.

3.2 RAM

-

Delivers Remote Application Management operations as SCP80 secured packets via SMS-PP-DOWNLOAD ENVELOPE. The card must support SCP03 (AES or 3DES). A saved card preset from the Cards tab provides the keys, the TAR table and the counter (RAM uses the preset's ISD TAR, 000000 by default; the SPI1 is derived from that TAR's MSL). The Keyset selector next to the preset selector picks the keyset whose keys and counter the operation uses. It is disabled until a card preset is selected. The start button stays disabled until a card preset and one of its keysets are selected. The Format selector pins the remote command format: Auto (detect) probes the read-only GET STATUS command compact and in both expanded codings and keeps the first that the card executes, Compact sends the bare C-APDU, and Expanded (AA, definite length) / Expanded (AE 80, indefinite length) wrap it in the TS 102 226 §5.2.1 scripting template (both execute on the live ISD; a template whose declared lengths do not match its body draws a Bad format TLV, which the operation shows as the step failure). The listing and registry queries follow that format: compact listings keep the chained GET RESPONSE, while an expanded run sends the same P2=00 queries wrapped and unchained (the reference TCA loader’s form, Le=’00’ returns the listing directly), and a registry query the card refuses in the current format is retried in the other formats — the report names the one that served it. The card-resources block also names the card’s inferred ETSI release (uicc.toolkit version → REL-N, the same mapping as the CAP analysis; shown only when the expanded registry returned the versions).

+

Delivers Remote Application Management operations as SCP80 secured packets via SMS-PP-DOWNLOAD ENVELOPE. The card must support SCP03 (AES or 3DES). A saved card preset from the Cards tab provides the keys, the TAR table and the counter (RAM uses the preset's ISD TAR, 000000 by default; the SPI1 is derived from that TAR's MSL). The Keyset selector next to the preset selector picks the keyset whose keys and counter the operation uses. It is disabled until a card preset is selected. The start button stays disabled until a card preset and one of its keysets are selected. The Format selector pins the remote command format: Auto (detect) probes the read-only GET STATUS command compact and in both expanded codings and keeps the first that the card executes, Compact sends the bare C-APDU, and Expanded (AA, definite length) / Expanded (AE 80, indefinite length) wrap it in the TS 102 226 §5.2.1 scripting template (both execute on the live ISD; a template whose declared lengths do not match its body draws a Bad format TLV, which the operation shows as the step failure). The listing and registry queries follow that format: compact listings keep the chained GET RESPONSE, while an expanded run sends the same P2=00 queries wrapped and unchained (the reference TCA loader’s form, Le=’00’ returns the listing directly), and a registry query the card refuses in the current format is retried in the other formats — the report names the one that served it. A next-occurrence page (P2=03) that a format refuses is retried in the other formats too — the expanded codings first (the serving one first when it is expanded), the compact command string last, because a compact page attempt can consume the page without delivering it (the card answers 61xx and the chained GET RESPONSE fails; every later format then sees the occurrence already gone) — some cards serve the expanded next page only wrapped in the AA template — and a listing is marked (first page only) only when the card promised more (6310) and no format could fetch the follow-up; in Auto the explorer also queries entries the bulk listing left without details individually by AID (a few extra packets). The card-resources block also names the card’s inferred ETSI release (uicc.toolkit version → REL-N, the same mapping as the CAP analysis; shown only when the expanded registry returned the versions).

Operations

diff --git a/frontend/index.html b/frontend/index.html index dd15392..de164a9 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1877,7 +1877,7 @@ // ===== Version ===== // Single source of truth for the PWA version: shown in the header and used // by the server version check in pysimConnect(). -const SIMPLE_VERSION = '3.13.0'; +const SIMPLE_VERSION = '3.13.1'; document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION; // ===== Tab switching ===== @@ -9564,6 +9564,16 @@ function ramMergeExpanded(collector, parsed) { }); } +// The app entries that still lack expanded-registry fields (pure): the per-AID +// completion queries only entries whose AID is known and which carry none of +// the expanded-only fields (version / SD AID / package AID / menu entries / +// raw objects) - the bulk listing may have skipped or truncated them. +function ramAppsMissingDetails(apps) { + return (apps || []).filter(e => e && e.aid && !e.version && !e.sdAid && + !e.elfAid && !(e.menuEntries && e.menuEntries.length) && + !(e.rawTlvs && e.rawTlvs.length)); +} + // Group the per-listing expanded outcomes (pure): one group per label, best // outcome wins (a pass that returned entries > an accepted-but-empty pass > // a refusal), so a listing queried twice (P1=10 and then the P1=20 fallback) @@ -9599,11 +9609,12 @@ function ramExpandedGroups(report) { return groups; } -// The "more occurrences" flag of one expanded-registry page pass (pure): a -// page that promised more data ('6310', or an accepted chained retry) is -// fulfilled by the next accepted page; it stays set when the follow-up was -// refused/failed or the page cap stopped the loop - only then is the listing -// "first page only". +// The "more occurrences" flag of one expanded-registry page pass (pure): only +// an explicit '6310' promise counts. The pass tries every command format for +// the follow-up pages, so a page that no format serves is the card's final +// answer when nothing was promised (the probe after a '61xx' page) - only a +// promise that stays unfulfilled (a refused follow-up or the page cap) makes +// the listing "first page only". function ramExpandedMore(promised, sw, page) { if (!promised) return false; if (page >= 8) return true; @@ -9885,6 +9896,10 @@ async function ramExplore(sp) { // told to raise the preset counter (v3.6.58). Every send of the run goes // through sendOta() so the verdict is caught wherever it appears. let cntrLow = false; + // The user's format selection before the run adopts a detected format: the + // exhaustive per-AID completion is an Auto behavior (a pinned format keeps + // the pass to that format's reach). + const autoFormat = !sp.ram_format || sp.ram_format === 'auto'; async function sendOta(apdu, params) { const res = await ramSendOta(apdu, params); if (!cntrLow && spCntrLow(res)) cntrLow = true; @@ -9924,16 +9939,18 @@ async function ramExplore(sp) { let accepted = true; let transportError = false; let usedFmt = fmt || runFmt; - const sendOne = async (apdu, what) => { + const sendOne = async (apdu, what, useFmt) => { ramShowProgress(label + ' P1=' + p1 + ' ' + what + '...'); const params = Object.assign({}, sp, { cntr, spi2 }); - if (fmt) params.ram_format = fmt; + const f = useFmt === undefined ? fmt : useFmt; + if (f) params.ram_format = f; const res = await ramSendOta(apdu, params); - usedFmt = res.ram_format || fmt || sp.ram_format || usedFmt; + usedFmt = res.ram_format || f || sp.ram_format || usedFmt; // an explicit fallback format is not adopted as the run's // format - the selected/pinned format keeps applying to the - // other passes - if (!fmt && res.ram_format) sp.ram_format = res.ram_format; + // other passes (only a ladder send without an explicit + // format adopts the one the server detected) + if (!f && res.ram_format) sp.ram_format = res.ram_format; if (res.final_cntr) cntr = res.final_cntr; else if (res.success && spPorAccepted(res.por)) cntr = ramIncrementCntr(cntr); if (!res.success || !res.por || !spPorAccepted(res.por)) { @@ -9950,29 +9967,49 @@ async function ramExplore(sp) { let promised = false; while (accepted && page++ < 8) { const apdu = ramExpandedQueryApdu(p1, form, page > 1); - const what = (page > 1 ? 'P2=03 (' : 'P2=02 (') + form + - (fmt ? ', ' + fmt : '') + ')'; - data += await sendOne(apdu, what); - if (cntrLow || !accepted) break; - // '61xx': the card holds the response data - the GET RESPONSE - // must ride in the same compact command string (a standalone - // one answers 6700 on the live card); the expanded format - // has no command string, so there is no retry there. - const pending = /^61[0-9A-F]{2}$/.test(sw); - if (pending && !ramFormatIsExpanded(usedFmt)) { - data += await sendOne(ramChainGetResponse(apdu), what + ' + GET RESPONSE'); + // A follow-up page is tried in every command format: a card + // may serve the next occurrence only in a specific one (the + // live card refuses the compact P2=03 and serves it wrapped + // in the 'AA' template) - Auto fetches every reachable page. + // The expanded codings come first: a compact page attempt can + // consume the page without delivering it. The format that + // serves a page is kept for the next ones. + const pageFmts = page > 1 ? ramPageFormats(usedFmt) : [fmt]; + let served = false; + let pending = false; + for (const pf of pageFmts) { + if (cntrLow) break; + const what = (page > 1 ? 'P2=03 (' : 'P2=02 (') + form + + (pf ? ', ' + pf : '') + ')'; + let chunk = await sendOne(apdu, what, pf); if (cntrLow || !accepted) break; + // '61xx': the card holds the response data - the GET + // RESPONSE must ride in the same compact command string (a + // standalone one answers 6700 on the live card); the + // expanded format has no command string, so there is no + // retry there. + pending = /^61[0-9A-F]{2}$/.test(sw); + if (pending && !ramFormatIsExpanded(usedFmt)) { + chunk += await sendOne(ramChainGetResponse(apdu), + what + ' + GET RESPONSE', pf); + if (cntrLow || !accepted) break; + } + if (!ramRemoteSwOk(sw)) continue; // refused: next format + data += chunk; + served = true; + break; } - // the ISD is a single occurrence (GP 11.4.2.2); '6310' means - // more occurrences, and a '61xx' page may mask that signal - // (the GET RESPONSE's own SW replaces it) - try one more page + if (cntrLow || !accepted) break; + if (!served) break; // no format serves this page + // the ISD is a single occurrence (GP 11.4.2.2) if (p1 === '80') break; - // continue only when the answer really says 'more occurrences' - // or the chained retry was accepted (a '61xx' page masks the - // 6310 signal with the GET RESPONSE's own SW); a refused retry - // must not cost another packet (v3.7.0) - if (sw !== '6310' && !(pending && ramRemoteSwOk(sw))) break; - promised = true; + // an explicit '6310' promises more occurrences + if (sw === '6310') { promised = true; continue; } + // a '61xx' page may mask the 6310 signal (the GET RESPONSE's + // own SW replaces it) - probe one more page, but only a real + // promise is reported when it stays unfulfilled + if (pending && ramRemoteSwOk(sw)) continue; + break; } // An unfulfilled 6310 only: a follow-up page that was accepted and // ended the listing fulfils the promise. @@ -10157,6 +10194,42 @@ async function ramExplore(sp) { } await elfTagListRetry(); + // Auto: the bulk listing may still miss expanded details (a card whose + // next-occurrence query is refused in every command format, or an entry + // the expanded listing skipped). Those entries are queried individually + // by AID - the AID search qualifier selects one entity, and the apps tag + // list works even where the ELF one is refused. Bounded (<= 6 packets), + // one entry per query, only for entries with no expanded fields at all; + // the compact command string carries the GET RESPONSE for a 61xx answer. + async function appsPerAidDetails() { + if (!autoFormat) return; + for (const e of ramAppsMissingDetails(apps).slice(0, 6)) { + if (cntrLow) break; + ramShowProgress(t('Details') + ' ' + e.aid + '...'); + const apdu = ramExpandedQueryApdu('40', 'tags', false, e.aid); + const send = async a => { + const res = await sendOta(a, Object.assign({}, sp, + { cntr, spi2: ramListingSpi2('40'), ram_format: 'compact' })); + if (res.ram_format) sp.ram_format = res.ram_format; + if (res.final_cntr) cntr = res.final_cntr; + else if (res.success && spPorAccepted(res.por)) cntr = ramIncrementCntr(cntr); + return res; + }; + let res = await send(apdu); + if (!res.success || !res.por || !spPorAccepted(res.por)) continue; + let sw = ((res.por.decoded && res.por.decoded.last_status_word) || '').toUpperCase(); + if (/^61[0-9A-F]{2}$/.test(sw)) { + res = await send(ramChainGetResponse(apdu)); + if (!res.success || !res.por || !spPorAccepted(res.por)) continue; + } + const data = res.por.decoded ? (res.por.decoded.last_response_data || '') : ''; + const parsed = data ? ramParseAppStatus(data) : []; + const hit = parsed.find(x => x.aid && x.aid.toUpperCase() === e.aid.toUpperCase()); + if (hit) ramMergeExpanded([e], [hit]); + } + } + await appsPerAidDetails(); + // A package whose own listing carries no version may still be known from // its applet instance (the app entry carries the load file version and the // package AID); the rows are annotated when this inference is used. @@ -12664,6 +12737,23 @@ function ramFmtLadder(runFmt, working) { return [working].concat(ladder.filter(f => f !== null && f !== working)); } +// The command formats to try for a next-occurrence page: the expanded codings +// first, the compact command string last. A compact next-occurrence attempt +// can *consume* the page on a card that processes it but cannot deliver the +// pending data (the live card answers '61xx' and the chained GET RESPONSE +// fails - every later format then sees the occurrence already consumed), while +// the expanded codings return the page inline (the live card serves it wrapped +// in the 'AA' template). The format that served the previous page is kept +// first when it is one of the expanded codings; the run format ('auto') is +// never returned: the server's format detection would cost an extra packet +// per page. +function ramPageFormats(working) { + const w = working && working !== 'auto' ? working : null; + const head = (w && w !== 'compact') ? [w] : []; + const tail = ['expanded', 'expanded-ae'].filter(f => f !== w); + return head.concat(tail, ['compact']); +} + function ramRemoteSwOk(sw) { const s = (sw || '').toUpperCase(); return !!s && (s === '9000' || s === 'CAFE' || diff --git a/frontend/sw.js b/frontend/sw.js index 04e7116..935f9cc 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simple-v369'; +const CACHE = 'simple-v371'; const URLS = [ 'index.html', 'help.html', diff --git a/frontend/tests/ram.test.js b/frontend/tests/ram.test.js index 26d28d4..a26ffdc 100644 --- a/frontend/tests/ram.test.js +++ b/frontend/tests/ram.test.js @@ -26,7 +26,7 @@ const FNS = ['berLenStr', 'buildApdu', 'escHtml', 'esc', 'chainInit', 'chainRamB 'ramFormatIsExpanded', 'ramFmtLadder', 'stkParamsBuild', 'ramRemoteSwOk', 'spPorAccepted', 'ramIncrementCntr', 'ramDeleteFromExplorer', 'ramListingSpi2', 'ramRemoveFromExplorer', 'ramHasInstance', 'ramExpandedQueryApdu', '_parseRawElfEntry', '_parseRawAppEntry', 'ramParseElfStatus', 'ramParseAppStatus', 'ramParseModuleAids', 'parseTLV', '_parseE3Entry', '_parseMenuEntries', - 'ramExpandedReport', 'ramExpandedMore', 'ramExpandedTags', 'ramExpandedGroups', 'ramExpandedElfForm', 'ramElfVersionHint', 'cardEtsiRelease', 'ramChainGetResponse', 'ramDeriveElfVersions', 'ramElfAppletCandidate', + 'ramExpandedReport', 'ramExpandedMore', 'ramPageFormats', 'ramAppsMissingDetails', 'ramExpandedTags', 'ramExpandedGroups', 'ramExpandedElfForm', 'ramElfVersionHint', 'cardEtsiRelease', 'ramChainGetResponse', 'ramDeriveElfVersions', 'ramElfAppletCandidate', 'spCntrLow', 'ramCntrLowHtml', 'ramCntrLowPresetIdx', 'ramCntrLowView', 'ramShowCntrLow', 'spKeysetKvnOf', 'spKeysetList', 'spKeysetFor', 'spKeysetCheck', 'spKeysetOptionsHtml', 'spPresetIdx', 'tarPresetIdx', @@ -1628,4 +1628,30 @@ test('ramExpandedMore marks only an unfulfilled 6310', () => { assert.strictEqual(ramExpandedMore(true, '6310', 8), true); // a transport failure (no remote status word) leaves it unfulfilled assert.strictEqual(ramExpandedMore(true, 'index out of range', 2), true); + // a probe after a '61xx' page (nothing promised) never flags the listing + assert.strictEqual(ramExpandedMore(false, '6A86', 2), false); +}); + +test('ramPageFormats tries the expanded codings first, compact last', () => { + // the compact command string is last: a compact page attempt can consume + // the page without delivering it (the live card's 61xx + failed chain) + assert.deepStrictEqual(ramPageFormats('compact'), ['expanded', 'expanded-ae', 'compact']); + assert.deepStrictEqual(ramPageFormats('expanded'), ['expanded', 'expanded-ae', 'compact']); + assert.deepStrictEqual(ramPageFormats('expanded-ae'), ['expanded-ae', 'expanded', 'compact']); + // the run format ('auto') is never a page format: the server's detection + // would cost an extra packet per page + assert.deepStrictEqual(ramPageFormats('auto'), ['expanded', 'expanded-ae', 'compact']); + assert.deepStrictEqual(ramPageFormats(null), ['expanded', 'expanded-ae', 'compact']); +}); + +test('ramAppsMissingDetails picks only the entries without expanded fields', () => { + assert.deepStrictEqual(ramAppsMissingDetails([ + { aid: 'A1', version: '1.0' }, + { aid: 'A2' }, + { aid: 'A3', sdAid: 'AA' }, + { aid: 'A4', menuEntries: [{ position: 1 }] }, + { aid: 'A5', rawTlvs: ['8F'] }, + { aid: '' }, + ]).map(e => e.aid), ['A2']); + assert.deepStrictEqual(ramAppsMissingDetails(null), []); }); diff --git a/pyproject.toml b/pyproject.toml index a730ace..7042c72 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pysim-simple-server" -version = "3.13.0" +version = "3.13.1" description = "HTTP REST server wrapping pysim for the SIMple PWA" requires-python = ">=3.8" # pysim is a git-only dependency installed explicitly by setup.bat/setup.sh. diff --git a/pysim_simple_server/server.py b/pysim_simple_server/server.py index dd8da4c..31b1d96 100644 --- a/pysim_simple_server/server.py +++ b/pysim_simple_server/server.py @@ -32,7 +32,7 @@ from osmocom.tlv import BER_TLV_IE from osmocom.utils import rpad -VERSION = '3.13.0' +VERSION = '3.13.1' MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE