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