0fd91d7771
Parser + BER constructor (TS 102 223 / TS 102 226): - DCS byte: genBerPcValue prepends DCS to 8D (00=GSM7, 08=UCS2, 04=unpacked) - decodeTextData: consume DCS first, legacy no-DCS fallback kept - BER_QUAL.display['80']: 'Clear after delay' -> 'Wait for user to clear message' - BER_QUAL.tone: 'Monotonous/Alternating' -> 'Vibrate alert' names - describeProactiveCommand: map type byte -> correct BER_QUAL key (01->refresh, 21->display, 20->tone); proper command names - parseActionRow: values 01-7F -> 'Reference to EFRMA record' - parseOneApdu: CLA 84-87 -> 'GlobalPlatform (secure messaging)' - PARSE_INS: added GET RESPONSE (INS C0) Version: 1.8.1 -> 1.9.0, SW cache otaman-v9 -> otaman-v10
280 lines
9.7 KiB
JavaScript
280 lines
9.7 KiB
JavaScript
const { test } = require('node:test');
|
|
const assert = require('node:assert');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
|
|
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
|
|
|
|
function extractFunc(src, name) {
|
|
const re = new RegExp('function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{');
|
|
const m = re.exec(src);
|
|
if (!m) throw new Error('function ' + name + ' not found');
|
|
let i = m.index + m[0].length - 1;
|
|
let depth = 0;
|
|
for (; i < src.length; i++) {
|
|
if (src[i] === '{') depth++;
|
|
else if (src[i] === '}') {
|
|
depth--;
|
|
if (depth === 0) break;
|
|
}
|
|
}
|
|
return src.slice(m.index, i + 1);
|
|
}
|
|
|
|
class StubEl {
|
|
constructor(value = '') {
|
|
this.value = value;
|
|
this.checked = false;
|
|
this.disabled = false;
|
|
this.children = [];
|
|
this.parentElement = null;
|
|
this.nextElementSibling = null;
|
|
}
|
|
querySelector(sel) {
|
|
return this._find(sel);
|
|
}
|
|
querySelectorAll(sel) {
|
|
const out = [];
|
|
this._findAll(sel, out);
|
|
return out;
|
|
}
|
|
_find(sel) {
|
|
for (const c of this.children) {
|
|
if (c._matches(sel)) return c;
|
|
const r = c._find(sel);
|
|
if (r) return r;
|
|
}
|
|
return null;
|
|
}
|
|
_findAll(sel, out) {
|
|
for (const c of this.children) {
|
|
if (c._matches(sel)) out.push(c);
|
|
c._findAll(sel, out);
|
|
}
|
|
}
|
|
_matches(sel) {
|
|
if (sel.startsWith('.')) return this.cls === sel.slice(1);
|
|
if (sel.startsWith('input[name="')) {
|
|
const m = sel.match(/^input\[name="([^"]+)"\]\[value="([^"]+)"\]$/);
|
|
if (m) return this.name === m[1] && this.value === m[2];
|
|
const m2 = sel.match(/^input\[name="([^"]+)"\]$/);
|
|
if (m2) return this.name === m2[1];
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function el(cls, value = '') {
|
|
const e = new StubEl(value);
|
|
e.cls = cls;
|
|
return e;
|
|
}
|
|
|
|
function buildRow() {
|
|
const row = new StubEl();
|
|
row.cls = 'ber-row';
|
|
row.dataset = { berIdx: '0' };
|
|
const body = el('error-action-body');
|
|
const type = el('error-action-type', 'proactive');
|
|
type.nextElementSibling = body;
|
|
body.parentElement = row;
|
|
row.children.push(type, body);
|
|
return row;
|
|
}
|
|
|
|
function buildPcHtml(extraFields) {
|
|
// Build the .ber-pc tree expected by genBerPcValue
|
|
const pc = el('ber-pc');
|
|
const type = el('ber-pc-type', 'display');
|
|
const num = el('ber-pc-num', '1');
|
|
const qual = el('ber-pc-qual', '01');
|
|
const src = el('ber-pc-src', '81');
|
|
const dst = el('ber-pc-dst', '82');
|
|
const text = el('ber-pc-text', 'HI');
|
|
const enc = el('ber-pc-enc', 'ucs2');
|
|
const alpha = el('ber-pc-alpha', '');
|
|
const durEnable = el('ber-pc-dur-enable');
|
|
durEnable.checked = true;
|
|
const durUnit = el('ber-pc-dur-unit', '01');
|
|
const durVal = el('ber-pc-dur-val', '30');
|
|
pc.children.push(type, num, qual, src, dst, text, enc, alpha, durEnable, durUnit, durVal);
|
|
if (extraFields) extraFields(pc);
|
|
return pc;
|
|
}
|
|
|
|
const FNS = ['berLenStr', 'gsm7TextToSeptets', 'gsm7Encode', 'genBerPcValue', 'genErrorActionValue',
|
|
'genScriptChainingValue'];
|
|
let code = '';
|
|
for (const f of FNS) code += extractFunc(html, f) + '\n';
|
|
const berTagsMatch = html.match(/const BER_TAGS = \{[^}]*\};/);
|
|
const berQualMatch = html.match(/const BER_QUAL = \{[\s\S]*?\n\};/);
|
|
const berDevMatch = html.match(/const BER_DEVICES = \[[\s\S]*?\];/);
|
|
const berTonesMatch = html.match(/const BER_TONES = \[[\s\S]*?\];/);
|
|
const gsm7AlphaMatch = html.match(/const GSM7_ALPHABET = \[[\s\S]*?\n\];/);
|
|
const gsm7ExtMatch = html.match(/const GSM7_EXT_MAP = \{[\s\S]*?\n\};/);
|
|
for (const m of [berTagsMatch, berQualMatch, berDevMatch, berTonesMatch, gsm7AlphaMatch, gsm7ExtMatch]) {
|
|
if (!m) throw new Error('constant not found');
|
|
code = m[0] + '\n' + code;
|
|
}
|
|
code += '\nvar __berConsts = {BER_TAGS, BER_QUAL, BER_DEVICES, BER_TONES, GSM7_ALPHABET, GSM7_EXT_MAP};';
|
|
eval(code);
|
|
const {BER_TAGS, BER_QUAL, BER_DEVICES, BER_TONES, GSM7_ALPHABET, GSM7_EXT_MAP} = __berConsts;
|
|
|
|
function mkRow(type) {
|
|
const row = new StubEl();
|
|
row.cls = 'ber-row';
|
|
row.dataset = { berIdx: '0' };
|
|
const body = el('error-action-body');
|
|
const actionType = el('error-action-type', type);
|
|
actionType.nextElementSibling = body;
|
|
row.children.push(actionType, body);
|
|
return row;
|
|
}
|
|
|
|
test('BER_QUAL.refresh matches TS 102 223 §8.6 mapping', () => {
|
|
const r = BER_QUAL.refresh;
|
|
assert.strictEqual(r[0][0], '00');
|
|
assert.strictEqual(r[0][1], 'NAA Initialization and Full File Change Notification');
|
|
assert.strictEqual(r[4][0], '04');
|
|
assert.strictEqual(r[4][1], 'UICC Reset');
|
|
assert.strictEqual(r[5][1], 'NAA Application Reset (not for 2G SIM)');
|
|
assert.strictEqual(r[6][1], 'NAA Session Reset (not for 2G SIM)');
|
|
assert.strictEqual(r[7][1], 'Steering of Roaming');
|
|
assert.strictEqual(r.length, 11);
|
|
});
|
|
|
|
test('genBerPcValue DISPLAY TEXT GSM7 "HI"', () => {
|
|
const pc = buildPcHtml();
|
|
pc.children.find(c => c.cls === 'ber-pc-enc').value = 'gsm7';
|
|
pc.children.find(c => c.cls === 'ber-pc-dur-enable').checked = false;
|
|
const val = genBerPcValue({ querySelector: (s) => s === '.ber-pc' ? pc : null }, '81');
|
|
assert.strictEqual(val, '810E8103012101820281828D0300C824');
|
|
});
|
|
|
|
test('genBerPcValue DISPLAY TEXT UCS2 "HI" + 30s duration', () => {
|
|
const pc = buildPcHtml();
|
|
const val = genBerPcValue({ querySelector: (s) => s === '.ber-pc' ? pc : null }, '81');
|
|
// 81 13 8103 01 21 01 8202 81 82 8D 04 0048 0049 84 02 01 1E
|
|
assert.strictEqual(val, '81148103012101820281828D0508004800498402011E');
|
|
});
|
|
|
|
test('genBerPcValue duration disabled omits 84', () => {
|
|
const pc = buildPcHtml();
|
|
pc.children.find(c => c.cls === 'ber-pc-dur-enable').checked = false;
|
|
const val = genBerPcValue({ querySelector: (s) => s === '.ber-pc' ? pc : null }, '81');
|
|
assert.ok(!val.includes('84'));
|
|
});
|
|
|
|
test('genBerPcValue PLAY TONE uses type byte 20', () => {
|
|
const pc = buildPcHtml();
|
|
pc.children.find(c => c.cls === 'ber-pc-type').value = 'tone';
|
|
pc.children.find(c => c.cls === 'ber-pc-qual').value = '00';
|
|
pc.children.push(el('ber-pc-tone', '01'));
|
|
const val = genBerPcValue({ querySelector: (s) => s === '.ber-pc' ? pc : null }, '81');
|
|
assert.strictEqual(val, '810C8103012000820281828E0101');
|
|
});
|
|
|
|
test('genBerPcValue REFRESH uses CR-set file list tag 92', () => {
|
|
const pc = buildPcHtml();
|
|
pc.children.find(c => c.cls === 'ber-pc-type').value = 'refresh';
|
|
pc.children.find(c => c.cls === 'ber-pc-qual').value = '04';
|
|
pc.children.push(el('ber-pc-flist', '3F00'));
|
|
const val = genBerPcValue({ querySelector: (s) => s === '.ber-pc' ? pc : null }, '81');
|
|
assert.strictEqual(val, '810D81030101048202818292023F00');
|
|
});
|
|
|
|
test('genErrorActionValue noaction emits 82 00', () => {
|
|
const row = mkRow('noaction');
|
|
const val = genErrorActionValue(row, '82');
|
|
assert.strictEqual(val, '8200');
|
|
});
|
|
|
|
test('genErrorActionValue reference emits 82 01 ref', () => {
|
|
const row = mkRow('reference');
|
|
row.children[1].children.push(el('error-ref', '05'));
|
|
const val = genErrorActionValue(row, '82');
|
|
assert.strictEqual(val, '820105');
|
|
});
|
|
|
|
test('genErrorActionValue reference rejects out-of-range ref', () => {
|
|
const row = mkRow('reference');
|
|
row.children[1].children.push(el('error-ref', '80'));
|
|
assert.strictEqual(genErrorActionValue(row, '82'), '');
|
|
});
|
|
|
|
test('genErrorActionValue proactive wraps proactive command', () => {
|
|
const row = mkRow('proactive');
|
|
const pc = buildPcHtml();
|
|
row.children[1].children.push(pc);
|
|
const val = genErrorActionValue(row, '82');
|
|
// DISPLAY TEXT UCS2 "HI" + 30s inside Error Action
|
|
assert.strictEqual(val, '82148103012101820281828D0508004800498402011E');
|
|
});
|
|
|
|
test('genScriptChainingValue first emits 01', () => {
|
|
const row = new StubEl();
|
|
row.dataset = { berIdx: '0' };
|
|
const first = el('chaining-first', 'first');
|
|
first.name = 'chaining-0';
|
|
first.checked = true;
|
|
const interm = el('chaining-inter', 'intermediary');
|
|
interm.name = 'chaining-0';
|
|
const last = el('chaining-last', 'last');
|
|
last.name = 'chaining-0-last';
|
|
const keep = el('chaining-keep', 'keep');
|
|
keep.name = 'chaining-0-keep';
|
|
row.children.push(first, interm, last, keep, el('chaining-script-id'), el('chaining-additional'));
|
|
const val = genScriptChainingValue(row, '83');
|
|
assert.strictEqual(val, '830101');
|
|
});
|
|
|
|
test('genScriptChainingValue first with keep emits 11', () => {
|
|
const row = new StubEl();
|
|
row.dataset = { berIdx: '0' };
|
|
const first = el('chaining-first', 'first');
|
|
first.name = 'chaining-0';
|
|
first.checked = true;
|
|
const interm = el('chaining-inter', 'intermediary');
|
|
interm.name = 'chaining-0';
|
|
const last = el('chaining-last', 'last');
|
|
last.name = 'chaining-0-last';
|
|
const keep = el('chaining-keep', 'keep');
|
|
keep.name = 'chaining-0-keep';
|
|
keep.checked = true;
|
|
row.children.push(first, interm, last, keep, el('chaining-script-id'), el('chaining-additional'));
|
|
const val = genScriptChainingValue(row, '83');
|
|
assert.strictEqual(val, '830111');
|
|
});
|
|
|
|
test('genScriptChainingValue intermediary last emits 03', () => {
|
|
const row = new StubEl();
|
|
row.dataset = { berIdx: '0' };
|
|
const first = el('chaining-first', 'first');
|
|
first.name = 'chaining-0';
|
|
const interm = el('chaining-inter', 'intermediary');
|
|
interm.name = 'chaining-0';
|
|
interm.checked = true;
|
|
const last = el('chaining-last', 'last');
|
|
last.name = 'chaining-0-last';
|
|
last.checked = true;
|
|
const keep = el('chaining-keep', 'keep');
|
|
keep.name = 'chaining-0-keep';
|
|
row.children.push(first, interm, last, keep, el('chaining-script-id'), el('chaining-additional'));
|
|
const val = genScriptChainingValue(row, '83');
|
|
assert.strictEqual(val, '830103');
|
|
});
|
|
|
|
test('genScriptChainingValue no position emits empty', () => {
|
|
const row = new StubEl();
|
|
row.dataset = { berIdx: '0' };
|
|
const first = el('chaining-first', 'first');
|
|
first.name = 'chaining-0';
|
|
const interm = el('chaining-inter', 'intermediary');
|
|
interm.name = 'chaining-0';
|
|
const last = el('chaining-last', 'last');
|
|
last.name = 'chaining-0-last';
|
|
const keep = el('chaining-keep', 'keep');
|
|
keep.name = 'chaining-0-keep';
|
|
row.children.push(first, interm, last, keep, el('chaining-script-id'), el('chaining-additional'));
|
|
assert.strictEqual(genScriptChainingValue(row, '83'), '');
|
|
}); |