diff --git a/frontend/index.html b/frontend/index.html index 795fc35..eb81e5b 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1414,7 +1414,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 = '2.7.20'; +const SIMPLE_VERSION = '2.7.21'; document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION; // ===== Tab switching ===== @@ -3767,16 +3767,22 @@ function decodePrivileges(hex) { return parts.join(', ') || 'None'; } -function gsm7Decode(octets) { +// GSM 03.38 7-bit unpacking. `septets` (optional) is the exact character +// count — the Network Name coding carries the number of spare bits, so the +// zero padding must not decode as a trailing '@'. A final used as +// padding (TS 23.038 6.1.2.1.1) is removed when the count is known. +function gsm7Decode(octets, septets) { const bits = []; for (const b of octets) { for (let j = 0; j < 8; j++) bits.push((b >> j) & 1); } + const max = Math.floor(bits.length / 7); + const count = septets === undefined ? max : Math.min(septets, max); let out = ''; let escape = false; - for (let i = 0; i + 7 <= bits.length; i += 7) { + for (let i = 0; i < count; i++) { let septet = 0; - for (let j = 0; j < 7; j++) septet |= bits[i + j] << j; + for (let j = 0; j < 7; j++) septet |= bits[i * 7 + j] << j; if (escape) { const ext = Object.entries(GSM7_EXT_MAP).find(([,v]) => v === septet); out += ext ? ext[0] : '?'; @@ -3787,6 +3793,7 @@ function gsm7Decode(octets) { out += septet < GSM7_ALPHABET.length ? GSM7_ALPHABET[septet] : '?'; } } + if (septets !== undefined && out.endsWith('\r')) out = out.slice(0, -1); return out; } @@ -12058,10 +12065,15 @@ function efPlmnAct(u16) { } // TS 23.038 Network Name (DCS byte + text) from EF_PNN. +// TS 24.008 10.5.3.5a Network Name (the EF.PNN 43/45 TLV value): the first +// byte is the coding octet — bit 8 ext, bits 5-7 coding scheme (000 GSM +// 7-bit, 001 UCS2), bit 4 Add CI, bits 1-3 the number of spare bits in the +// last octet (the value is the count, 1-7; 000 = no information). function efPnnText(value) { if (value.length < 2) return ''; const dcs = value[0]; const coding = (dcs >> 4) & 0x07; + const spare = dcs & 0x07; let data = efRstripFf(value.subarray(1)); if (coding === 1) { for (let i = 0; i + 1 < data.length; i += 2) { @@ -12072,7 +12084,11 @@ function efPnnText(value) { if (coding === 0) { let allLow = data.length > 0; for (let i = 0; i < data.length; i++) if (data[i] >= 0x80) { allLow = false; break; } - return allLow ? efGsm7Octets(data) : gsm7Decode(data); + if (allLow) return efGsm7Octets(data); + const nbits = data.length * 8; + const septets = spare > 0 ? Math.floor((nbits - spare) / 7) + : Math.floor(nbits / 7); + return gsm7Decode(data, septets); } return ''; } diff --git a/frontend/sw.js b/frontend/sw.js index 3cbc0d0..02f1981 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simple-v223'; +const CACHE = 'simple-v224'; const URLS = [ 'index.html', 'help.html', diff --git a/frontend/tests/ef_decode.test.js b/frontend/tests/ef_decode.test.js index affd86d..b421641 100644 --- a/frontend/tests/ef_decode.test.js +++ b/frontend/tests/ef_decode.test.js @@ -73,6 +73,28 @@ test('efAnnexA decodes GSM7 octets, UCS2 and packed variants', () => { assert.strictEqual(efAnnexA(efBytes('FFFFFFFF')), ''); }); +test('efPnnText uses the spare-bit count (TS 24.008 10.5.3.5a)', () => { + // Live card EF.PNN records: coding octet + packed GSM-7 text. The spare + // bits in the coding octet give the exact character count, so the zero + // padding must not decode as a trailing '@' (record 2 was 'Miranda@'). + const cases = [ + ['8441b6390c', 'Alfa'], + ['87cdb43cec268701', 'Miranda'], + ['83d7b41b', 'Win'], + ['83cdb41c442db3cbeb771b', 'Mir Telekom'], + ['82ab1b885a6697d7ef36', '+7 Telekom'], + ['85c6b23b8d07', 'Fenix'], + ['83cde514', 'MKS'], + ]; + for (const [hex, text] of cases) { + assert.strictEqual(efPnnText(efBytes(hex)), text, hex); + } + // UCS2 coding scheme (octet 3 bits 5-7 = 001) + assert.strictEqual(efPnnText(efBytes('91004d006900720061006e00640061')), 'Miranda'); + // a final used as padding is removed (TS 23.038 6.1.2.1.1) + assert.strictEqual(gsm7Decode(efBytes('cdb43cec26871b'), 8), 'Miranda'); +}); + test('EF decoders match the pinned spec / pySim test vectors', () => { assert.deepStrictEqual(efDecIccid(efBytes('988812010000400310f0')), { iccid: '8988211000000430010' }); assert.deepStrictEqual(efDecImsi(efBytes('082982608200002080')), { imsi: '228062800000208' }); diff --git a/pyproject.toml b/pyproject.toml index d7ed51c..c613970 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pysim-simple-server" -version = "2.7.20" +version = "2.7.21" 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 d3f03d1..cbd86b3 100644 --- a/pysim_simple_server/server.py +++ b/pysim_simple_server/server.py @@ -26,7 +26,7 @@ from osmocom.tlv import BER_TLV_IE from osmocom.utils import rpad -VERSION = '2.7.20' +VERSION = '2.7.21' MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE