mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
feat: warn about huge backdrops and offer a safe reduced copy (#39)
A picked raster is now classified from its HEADER BYTES ONLY before anything heavy happens: src/backdrop-probe.ts parses PNG IHDR (+colour type/tRNS for alpha), JPEG SOF and WebP VP8/VP8L/VP8X at fixed offsets, never using a file field as an allocation size; hostile or truncated headers collapse to 'unknown', which warns without numbers instead of passing silently. The thresholds live in that module as the single calibration point (WARN_DECODED_BYTES 128 MiB ≈ 32 MP, HARD_DIMENSION 16384 — the browser canvas cap, DOWNSCALE_TARGET_PX 4096), derived from the desktop-Chromium matrix now committed as demo/benchmark_backdrop_decode.mjs with a conservative tablet margin documented in the spec. The shared pick flow (src/backdrop-pick.ts) feeds BOTH lazy runtimes — the editor space dialog and the onboarding first-space dialog — so the guard cannot drift between them, and nothing of it enters the eager View graph. Warn shows the real numbers and three actions; the reduced copy decodes EXIF-aware, keeps aspect and alpha (PNG stays PNG, opaque becomes JPEG q0.9) and flows through the ordinary planFile → upload path. Hard has two phases with one outcome: beyond 16384 px only Cancel; a failed or timed-out (10 s) reduce closes with a toast, clean staging and NO silent fallback to the original the user just declined. SVG never reaches the probe. The safe path swaps the manual byte-loop base64 for FileReader — half the JS-heap peak on every upload, byte-identical output (parity asserted in the smoke). Proofs: header-table units incl. a fuzz set of hostile headers and ±1 threshold bounds; smoke_backdrop_guard on the real bundle — zero decode calls before the choice, byte parity of keep-original, a real 6200 px reduce to 4096 for both alpha and opaque branches, cancel-only hard dialog, both phase-2 failures (reject and hang under the test-only timeout override), re-pick after refusal, SVG bypass; four registry mutants (probe-always-safe, alpha-dropped, hard-demoted, phase-2 silent fallback). Spec anchor corrected alongside: the server plan limit is 8 MB (MAX_PLAN_BYTES), attachments are the 50 MB path — an 8 MB JPEG is easily 80-160 MP decoded, so the client-side guard stays the primary defence. Issue: #39 User-Visible: yes
This commit is contained in:
@@ -680,6 +680,62 @@ const MUTANT_DEFINITIONS = [
|
||||
replace: ' this._languageFailureUnsub = undefined;',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'backdrop-probe-always-safe',
|
||||
guard: 'node demo/smoke_backdrop_guard.mjs',
|
||||
because: 'a probe that waves every raster through reopens the original hole — a 100 MP scan '
|
||||
+ 'decodes unwarned and kills the tablet tab (#39 AC1)',
|
||||
patches: [{
|
||||
file: 'src/backdrop-pick.ts',
|
||||
find: " if (probe.kind === 'safe') return { kind: 'pass', ext };",
|
||||
replace: " return { kind: 'pass', ext };",
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'backdrop-downscale-drops-alpha',
|
||||
guard: 'node demo/smoke_backdrop_guard.mjs',
|
||||
because: 'a transparent PNG reduced into JPEG silently paints the plan background black — '
|
||||
+ 'alpha must survive the reduced copy (#39 AC2)',
|
||||
patches: [{
|
||||
file: 'src/backdrop-pick.ts',
|
||||
find: " const alpha = state.probe.alpha;",
|
||||
replace: ' const alpha = false;',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'backdrop-hard-demoted-to-warn',
|
||||
guard: 'node demo/smoke_backdrop_guard.mjs',
|
||||
because: 'beyond the 16384 px canvas cap the reduced copy CANNOT be built — offering it is a '
|
||||
+ 'lie that ends in a decode failure (#39 AC4 phase 1)',
|
||||
patches: [{
|
||||
file: 'src/backdrop-probe.ts',
|
||||
find: " const kind: BackdropVerdict = Math.max(width, height) > HARD_DIMENSION\n"
|
||||
+ " ? 'hard'\n"
|
||||
+ " : decodedBytes > WARN_DECODED_BYTES ? 'warn' : 'safe';",
|
||||
replace: " const kind: BackdropVerdict = "
|
||||
+ "decodedBytes > WARN_DECODED_BYTES ? 'warn' : 'safe';",
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'backdrop-phase2-falls-back-to-original',
|
||||
guard: 'node demo/smoke_backdrop_guard.mjs',
|
||||
because: 'silently uploading the original the user just declined is the exact dishonesty the '
|
||||
+ 'phase-2 contract forbids — staging must stay clean after a failed reduce (#39 AC4b)',
|
||||
patches: [{
|
||||
file: 'src/backdrop-pick.ts',
|
||||
find: ' } catch {\n'
|
||||
+ ' // Honest phase 2 (spec §UX): no silent fallback to the original the\n'
|
||||
+ ' // user just declined — staging stays clean, the toast says what happened.\n'
|
||||
+ ' close();\n'
|
||||
+ " host._showToast(host._t('backdrop.downscale_failed'));\n"
|
||||
+ ' }',
|
||||
replace: ' } catch {\n'
|
||||
+ ' const payload = await encodePlanFile(guard.file, guard.ext, guard.file.name);\n'
|
||||
+ ' apply(payload);\n'
|
||||
+ ' close();\n'
|
||||
+ ' }',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'cold-view-vacuum-mapid-delegated',
|
||||
guard: 'node demo/smoke_cold_view_vacuum.mjs',
|
||||
|
||||
@@ -44,6 +44,17 @@ export const SMOKE_LINKS = [
|
||||
+ 'requests and four root render gates: neutral cold frame, atomic German commit, page-cache '
|
||||
+ 'reuse and the bounded retry-to-English path cannot be proven from pure registry tests (#348)',
|
||||
},
|
||||
{
|
||||
symbols: [
|
||||
'probeBackdrop', 'classifyPlanFile', 'downscaleBackdrop', 'renderBackdropGuard',
|
||||
'WARN_DECODED_BYTES', 'HARD_DIMENSION', 'DOWNSCALE_TARGET_PX',
|
||||
],
|
||||
smokes: ['smoke_backdrop_guard.mjs'],
|
||||
because: 'the production-bundle scenario proves the guard end to end: header-only warning '
|
||||
+ 'with zero decode calls, byte-identical keep-original path, a real 6200px reduce to 4096 '
|
||||
+ 'with alpha preserved, the cancel-only hard dialog, the honest phase-2 failure (toast, '
|
||||
+ 'clean staging, no silent original upload) and the SVG bypass (#39)',
|
||||
},
|
||||
{
|
||||
symbols: ['vacMapIdWithFallback', 'vacMapIdFromAttrs', 'readVacTelemetry'],
|
||||
smokes: ['smoke_cold_view_vacuum.mjs'],
|
||||
|
||||
Reference in New Issue
Block a user