From c18b6cd0d42a4d1246076c2c8b6b57711bd08b01 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 14:34:47 +0300
Subject: [PATCH] v3.14.0: event form object-set fixes + the slices
single-envelope mandate
An audit of every event form against the pinned specs (31.111 7.5.x/9.3,
102 223 8.87, TS 101 220's master tag table) found three missing objects,
three malformed/wrong ones, and settled the slice-data scope.
- 0x00 MT call: the IMS URI object is cut at 100 bytes and now carries the
URI truncated object (8.135, F3 00) when the cut happened.
- 0x1F Slices: the form gains the Partial NSSAI object (8.154, F9, rel 18);
a single envelope carrying it is marked as the Last Envelope (8.79,
F0 00) automatically (7.5.27.2).
- 0x06 Card reader status and 0x07 Language: the builders now emit their
object tags (A0 01 ..., AD ...) - they emitted bare values before.
- 0x18 Incoming IMS data: the IARI tag is F6 (76/F6), not F7 (which is the
IMPU list tag - the two-column 9.3 table had been misread).
- Single-envelope mandate: the event data is capped at 244 bytes (inner 252;
the D6 long-form length and the APDU Lc stay in one byte). The server
refuses more with a 400 and the PWA shows the size error before sending.
Chained (multi-envelope) delivery, the More Data object and envelope
sequences are not implemented - documented in the help and the repo AGENTS.
- Tests: event form vectors (URI truncation, the restored tags, partial /
Last Envelope, the size helper) and the server guard; the release
reference and its PWA/code mirrors gain the three objects and the
corrected IARI key.
- sw cache simple-v375.
---
frontend/help-ru.html | 4 +--
frontend/help.html | 4 +--
frontend/index.html | 52 ++++++++++++++++++++++++++----
frontend/sw.js | 2 +-
frontend/tests/event_forms.test.js | 28 ++++++++++++++--
pyproject.toml | 2 +-
pysim_simple_server/catrelease.py | 5 ++-
pysim_simple_server/server.py | 19 ++++++++++-
tests/test_ota_helpers.py | 14 ++++++++
9 files changed, 114 insertions(+), 16 deletions(-)
diff --git a/frontend/help-ru.html b/frontend/help-ru.html
index 4217da0..2eeafa9 100644
--- a/frontend/help-ru.html
+++ b/frontend/help-ru.html
@@ -507,11 +507,11 @@
События звонка — MT call (transaction identifier, номер вызывающего + TON → Address, subaddress, IMS URI, media type; источник network), Call connected и Call disconnected (transaction identifier, источник near/far, media type и причина — отсутствует, нулевая форма radio link timeout или свои байты), а также необязательная причина IMS-разрыва (SIP / Q.850 + cause)
Browser termination (причина user / error), (I-)WLAN access status (нет покрытия / покрытие / соединение) и CSG cell selection (статус + механизм выбора, access technology, CSG ID, HNB name, PLMN ID; источник network)
Channel status (канал + состояние звена + инфо, тег B8, с необязательными bearer description B5 и local address BE) и Data available (тот же channel status + длина данных канала, тег B7), Display parameters changed (строки/столбцы + флаги sizing/fonts/effects, тег C0), Network search mode change (manual/automatic, тег E5), Browsing status (код ошибки браузера, тег E4) и Frames information change (фрейм по умолчанию + инфо по фреймам, тег E7)
- IMS registration (IMPU URI + IMS status code) и Incoming IMS data (IARI) — источник network; CAG cell selection (статус + механизм, access technology, CAG information list + HRNN list) и Slices status change (allowed / rejected / served / partially, access technology, списки allowed / served / rejected S-NSSAI, с маппингом и без) — источник network
+ IMS registration (IMPU URI + IMS status code) и Incoming IMS data (IARI) — источник network; CAG cell selection (статус + механизм, access technology, CAG information list + HRNN list) и Slices status change (allowed / rejected / served / partially, access technology, списки allowed / served / rejected S-NSSAI, с маппингом и без, partial NSSAI) — источник network
Data Connection Status Change (0x1D) — статус (successful / rejected / dropped-deactivated), тип соединения (PDP / PDN / PDU), необязательная причина (E/5G)SM, transaction identifier и location status, а также необязательные date-time, access technology, APN, тип PDP/PDN/PDU и location information (MCC/MNC + LAC + Cell ID) — TS 131 111 §7.5.25
Poll Interval Negotiation (0x1C) — предлагает интервал опроса в виде Duration (единица + значение); после отправки показывается ответ UICC (принято / отклонено / изменено + длительность), а значение изменено становится интервалом фонового опроса (TS 102 223 §7.5.22). Форма появляется, когда карта подписана на это событие.
-
Отправка события использует ENVELOPE(Event Download) по TS 102 223 / TS 131 111. Форма скрывает параметры, появившиеся после действующего релиза CAT — релиза пресета подключённой карты (совпадение по ICCID) или Rel-12, если он не указан, — а примечание под формой называет источник и то, что было скрыто (релиз задаётся на вкладке «Карты»).
+ Отправка события использует ENVELOPE(Event Download) по TS 102 223 / TS 131 111. Форма скрывает параметры, появившиеся после действующего релиза CAT — релиза пресета подключённой карты (совпадение по ICCID) или Rel-12, если он не указан, — а примечание под формой называет источник и то, что было скрыто (релиз задаётся на вкладке «Карты»). Информация о срезах отправляется в одном ENVELOPE (не более 252 байт внутренних данных) — доставка цепочкой из нескольких ENVELOPE не реализована, поэтому данные должны помещаться в один конверт; конверт с partial NSSAI автоматически помечается как Last Envelope.
8.3 Журнал проактивных команд
Хронологический список извлечённых проактивных команд. Каждая строка показывает время, код типа, имя и декодированный квалификатор (для команд, у которых он есть). Раскрытие строки показывает декодированную команду: текст и пункты DISPLAY TEXT / SELECT ITEM / SET UP MENU, параметры BIP-каналов, действия TIMER MANAGEMENT, названия квалификаторов PROVIDE LOCAL INFORMATION (все стандартные, включая ESN 07, MEID 0B и Supported RATs 1A) и SEND SHORT MESSAGE — разбирается SMS TPDU (тип, TP-MR, TP-DA, TP-PID, TP-DCS, срок действия, TP-UDL), а пользовательские данные показываются как текст для текстовых кодировок (GSM 7-бит, UCS2, 8-бит) или как secured-пакет (TS 31.115) при TP-PID = 7F (SIM data download); заголовки конкатенации UDH декодируются. Для команд с данными ответа показывается строка Ответ: с байтами TERMINAL RESPONSE (без служебных TLV); ответы PROVIDE LOCAL INFORMATION декодируются через словарь данных PLI.
diff --git a/frontend/help.html b/frontend/help.html
index 987afb6..eb3591d 100644
--- a/frontend/help.html
+++ b/frontend/help.html
@@ -506,11 +506,11 @@
Call events — MT call (transaction identifier, calling party number + TON → Address, subaddress, IMS URI, media type; source network), Call connected and Call disconnected (transaction identifier, near/far-end source, media type, and a cause — absent, the zero-length radio-link-timeout form, or custom bytes), plus the optional IMS disconnection cause (SIP / Q.850 + cause)
Browser termination (user / error cause), (I-)WLAN access status (no coverage / coverage / connection on-going) and CSG cell selection (status + selection mechanism, access technology, CSG ID, HNB name, PLMN ID; source network)
Channel status (channel + link state + channel info, tag B8, with an optional bearer description B5 and local address BE) and Data available (the same channel status plus the channel data length, tag B7), Display parameters changed (rows/columns + sizing/fonts/effects flags, tag C0), Network search mode change (manual/automatic, tag E5), Browsing status (browser error code, tag E4) and Frames information change (default frame + per-frame info, tag E7)
- IMS registration (IMPU URIs + IMS status code) and Incoming IMS data (IARI) — source network; CAG cell selection (status + mechanism, access technology, CAG information list + HRNN list) and Slices status change (allowed / rejected / served / partially status, access technology, allowed / served / rejected S-NSSAI lists, with and without mapping) — source network
+ IMS registration (IMPU URIs + IMS status code) and Incoming IMS data (IARI) — source network; CAG cell selection (status + mechanism, access technology, CAG information list + HRNN list) and Slices status change (allowed / rejected / served / partially status, access technology, allowed / served / rejected S-NSSAI lists, with and without mapping, partial NSSAI) — source network
Data Connection Status Change (0x1D) — status (successful / rejected / dropped-deactivated), connection type (PDP / PDN / PDU), optional (E/5G)SM cause, transaction identifier and location status, plus optional date-time, access technology, APN, PDP/PDN/PDU type and location information (MCC/MNC + LAC + Cell ID) — TS 131 111 §7.5.25
Poll Interval Negotiation (0x1C) — proposes a poll interval as a Duration (unit + value); after sending, the UICC's answer is shown (accepted / rejected / modified + duration), and a modified duration becomes the background poll interval (TS 102 223 §7.5.22). The form appears when the card subscribed to this event.
- Sending an event uses ENVELOPE(Event Download) per TS 102 223 / TS 131 111. The form hides the parameters introduced after the effective CAT release — the equipped card preset's CAT release (matched by ICCID), or Rel-12 when unspecified — and a note under the form names the source and what was hidden (the release is set in the Cards tab).
+ Sending an event uses ENVELOPE(Event Download) per TS 102 223 / TS 131 111. The form hides the parameters introduced after the effective CAT release — the equipped card preset's CAT release (matched by ICCID), or Rel-12 when unspecified — and a note under the form names the source and what was hidden (the release is set in the Cards tab). Slice information is sent in a single ENVELOPE (max 252 bytes of inner data) — chained (multi-envelope) delivery is not implemented, so the data must fit one envelope; an envelope carrying the partial NSSAI is marked as the Last Envelope automatically.
8.3 Proactive command log
Chronological list of fetched proactive commands. Each row shows the elapsed time, type code, name, and a decoded qualifier (for commands that have one). Expanding a row shows the decoded command: DISPLAY TEXT / SELECT ITEM / SET UP MENU text and items, BIP channel parameters, TIMER MANAGEMENT actions, PROVIDE LOCAL INFORMATION qualifier names (all standard qualifiers, including ESN 07, MEID 0B and Supported RATs 1A), and SEND SHORT MESSAGE — the SMS TPDU is parsed (type, TP-MR, TP-DA, TP-PID, TP-DCS, validity period, TP-UDL) with the user data shown as text for text codings (GSM 7-bit, UCS2, 8-bit) or as a secured packet (TS 31.115) when TP-PID = 7F (SIM data download); UDH concatenation headers are decoded. Commands with response data show a Response: line with the TERMINAL RESPONSE bytes (boilerplate TLVs stripped); PROVIDE LOCAL INFORMATION responses are decoded using the PLI data dictionary decoders.
diff --git a/frontend/index.html b/frontend/index.html
index 3df769e..c604979 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.4';
+const SIMPLE_VERSION = '3.14.0';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== Tab switching =====
@@ -9136,10 +9136,11 @@ const CAT_RELEASE = {
'56/D6 CSG ID': 9,
'57/D7 HNB name': 9,
'31/B1 IMS URI': 10,
- '77/F7 IARI': 10,
+ '76/F6 IARI': 10,
'77/F7 IMPU list': 11,
'78/F8 IMS status code': 11,
'7E/FE Media Type': 12,
+ '73/F3 URI truncated': 12,
'09/89 PLMN ID': 4,
'3F/BF Access technology': 4,
'B4 Supported Radio Access Technologies': 13,
@@ -9159,6 +9160,8 @@ const CAT_RELEASE = {
'77/F7 Allowed slices with S-NSSAI mapping': 18,
'D7 Rejected slices with S-NSSAI mapping': 18,
'B1 Rejected slices information': 18,
+ '79/F9 Partial NSSAI': 18,
+ '70/F0 Last Envelope': 18,
},
pli: {
'00': 4, '01': 4, '02': 4, '03': 4, '04': 4, '06': 4, '07': 4, '08': 6,
@@ -13897,8 +13900,12 @@ const EVENT_FORMS = {
d += '88' + (h.length / 2).toString(16).padStart(2, '0') + h;
}
if (v.ims_uri) {
- const b = new TextEncoder().encode(v.ims_uri).slice(0, 100);
+ let b = new TextEncoder().encode(v.ims_uri);
+ const truncated = b.length > 100;
+ b = b.slice(0, 100);
d += 'B1' + b.length.toString(16).padStart(2, '0') + bytesToHex(Array.from(b));
+ // 8.135: the object is included when the URI was truncated
+ if (truncated) d += 'F300';
}
if (v.media !== '' && v.media !== undefined)
d += 'FE01' + ((parseInt(v.media) || 0) & 0xFF).toString(16).padStart(2, '0');
@@ -13973,13 +13980,13 @@ const EVENT_FORMS = {
{ id: 'status', label: 'Status', type: 'select',
opts: [{v:0,l:'Card reader removed'},{v:1,l:'Card reader inserted'}] }
],
- build(v) { return '' + (v.status|0).toString(16).padStart(2,'0'); }
+ build(v) { return 'A001' + ((parseInt(v.status) || 0) & 0xFF).toString(16).padStart(2,'0'); }
},
0x07: { title: 'Language selection',
fields: [
{ id: 'lang', label: 'Language code', type: 'text', placeholder: 'ISO 639, e.g. en, ru', pattern: '[a-zA-Z]{2,3}' }
],
- build(v) { const b = new TextEncoder().encode(v.lang||'en'); return b.length.toString(16).padStart(2,'0') + bytesToHex(Array.from(b)); }
+ build(v) { const b = new TextEncoder().encode(v.lang||'en'); return 'AD' + b.length.toString(16).padStart(2,'0') + bytesToHex(Array.from(b)); }
},
0x08: { title: 'Browser termination',
fields: [
@@ -14240,7 +14247,7 @@ const EVENT_FORMS = {
build(v) {
if (!v.iari) return '';
const b = new TextEncoder().encode(v.iari).slice(0, 100);
- return ('F7' + b.length.toString(16).padStart(2, '0') + bytesToHex(Array.from(b))).toUpperCase();
+ return ('F6' + b.length.toString(16).padStart(2, '0') + bytesToHex(Array.from(b))).toUpperCase();
}
},
0x1C: { title: 'Poll interval negotiation',
@@ -14357,6 +14364,7 @@ const EVENT_FORMS = {
{ id: 'allowed', label: 'Allowed slices (optional, hex)', type: 'text', pattern: '[0-9A-Fa-f]{8,80}', placeholder: '4 bytes per S-NSSAI (SST + SD)' },
{ id: 'rejected_map', label: 'Rejected slices with S-NSSAI mapping (optional, hex)', type: 'text', pattern: '[0-9A-Fa-f]{2,80}', placeholder: 'TS 24.501 elements + rejection cause', rel: 18 },
{ id: 'rejected', label: 'Rejected slices (optional, hex)', type: 'text', pattern: '[0-9A-Fa-f]{2,80}', placeholder: 'TS 24.501 rejected S-NSSAI elements', rel: 18 },
+ { id: 'partial', label: 'Partial NSSAI (optional, hex)', type: 'text', pattern: '[0-9A-Fa-f]{2,80}', placeholder: 'TS 24.501 partial NSSAI elements', rel: 18 },
],
build(v) {
let d = '';
@@ -14377,6 +14385,11 @@ const EVENT_FORMS = {
list(v.allowed, 'F8');
list(v.rejected_map, 'D7');
list(v.rejected, 'B1');
+ list(v.partial, 'F9');
+ // 7.5.27.2: a single envelope carrying the partial NSSAI is the
+ // last one - the chaining marker goes with it (multi-envelope
+ // delivery is not implemented; the data must fit one envelope)
+ if ((v.partial || '').replace(/[^0-9a-fA-F]/g, '')) d += 'F000';
return d.toUpperCase();
}
},
@@ -14799,6 +14812,24 @@ function eventFieldsForRelease(cfg, rel) {
return { fields: fields, hidden: hidden };
}
+// The single-ENVELOPE budget: the inner data (event list + device identities +
+// the event objects) is wrapped as D6 81 + inner, and the APDU Lc must
+// stay within one byte (255) - so the inner data is capped at 252 bytes
+// (8 header bytes + at most 244 bytes of event data). Chained
+// (multi-envelope) delivery is not implemented: the slice/NSSAI data must fit
+// one envelope (see the help).
+const EVENT_INNER_MAX = 252;
+
+// The ENVELOPE inner size of an event-data hex string (pure).
+function eventEnvelopeSize(eventDataHex) {
+ return 8 + Math.floor((eventDataHex || '').length / 2);
+}
+
+// Whether the event data fits one ENVELOPE (pure).
+function eventFitsOneEnvelope(eventDataHex) {
+ return eventEnvelopeSize(eventDataHex) <= EVENT_INNER_MAX;
+}
+
function pysimEventSendForm(eventType) {
const modal = document.getElementById('event-send-modal');
const title = document.getElementById('event-send-title');
@@ -14925,6 +14956,13 @@ async function pysimEventSend() {
err.classList.remove('hidden');
return;
}
+ if (eventData && !eventFitsOneEnvelope(eventData)) {
+ err.textContent = t('The event data does not fit one ENVELOPE') + ' (' +
+ eventEnvelopeSize(eventData) + ' > ' + EVENT_INNER_MAX + ' B). ' +
+ t('Multi-envelope (chained) delivery is not implemented - reduce the slice lists.');
+ err.classList.remove('hidden');
+ return;
+ }
}
btn.disabled = true;
@@ -20738,6 +20776,8 @@ const LANG_RU = {
'Event sent.': 'Событие отправлено.',
'Event data sent.': 'Данные события отправлены.',
'Sending...': 'Отправка...',
+ 'The event data does not fit one ENVELOPE': 'Данные события не помещаются в один ENVELOPE',
+ 'Multi-envelope (chained) delivery is not implemented - reduce the slice lists.': 'Доставка в несколько ENVELOPE (цепочкой) не реализована — сократите списки срезов.',
'Yes': 'Да',
'No': 'Нет',
'Help': 'Справка',
diff --git a/frontend/sw.js b/frontend/sw.js
index 0e0e25d..8e4241c 100644
--- a/frontend/sw.js
+++ b/frontend/sw.js
@@ -1,4 +1,4 @@
-const CACHE = 'simple-v374';
+const CACHE = 'simple-v375';
const URLS = [
'index.html',
'help.html',
diff --git a/frontend/tests/event_forms.test.js b/frontend/tests/event_forms.test.js
index f308d19..0b87745 100644
--- a/frontend/tests/event_forms.test.js
+++ b/frontend/tests/event_forms.test.js
@@ -34,6 +34,7 @@ eval(extractFunc(html, 'cmdQualifierShort'));
eval(extractBlock('const EVENT_NAMES = {', 'const REJECTION_CAUSES = [').replace(/^const /gm, 'var '));
eval(extractBlock('const REJECTION_CAUSES = [', 'const EVENT_FORMS = {').replace(/^const /gm, 'var '));
eval(extractBlock('const EVENT_FORMS = {', 'const PLI_QUALIFIERS = [').replace(/^const /gm, 'var '));
+eval(extractBlock('const EVENT_INNER_MAX = ', 'function pysimEventSendForm').replace(/^const /gm, 'var '));
eval(extractFunc(html, 'eventFieldsForRelease'));
eval(extractFunc(html, 'encPlmn'));
eval(extractFunc(html, 'bytesToHex'));
@@ -172,6 +173,11 @@ test('the call events build their 31.111 object sets', () => {
// call disconnected: radio link timeout = zero-length cause, custom = bytes
assert.strictEqual(EVENT_FORMS[0x02].build({ ti: '00', cause_mode: 'rlt', media: '' }), '1C01009A00');
assert.strictEqual(EVENT_FORMS[0x02].build({ ti: '00', cause_mode: 'custom', cause: '10', media: '' }), '1C01009A0110');
+ // a URI longer than the 100-byte limit is cut and tagged (8.135)
+ const long = EVENT_FORMS[0x00].build({ ti: '00', ims_uri: 'x'.repeat(120) });
+ assert.ok(long.includes('B164' + '78'.repeat(100)), 'the URI is cut to 100 bytes');
+ assert.ok(long.endsWith('F300'), long.slice(-8));
+ assert.ok(!EVENT_FORMS[0x00].build({ ti: '00', ims_uri: 'sip:a@b' }).includes('F300'));
});
test('the browser, WLAN and CSG events build their object sets', () => {
@@ -199,6 +205,9 @@ test('the data/display/search/frames events build their object sets', () => {
assert.strictEqual(EVENT_FORMS[0x0F].build({ status: '12' }), 'E40112');
assert.strictEqual(EVENT_FORMS[0x0F].build({ status: '' }), 'E40100');
assert.strictEqual(EVENT_FORMS[0x10].build({ frame: '01', list: '0410' }), 'E703010410');
+ // Card reader status (8.33) and Language (8.45) carry their object tags
+ assert.strictEqual(EVENT_FORMS[0x06].build({ status: '1' }), 'A00101');
+ assert.strictEqual(EVENT_FORMS[0x07].build({ lang: 'en' }), 'AD02656E');
});
test('the IMS, CAG and slices events build their object sets', () => {
@@ -207,7 +216,7 @@ test('the IMS, CAG and slices events build their object sets', () => {
assert.strictEqual(EVENT_FORMS[0x17].build({ impu: 'sip:a@b', code: '200' }),
'F70980077369703A614062F803323030');
assert.strictEqual(EVENT_FORMS[0x18].src, '83');
- assert.strictEqual(EVENT_FORMS[0x18].build({ iari: 'a' }), 'F70161');
+ assert.strictEqual(EVENT_FORMS[0x18].build({ iari: 'a' }), 'F60161');
// CAG cell selection: tech + status/mechanism + information list
assert.strictEqual(EVENT_FORMS[0x1E].src, '83');
assert.strictEqual(EVENT_FORMS[0x1E].build({ status: '1', mech: 'manual', tech: '8', list: '00000001' }),
@@ -216,6 +225,9 @@ test('the IMS, CAG and slices events build their object sets', () => {
assert.strictEqual(EVENT_FORMS[0x1F].src, '83');
assert.strictEqual(EVENT_FORMS[0x1F].build({ status: '1', tech: '8', served: '01020304' }),
'BF0108D50101D6050101020304');
+ // partial NSSAI: the F9 object + the automatic Last Envelope marker
+ assert.strictEqual(EVENT_FORMS[0x1F].build({ status: '1', partial: '0102' }), 'D50101F9020102F000');
+ assert.ok(!EVENT_FORMS[0x1F].build({ status: '1', served: '01020304' }).includes('F000'));
});
test('the location-status event can carry the location information', () => {
@@ -265,10 +277,22 @@ test('event fields introduced after the effective release are hidden', () => {
assert.deepStrictEqual(r.hidden.map(f => f.id).sort(), ['code', 'impu']);
// slices: the mapping/rejected objects came in Rel-18
r = eventFieldsForRelease(EVENT_FORMS[0x1F], 16);
- assert.deepStrictEqual(r.hidden.map(f => f.id).sort(), ['allowed_map', 'rejected', 'rejected_map']);
+ assert.deepStrictEqual(r.hidden.map(f => f.id).sort(), ['allowed_map', 'partial', 'rejected', 'rejected_map']);
assert.strictEqual(eventFieldsForRelease(EVENT_FORMS[0x1F], 18).hidden.length, 0);
// a field without a known release is never hidden
r = eventFieldsForRelease(EVENT_FORMS[0x12], 4);
assert.strictEqual(r.hidden.length, 0);
assert.strictEqual(r.fields.length, EVENT_FORMS[0x12].fields.length);
});
+
+test('the single-envelope budget is enforced by the size helper', () => {
+ // inner = 8 header bytes + the event data; D6 81 + inner must stay
+ // within the 1-byte APDU Lc, so the inner data is capped at 252 bytes
+ assert.strictEqual(EVENT_INNER_MAX, 252);
+ assert.strictEqual(eventEnvelopeSize(''), 8);
+ assert.strictEqual(eventEnvelopeSize('AABB'), 10);
+ assert.strictEqual(eventEnvelopeSize('AB'.repeat(244)), 252);
+ assert.ok(eventFitsOneEnvelope('AB'.repeat(244)));
+ assert.ok(!eventFitsOneEnvelope('AB'.repeat(245)));
+ assert.ok(eventFitsOneEnvelope(null));
+});
diff --git a/pyproject.toml b/pyproject.toml
index f84dc67..6ba132d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "pysim-simple-server"
-version = "3.13.4"
+version = "3.14.0"
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/catrelease.py b/pysim_simple_server/catrelease.py
index 587f1d9..478a9f1 100644
--- a/pysim_simple_server/catrelease.py
+++ b/pysim_simple_server/catrelease.py
@@ -73,10 +73,11 @@ CAT_RELEASE = {
'56/D6 CSG ID': 9,
'57/D7 HNB name': 9,
'31/B1 IMS URI': 10,
- '77/F7 IARI': 10,
+ '76/F6 IARI': 10,
'77/F7 IMPU list': 11,
'78/F8 IMS status code': 11,
'7E/FE Media Type': 12,
+ '73/F3 URI truncated': 12,
'09/89 PLMN ID': 4,
'3F/BF Access technology': 4,
'B4 Supported Radio Access Technologies': 13,
@@ -96,6 +97,8 @@ CAT_RELEASE = {
'77/F7 Allowed slices with S-NSSAI mapping': 18,
'D7 Rejected slices with S-NSSAI mapping': 18,
'B1 Rejected slices information': 18,
+ '79/F9 Partial NSSAI': 18,
+ '70/F0 Last Envelope': 18,
},
'pli': {
'00': 4, '01': 4, '02': 4, '03': 4, '04': 4, '06': 4, '07': 4, '08': 6,
diff --git a/pysim_simple_server/server.py b/pysim_simple_server/server.py
index ac08996..076abc1 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.4'
+VERSION = '3.14.0'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
@@ -4595,11 +4595,24 @@ def _verify_adm(scc, app, adm_hex):
'error': 'Security status not satisfied' if sw == '6982' else 'Error'}
+# The single-ENVELOPE budget for event data: the inner data (event list +
+# device identities + the event objects) is wrapped as D6 81 + inner,
+# and the APDU Lc must stay within one byte (255), so the inner data is
+# capped at 252 bytes (244 bytes of event data + the 8-byte header).
+# Chained (multi-envelope) delivery is not implemented - the slice/S-NSSAI
+# data must fit one envelope (see the help).
+_EVENT_INNER_MAX = 252
+_EVENT_DATA_MAX = _EVENT_INNER_MAX - 8
+
+
def _send_event_download(scc, event_type, event_data=None, drain=True, src=None):
"""Send ENVELOPE(Event Download) for the given event type.
Builds: CLA C2 0000 Lc D6 [len] (99 01 [type] 82 02 81 [extra]).
`src` is the source device identity (TS 102 223 8.7: '82' terminal,
'83' network); the default is the terminal."""
+ if event_data and len(event_data) > _EVENT_DATA_MAX:
+ raise ValueError('event data does not fit one ENVELOPE (max %d bytes; '
+ 'chained delivery is not implemented)' % _EVENT_DATA_MAX)
src_byte = 0x82
if src:
try:
@@ -7205,6 +7218,10 @@ class PysimHandler(BaseHTTPRequestHandler):
resp['net_state'] = self.server.net_state
self._send_json(resp)
self._log_resp(resp)
+ except ValueError as e:
+ # the single-envelope budget (no chained delivery)
+ self._send_json({'error': str(e)}, 400)
+ self._log_resp({'error': str(e)})
except Exception as e:
sys.stderr.write('Event send error: %s\n' % e)
_handle_card_disconnect(stale=_is_transport_fatal(e))
diff --git a/tests/test_ota_helpers.py b/tests/test_ota_helpers.py
index b35f987..ac75da5 100644
--- a/tests/test_ota_helpers.py
+++ b/tests/test_ota_helpers.py
@@ -934,6 +934,20 @@ class TestEventDownload(unittest.TestCase):
d6 = bytes([0xD6, 0x81, len(inner)]) + inner
self.assertEqual(apdu, '80c20000%02x%s' % (len(d6), d6.hex()))
+ def test_event_data_beyond_one_envelope_is_refused(self):
+ # the tool sends one envelope per event: the inner data is capped at
+ # 252 bytes (the D6 long-form length and the APDU Lc stay in one byte);
+ # chained (multi-envelope) delivery is not implemented
+ with self.assertRaises(ValueError) as ctx:
+ self._send(0x1F, bytes(245))
+ self.assertIn('one ENVELOPE', str(ctx.exception))
+ # 244 bytes of event data still fit (inner = 252, the maximum)
+ apdu, sw = self._send(0x1F, bytes(244))
+ self.assertEqual(sw, '9000')
+ inner = bytes([0x99, 0x01, 0x1F, 0x82, 0x02, 0x82, 0x81]) + bytes(244)
+ d6 = bytes([0xD6, 0x81, len(inner)]) + inner
+ self.assertEqual(apdu, '80c20000%02x%s' % (len(d6), d6.hex()))
+
class TestTimerManagement(unittest.TestCase):
"""Terminal side of TIMER MANAGEMENT (TS 102 223 6.6.21, 6.8.13/14, 7.4).