profiler: explicit decode-failure display for corrupt FCI, keep partial results

fcpDecode now parses with a partial-aware walker (fcpParseTlvs) that keeps
every complete TLV it encounters and reports why it stopped:
- 'TLV 62 declares N bytes, only M available' (truncation)
- 'Truncated length field at offset X' / 'incomplete TLV header'
- 'Invalid length form' / 'Trailing data after TLV 62'
- inner A5/C6 errors prefixed with their context ('In A5: ...')

The editor preview shows the decoded parameters plus a red
'Decode failed: <reason>' line (RU: 'Ошибка декодирования'), and the check
report's decoded FCI diff appends per-side error notes (expected/actual)
while still showing whatever decoded on either side.

SW cache v74 -> v75.
This commit is contained in:
2026-09-11 21:38:32 +03:00
parent 1e067dc883
commit 130b9d7e3b
5 changed files with 163 additions and 46 deletions
+1 -1
View File
@@ -381,7 +381,7 @@
<li><strong>Атрибуты файла</strong> — тип файла, размер, длина записи и число записей из FCP-шаблона (любой можно оставить незаданным).</li> <li><strong>Атрибуты файла</strong> — тип файла, размер, длина записи и число записей из FCP-шаблона (любой можно оставить незаданным).</li>
<li><strong>Проверить содержимое</strong> (опционально) — <strong>Точное</strong> (точное совпадение hex) или <strong>Маска</strong>, где <code class="font-mono text-sm">?</code> — шаблон на один полубайт (маска без <code class="font-mono text-sm">?</code> — совпадение префикса, например <code class="font-mono text-sm">0891</code> для MCC/MNC из IMSI). Для record-файлов хранится список по записям.</li> <li><strong>Проверить содержимое</strong> (опционально) — <strong>Точное</strong> (точное совпадение hex) или <strong>Маска</strong>, где <code class="font-mono text-sm">?</code> — шаблон на один полубайт (маска без <code class="font-mono text-sm">?</code> — совпадение префикса, например <code class="font-mono text-sm">0891</code> для MCC/MNC из IMSI). Для record-файлов хранится список по записям.</li>
</ul> </ul>
<p class="text-sm mb-3"><strong>Проверить карту</strong> выполняет каждое правило на подключённой карте и показывает строку прогресса и отчёт прохождения. Рядом с путём файла указывается, что именно проверялось (например, <em>тип файла и размер, содержимое</em> или <em>полный FCI</em>); если часть проверок прошла, а часть нет — каждый аспект помечается (<em>тип файла ✓, размер ✗, содержимое ✓</em>), а расхождения расписываются ниже. Несовпавшие сырые данные (FCI, содержимое, данные записей) показываются как поля только для чтения с моноширинным шрифтом — ожидаемое над фактическим, в одной и той же колонке — для удобного сравнения; для расхождений FCI дополнительно показывается декодированное сравнение по параметрам (размер файла, дескриптор/структура, жизненный цикл, FID, SFI, проприетарные параметры…). Декодированный просмотр FCP также отображается рядом с полем FCI hex при редактировании правила. Для record-файлов при расхождении содержимого добавляется пометка <em>совпадающие записи: 1-5, 7-10</em> со списком записей, которые совпали.</p> <p class="text-sm mb-3"><strong>Проверить карту</strong> выполняет каждое правило на подключённой карте и показывает строку прогресса и отчёт прохождения. Рядом с путём файла указывается, что именно проверялось (например, <em>тип файла и размер, содержимое</em> или <em>полный FCI</em>); если часть проверок прошла, а часть нет — каждый аспект помечается (<em>тип файла ✓, размер ✗, содержимое ✓</em>), а расхождения расписываются ниже. Несовпавшие сырые данные (FCI, содержимое, данные записей) показываются как поля только для чтения с моноширинным шрифтом — ожидаемое над фактическим, в одной и той же колонке — для удобного сравнения; для расхождений FCI дополнительно показывается декодированное сравнение по параметрам (размер файла, дескриптор/структура, жизненный цикл, FID, SFI, проприетарные параметры…). Декодированный просмотр FCP также отображается рядом с полем FCI hex при редактировании правила. Если данные FCI повреждены, показывается всё, что удалось декодировать до места ошибки, вместе с явным сообщением об ошибке декодирования. Для record-файлов при расхождении содержимого добавляется пометка <em>совпадающие записи: 1-5, 7-10</em> со списком записей, которые совпали.</p>
<h4 class="font-medium mb-1">Опции сканирования &laquo;Профиль с карты&raquo;</h4> <h4 class="font-medium mb-1">Опции сканирования &laquo;Профиль с карты&raquo;</h4>
<p class="text-sm mb-2">Диалог сканирования запрашивает имя профиля и предлагает селектор <strong>&laquo;Проверка FCP/FCI&raquo;</strong> (те же три режима, по умолчанию <strong>Тип файла + размер (FCP)</strong>), применяемый ко всем создаваемым правилам, а также список <strong>&laquo;Игнорировать содержимое файлов&raquo;</strong> (все отмечены по умолчанию) часто перезаписываемых файлов, содержимое которых пропускается: <code class="font-mono text-sm">EF.LOCI</code>, <code class="font-mono text-sm">EF.PSLOCI</code>, <code class="font-mono text-sm">EF.EPSLOCI</code>, <code class="font-mono text-sm">EF.5GS3GPPLOCI</code>, <code class="font-mono text-sm">EF.Keys</code>, <code class="font-mono text-sm">EF.KeysPS</code>, <code class="font-mono text-sm">EF.SMS</code>, <code class="font-mono text-sm">EF.Kc</code>, <code class="font-mono text-sm">EF.KcGPRS</code>, <code class="font-mono text-sm">EF.LOCIGPRS</code>, <code class="font-mono text-sm">EF.CBMID</code>, <code class="font-mono text-sm">EF.SMSS</code>. Ещё две отмеченные по умолчанию опции <strong>&laquo;Сравнивать первые 4 байта для&raquo;</strong> <code class="font-mono text-sm">EF.IMSI</code> и <code class="font-mono text-sm">EF.ICCID</code> захватывают содержимое этих файлов как маску только первых 4 байт (снимите для точного сравнения). Строка прогресса показывает <em>N / всего файлов</em> с текущим путём файла во время сканирования; при сканировании опции скрываются, а кнопки блокируются. Правила создаются только для файлов, которые реально существуют на карте (возвращён FCP-шаблон); отсутствующие файлы пропускаются. Пользовательские файлы из подвкладки <strong>&laquo;Пользовательские файлы&raquo;</strong> включаются с той же проверкой существования.</p> <p class="text-sm mb-2">Диалог сканирования запрашивает имя профиля и предлагает селектор <strong>&laquo;Проверка FCP/FCI&raquo;</strong> (те же три режима, по умолчанию <strong>Тип файла + размер (FCP)</strong>), применяемый ко всем создаваемым правилам, а также список <strong>&laquo;Игнорировать содержимое файлов&raquo;</strong> (все отмечены по умолчанию) часто перезаписываемых файлов, содержимое которых пропускается: <code class="font-mono text-sm">EF.LOCI</code>, <code class="font-mono text-sm">EF.PSLOCI</code>, <code class="font-mono text-sm">EF.EPSLOCI</code>, <code class="font-mono text-sm">EF.5GS3GPPLOCI</code>, <code class="font-mono text-sm">EF.Keys</code>, <code class="font-mono text-sm">EF.KeysPS</code>, <code class="font-mono text-sm">EF.SMS</code>, <code class="font-mono text-sm">EF.Kc</code>, <code class="font-mono text-sm">EF.KcGPRS</code>, <code class="font-mono text-sm">EF.LOCIGPRS</code>, <code class="font-mono text-sm">EF.CBMID</code>, <code class="font-mono text-sm">EF.SMSS</code>. Ещё две отмеченные по умолчанию опции <strong>&laquo;Сравнивать первые 4 байта для&raquo;</strong> <code class="font-mono text-sm">EF.IMSI</code> и <code class="font-mono text-sm">EF.ICCID</code> захватывают содержимое этих файлов как маску только первых 4 байт (снимите для точного сравнения). Строка прогресса показывает <em>N / всего файлов</em> с текущим путём файла во время сканирования; при сканировании опции скрываются, а кнопки блокируются. Правила создаются только для файлов, которые реально существуют на карте (возвращён FCP-шаблон); отсутствующие файлы пропускаются. Пользовательские файлы из подвкладки <strong>&laquo;Пользовательские файлы&raquo;</strong> включаются с той же проверкой существования.</p>
+1 -1
View File
@@ -381,7 +381,7 @@
<li><strong>File attributes</strong> — file type, size, record length and record count, taken from the FCP template (any may be left unset).</li> <li><strong>File attributes</strong> — file type, size, record length and record count, taken from the FCP template (any may be left unset).</li>
<li><strong>Check contents</strong> (optional) — <strong>Exact</strong> hex equality, or <strong>Mask</strong> where <code class="font-mono text-sm">?</code> is a per-nibble wildcard (a mask with no <code class="font-mono text-sm">?</code> is a prefix match, e.g. <code class="font-mono text-sm">0891</code> for the IMSI MCC/MNC). Record files store a per-record list.</li> <li><strong>Check contents</strong> (optional) — <strong>Exact</strong> hex equality, or <strong>Mask</strong> where <code class="font-mono text-sm">?</code> is a per-nibble wildcard (a mask with no <code class="font-mono text-sm">?</code> is a prefix match, e.g. <code class="font-mono text-sm">0891</code> for the IMSI MCC/MNC). Record files store a per-record list.</li>
</ul> </ul>
<p class="text-sm mb-3"><strong>Check card</strong> runs every rule against the equipped card and shows a live progress line plus a pass/fail report. Each row states exactly what was verified next to the file path (e.g. <em>filetype and size, contents</em> or <em>exact FCI</em>); when some checks pass and others fail, each aspect is marked (<em>filetype ✓, size ✗, contents ✓</em>) with the mismatches detailed below. Mismatched raw data (FCI, contents, record data) is shown as read-only monospace fields — expected above actual, aligned in the same column — for easy comparison; FCI mismatches additionally show a decoded per-parameter comparison (file size, file descriptor/structure, life cycle, FID, SFI, proprietary parameters…). A decoded FCP preview is also shown beside the FCI hex field while editing a rule. For record files with a contents mismatch, a <em>matching records: 1-5, 7-10</em> note lists the records that did match.</p> <p class="text-sm mb-3"><strong>Check card</strong> runs every rule against the equipped card and shows a live progress line plus a pass/fail report. Each row states exactly what was verified next to the file path (e.g. <em>filetype and size, contents</em> or <em>exact FCI</em>); when some checks pass and others fail, each aspect is marked (<em>filetype ✓, size ✗, contents ✓</em>) with the mismatches detailed below. Mismatched raw data (FCI, contents, record data) is shown as read-only monospace fields — expected above actual, aligned in the same column — for easy comparison; FCI mismatches additionally show a decoded per-parameter comparison (file size, file descriptor/structure, life cycle, FID, SFI, proprietary parameters…). A decoded FCP preview is also shown beside the FCI hex field while editing a rule. If the FCI data is corrupt, whatever was decoded before the faulty part is shown together with an explicit decode-failure note. For record files with a contents mismatch, a <em>matching records: 1-5, 7-10</em> note lists the records that did match.</p>
<h4 class="font-medium mb-1">&ldquo;Profile from card&rdquo; scan options</h4> <h4 class="font-medium mb-1">&ldquo;Profile from card&rdquo; scan options</h4>
<p class="text-sm mb-2">The scan dialog asks for a profile name and offers a <strong>&ldquo;FCP/FCI check&rdquo;</strong> selector (the same three modes above, default <strong>Filetype + size</strong>) applied to every generated rule, plus an <strong>&ldquo;Ignore contents of files&rdquo;</strong> checklist (all checked by default) of frequently-overwritten files whose contents are skipped: <code class="font-mono text-sm">EF.LOCI</code>, <code class="font-mono text-sm">EF.PSLOCI</code>, <code class="font-mono text-sm">EF.EPSLOCI</code>, <code class="font-mono text-sm">EF.5GS3GPPLOCI</code>, <code class="font-mono text-sm">EF.Keys</code>, <code class="font-mono text-sm">EF.KeysPS</code>, <code class="font-mono text-sm">EF.SMS</code>, <code class="font-mono text-sm">EF.Kc</code>, <code class="font-mono text-sm">EF.KcGPRS</code>, <code class="font-mono text-sm">EF.LOCIGPRS</code>, <code class="font-mono text-sm">EF.CBMID</code>, <code class="font-mono text-sm">EF.SMSS</code>. Two further checked-by-default options <strong>&ldquo;Match first 4 bytes for&rdquo;</strong> <code class="font-mono text-sm">EF.IMSI</code> and <code class="font-mono text-sm">EF.ICCID</code> capture those files&rsquo; contents as a mask of only the first 4 bytes (uncheck for exact matching). A progress line shows <em>N / total files</em> with the current file path while scanning; during the scan the options are hidden and the buttons are locked. Rules are created only for files that actually exist on the card (a FCP template is returned); missing files are skipped. Custom files from the <strong>Custom files</strong> sub-tab are included under the same existence check.</p> <p class="text-sm mb-2">The scan dialog asks for a profile name and offers a <strong>&ldquo;FCP/FCI check&rdquo;</strong> selector (the same three modes above, default <strong>Filetype + size</strong>) applied to every generated rule, plus an <strong>&ldquo;Ignore contents of files&rdquo;</strong> checklist (all checked by default) of frequently-overwritten files whose contents are skipped: <code class="font-mono text-sm">EF.LOCI</code>, <code class="font-mono text-sm">EF.PSLOCI</code>, <code class="font-mono text-sm">EF.EPSLOCI</code>, <code class="font-mono text-sm">EF.5GS3GPPLOCI</code>, <code class="font-mono text-sm">EF.Keys</code>, <code class="font-mono text-sm">EF.KeysPS</code>, <code class="font-mono text-sm">EF.SMS</code>, <code class="font-mono text-sm">EF.Kc</code>, <code class="font-mono text-sm">EF.KcGPRS</code>, <code class="font-mono text-sm">EF.LOCIGPRS</code>, <code class="font-mono text-sm">EF.CBMID</code>, <code class="font-mono text-sm">EF.SMSS</code>. Two further checked-by-default options <strong>&ldquo;Match first 4 bytes for&rdquo;</strong> <code class="font-mono text-sm">EF.IMSI</code> and <code class="font-mono text-sm">EF.ICCID</code> capture those files&rsquo; contents as a mask of only the first 4 bytes (uncheck for exact matching). A progress line shows <em>N / total files</em> with the current file path while scanning; during the scan the options are hidden and the buttons are locked. Rules are created only for files that actually exist on the card (a FCP template is returned); missing files are skipped. Custom files from the <strong>Custom files</strong> sub-tab are included under the same existence check.</p>
+102 -40
View File
@@ -7871,75 +7871,136 @@ function fcpDo(key, value) {
// Decode a SELECT response (FCI): accepts the FCP template '62', an FCI template // Decode a SELECT response (FCI): accepts the FCP template '62', an FCI template
// '6F' wrapping FCP/FMD, or bare FCP content. Returns { ok, template, items }. // '6F' wrapping FCP/FMD, or bare FCP content. Returns { ok, template, items }.
// Partial-aware BER-TLV walk: parses as many complete TLVs as possible and
// reports where/why it stopped (truncated header/length/value, trailing data).
function fcpParseTlvs(hex) {
const tlvs = [];
let i = 0;
let error = '';
while (i < hex.length) {
const remaining = hex.length - i;
if (remaining < 2) { error = 'Truncated at offset ' + (i / 2) + ': incomplete tag'; break; }
const tag = hex.substr(i, 2);
let tagBytes = 2;
if ((parseInt(tag, 16) & 0x1F) === 0x1F) {
tagBytes = 4;
if (remaining < 4) { error = 'Truncated at offset ' + (i / 2) + ': incomplete 2-byte tag'; break; }
}
if (remaining < tagBytes + 2) { error = 'Truncated at offset ' + (i / 2) + ': incomplete TLV header'; break; }
const lb = parseInt(hex.substr(i + tagBytes, 2), 16);
const nLen = lb < 128 ? 0 : (lb & 0x7F);
if (i + tagBytes + 2 + nLen * 2 > hex.length) { error = 'Truncated length field at offset ' + ((i + tagBytes) / 2); break; }
const r = parseBerLen(hex, i + tagBytes);
if (!(r.len >= 0)) { error = 'Invalid length form at offset ' + ((i + tagBytes) / 2); break; }
const consumed = tagBytes + r.consumed;
const total = consumed + r.len * 2;
if (remaining < total) {
error = 'TLV ' + tag + ' declares ' + r.len + ' bytes, only ' + Math.floor(Math.max(remaining - consumed, 0) / 2) + ' available';
break;
}
tlvs.push({ tag, length: r.len, value: hex.substr(i + consumed, r.len * 2), raw: hex.substr(i, total) });
i += total;
}
return { tlvs, error, consumed: i };
}
function fcpDecode(hex) { function fcpDecode(hex) {
const h = (hex || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase(); const h = (hex || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase();
if (!h) return { ok: false, template: null, items: [] }; if (!h) return { ok: false, template: null, items: [], error: '' };
let template = null; let template = null;
let body = h; let body = h;
const top = parseTlvList(h); let error = '';
if (top.length === 1 && top[0].raw.length === h.length && (top[0].tag === '62' || top[0].tag === '6F' || top[0].tag === '64')) { const first = h.substr(0, 2);
template = top[0].tag; if ((first === '62' || first === '64' || first === '6F') && h.length >= 4) {
body = top[0].value; const lb = parseInt(h.substr(2, 2), 16);
if (template === '6F') { const nLen = lb < 128 ? 0 : (lb & 0x7F);
const fcp = parseTlvList(body).find(t => t.tag === '62'); if (4 + nLen * 2 > h.length) {
if (fcp) { template = '62'; body = fcp.value; } error = 'TLV ' + first + ' has a truncated length field';
body = '';
} else {
const r = parseBerLen(h, 2);
const consumed = 2 + r.consumed;
const avail = h.length - consumed;
template = first;
body = h.substr(consumed, Math.min(r.len * 2, Math.max(avail, 0)));
if (r.len * 2 > avail) error = 'TLV ' + first + ' declares ' + r.len + ' bytes, only ' + Math.floor(Math.max(avail, 0) / 2) + ' available';
else if (consumed + r.len * 2 < h.length) error = 'Trailing data after TLV ' + first;
} }
} if (template === '6F' && body) {
const tlvs = parseTlvList(body); const inner = fcpParseTlvs(body);
const consumed = tlvs.reduce((n, t) => n + t.raw.length, 0); const fcp = inner.tlvs.find(t => t.tag === '62');
const ok = tlvs.length > 0 && consumed === body.length; if (fcp) {
const items = []; template = '62';
for (const t of tlvs) { body = fcp.value;
const d = fcpDo(t.tag, t.value); if (!error && inner.error) error = inner.error;
items.push({ key: t.tag, tag: t.tag, name: d.name, value: t.value, decoded: d.decoded });
if (t.tag === 'A5' || t.tag === 'C6') {
for (const sub of parseTlvList(t.value)) {
const sd = fcpDo(t.tag + '/' + sub.tag, sub.value);
items.push({ key: t.tag + '/' + sub.tag, tag: sub.tag, name: sd.name, value: sub.value, decoded: sd.decoded });
} }
} }
} }
return { ok, template, items }; const parsed = fcpParseTlvs(body);
if (!error && parsed.error) error = parsed.error;
const items = [];
for (const t of parsed.tlvs) {
const d = fcpDo(t.tag, t.value);
items.push({ key: t.tag, tag: t.tag, name: d.name, value: t.value, decoded: d.decoded });
if (t.tag === 'A5' || t.tag === 'C6') {
const sub = fcpParseTlvs(t.value);
if (!error && sub.error) error = 'In ' + t.tag + ': ' + sub.error;
for (const s of sub.tlvs) {
const sd = fcpDo(t.tag + '/' + s.tag, s.value);
items.push({ key: t.tag + '/' + s.tag, tag: s.tag, name: sd.name, value: s.value, decoded: sd.decoded });
}
}
}
return { ok: !error && items.length > 0, template, items, error };
} }
function fcpDiffHtml(expectedHex, actualHex) { function fcpDiffHtml(expectedHex, actualHex) {
const e = fcpDecode(expectedHex); const e = fcpDecode(expectedHex);
const a = fcpDecode(actualHex); const a = fcpDecode(actualHex);
if (!e.ok || !a.ok || !e.items.length || !a.items.length) return ''; const eBad = !!e.error, aBad = !!a.error;
if (!eBad && !aBad && (!e.items.length || !a.items.length)) return '';
const eMap = {}, aMap = {}; const eMap = {}, aMap = {};
e.items.forEach(it => { eMap[it.key] = it; }); e.items.forEach(it => { eMap[it.key] = it; });
a.items.forEach(it => { aMap[it.key] = it; }); a.items.forEach(it => { aMap[it.key] = it; });
const keys = e.items.map(it => it.key); const keys = e.items.map(it => it.key);
for (const it of a.items) if (!eMap[it.key]) keys.push(it.key); for (const it of a.items) if (!eMap[it.key]) keys.push(it.key);
let html = '<div class="mt-1 text-xs text-gray-500 dark:text-slate-400">' + esc(t('FCP parameters')) + '</div>'; let html = '';
html += '<table class="w-full text-xs border-collapse">'; if (keys.length) {
html += '<thead><tr class="border-b border-gray-200 dark:border-slate-700">' + html += '<div class="mt-1 text-xs text-gray-500 dark:text-slate-400">' + esc(t('FCP parameters')) + '</div>';
'<th class="text-left py-0.5 px-1 font-medium text-gray-500 dark:text-slate-400">' + esc(t('Parameter')) + '</th>' + html += '<table class="w-full text-xs border-collapse">';
'<th class="text-left py-0.5 px-1 font-medium text-gray-500 dark:text-slate-400">' + esc(t('expected')) + '</th>' + html += '<thead><tr class="border-b border-gray-200 dark:border-slate-700">' +
'<th class="text-left py-0.5 px-1 font-medium text-gray-500 dark:text-slate-400">' + esc(t('actual')) + '</th></tr></thead><tbody>'; '<th class="text-left py-0.5 px-1 font-medium text-gray-500 dark:text-slate-400">' + esc(t('Parameter')) + '</th>' +
for (const k of keys) { '<th class="text-left py-0.5 px-1 font-medium text-gray-500 dark:text-slate-400">' + esc(t('expected')) + '</th>' +
const ev = eMap[k], av = aMap[k]; '<th class="text-left py-0.5 px-1 font-medium text-gray-500 dark:text-slate-400">' + esc(t('actual')) + '</th></tr></thead><tbody>';
const ed = ev ? (ev.decoded === null ? '' : (ev.decoded || ev.value)) : '—'; for (const k of keys) {
const ad = av ? (av.decoded === null ? '' : (av.decoded || av.value)) : '—'; const ev = eMap[k], av = aMap[k];
const eq = !!ev && !!av && ev.decoded === av.decoded && ev.value === av.value; const ed = ev ? (ev.decoded === null ? '' : (ev.decoded || ev.value)) : '—';
const color = eq ? 'text-gray-500 dark:text-slate-400' : 'text-red-600 font-medium'; const ad = av ? (av.decoded === null ? '' : (av.decoded || av.value)) : '—';
html += '<tr class="border-b border-gray-100 dark:border-slate-800">' + const eq = !!ev && !!av && ev.decoded === av.decoded && ev.value === av.value;
'<td class="py-0.5 px-1 ' + color + '">' + esc((ev || av).name) + '</td>' + const color = eq ? 'text-gray-500 dark:text-slate-400' : 'text-red-600 font-medium';
'<td class="py-0.5 px-1 font-mono break-all ' + color + '">' + esc(ed) + '</td>' + html += '<tr class="border-b border-gray-100 dark:border-slate-800">' +
'<td class="py-0.5 px-1 font-mono break-all ' + color + '">' + esc(ad) + '</td></tr>'; '<td class="py-0.5 px-1 ' + color + '">' + esc((ev || av).name) + '</td>' +
'<td class="py-0.5 px-1 font-mono break-all ' + color + '">' + esc(ed) + '</td>' +
'<td class="py-0.5 px-1 font-mono break-all ' + color + '">' + esc(ad) + '</td></tr>';
}
html += '</tbody></table>';
} }
html += '</tbody></table>'; if (eBad) html += '<div class="text-xs text-red-600 mt-0.5 pl-2">' + esc(t('expected')) + ': ' + esc(e.error) + '</div>';
if (aBad) html += '<div class="text-xs text-red-600 mt-0.5 pl-2">' + esc(t('actual')) + ': ' + esc(a.error) + '</div>';
return html; return html;
} }
function profilerFciPreviewItems(hex) { function profilerFciPreviewItems(hex) {
const d = fcpDecode(hex); const d = fcpDecode(hex);
if (!d.ok || !d.items.length) return ''; if (!d.items.length && !d.error) return '';
let html = ''; let html = '';
for (const it of d.items) { for (const it of d.items) {
if (it.decoded === null) continue; if (it.decoded === null) continue;
html += '<div>' + esc(it.name) + ': <span class="text-gray-800 dark:text-slate-200">' + esc(it.decoded || it.value) + '</span></div>'; html += '<div>' + esc(it.name) + ': <span class="text-gray-800 dark:text-slate-200">' + esc(it.decoded || it.value) + '</span></div>';
} }
if (d.error) {
html += '<div class="text-red-600">' + esc(t('Decode failed')) + ': ' + esc(d.error) + '</div>';
}
return html; return html;
} }
@@ -8212,6 +8273,7 @@ const LANG_RU = {
'FCP parameters': 'Параметры FCP', 'FCP parameters': 'Параметры FCP',
'Parameter': 'Параметр', 'Parameter': 'Параметр',
'Decoded FCP': 'Декодированный FCP', 'Decoded FCP': 'Декодированный FCP',
'Decode failed': 'Ошибка декодирования',
'No profiles defined.': 'Профили не заданы.', 'No profiles defined.': 'Профили не заданы.',
'rules': 'правил', 'rules': 'правил',
'Check card': 'Проверить карту', 'Check card': 'Проверить карту',
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'otaman-v74'; const CACHE = 'otaman-v75';
const URLS = [ const URLS = [
'index.html', 'index.html',
'help.html', 'help.html',
+58 -3
View File
@@ -21,7 +21,7 @@ function extractFunc(src, name, asyncFn) {
return (asyncFn ? 'async ' : '') + src.slice(m.index, i + 1); return (asyncFn ? 'async ' : '') + src.slice(m.index, i + 1);
} }
const FNS = ['profilerNormHex', 'profilerNormHexStrict', 'profilerMatch', 'profilerMatchMin', 'profilerMaskPrefix4', 'profilerFileFields', 'profilerContentKindForFileType', 'profilerEmptyRecordContent', 'profilerValidateProfile', 'profilerCustomNameForPath', 'profilerUpdateRulePath', 'profilerResultAspects', 'profilerAspectSummary', 'profilerNumRanges', 'esc', 'escHtml', 'profilerRawDataCheck', 'profilerRenderReport', 'parseBerLen', 'parseTlvList', 'fcpInt', 'fcpFileDescriptor', 'fcpLifeCycle', 'fcpSfi', 'fcpDo', 'fcpDecode', 'fcpDiffHtml', 'profilerFciPreviewItems', 'profilerUpdateFciPreview', 'profilerUpdateRule', 'profilerFciInput']; const FNS = ['profilerNormHex', 'profilerNormHexStrict', 'profilerMatch', 'profilerMatchMin', 'profilerMaskPrefix4', 'profilerFileFields', 'profilerContentKindForFileType', 'profilerEmptyRecordContent', 'profilerValidateProfile', 'profilerCustomNameForPath', 'profilerUpdateRulePath', 'profilerResultAspects', 'profilerAspectSummary', 'profilerNumRanges', 'esc', 'escHtml', 'profilerRawDataCheck', 'profilerRenderReport', 'parseBerLen', 'parseTlvList', 'fcpInt', 'fcpParseTlvs', 'fcpFileDescriptor', 'fcpLifeCycle', 'fcpSfi', 'fcpDo', 'fcpDecode', 'fcpDiffHtml', 'profilerFciPreviewItems', 'profilerUpdateFciPreview', 'profilerUpdateRule', 'profilerFciInput'];
let code = ''; let code = '';
for (const f of FNS) code += extractFunc(html, f) + '\n'; for (const f of FNS) code += extractFunc(html, f) + '\n';
code += extractFunc(html, 'profilerBuildFileRule', true) + '\n'; code += extractFunc(html, 'profilerBuildFileRule', true) + '\n';
@@ -679,14 +679,15 @@ test('fcpDiffHtml highlights differing FCP parameters', () => {
assert.ok(diff.includes('9 bytes')); assert.ok(diff.includes('9 bytes'));
assert.ok(diff.includes('10 bytes')); assert.ok(diff.includes('10 bytes'));
assert.ok(diff.includes('text-red-600')); assert.ok(diff.includes('text-red-600'));
assert.ok(!fcpDiffHtml('garbage', FCP_TRANSPARENT)); assert.ok(fcpDiffHtml('garbage', FCP_TRANSPARENT).includes('Truncated length field'));
assert.strictEqual(fcpDiffHtml('', ''), '');
delete global.t; delete global.t;
}); });
test('profilerFciPreviewItems renders decoded items and degrades gracefully', () => { test('profilerFciPreviewItems renders decoded items and degrades gracefully', () => {
global.t = s => s; global.t = s => s;
assert.ok(profilerFciPreviewItems(FCP_TRANSPARENT).includes('File size: ')); assert.ok(profilerFciPreviewItems(FCP_TRANSPARENT).includes('File size: '));
assert.strictEqual(profilerFciPreviewItems('not hex'), ''); assert.ok(profilerFciPreviewItems('not hex').includes('Decode failed'));
assert.strictEqual(profilerFciPreviewItems(''), ''); assert.strictEqual(profilerFciPreviewItems(''), '');
delete global.t; delete global.t;
}); });
@@ -720,3 +721,57 @@ test('profilerRenderReport shows the decoded FCI diff for a raw fci mismatch', (
assert.ok(html.includes('text-red-600')); assert.ok(html.includes('text-red-600'));
delete global.t; delete global.t;
}); });
// --- partial/corrupt FCI decoding ---
test('fcpDecode reports truncation but keeps the TLVs parsed before it', () => {
// outer declares 22 bytes (0x16) but only the 18-byte body is present
const truncOuter = '6216' + FCP_TRANSPARENT.slice(4);
const d1 = fcpDecode(truncOuter);
assert.strictEqual(d1.ok, false);
assert.match(d1.error, /TLV 62 declares 22 bytes, only 18 available/);
assert.strictEqual(d1.items.find(it => it.key === '80').decoded, '9 bytes');
// correct outer length, truncated inner TLV (82 declares 5 bytes, 2 present)
const d2 = fcpDecode('620880020009820541 21'.replace(/ /g, ''));
assert.strictEqual(d2.ok, false);
assert.match(d2.error, /TLV 82 declares 5 bytes, only 2 available/);
assert.strictEqual(d2.items.find(it => it.key === '80').decoded, '9 bytes');
assert.ok(!d2.items.some(it => it.key === '82'));
// truncated length field
const d3 = fcpDecode('6281');
assert.strictEqual(d3.ok, false);
assert.match(d3.error, /truncated length field/);
assert.strictEqual(d3.items.length, 0);
// truncated inner A5 sub-TLV reports the context
const d4 = fcpDecode('620A82024121A50488050102');
assert.strictEqual(d4.ok, false);
assert.match(d4.error, /In A5: TLV 88 declares 5 bytes, only 2 available/);
});
test('profilerFciPreviewItems shows decoded data plus an explicit failure note', () => {
global.t = s => s;
const trunc = '6216' + FCP_TRANSPARENT.slice(4);
const html = profilerFciPreviewItems(trunc);
assert.ok(html.includes('File size: '), html);
assert.ok(html.includes('Decode failed'), html);
assert.ok(html.includes('declares 22 bytes'), html);
// empty input renders nothing; garbage reports the failure
assert.strictEqual(profilerFciPreviewItems(''), '');
assert.ok(profilerFciPreviewItems('not hex').includes('Decode failed'));
delete global.t;
});
test('fcpDiffHtml appends decode-failure notes for corrupt sides', () => {
global.t = s => s;
const trunc = '6216' + FCP_TRANSPARENT.slice(4);
const html = fcpDiffHtml(trunc, FCP_TRANSPARENT);
assert.ok(html.includes('FCP parameters'), html);
assert.ok(html.includes('9 bytes'));
assert.ok(html.includes('expected: ') && html.includes('TLV 62 declares 22 bytes'));
// both sides empty -> nothing rendered
assert.strictEqual(fcpDiffHtml('', ''), '');
delete global.t;
});