v3.29.2: the strict row rule for the outfile import
The format is positional: an empty value drops its token, so the parser now requires every data row to have exactly the header's column count. That catches an empty value anywhere - including a middle one, which would otherwise shift the following columns (a missing ADM made the KIC columns read the KIK values: valid hex, wrong preset) - and a KICn/KIDn pair declared in the header must carry a usable hex value in every row (a placeholder fails). A violation makes the whole file invalid: the report's error names the line, no presets are returned, and an import (mode) answers 400 - it is never partially imported. The PWA disables the Import button while the report carries an error and guards the call. Unchanged per-row reports: a non-hex PIN/PUK/ADM/PSK value (dropped, the card kept), an unsupported key size (that keyset skipped), a card whose remaining keysets are unusable (skipped). All three real samples satisfy the rule exactly (11/11, 18/18, 18/18). Docs: help EN/RU, api.md, the repo AGENTS. Tests: the short/long row and the placeholder-keyset file errors, the valid-file round trip, the endpoint's refusal (nothing stored), the PWA's disabled button. 926 frontend + 945 Python; sw cache simple-v449.
This commit is contained in:
+7
-1
@@ -324,7 +324,13 @@ into a preview.
|
||||
- `text` (required) — the outfile contents. A file without a column line
|
||||
answers `{"ok": true, "preview": true, "presets": [], "report": {"error":
|
||||
"no column-name line found - …"}}` (a preview, not a `400`: the panel
|
||||
shows the message).
|
||||
shows the message). The same report error covers a structurally invalid
|
||||
file (v3.29.2): a data row whose token count differs from the header's column
|
||||
count (the format is positional, so an empty value drops its token - including
|
||||
in the middle, which would otherwise shift the columns), or a
|
||||
`KICn`/`KIDn` pair declared in the header without a usable hex value in some
|
||||
row. An import (`mode`) of such a file answers `400` with that error - it is
|
||||
never partially imported.
|
||||
- `name` — one common name applied to every imported card; empty falls back
|
||||
to the card's ICCID.
|
||||
- `algorithm16` — the algorithm nibble for a **16-byte** key: `"3des2"`
|
||||
|
||||
@@ -371,7 +371,7 @@
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="text-sm mb-3">Столбец <strong>SCP81</strong> показывает, задана ли в пресете рабочая пара PSK: <strong>✓</strong> (зелёный) — идентификатор и ключ заданы; <strong>⚠</strong> (жёлтый) — задано только одно из двух, и слушатель такой пресет игнорирует; <strong>—</strong> — PSK нет. Идентификатор и ключ PSK задаются вместе. Кнопка <strong>Добавить пресет</strong> открывает редактор с новым пресетом; <strong>Редактировать</strong> загружает пресет строки в редактор, поэтому поля можно менять без повторного ввода карты; <strong>Сохранить</strong> сохраняет и возвращает к списку, <strong>Отмена</strong> отменяет правки; <strong>Удалить</strong> удаляет строку (только в списке). Обмен пресетами: <strong>Экспорт в файл</strong> для выгрузки и <strong>Импорт из файла</strong> для загрузки. Выбранный пресет автоматически заполняет форму Secured Packet и устанавливает TAR для операции (RAM/GP → ISD TAR, SIM RFM → UICC RFM TAR, USIM RFM → ADF RFM TAR); TAR, введённый в форму Secured Packet вручную, сохраняется при отправке - счётчик, ключи и MSL этого TAR по-прежнему берутся из пресета; SPI1 берётся из MSL, а меньшее значение даёт предупреждение (сервер тоже возвращает его как <code class="font-mono text-sm">warning</code>); изменения сразу передаются работающему слушателю SCP81.</p>
|
||||
<p class="text-sm mb-3"><strong>Импорт outfile</strong> загружает сразу много карт из <em>outfile</em> поставщика карт — текстового дампа инструмента персонализации: шапка с баннером и параметрами, затем разделённая пробелами <strong>строка колонок</strong> (среди токенов есть <code class="font-mono text-sm">ICCID</code>; первый токен может нести префикс вида <code class="font-mono text-sm">var_out:</code>) и по одной строке на карту. Всё до строки колонок и все неиспользуемые колонки игнорируются. Разбираются <code class="font-mono text-sm">ICCID</code> (цифры, цифры с добивочным <code class="font-mono text-sm">F</code> до чётного числа нибблов, цифры с разделителями или сырой hex EF — всё приводится к одной форме), <code class="font-mono text-sm">PIN1</code>/<code class="font-mono text-sm">PIN2</code>/<code class="font-mono text-sm">PUK1</code>/<code class="font-mono text-sm">PUK2</code>, <code class="font-mono text-sm">ADM</code> (hex-значение ключа, 4–16 hex-цифр (чётное число) — например <code class="font-mono text-sm">3632333239463337</code>), <code class="font-mono text-sm">PSK</code> (16 байт; необязательная колонка <code class="font-mono text-sm">PSKID</code>/<code class="font-mono text-sm">PSK_ID</code> задаёт её идентификатор, по умолчанию берётся ICCID карты) и нумерованные пары ключей <code class="font-mono text-sm">KICn</code>/<code class="font-mono text-sm">KIDn</code> — по одному keyset на пару, до 15. <strong>Байтов KIc/KID в файле нет</strong>: номер keyset — это номер колонки (<code class="font-mono text-sm">KIC2</code> → keyset 2), а ниббл алгоритма определяется размером ключа — 8 байт DES, 24 байта 3DES3, а для <strong>16 байт</strong> — выбором в предпросмотре: <strong>3DES2</strong> (по умолчанию) или <strong>AES</strong>; любой другой размер пропускает этот keyset с пометкой. TAR также не берутся из файла: предпросмотр показывает таблицу TAR, которую получат импортируемые карты (по умолчанию три ролевые записи с TAR по спецификации и MSL 16; их можно править, кнопка <strong>+ TAR</strong> добавляет записи), и <strong>у каждой строки свой keyset</strong> — список из найденных в файле keysets (первый выбран заранее; при ненулевом MSL keyset обязателен и помечается жёлтым), поэтому пакеты к каждому TAR используют именно выбранный keyset. <strong>Общее имя</strong> применяется ко всем импортируемым картам (пустое — берётся ICCID); <strong>режим импорта</strong> выбирает <strong>Дополнить</strong> (по умолчанию — существующие пресеты сохраняются, дубли ICCID пропускаются и перечисляются) или <strong>Заменить</strong> (хранилище сначала очищается). Предпросмотр перечисляет разобранные карты (ICCID, keysets, найденные коды) и каждую пропущенную строку/ключ с причиной — до того, как что-либо сохранено.</p>
|
||||
<p class="text-sm mb-3"><strong>Импорт outfile</strong> загружает сразу много карт из <em>outfile</em> поставщика карт — текстового дампа инструмента персонализации: шапка с баннером и параметрами, затем разделённая пробелами <strong>строка колонок</strong> (среди токенов есть <code class="font-mono text-sm">ICCID</code>; первый токен может нести префикс вида <code class="font-mono text-sm">var_out:</code>) и по одной строке на карту. Всё до строки колонок и все неиспользуемые колонки игнорируются. Каждая строка данных должна содержать значение для <strong>каждой объявленной колонки</strong> — формат позиционный, поэтому пустое значение неотличимо от отсутствующего — а пара <code class="font-mono text-sm">KICn</code>/<code class="font-mono text-sm">KIDn</code>, объявленная в заголовке, должна иметь пригодное значение в каждой строке; нарушение делает весь файл некорректным (предпросмотр называет строку, импорт отклоняется). Разбираются <code class="font-mono text-sm">ICCID</code> (цифры, цифры с добивочным <code class="font-mono text-sm">F</code> до чётного числа нибблов, цифры с разделителями или сырой hex EF — всё приводится к одной форме), <code class="font-mono text-sm">PIN1</code>/<code class="font-mono text-sm">PIN2</code>/<code class="font-mono text-sm">PUK1</code>/<code class="font-mono text-sm">PUK2</code>, <code class="font-mono text-sm">ADM</code> (hex-значение ключа, 4–16 hex-цифр (чётное число) — например <code class="font-mono text-sm">3632333239463337</code>), <code class="font-mono text-sm">PSK</code> (16 байт; необязательная колонка <code class="font-mono text-sm">PSKID</code>/<code class="font-mono text-sm">PSK_ID</code> задаёт её идентификатор, по умолчанию берётся ICCID карты) и нумерованные пары ключей <code class="font-mono text-sm">KICn</code>/<code class="font-mono text-sm">KIDn</code> — по одному keyset на пару, до 15. <strong>Байтов KIc/KID в файле нет</strong>: номер keyset — это номер колонки (<code class="font-mono text-sm">KIC2</code> → keyset 2), а ниббл алгоритма определяется размером ключа — 8 байт DES, 24 байта 3DES3, а для <strong>16 байт</strong> — выбором в предпросмотре: <strong>3DES2</strong> (по умолчанию) или <strong>AES</strong>; любой другой размер пропускает этот keyset с пометкой. TAR также не берутся из файла: предпросмотр показывает таблицу TAR, которую получат импортируемые карты (по умолчанию три ролевые записи с TAR по спецификации и MSL 16; их можно править, кнопка <strong>+ TAR</strong> добавляет записи), и <strong>у каждой строки свой keyset</strong> — список из найденных в файле keysets (первый выбран заранее; при ненулевом MSL keyset обязателен и помечается жёлтым), поэтому пакеты к каждому TAR используют именно выбранный keyset. <strong>Общее имя</strong> применяется ко всем импортируемым картам (пустое — берётся ICCID); <strong>режим импорта</strong> выбирает <strong>Дополнить</strong> (по умолчанию — существующие пресеты сохраняются, дубли ICCID пропускаются и перечисляются) или <strong>Заменить</strong> (хранилище сначала очищается). Предпросмотр перечисляет разобранные карты (ICCID, keysets, найденные коды) и каждую пропущенную строку/ключ с причиной — до того, как что-либо сохранено.</p>
|
||||
</section>
|
||||
|
||||
<section class="mb-10">
|
||||
|
||||
+1
-1
@@ -370,7 +370,7 @@
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="text-sm mb-3">The <strong>SCP81</strong> column shows whether the preset supplies a usable PSK pair: <strong>✓</strong> (green) — identity and key set; <strong>⚠</strong> (amber) — only one of the two, so the listener ignores the preset; <strong>—</strong> — no PSK. PSK identity and key must be set together. <strong>Add preset</strong> opens the editor with a fresh preset; <strong>Edit</strong> loads the row's preset into it so fields can be changed without re-entering the card; <strong>Save</strong> stores it and returns to the list, <strong>Cancel</strong> discards the edits; <strong>Remove</strong> deletes the row (list only). Presets can be shared with <strong>Export to file</strong> and restored with <strong>Import from file</strong>. The selected card preset auto-fills the Secured Packet form and sets the TAR for the operation (RAM/GP → ISD TAR, SIM RFM → UICC RFM TAR, USIM RFM → ADF RFM TAR); a TAR typed into the Secured Packet form by hand is kept when sending - the counter, keys and the TAR's MSL are still re-read from the preset; the SPI1 is seeded from the TAR's MSL and a lower value warns (the server returns it as a <code class="font-mono text-sm">warning</code> too); edits are pushed into a running SCP81 listener automatically.</p>
|
||||
<p class="text-sm mb-3"><strong>Import outfile</strong> imports many cards at once from a card vendor's <em>outfile</em> — the personalization tool's plain-text dump: a banner and a configuration header, then a whitespace-separated <strong>column line</strong> (the tokens include <code class="font-mono text-sm">ICCID</code>; the first token may carry a <code class="font-mono text-sm">var_out:</code>-style prefix) and one row per card. Everything before the column line and every unused column is ignored. The parser reads <code class="font-mono text-sm">ICCID</code> (digits, the digits with an odd-nibble <code class="font-mono text-sm">F</code> pad, spaced digits or the raw EF hex — all normalised to one form), <code class="font-mono text-sm">PIN1</code>/<code class="font-mono text-sm">PIN2</code>/<code class="font-mono text-sm">PUK1</code>/<code class="font-mono text-sm">PUK2</code>, <code class="font-mono text-sm">ADM</code> (the key's hex value, 4–16 hex digits in byte pairs — e.g. <code class="font-mono text-sm">3632333239463337</code>), <code class="font-mono text-sm">PSK</code> (16 bytes; an optional <code class="font-mono text-sm">PSKID</code>/<code class="font-mono text-sm">PSK_ID</code> names its identity, defaulting to the card's ICCID) and the numbered key pairs <code class="font-mono text-sm">KICn</code>/<code class="font-mono text-sm">KIDn</code> — one keyset per pair, up to 15. The <strong>KIc/KID bytes are never in the file</strong>: the keyset number is the column number (<code class="font-mono text-sm">KIC2</code> → keyset 2) and the algorithm nibble follows the key size — 8 bytes DES, 24 bytes 3DES3, and for <strong>16 bytes</strong> the preview's choice, <strong>3DES2</strong> (default) or <strong>AES</strong>; any other size skips that keyset with a note. The TARs never come from the file either: the preview shows the TAR table the imported cards get (the three role entries with their spec TARs and MSL 16 by default, editable, with <strong>+ TAR</strong> for extra entries) and <strong>every row carries its own keyset</strong> — a select over the keysets found in the file (the first one is preselected; a non-zero MSL needs one, marked amber), so each TAR's packets use exactly the keyset you choose. A <strong>common name</strong> is applied to every imported card (empty falls back to the ICCID); <strong>Import mode</strong> chooses <strong>Merge</strong> (default — existing presets are kept, duplicate ICCIDs are skipped and reported) or <strong>Replace</strong> (the store is wiped first). The preview lists the parsed cards (ICCID, keysets, the codes found) and every skipped row/key with its reason before anything is stored.</p>
|
||||
<p class="text-sm mb-3"><strong>Import outfile</strong> imports many cards at once from a card vendor's <em>outfile</em> — the personalization tool's plain-text dump: a banner and a configuration header, then a whitespace-separated <strong>column line</strong> (the tokens include <code class="font-mono text-sm">ICCID</code>; the first token may carry a <code class="font-mono text-sm">var_out:</code>-style prefix) and one row per card. Everything before the column line and every unused column is ignored. Every data row must carry a value for <strong>every declared column</strong> — the format is positional, so an empty value is indistinguishable from a missing one — and a <code class="font-mono text-sm">KICn</code>/<code class="font-mono text-sm">KIDn</code> pair named in the header must have a usable value in every row; a violation makes the whole file invalid (the preview reports the line and the import is refused). The parser reads <code class="font-mono text-sm">ICCID</code> (digits, the digits with an odd-nibble <code class="font-mono text-sm">F</code> pad, spaced digits or the raw EF hex — all normalised to one form), <code class="font-mono text-sm">PIN1</code>/<code class="font-mono text-sm">PIN2</code>/<code class="font-mono text-sm">PUK1</code>/<code class="font-mono text-sm">PUK2</code>, <code class="font-mono text-sm">ADM</code> (the key's hex value, 4–16 hex digits in byte pairs — e.g. <code class="font-mono text-sm">3632333239463337</code>), <code class="font-mono text-sm">PSK</code> (16 bytes; an optional <code class="font-mono text-sm">PSKID</code>/<code class="font-mono text-sm">PSK_ID</code> names its identity, defaulting to the card's ICCID) and the numbered key pairs <code class="font-mono text-sm">KICn</code>/<code class="font-mono text-sm">KIDn</code> — one keyset per pair, up to 15. The <strong>KIc/KID bytes are never in the file</strong>: the keyset number is the column number (<code class="font-mono text-sm">KIC2</code> → keyset 2) and the algorithm nibble follows the key size — 8 bytes DES, 24 bytes 3DES3, and for <strong>16 bytes</strong> the preview's choice, <strong>3DES2</strong> (default) or <strong>AES</strong>; any other size skips that keyset with a note. The TARs never come from the file either: the preview shows the TAR table the imported cards get (the three role entries with their spec TARs and MSL 16 by default, editable, with <strong>+ TAR</strong> for extra entries) and <strong>every row carries its own keyset</strong> — a select over the keysets found in the file (the first one is preselected; a non-zero MSL needs one, marked amber), so each TAR's packets use exactly the keyset you choose. A <strong>common name</strong> is applied to every imported card (empty falls back to the ICCID); <strong>Import mode</strong> chooses <strong>Merge</strong> (default — existing presets are kept, duplicate ICCIDs are skipped and reported) or <strong>Replace</strong> (the store is wiped first). The preview lists the parsed cards (ICCID, keysets, the codes found) and every skipped row/key with its reason before anything is stored.</p>
|
||||
</section>
|
||||
|
||||
<section class="mb-10">
|
||||
|
||||
+11
-2
@@ -1928,7 +1928,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.29.1';
|
||||
const SIMPLE_VERSION = '3.29.2';
|
||||
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
||||
|
||||
// ===== view history (the browser Back/Forward buttons) =====
|
||||
@@ -15101,8 +15101,11 @@ function cardsOutfileRender() {
|
||||
html += '<div id="cards-of-tars">' + cardsOutfileTarRowsHtml() + '</div>';
|
||||
html += '<button type="button" onclick="cardsOutfileTarAdd()" class="mb-1 text-xs px-2 py-0.5 rounded bg-gray-200 dark:bg-slate-700 hover:bg-gray-300 dark:hover:bg-slate-600 text-gray-700 dark:text-slate-300">+ ' + esc(t('TAR')) + '</button>';
|
||||
html += cardsOutfileResultHtml();
|
||||
// A structurally invalid file cannot be imported (v3.29.2): the server
|
||||
// refuses it, so the button stays disabled while the report has an error.
|
||||
const fileErr = !!(o.preview && o.preview.report && o.preview.report.error);
|
||||
html += '<div class="flex gap-2 mt-2">';
|
||||
html += '<button type="button" id="cards-of-import" onclick="cardsOutfileImport()" class="px-3 py-1.5 text-sm rounded bg-emerald-600 text-white hover:bg-emerald-700 disabled:opacity-40">' + esc(t('Import')) + '</button>';
|
||||
html += '<button type="button" id="cards-of-import"' + (fileErr ? ' disabled' : '') + ' onclick="cardsOutfileImport()" class="px-3 py-1.5 text-sm rounded bg-emerald-600 text-white hover:bg-emerald-700 disabled:opacity-40">' + esc(t('Import')) + '</button>';
|
||||
html += '<button type="button" onclick="cardsOutfilePreview()" class="px-3 py-1.5 text-sm rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700">' + esc(t('Refresh preview')) + '</button>';
|
||||
html += '</div>';
|
||||
panel.innerHTML = html;
|
||||
@@ -15129,6 +15132,11 @@ async function cardsOutfileImport() {
|
||||
const o = _cardsOutfile;
|
||||
if (!o) return;
|
||||
cardsOutfileControls();
|
||||
// A structurally invalid file is refused by the server (v3.29.2)
|
||||
if (o.preview && o.preview.report && o.preview.report.error) {
|
||||
alert(t('The file is invalid - fix it and refresh the preview'));
|
||||
return;
|
||||
}
|
||||
// Replace wipes the whole store - the destructive choice asks first
|
||||
if (o.mode === 'replace' && !confirm(t('Replace mode wipes the whole preset store. Continue?'))) return;
|
||||
const btn = document.getElementById('cards-of-import');
|
||||
@@ -23638,6 +23646,7 @@ const LANG_RU = {
|
||||
'Common name': 'Общее имя',
|
||||
'16-byte keys': '16-байтные ключи',
|
||||
'This TAR needs a keyset (its MSL requires security)': 'Этому TAR нужен keyset (его MSL требует защиты)',
|
||||
'The file is invalid - fix it and refresh the preview': 'Файл некорректен — исправьте его и обновите предпросмотр',
|
||||
'Import mode': 'Режим импорта',
|
||||
'Merge': 'Дополнить',
|
||||
'Replace': 'Заменить',
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'simple-v448';
|
||||
const CACHE = 'simple-v449';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
@@ -310,6 +310,22 @@ test('a declined replace confirmation aborts the import', async () => {
|
||||
assert.strictEqual(app.calls.length, 1);
|
||||
});
|
||||
|
||||
test('the Import button is disabled while the report has a file error (v3.29.2)', () => {
|
||||
clearEls();
|
||||
setEl('cards-outfile-panel', '');
|
||||
app.state = { text: TEXT, file: 'bad.out', name: 'x', algorithm16: '3des2', mode: 'merge',
|
||||
preview: { presets: [], report: { error: 'line 2 has 2 values, the header declares 3 columns - the file is invalid' } } };
|
||||
app.tars = 0;
|
||||
app.cardsOutfileRender();
|
||||
assert.match(els['cards-outfile-panel'].innerHTML, /id="cards-of-import" disabled/);
|
||||
assert.match(els['cards-outfile-panel'].innerHTML, /the header declares 3 columns/);
|
||||
// a valid report keeps it enabled
|
||||
app.state.preview = { presets: [{ iccid: '8970119000004600098', keysets: [{ kic: '15', kid: '15' }] }],
|
||||
report: { columns: ['ICCID', 'KIC1', 'KID1'], rows: 1, cards: 1 } };
|
||||
app.cardsOutfileRender();
|
||||
assert.match(els['cards-outfile-panel'].innerHTML, /id="cards-of-import" onclick/);
|
||||
});
|
||||
|
||||
test('a row keeps its chosen keyset through the DOM round-trip', () => {
|
||||
clearEls();
|
||||
app.state = { text: TEXT, name: 'x', algorithm16: '3des2', mode: 'merge',
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "pysim-simple-server"
|
||||
version = "3.29.1"
|
||||
version = "3.29.2"
|
||||
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.
|
||||
|
||||
@@ -208,7 +208,6 @@ def parse_outfile(text, name='', algorithm16='3des2', tars=None, tar_keyset=None
|
||||
report['keysets'] = ['KIC%d/KID%d' % (n, n) for n in pairs]
|
||||
tars_base = normalise_tars(tars, tar_keyset)
|
||||
report['tars'] = len(tars_base)
|
||||
used_max = max([idx['ICCID']] + [i for pair in pairs.values() for i in pair])
|
||||
|
||||
def skip(msg):
|
||||
if len(report['skipped']) < SKIP_REPORT_MAX:
|
||||
@@ -224,9 +223,15 @@ def parse_outfile(text, name='', algorithm16='3des2', tars=None, tar_keyset=None
|
||||
toks = str(line).split()
|
||||
if not toks:
|
||||
continue
|
||||
if len(toks) <= used_max:
|
||||
skip('line %d: too few columns' % row_no)
|
||||
continue
|
||||
# The file is positional: an empty value drops its token, so a row whose
|
||||
# token count differs from the header's column count carries an empty
|
||||
# (or extra) value somewhere. The file is invalid then - it is never
|
||||
# partially imported (v3.29.2).
|
||||
if len(toks) != len(columns):
|
||||
report['error'] = ('line %d has %d values, the header declares %d '
|
||||
'columns - the file is invalid'
|
||||
% (row_no, len(toks), len(columns)))
|
||||
return {'presets': [], 'report': report}
|
||||
report['rows'] += 1
|
||||
iccid = presets.normalize_iccid(toks[idx['ICCID']]) if idx['ICCID'] >= 0 else ''
|
||||
if not iccid:
|
||||
@@ -237,8 +242,11 @@ def parse_outfile(text, name='', algorithm16='3des2', tars=None, tar_keyset=None
|
||||
kic_key = _hex_value(toks[ki])
|
||||
kid_key = _hex_value(toks[di])
|
||||
if not kic_key or not kid_key:
|
||||
skip('line %d: keyset %d has no KIC/KID' % (row_no, n))
|
||||
continue
|
||||
# The header declares this keyset: every row must carry a usable
|
||||
# value for it (v3.29.2)
|
||||
report['error'] = ('line %d: keyset %d has no usable KIC/KID '
|
||||
'value - the file is invalid' % (row_no, n))
|
||||
return {'presets': [], 'report': report}
|
||||
if len(kic_key) != len(kid_key):
|
||||
skip('line %d: keyset %d KIC/KID sizes differ' % (row_no, n))
|
||||
continue
|
||||
|
||||
@@ -37,7 +37,7 @@ from osmocom.tlv import BER_TLV_IE
|
||||
from osmocom.utils import rpad
|
||||
|
||||
|
||||
VERSION = '3.29.1'
|
||||
VERSION = '3.29.2'
|
||||
|
||||
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
||||
|
||||
@@ -11360,6 +11360,13 @@ class PysimHandler(BaseHTTPRequestHandler):
|
||||
self._send_json(resp)
|
||||
self._log_resp(resp)
|
||||
return
|
||||
if parsed['report'].get('error'):
|
||||
# A structurally invalid file (v3.29.2) is never partially
|
||||
# imported - the preview shows the error, the import refuses.
|
||||
resp = {'error': parsed['report']['error']}
|
||||
self._send_json(resp, 400)
|
||||
self._log_resp(resp)
|
||||
return
|
||||
try:
|
||||
result = store.import_presets(parsed['presets'], mode)
|
||||
except OSError as e:
|
||||
|
||||
@@ -966,6 +966,19 @@ class PresetStoreHttpTests(unittest.TestCase):
|
||||
'text': text, 'tars': [{'tar': '000000', 'msl': '16', 'keyset': 99}]})
|
||||
self.assertEqual(status, 400, resp)
|
||||
self.assertIn('keyset', resp['error'])
|
||||
# an invalid file (a row with a missing value, v3.29.2): the preview
|
||||
# shows the error, the import refuses it - never a partial import
|
||||
bad = ('ICCID KIC1 KID1\n' + '8970119000004600098F 00112233445566778899AABBCCDDEEFF\n')
|
||||
status, resp = self._post('/api/presets/import-outfile', {'text': bad})
|
||||
self.assertEqual(status, 200, resp)
|
||||
self.assertEqual(resp['presets'], [])
|
||||
self.assertIn('the header declares 3 columns', resp['report']['error'])
|
||||
before = len(self.store.list())
|
||||
status, resp = self._post('/api/presets/import-outfile', {
|
||||
'text': bad, 'mode': 'merge'})
|
||||
self.assertEqual(status, 400, resp)
|
||||
self.assertIn('the file is invalid', resp['error'])
|
||||
self.assertEqual(len(self.store.list()), before)
|
||||
# a non-list TAR table is the caller's error, never a silent fallback
|
||||
status, resp = self._post('/api/presets/import-outfile', {
|
||||
'text': text, 'tars': 'nope'})
|
||||
|
||||
+32
-10
@@ -244,19 +244,41 @@ class OptionTests(unittest.TestCase):
|
||||
self.assertNotIn('pskKey', res['presets'][0])
|
||||
self.assertTrue(any('PSK is not 16 bytes' in s for s in res['report']['skipped']))
|
||||
|
||||
def test_short_rows_and_bad_optional_codes_are_reported(self):
|
||||
# a row shorter than the *used* columns (the keyset) is skipped
|
||||
def test_a_row_with_a_missing_or_extra_value_is_a_file_error(self):
|
||||
"""The file is positional: an empty value drops its token, so a token
|
||||
count other than the header's column count means an empty (or extra)
|
||||
value - the whole file is refused (v3.29.2)."""
|
||||
# a missing trailing value
|
||||
text = ('ICCID KIC1 KID1\n' + DIGITS_F + ' ' + K16A + '\n')
|
||||
res = outfile.parse_outfile(text)
|
||||
self.assertEqual(res['presets'], [])
|
||||
self.assertTrue(any('too few columns' in s for s in res['report']['skipped']))
|
||||
# a missing optional column is simply absent, the card kept
|
||||
self.assertIn('line 2 has 2 values, the header declares 3 columns',
|
||||
res['report']['error'])
|
||||
# ...a missing middle value (which used to shift the columns silently)
|
||||
text = ('ICCID KIC1 KID1 ADM\n' + DIGITS_F + ' ' + K16A + ' ' + K16B + '\n')
|
||||
res = outfile.parse_outfile(text)
|
||||
self.assertEqual(res['presets'], [])
|
||||
self.assertIn('the file is invalid', res['report']['error'])
|
||||
# ...and an extra value
|
||||
text = ('ICCID KIC1 KID1\n' + DIGITS_F + ' ' + K16A + ' ' + K16B + ' EXTRA\n')
|
||||
res = outfile.parse_outfile(text)
|
||||
self.assertEqual(res['presets'], [])
|
||||
self.assertIn('4 values', res['report']['error'])
|
||||
# a declared keyset without a usable value is a file error too
|
||||
text = ('ICCID KIC1 KID1 KIC2 KID2\n' + DIGITS_F + ' ' + K16A + ' ' + K16B +
|
||||
' - ' + K16C + '\n')
|
||||
res = outfile.parse_outfile(text)
|
||||
self.assertEqual(res['presets'], [])
|
||||
self.assertIn('keyset 2 has no usable KIC/KID value', res['report']['error'])
|
||||
# a valid file keeps parsing (an exact count everywhere)
|
||||
text = ('ICCID KIC1 KID1 PIN1 ADM\n' + DIGITS_F + ' ' + K16A + ' ' + K16B +
|
||||
' 1234\n')
|
||||
' 1234 3632333239463337\n')
|
||||
res = outfile.parse_outfile(text)
|
||||
self.assertEqual(len(res['presets']), 1)
|
||||
self.assertEqual(res['presets'][0]['pin1'], '1234')
|
||||
self.assertNotIn('adm', res['presets'][0])
|
||||
self.assertEqual(res['presets'][0]['adm'], '3632333239463337')
|
||||
|
||||
def test_bad_optional_codes_are_reported_per_row(self):
|
||||
# the ADM column is the key's hex value, stored as-is (v3.28.2): the
|
||||
# length varies, and the value is never ASCII-decoded
|
||||
text = ('ICCID KIC1 KID1 ADM\n' + DIGITS_F + ' ' + K16A + ' ' + K16B +
|
||||
@@ -314,9 +336,9 @@ class OptionTests(unittest.TestCase):
|
||||
res = outfile.parse_outfile(text)
|
||||
self.assertNotIn('puk1', res['presets'][0])
|
||||
self.assertTrue(any("PUK1 'abcd'" in s for s in res['report']['skipped']))
|
||||
# ...while a column absent from a short row stays silent (not present)
|
||||
# ...while an *empty* PUK1 token cannot exist in a whitespace-separated
|
||||
# row: the count would change, which is a file error (v3.29.2)
|
||||
text = ('ICCID KIC1 KID1 PIN1 PUK1\n' + DIGITS_F + ' ' + K16A + ' ' + K16B + ' 1234\n')
|
||||
res = outfile.parse_outfile(text)
|
||||
self.assertEqual(res['presets'][0]['pin1'], '1234')
|
||||
self.assertNotIn('puk1', res['presets'][0])
|
||||
self.assertEqual([s for s in res['report']['skipped'] if 'PUK1' in s], [])
|
||||
self.assertEqual(res['presets'], [])
|
||||
self.assertIn('the file is invalid', res['report']['error'])
|
||||
|
||||
Reference in New Issue
Block a user