Skip to content

Commit 6729a2d

Browse files
committed
fix(recover): wrap embedded-manifest atob in try/catch for corrupt base64
atob() throws DOMException when its input isn't valid base64. The embedded manifest lives in recover.html, which is meant to be archived long-term; bit rot, truncated copies, or storage degradation can leave the base64 payload unreadable. Previously the exception propagated, leaving Step 2 asking the user to load MANIFEST.age manually — but the whole point of embedding was to avoid needing the separate file. Catch the decode failure and surface a toast so the user knows their copy of recover.html is damaged and to try another copy. The i18n function t() already falls back to English when a locale is missing translations, so the three new keys work across all languages even before translators catch up. Fixes #107
1 parent b29c15c commit 6729a2d

2 files changed

Lines changed: 23 additions & 6 deletions

File tree

internal/html/assets/src/app.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -341,13 +341,27 @@ type UIShare = ParsedShare & { isHolder?: boolean };
341341

342342
// Load embedded manifest if available (included when MANIFEST.age is small enough)
343343
if (personalization.manifestB64) {
344-
const binary = atob(personalization.manifestB64);
345-
const bytes = new Uint8Array(binary.length);
346-
for (let i = 0; i < binary.length; i++) {
347-
bytes[i] = binary.charCodeAt(i);
344+
try {
345+
const binary = atob(personalization.manifestB64);
346+
const bytes = new Uint8Array(binary.length);
347+
for (let i = 0; i < binary.length; i++) {
348+
bytes[i] = binary.charCodeAt(i);
349+
}
350+
state.manifest = bytes;
351+
showManifestLoaded('MANIFEST.age', state.manifest.length, 'embedded');
352+
} catch (err) {
353+
// atob throws DOMException on corrupt base64 (bit rot, truncated copy,
354+
// storage degradation). Surface it so the user knows to try another
355+
// copy of recover.html rather than assuming they need MANIFEST.age
356+
// separately.
357+
showError(
358+
t('error_corrupt_embedded_manifest_message'),
359+
{
360+
title: t('error_corrupt_embedded_manifest_title'),
361+
guidance: t('error_corrupt_embedded_manifest_guidance'),
362+
}
363+
);
348364
}
349-
state.manifest = bytes;
350-
showManifestLoaded('MANIFEST.age', state.manifest.length, 'embedded');
351365
}
352366

353367
checkRecoverReady();

internal/translations/recover/en.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@
7777
"error_extract_status": "Extraction failed. The archive may be corrupted.",
7878
"error_recovery_title": "Recovery failed",
7979
"error_recovery_guidance": "Check that you have the correct pieces and the right MANIFEST.age file. You can try again with different pieces.",
80+
"error_corrupt_embedded_manifest_title": "Corrupted embedded archive",
81+
"error_corrupt_embedded_manifest_message": "The archive embedded in this recover.html is damaged and couldn't be loaded.",
82+
"error_corrupt_embedded_manifest_guidance": "Your copy of recover.html may be truncated or corrupted. Try another copy of the file, or load MANIFEST.age manually in Step 2.",
8083
"action_reload": "Reload page",
8184
"action_use_cli": "Use CLI tool",
8285
"action_try_different_shares": "Try different pieces",

0 commit comments

Comments
 (0)