feat: SCP80 SMS concatenation + packet size / SMS count display (v3.2.0)
Packets longer than one SMS now go out as concatenated SMS-PP downloads per TS 31.115 4.3 and the UI shows how many SMS a packet needs. Server: - `_split_secured_packet` cuts the command packet at the exact SMS user-data capacities (first SM 132 octets: concat IE 5 + CPI IE 2; following ones 134; a single-SM packet may be 137 with the CPI IE) and `_build_sms_tpdu` tags every segment with the fixed concatenation reference 01; `_build_sms_tpdu` also enforces the 140-octet budget. - `_send_secured_packet` (shared by /api/send-ota and /api/ram-install) sends one ENVELOPE per segment in order, refuses more than MAX_ENVELOPE_SEGMENTS (5, the card's concatenation buffer) and reports `bytes`/`segments` in the response (RAM install per step as well). - `_build_secured_packet`/`_encode_cmd_unlimited`: our own TS 102 225 5.1.1 encoder on pySim's keyset/header constructors, byte-identical to pySim for packets <= 140 octets (tests) and not limited to one SMS (pySim refuses the longer ones, which is why they never went out). - RAM LOAD blocks are no longer clamped to one SMS: 1-240 bytes of payload with the default 240 (the GP maximum); `load_block_size_auto` replaces `load_block_size_clamped`. PWA: - `scp80SegmentInfo` / `spSizeInfoText` show "N bytes . M SMS (concatenated)" under the packet field, turn red past 5 SMS and report size/SMS in the send result and the RAM step log; LOAD block hints and placeholders updated; EN/RU. Tests/docs: Python +2 cases incl. segment order/capacities and byte identity with pySim; Node drift guard against the server constants and UI text tests; README/README_RUS, help EN/RU, docs/api.md, AGENTS.
This commit is contained in:
@@ -273,7 +273,7 @@
|
||||
<li>AES требует счётчик с защитой от повтора: биты SPI1 b5 b4 должны быть <code class="font-mono text-sm">10</code> (счётчик больше) или <code class="font-mono text-sm">11</code> (счётчик +1) согласно TS 102 225 §5.1.2/§5.1.3.1</li>
|
||||
<li>Байт паддинга настраивается (<code class="font-mono text-sm">00</code> по умолчанию или <code class="font-mono text-sm">FF</code>)</li>
|
||||
</ul>
|
||||
<p class="text-sm mb-3">Кнопка <strong>Проверить в pySim</strong> сверяет собранный пакет с эталонной реализацией <code class="font-mono text-sm">OtaDialectSms.encode_cmd</code>. Кнопка <strong>Отправить на карту</strong> доставляет пакет через ENVELOPE SMS-PP-DOWNLOAD (при подключении к серверу). Полученный Proof of Receipt декодируется и показывается строкой статуса PoR (статус, TAR, счётчик, сырой PoR); статусное слово и данные ответа последней команды подставляются в подвкладку <strong>«Парсер ответов»</strong> (Remote APDU), а успешный PoR увеличивает счётчик повторов и очищает пакет.</p>
|
||||
<p class="text-sm mb-3">Кнопка <strong>Проверить в pySim</strong> сверяет собранный пакет с эталонной реализацией <code class="font-mono text-sm">OtaDialectSms.encode_cmd</code>. Кнопка <strong>Отправить на карту</strong> доставляет пакет через ENVELOPE SMS-PP-DOWNLOAD (при подключении к серверу). Под полем пакета показывается его размер и число SMS; пакет, не помещающийся в одно SMS, отправляется <strong>конкатенированной</strong> загрузкой (TS 31.115 §4.3) — пакет делится на части SMS-пользовательских данных (первое SMS 132 октета, последующие по 134; первое дополнительно несёт информационные элементы конкатенации и CPI), и сегменты отправляются по порядку. Более 5 сегментов отклоняется: это практический предел буфера конкатенации карты. Полученный Proof of Receipt декодируется и показывается строкой статуса PoR (статус, TAR, счётчик, сырой PoR); статусное слово и данные ответа последней команды подставляются в подвкладку <strong>«Парсер ответов»</strong> (Remote APDU), а успешный PoR увеличивает счётчик повторов и очищает пакет.</p>
|
||||
|
||||
<h3 id="ram" class="text-lg font-medium mb-2">3.2 RAM</h3>
|
||||
<p class="mb-2">Выполняет операции удалённого управления приложениями (Remote Application Management) как защищённые пакеты SCP80 через SMS-PP-DOWNLOAD ENVELOPE. Карта должна поддерживать SCP03 (AES или 3DES). Предустановка карты со вкладки <strong>Карты</strong> обеспечивает SPI, ключи, TAR и счётчик (RAM использует <strong>ISD TAR</strong> предустановки, по умолчанию <code class="font-mono text-sm">000000</code>).</p>
|
||||
@@ -283,7 +283,7 @@
|
||||
<thead><tr class="border-b border-gray-300 dark:border-slate-700"><th class="text-left py-1 px-2">Операция</th><th class="text-left py-1 px-2">Описание</th></tr></thead>
|
||||
<tbody>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Обзор карты (все данные GP)</td><td class="py-1 px-2">Запрос GET STATUS для ISD, приложений, ELF и модулей ELF, а также GET DATA FF21 для информации о памяти. Результаты отображаются в обзоре с кнопками <strong>Удалить</strong> для каждого элемента.</td></tr>
|
||||
<tr><td class="py-1 px-2">Установка пакета (.cap файл)</td><td class="py-1 px-2">Отправка <code class="font-mono text-sm">.cap</code> файла на карту через сервер: INSTALL[for load] → LOAD ×N → INSTALL[for install (+make selectable)]. Load-файл делится на LOAD APDU, каждый из которых помещается в один SMS SCP80; поле <strong>размер блока LOAD</strong> переопределяет авто-подобранный размер (пусто = максимальный размер, чей secured-пакет укладывается в 140 октетов), так что большой <code>.cap</code> просто занимает несколько SMS.</td></tr>
|
||||
<tr><td class="py-1 px-2">Установка пакета (.cap файл)</td><td class="py-1 px-2">Отправка <code class="font-mono text-sm">.cap</code> файла на карту через сервер: INSTALL[for load] → LOAD ×N → INSTALL[for install (+make selectable)]. Load-файл делится на LOAD APDU размера <strong>размер блока LOAD</strong> (1–240 байт полезной нагрузки, по умолчанию 240); каждый защищённый пакет доставляется одним SMS или, если он больше, конкатенированной SMS-PP загрузкой до 5 сегментов, так что большой <code>.cap</code> просто занимает несколько SMS.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
+2
-2
@@ -273,7 +273,7 @@
|
||||
<li>AES requires a replay-protected counter: SPI1 bits b5 b4 must be <code class="font-mono text-sm">10</code> (counter higher) or <code class="font-mono text-sm">11</code> (counter +1) per TS 102 225 §5.1.2/§5.1.3.1</li>
|
||||
<li>Padding byte configurable (<code class="font-mono text-sm">00</code> default, or <code class="font-mono text-sm">FF</code>)</li>
|
||||
</ul>
|
||||
<p class="text-sm mb-3">A “Verify vs pySim” button cross-checks the assembled packet against pySim’s reference <code class="font-mono text-sm">OtaDialectSms.encode_cmd</code>. A “Send to Card” button delivers it via SMS-PP-DOWNLOAD ENVELOPE (when connected to the server). 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 <strong>Response parser</strong> tab, and a successful PoR advances the replay counter and clears the packet.</p>
|
||||
<p class="text-sm mb-3">A “Verify vs pySim” button cross-checks the assembled packet against pySim’s reference <code class="font-mono text-sm">OtaDialectSms.encode_cmd</code>. A “Send to Card” button delivers it via SMS-PP-DOWNLOAD ENVELOPE (when connected to the server). 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 <strong>concatenated</strong> 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 <strong>Response parser</strong> tab, and a successful PoR advances the replay counter and clears the packet.</p>
|
||||
|
||||
<h3 id="ram" class="text-lg font-medium mb-2">3.2 RAM</h3>
|
||||
<p class="mb-2">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 <strong>Cards</strong> tab provides the SPI, keys, TAR, and counter (RAM uses the preset's <strong>ISD TAR</strong>, <code class="font-mono text-sm">000000</code> by default).</p>
|
||||
@@ -283,7 +283,7 @@
|
||||
<thead><tr class="border-b border-gray-300 dark:border-slate-700"><th class="text-left py-1 px-2">Operation</th><th class="text-left py-1 px-2">Description</th></tr></thead>
|
||||
<tbody>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Explore Card (all GP data)</td><td class="py-1 px-2">Queries GET STATUS for ISD, Applications, ELFs, and ELF Modules, plus GET DATA FF21 for memory info. Results appear in an explorer view with per-item <strong>Delete</strong> buttons.</td></tr>
|
||||
<tr><td class="py-1 px-2">Install Package (.cap file)</td><td class="py-1 px-2">Sends a <code class="font-mono text-sm">.cap</code> file to the card via the server: INSTALL[for load] → LOAD ×N → INSTALL[for install (+make selectable)]. The load file is split into LOAD APDUs that each fit one SCP80 SMS; the <strong>LOAD block size</strong> field overrides the auto-fitted size (empty = largest size whose secured packet still encodes into 140 octets), so a large <code>.cap</code> simply takes several SMS.</td></tr>
|
||||
<tr><td class="py-1 px-2">Install Package (.cap file)</td><td class="py-1 px-2">Sends a <code class="font-mono text-sm">.cap</code> file to the card via the server: INSTALL[for load] → LOAD ×N → INSTALL[for install (+make selectable)]. The load file is split into LOAD APDUs of the <strong>LOAD block size</strong> (1–240 bytes of payload, 240 by default); each secured packet is delivered as a single SMS or, when larger, as a concatenated SMS-PP download of up to 5 segments, so a large <code class="font-mono text-sm">.cap</code> simply takes several SMS.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
+69
-14
@@ -550,6 +550,7 @@
|
||||
<button data-needs="card" onclick="pysimSendOta()" id="sp-send-btn" class="hidden mb-3 px-5 py-2.5 bg-emerald-600 text-white text-sm font-medium rounded hover:bg-emerald-700 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Send to Card">Send to Card</button>
|
||||
<div id="sp-por-status" class="mt-2 text-base font-semibold font-mono hidden"></div>
|
||||
<textarea id="sp-result" rows="3" readonly class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 bg-gray-100 dark:bg-slate-800"></textarea>
|
||||
<div id="sp-size-info" class="text-xs mt-1 text-gray-500 dark:text-slate-400"></div>
|
||||
<div id="sp-send-result" class="mt-2 text-xs font-mono hidden"></div>
|
||||
<div id="sp-verify-result" class="mt-2 text-xs font-mono hidden"></div>
|
||||
|
||||
@@ -693,9 +694,9 @@
|
||||
<label class="flex items-center gap-2 mt-2">
|
||||
<input type="checkbox" id="ram-make-sel" checked> <span class="text-sm" data-l10n="Make selectable">Make selectable</span>
|
||||
</label>
|
||||
<label class="block mb-1 text-xs font-medium text-gray-500 dark:text-slate-400 mt-2" data-l10n="LOAD block size (bytes, empty = auto-fit)">LOAD block size (bytes, empty = auto-fit)</label>
|
||||
<input id="ram-load-size" type="number" min="1" max="240" class="w-32 font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="auto">
|
||||
<div class="text-xs text-gray-500 dark:text-slate-400 mt-1" data-l10n="Splits the load file into LOAD APDUs that fit one SCP80 SMS; auto-fit picks the largest size that encodes into 140 octets.">Splits the load file into LOAD APDUs that fit one SCP80 SMS; auto-fit picks the largest size that encodes into 140 octets.</div>
|
||||
<label class="block mb-1 text-xs font-medium text-gray-500 dark:text-slate-400 mt-2" data-l10n="LOAD block size (bytes, empty = 240)">LOAD block size (bytes, empty = 240)</label>
|
||||
<input id="ram-load-size" type="number" min="1" max="240" class="w-32 font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="240">
|
||||
<div class="text-xs text-gray-500 dark:text-slate-400 mt-1" data-l10n="Splits the load file into LOAD APDUs (1-240 bytes of payload). A secured packet larger than one SMS is sent as a concatenated SMS-PP download (up to 5 SMs).">Splits the load file into LOAD APDUs (1-240 bytes of payload). A secured packet larger than one SMS is sent as a concatenated SMS-PP download (up to 5 SMs).</div>
|
||||
</div>
|
||||
|
||||
<div id="ram-progress" class="hidden mb-3">
|
||||
@@ -1436,7 +1437,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.1.4';
|
||||
const SIMPLE_VERSION = '3.2.0';
|
||||
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
||||
|
||||
// ===== Tab switching =====
|
||||
@@ -3268,6 +3269,7 @@ function spCntrAdjust(delta) {
|
||||
}
|
||||
function spInvalidate() {
|
||||
document.getElementById('sp-result').value = '';
|
||||
spShowSizeInfo();
|
||||
}
|
||||
|
||||
function updateSp() {
|
||||
@@ -3468,6 +3470,56 @@ function aesCmac(data, keyBytes) {
|
||||
// CPL = octets from CHL to end incl. padding; CHL = 13 + len_sig.
|
||||
// CPI (0x70) and CHI (null) are NOT part of the SMS packet data.
|
||||
function genSp() {
|
||||
_genSpBuild();
|
||||
spShowSizeInfo();
|
||||
}
|
||||
|
||||
// ===== SCP80 SMS segmentation (TS 31.115 4.2/4.3) =====
|
||||
// Capacities mirror the server's SCP80_*_BYTES constants (pysim_simple_server/
|
||||
// server.py): 140 octets of SMS user data minus the UDH, which carries the
|
||||
// concatenation IE (5 octets) in every SM and the CPI IE (2 octets) in the
|
||||
// first one of a concatenated command.
|
||||
const SCP80_MAX_SMS = 5;
|
||||
const SCP80_SINGLE_BYTES = 137;
|
||||
const SCP80_FIRST_BYTES = 132;
|
||||
const SCP80_NEXT_BYTES = 134;
|
||||
|
||||
// Secured packet hex -> {bytes, segments} of the SMS-PP download.
|
||||
function scp80SegmentInfo(hex) {
|
||||
const bytes = Math.floor((hex || '').replace(/[^0-9a-fA-F]/g, '').length / 2);
|
||||
if (!bytes) return { bytes: 0, segments: 0 };
|
||||
if (bytes <= SCP80_SINGLE_BYTES) return { bytes: bytes, segments: 1 };
|
||||
let segments = 1;
|
||||
let rest = bytes - SCP80_FIRST_BYTES;
|
||||
while (rest > 0) { segments += 1; rest -= SCP80_NEXT_BYTES; }
|
||||
return { bytes: bytes, segments: segments };
|
||||
}
|
||||
|
||||
function spSizeInfoText(hex) {
|
||||
const info = scp80SegmentInfo(hex);
|
||||
if (!info.segments) return '';
|
||||
let s = info.bytes + ' ' + t('bytes') + ' · ' + info.segments + ' SMS';
|
||||
if (info.segments > 1) s += ' (' + t('concatenated') + ')';
|
||||
if (info.segments > SCP80_MAX_SMS) {
|
||||
s += ' — ' + t('too large for the card concatenation buffer') + ' (' + SCP80_MAX_SMS + ' SMS)';
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
// Refresh the packet size / SMS count line under the packet field.
|
||||
function spShowSizeInfo() {
|
||||
const el = document.getElementById('sp-size-info');
|
||||
if (!el) return;
|
||||
const value = (document.getElementById('sp-result') || {}).value || '';
|
||||
const isPacket = /^\s*[0-9a-fA-F\s]+$/.test(value);
|
||||
const info = isPacket ? scp80SegmentInfo(value) : { bytes: 0, segments: 0 };
|
||||
el.textContent = info.segments ? spSizeInfoText(value) : '';
|
||||
el.className = 'text-xs mt-1 ' + (info.segments > SCP80_MAX_SMS
|
||||
? 'text-red-600 dark:text-red-400'
|
||||
: 'text-gray-500 dark:text-slate-400');
|
||||
}
|
||||
|
||||
function _genSpBuild() {
|
||||
const apduHex = (document.getElementById('sp-apdu').value || '').replace(/[^0-9a-fA-F]/g, '');
|
||||
if (!apduHex) { document.getElementById('sp-result').value = 'Error: specify APDU'; return; }
|
||||
|
||||
@@ -5804,6 +5856,9 @@ async function pysimSendOta() {
|
||||
cApduSwitchSubtab('response');
|
||||
}
|
||||
let msg = 'OTA sent. SW: ' + data.sw;
|
||||
if (data.bytes && data.segments) {
|
||||
msg += ' | ' + data.bytes + ' ' + t('bytes') + ' · ' + data.segments + ' SMS';
|
||||
}
|
||||
const por = data.por;
|
||||
if (por && por.response_status) {
|
||||
msg += ' | PoR status: ' + por.response_status + ' (TAR ' + por.tar + ')';
|
||||
@@ -5821,6 +5876,7 @@ async function pysimSendOta() {
|
||||
cntrEl.value = spNextCntr(cntrEl.value);
|
||||
msg += ' | CNTR -> ' + cntrEl.value;
|
||||
document.getElementById('sp-result').value = '';
|
||||
spShowSizeInfo();
|
||||
// keep the selected card preset in sync with the new counter (v1.9.8)
|
||||
spCntrSyncPreset();
|
||||
sendResultEl.textContent = msg;
|
||||
@@ -6429,7 +6485,9 @@ async function ramInstallCap(sp) {
|
||||
let txt = '';
|
||||
(data.steps || []).forEach((s, idx) => {
|
||||
const mark = s.por_status === 'por_ok' ? '✅' : '❌';
|
||||
txt += mark + ' ' + t('Step') + ' ' + (idx + 1) + ': ' + s.name + ' — ' + s.por_status + ' (SW ' + s.sw + ')\n';
|
||||
let line = mark + ' ' + t('Step') + ' ' + (idx + 1) + ': ' + s.name + ' — ' + s.por_status + ' (SW ' + s.sw + ')';
|
||||
if (s.bytes && s.segments) line += ' · ' + s.bytes + ' ' + t('bytes') + ' / ' + s.segments + ' SMS';
|
||||
txt += line + '\n';
|
||||
});
|
||||
stepsEl.textContent = txt;
|
||||
|
||||
@@ -6438,11 +6496,7 @@ async function ramInstallCap(sp) {
|
||||
let sizeInfo = '';
|
||||
if (data.load_block_size) {
|
||||
sizeInfo = ' — ' + t('LOAD blocks') + ': ' + data.load_block_size + ' B';
|
||||
if (data.load_block_size_clamped) {
|
||||
sizeInfo += ' (' + t('clamped from') + ' ' + data.load_block_size_requested + ')';
|
||||
} else if (!data.load_block_size_requested) {
|
||||
sizeInfo += ' (' + t('auto-fit') + ')';
|
||||
}
|
||||
if (data.load_block_size_auto) sizeInfo += ' (' + t('default') + ')';
|
||||
}
|
||||
resultEl.textContent = t('Install OK') + ' — load_file_aid=' + data.load_file_aid + ' module_aid=' + data.module_aid + sizeInfo;
|
||||
resultEl.classList.remove('hidden', 'text-red-600'); resultEl.classList.add('text-green-600');
|
||||
@@ -13993,12 +14047,13 @@ const LANG_RU = {
|
||||
'Object and related objects': 'Объект и связанные объекты',
|
||||
'SD AID (empty = ISD)': 'AID SD (пусто = ISD)',
|
||||
'CAP file': 'CAP-файл',
|
||||
'LOAD block size (bytes, empty = auto-fit)': 'Размер блока LOAD (байт, пусто = авто)',
|
||||
'Splits the load file into LOAD APDUs that fit one SCP80 SMS; auto-fit picks the largest size that encodes into 140 octets.': 'Разбивает load-файл на LOAD APDU, помещающиеся в один SMS SCP80; авто-подбор выбирает максимальный размер, укладывающийся в 140 октетов.',
|
||||
'LOAD block size (bytes, empty = 240)': 'Размер блока LOAD (байт, пусто = 240)',
|
||||
'default': 'по умолчанию',
|
||||
'concatenated': 'конкатенация',
|
||||
'too large for the card concatenation buffer': 'слишком велик для буфера конкатенации карты',
|
||||
'Splits the load file into LOAD APDUs (1-240 bytes of payload). A secured packet larger than one SMS is sent as a concatenated SMS-PP download (up to 5 SMs).': 'Разбивает load-файл на LOAD APDU (полезная нагрузка 1–240 байт). Защищённый пакет больше одного SMS отправляется как конкатенированная SMS-PP загрузка (до 5 SMS).',
|
||||
'LOAD block size must be 1..240': 'Размер блока LOAD должен быть в диапазоне 1..240',
|
||||
'LOAD blocks': 'Блоки LOAD',
|
||||
'clamped from': 'ограничено с',
|
||||
'auto-fit': 'авто',
|
||||
'Redirect to external server': 'Перенаправление на внешний сервер',
|
||||
'Pass-through (card destination)': 'Проброс (адрес карты)',
|
||||
'Redirect: every BIP channel the card opens is connected to this Host:Port (the external HTTP OTA platform); TLS is terminated there, and the address the card requests is only logged.': 'Перенаправление: каждый открываемый картой BIP-канал подключается к этому Host:Port (внешняя платформа HTTP OTA); TLS завершается там, а запрошенный картой адрес только журналируется.',
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'simple-v239';
|
||||
const CACHE = 'simple-v240';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
@@ -28,9 +28,14 @@ function extractFunc(src, name) {
|
||||
|
||||
const FNS = ['hexToBytes', 'bytesToHex', 'des3Keys', 'des3EncryptBlock', 'des3CbcEncrypt',
|
||||
'xorBytes', 'zeroPad', 'cbcMac', 'aesCbcEncrypt', 'aesShiftLeft1', 'aesCmacSubkeys',
|
||||
'aesCmac', 'genSp', 'spNextCntr'];
|
||||
'aesCmac', '_genSpBuild', 'genSp', 'spNextCntr', 'scp80SegmentInfo', 'spSizeInfoText',
|
||||
'spShowSizeInfo'];
|
||||
let code = '';
|
||||
for (const f of FNS) code += extractFunc(html, f) + '\n';
|
||||
for (const c of ['SCP80_MAX_SMS', 'SCP80_SINGLE_BYTES', 'SCP80_FIRST_BYTES', 'SCP80_NEXT_BYTES']) {
|
||||
code += html.match(new RegExp('const ' + c + ' = \\d+;'))[0].replace('const ', 'var ') + '\n';
|
||||
}
|
||||
code += 'function t(s){return s;}\n';
|
||||
|
||||
// All test vectors are computed with the synthetic dummy key material below
|
||||
// (no live/sample card keys, no ICCIDs). They are cross-checked byte-for-byte
|
||||
@@ -229,3 +234,39 @@ test('spNextCntr tolerates lower case and separators', () => {
|
||||
assert.strictEqual(spNextCntr('00000000 0a'), '000000000B');
|
||||
assert.strictEqual(spNextCntr(''), '0000000001');
|
||||
});
|
||||
|
||||
test('scp80SegmentInfo mirrors the server segmentation rules', () => {
|
||||
assert.deepStrictEqual(scp80SegmentInfo(''), { bytes: 0, segments: 0 });
|
||||
assert.deepStrictEqual(scp80SegmentInfo('AA'.repeat(137)), { bytes: 137, segments: 1 });
|
||||
assert.deepStrictEqual(scp80SegmentInfo('AA'.repeat(138)), { bytes: 138, segments: 2 });
|
||||
assert.deepStrictEqual(scp80SegmentInfo('AA'.repeat(132 + 134)), { bytes: 266, segments: 2 });
|
||||
assert.deepStrictEqual(scp80SegmentInfo('AA'.repeat(132 + 134 * 2)), { bytes: 400, segments: 3 });
|
||||
});
|
||||
|
||||
test('spSizeInfoText reports size, SMS count and the card buffer limit', () => {
|
||||
assert.strictEqual(spSizeInfoText('AA'.repeat(18)), '18 bytes · 1 SMS');
|
||||
assert.strictEqual(spSizeInfoText('AA'.repeat(266)), '266 bytes · 2 SMS (concatenated)');
|
||||
assert.match(spSizeInfoText('AA'.repeat(132 + 134 * 5)),
|
||||
/too large for the card concatenation buffer \(5 SMS\)/);
|
||||
assert.strictEqual(spSizeInfoText(''), '');
|
||||
});
|
||||
|
||||
test('the SCP80 UI constants match the server segmentation constants', () => {
|
||||
const py = fs.readFileSync(
|
||||
path.join(__dirname, '..', '..', 'pysim_simple_server', 'server.py'), 'utf8');
|
||||
const read = (name) => {
|
||||
const m = new RegExp('^' + name + '\\s*=\\s*(\\d+)', 'm').exec(py);
|
||||
assert.ok(m, name + ' not found in server.py');
|
||||
return parseInt(m[1], 10);
|
||||
};
|
||||
assert.strictEqual(SCP80_SINGLE_BYTES, read('SCP80_SINGLE_BYTES'));
|
||||
assert.strictEqual(SCP80_FIRST_BYTES, read('SCP80_FIRST_BYTES'));
|
||||
assert.strictEqual(SCP80_NEXT_BYTES, read('SCP80_NEXT_BYTES'));
|
||||
assert.strictEqual(SCP80_MAX_SMS, read('MAX_ENVELOPE_SEGMENTS'));
|
||||
});
|
||||
|
||||
test('a generated single-SMS packet shows its size and SMS count', () => {
|
||||
const pkt = makeRun({});
|
||||
const info = elements['sp-size-info'] || {};
|
||||
assert.strictEqual(info.textContent, (pkt.length / 2) + ' bytes · 1 SMS');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user