v0.7.5: Joliet namespace fallback, gap-tolerant El Torito walk, Unattended pagination
Follow-up to the v0.7.4 dashboard triage: VCSA/ultravnc data ISOs are
*correctly* flagged non-bootable (no boot catalog exists to find), but
two real false-negative holes could mislabel genuinely bootable
appliance ISOs — both closed here.
iso_fs:
- Joliet fallback (the big one): lookup() now tries the primary
ISO9660 namespace first and falls back to the Joliet SVD (UCS-2
big-endian identifiers, escape-sequence detected). Windows-oriented
mastering tools — common for vendor/appliance ISOs — write a minimal
or mangled primary tree and keep the real filenames only in Joliet;
those images probed as "no installer files" and their in-ISO fetches
404'd. Applies everywhere the walker is used: introspection probes
(local + NFS/SFTP) and /iso/{id}/{*path} serving.
- find_descriptor(): the PVD/SVD search scans the whole descriptor
area (LBA 16..32), skipping non-CD001 filler sectors instead of
requiring a pristine sector 16.
- TestIsoBuilder grows a joliet_only mode (bare primary tree, real
names only in the SVD) modeling the mastering worst case.
introspect:
- detect_el_torito() no longer aborts at the first non-CD001 sector or
stops at a Set Terminator — sloppy mastering leaves zeroed filler
sectors that used to hide a real boot record and flag a bootable
image as a data ISO. All 16 descriptor sectors are examined; the
25-byte exact signature can't false-positive on what follows the set.
- Volume-label read now uses the same tolerant descriptor scan, and
label + El Torito + namespace probes all share one CachingReadAt, so
remote probes spend fewer round-trips than before despite scanning
more sectors.
- INTROSPECT_REV bumped to 3 so everything probed by the rev-2 logic
re-probes with the Joliet fallback: local ISOs on first startup, and
remote ISOs via the rev-gated cache self-invalidating.
webui:
- Unattended files: the same 5-per-page pager as Available images
(Showing X–Y of N · Prev/Next), composed with the existing filter,
page resets on input.
Validation: clippy pedantic clean, fmt clean, 319 workspace tests
green (+3: joliet fallback lookup, joliet-only classification, filler-
sector boot record), webui syntax-checked.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
6524aa4118
commit
1ded291c7b
+38
-11
@@ -1294,6 +1294,36 @@
|
||||
]))
|
||||
: [el('div', {class:'empty'}, 'No unattended files yet.')];
|
||||
|
||||
// v0.7.2: filter for big answer-file libraries; v0.7.5: paged 5 at
|
||||
// a time, the same filter-then-page view the image table uses.
|
||||
const UNATT_PAGE_SIZE = 5;
|
||||
let unattPage = 0;
|
||||
const unattPagerInfo = el('span', {});
|
||||
const unattPrev = el('button', {class:'ghost', onclick: () => { unattPage -= 1; applyUnattListView(); }}, '‹ Prev');
|
||||
const unattNext = el('button', {class:'ghost', onclick: () => { unattPage += 1; applyUnattListView(); }}, 'Next ›');
|
||||
const unattSearch = el('input', {type:'search', placeholder:'Filter files by name or kind',
|
||||
spellcheck:'false', oninput: () => { unattPage = 0; applyUnattListView(); }});
|
||||
function applyUnattListView() {
|
||||
if (!unattendedFiles.length) return; // empty-state div carries no dataset
|
||||
const q = unattSearch.value.trim().toLowerCase();
|
||||
const visible = unattRows.filter(r => {
|
||||
r.style.display = 'none';
|
||||
return !q || (r.dataset.search || '').includes(q);
|
||||
});
|
||||
const pages = Math.max(1, Math.ceil(visible.length / UNATT_PAGE_SIZE));
|
||||
if (unattPage >= pages) unattPage = pages - 1;
|
||||
if (unattPage < 0) unattPage = 0;
|
||||
visible.slice(unattPage * UNATT_PAGE_SIZE, (unattPage + 1) * UNATT_PAGE_SIZE)
|
||||
.forEach(r => { r.style.display = ''; });
|
||||
unattPagerInfo.textContent = visible.length
|
||||
? 'Showing ' + (unattPage * UNATT_PAGE_SIZE + 1) + '–' +
|
||||
Math.min(visible.length, (unattPage + 1) * UNATT_PAGE_SIZE) + ' of ' + visible.length
|
||||
: 'No files match';
|
||||
unattPrev.disabled = unattPage === 0;
|
||||
unattNext.disabled = unattPage >= pages - 1;
|
||||
}
|
||||
applyUnattListView();
|
||||
|
||||
const unattendedAdvanced = el('details', {class:'advanced-disclosure', style:'margin-top:18px'}, [
|
||||
el('summary', {class:'advanced-summary'}, 'Advanced'),
|
||||
el('div', {class:'card', style:'margin-top:14px'}, [
|
||||
@@ -1303,18 +1333,15 @@
|
||||
]),
|
||||
el('div', {class:'body'}, [
|
||||
unattDrop, unattFile, unattMsg,
|
||||
// v0.7.2: filter for big answer-file libraries.
|
||||
unattendedFiles.length > 1 ? (() => {
|
||||
const search = el('input', {type:'search', placeholder:'Filter files by name or kind',
|
||||
spellcheck:'false', oninput: () => {
|
||||
const q = search.value.trim().toLowerCase();
|
||||
unattRows.forEach(r => {
|
||||
r.style.display = (!q || (r.dataset.search || '').includes(q)) ? '' : 'none';
|
||||
});
|
||||
}});
|
||||
return el('label', {class:'field', style:'margin-top:14px;margin-bottom:0'}, search);
|
||||
})() : null,
|
||||
unattendedFiles.length > 1
|
||||
? el('label', {class:'field', style:'margin-top:14px;margin-bottom:0'}, unattSearch)
|
||||
: null,
|
||||
el('div', {style:'margin-top:16px;display:grid;gap:8px'}, unattRows),
|
||||
unattendedFiles.length > UNATT_PAGE_SIZE
|
||||
? el('div', {class:'list-pager', style:'padding:12px 0 0'}, [
|
||||
unattPagerInfo, el('span', {class:'spacer'}), unattPrev, unattNext,
|
||||
])
|
||||
: null,
|
||||
el('p', {class:'msg', style:'margin-top:14px'},
|
||||
'These answer files drive unattended installs. Attach one to a ' +
|
||||
'host pin (Hosts tab) or a queued device (Queue → Profile); on ' +
|
||||
|
||||
Reference in New Issue
Block a user