123import { createMemo, createSignal, Show } from "solid-js";45import { sendMessage } from "../App";6import { PeachWordmark } from "../components/PeachMark";7import {8 parseImportSource,9 type ImportParseResult,10} from "../../shared/import";11import {12 decodePeachScript,13 isPeachScript,14 normalisePeachScript,15} from "../../crypto/peachScript";16import { parsePayload } from "../../crypto/peachPayload";17import type { VaultEntry } from "../../shared/types";1819type ImportSubview = "menu" | "peachscript" | "file";20type ImportMergeMode = "add" | "replace";2122interface ImportProps {23 mode?: "existing" | "onboarding";24 replacesExistingVault?: boolean;25 26 initialView?: ImportSubview;27 onBack: () => void;28 onImport: () => void | Promise<void>;29}3031export function ImportView(props: ImportProps) {32 const [view, setView] = createSignal<ImportSubview>(props.initialView ?? "menu");33 const mode = () => props.mode ?? "existing";34 const isOnboarding = () => mode() === "onboarding";35 36 37 const subviewBack = () => (props.initialView ? props.onBack() : setView("menu"));3839 return (40 <div class={isOnboarding() ? "peach-lock-view" : "peach-overlay-view ov-enter-right"} style="display:flex">41 <Show when={view() === "menu"}>42 <ImportMenu43 mode={mode()}44 replacesExistingVault={props.replacesExistingVault}45 onBack={props.onBack}46 onPick={(v) => setView(v)}47 />48 </Show>49 <Show when={view() === "peachscript"}>50 <PeachScriptImport51 mode={mode()}52 replacesExistingVault={props.replacesExistingVault}53 onBack={subviewBack}54 onImport={props.onImport}55 />56 </Show>57 <Show when={view() === "file"}>58 <FileImport59 mode={mode()}60 replacesExistingVault={props.replacesExistingVault}61 onBack={subviewBack}62 onImport={props.onImport}63 />64 </Show>65 </div>66 );67}686970717273function ImportMenu(props: {74 mode: "existing" | "onboarding";75 replacesExistingVault?: boolean;76 onBack: () => void;77 onPick: (view: ImportSubview) => void;78}) {79 const isOnboarding = props.mode === "onboarding";80 return (81 <>82 <Show83 when={isOnboarding}84 fallback={<OverlayHeader title="import" onBack={props.onBack} />}85 >86 <div class="peach-lock-view-header">87 <PeachWordmark class="peach-wordmark-wrap" />88 <button class="peach-lock-restore-link" onClick={props.onBack}>89 ‹ back90 </button>91 </div>92 </Show>93 <div class="peach-settings-body peach-export-menu">94 <Show when={isOnboarding && props.replacesExistingVault}>95 <div class="peach-settings-group">96 <div class="peach-restore-warning">97 Restoring now replaces the locked local vault on this device with98 a fresh one from your backup or export file.99 </div>100 </div>101 </Show>102103 <div class="peach-settings-group peach-export-card peach-export-card--primary">104 <p class="peach-settings-label">peachscript</p>105 <p class="peach-export-card-copy">106 Paste a PeachScript backup from another Peach device.107 {props.mode === "existing" ? " We'll merge it into your vault." : ""}108 </p>109 <button110 class="peach-btn peach-btn--subtle peach-btn--sm"111 onClick={() => props.onPick("peachscript")}112 >113 Paste PeachScript →114 </button>115 </div>116117 <div class="peach-settings-group peach-export-card">118 <p class="peach-settings-label">backup file</p>119 <p class="peach-export-card-copy">120 A Peach JSON export, or a file from Bitwarden, Proton Pass,121 1Password, or a browser CSV (Chrome, Safari, Edge).122 </p>123 <button124 class="peach-btn peach-btn--subtle peach-btn--sm"125 onClick={() => props.onPick("file")}126 >127 Choose file →128 </button>129 </div>130 </div>131 </>132 );133}134135136137138139function PeachScriptImport(props: {140 mode: "existing" | "onboarding";141 replacesExistingVault?: boolean;142 onBack: () => void;143 onImport: () => void | Promise<void>;144}) {145 const [scriptInput, setScriptInput] = createSignal("");146 const [scriptPass, setScriptPass] = createSignal("");147 const [masterPassword, setMasterPassword] = createSignal("");148 const [masterConfirm, setMasterConfirm] = createSignal("");149 const [working, setWorking] = createSignal(false);150 const [error, setError] = createSignal<string | null>(null);151 const [result, setResult] = createSignal<{ entryCount: number } | null>(null);152153 const scriptLooksValid = createMemo(() => isPeachScript(scriptInput()));154 const isOnboarding = props.mode === "onboarding";155 const masterPasswordReady = createMemo(156 () => masterPassword().length >= 8 && masterPassword() === masterConfirm()157 );158159 async function handleImport() {160 setError(null);161 setResult(null);162 if (!scriptLooksValid()) {163 setError("Paste your PeachScript first.");164 return;165 }166 if (!scriptPass().trim()) {167 setError("Enter the backup password.");168 return;169 }170 setWorking(true);171 try {172 const bytes = decodePeachScript(normalisePeachScript(scriptInput()));173 const payload = await parsePayload(bytes, scriptPass());174175 if (isOnboarding) {176 if (!masterPasswordReady()) {177 throw new Error("MASTER_PASSWORD_REQUIRED");178 }179 const createRes = await sendMessage({180 type: "CREATE_VAULT",181 payload: { password: masterPassword() },182 });183 if (!(createRes as { ok: boolean }).ok) throw new Error("CREATE_VAULT_FAILED");184 }185186 const restoreRes = await sendMessage({187 type: "RESTORE_ENTRIES",188 payload: {189 entries: payload.entries,190 mode: "merge",191 settings: payload.settings,192 },193 });194 if (!(restoreRes as { ok: boolean }).ok) throw new Error("RESTORE_FAILED");195196 if (payload.license?.key) {197 await sendMessage({198 type: "ACTIVATE_LICENSE",199 payload: { key: payload.license.key },200 }).catch(() => {});201 }202203 setResult({ entryCount: payload.entries.length });204 await props.onImport();205 } catch (err) {206 const code = err instanceof Error ? err.message : String(err);207 setError(getImportErrorCopy(code));208 } finally {209 setWorking(false);210 }211 }212213 return (214 <>215 <SubviewHeader title="peachscript" onBack={props.onBack} mode={props.mode} />216 <div class="peach-settings-body">217 <Show when={isOnboarding && props.replacesExistingVault}>218 <div class="peach-settings-group">219 <div class="peach-restore-warning">220 Restoring now replaces the locked local vault on this device with221 a fresh one from your backup.222 </div>223 </div>224 </Show>225 <div class="peach-settings-group peach-export-card peach-export-card--primary">226 <p class="peach-settings-label">paste peachscript</p>227 <p class="peach-export-card-copy">228 {isOnboarding229 ? "Paste a PeachScript backup from another Peach device to restore your vault."230 : "We'll merge the entries into your existing vault."}231 </p>232 <textarea233 class="peach-script-input"234 placeholder="paste PeachScript here…"235 value={scriptInput()}236 onInput={(e) => setScriptInput((e.target as HTMLTextAreaElement).value)}237 />238 <input239 class="peach-restore-input"240 type="password"241 placeholder="PeachScript password"242 value={scriptPass()}243 onInput={(e) => setScriptPass((e.target as HTMLInputElement).value)}244 />245 <Show when={isOnboarding}>246 <div class="peach-auth-grid">247 <input248 class="peach-restore-input"249 type="password"250 placeholder="new master password"251 value={masterPassword()}252 onInput={(e) => setMasterPassword((e.target as HTMLInputElement).value)}253 />254 <input255 class="peach-restore-input"256 type="password"257 placeholder="confirm master password"258 value={masterConfirm()}259 onInput={(e) => setMasterConfirm((e.target as HTMLInputElement).value)}260 />261 </div>262 </Show>263 <button264 class="peach-btn peach-btn--subtle peach-btn--sm"265 onClick={() => void handleImport()}266 disabled={267 working() ||268 !scriptLooksValid() ||269 !scriptPass().trim() ||270 (isOnboarding && !masterPasswordReady())271 }272 >273 {working() ? "decoding…" : "Restore from PeachScript"}274 </button>275 <Show when={error()}>276 <div class="peach-restore-error">{error()}</div>277 </Show>278 <Show when={result()}>279 <div class="peach-export-success">280 <span class="peach-export-success-check">✓</span>281 <span>282 Restored {result()!.entryCount} entr283 {result()!.entryCount === 1 ? "y" : "ies"}.284 </span>285 </div>286 </Show>287 </div>288 </div>289 </>290 );291}292293294295296297function FileImport(props: {298 mode: "existing" | "onboarding";299 replacesExistingVault?: boolean;300 onBack: () => void;301 onImport: () => void | Promise<void>;302}) {303 const isOnboarding = props.mode === "onboarding";304 const needsMasterPassword = () => isOnboarding;305306 const [preview, setPreview] = createSignal<ImportParseResult | null>(null);307 const [rawImport, setRawImport] = createSignal<string | null>(null);308 const [backupPassword, setBackupPassword] = createSignal("");309 const [masterPassword, setMasterPassword] = createSignal("");310 const [masterConfirm, setMasterConfirm] = createSignal("");311 const [importing, setImporting] = createSignal(false);312 const [error, setError] = createSignal<string | null>(null);313 const [mergeMode, setMergeMode] = createSignal<ImportMergeMode>("add");314 const [awaitingReplaceConfirm, setAwaitingReplaceConfirm] = createSignal(false);315 const [importProgress, setImportProgress] = createSignal<{ current: number; total: number } | null>(null);316 const [duplicateWarning, setDuplicateWarning] = createSignal<number>(0);317 const [existingCount, setExistingCount] = createSignal<number>(0);318319 const needsBackupPassword = createMemo(() => preview()?.requiresPassword ?? false);320 const masterPasswordReady = createMemo(321 () => masterPassword().length >= 8 && masterPassword() === masterConfirm()322 );323324 async function refreshExistingCount() {325 if (isOnboarding) return;326 const res = await sendMessage({ type: "GET_ENTRIES" });327 const list = (res as { ok: boolean; data?: VaultEntry[] }).data ?? [];328 setExistingCount(list.length);329 }330 void refreshExistingCount();331332 async function parseSource(text: string, password?: string) {333 try {334 const result = await parseImportSource(text, password);335 setPreview(result);336 setError(null);337 if (result.format === "unknown") {338 setError(339 "We couldn't recognise this file format. Peach supports: Peach JSON backups, Bitwarden JSON/CSV, Proton Pass JSON/CSV, 1Password JSON, and generic browser CSV exports."340 );341 }342 return result;343 } catch (err) {344 const message = err instanceof Error ? err.message : String(err);345 setPreview(null);346 setError(getImportErrorCopy(message));347 return null;348 }349 }350351 async function handleFileSelect(event: Event) {352 const file = (event.target as HTMLInputElement).files?.[0];353 if (!file) return;354355 setPreview(null);356 setRawImport(null);357 setBackupPassword("");358 setError(null);359 setAwaitingReplaceConfirm(false);360361 const text = await file.text();362 setRawImport(text);363 await parseSource(text);364 }365366 async function handleUnlockBackup() {367 const raw = rawImport();368 if (!raw || !backupPassword().trim()) return;369 await parseSource(raw, backupPassword());370 }371372 async function countDuplicates(incoming: ImportParseResult["entries"]): Promise<number> {373 if (incoming.length === 0) return 0;374 const res = await sendMessage({ type: "GET_ENTRIES" });375 const existing = ((res as { ok: boolean; data?: VaultEntry[] }).data ?? []).filter((e) => !e.deletedAt);376 const keyOf = (title?: string, url?: string, username?: string) =>377 `${(title ?? "").toLowerCase()}|${(url ?? "").toLowerCase()}|${(username ?? "").toLowerCase()}`;378 const existingKeys = new Set(existing.map((e) => keyOf(e.title, e.url, e.username)));379 let dupes = 0;380 for (const entry of incoming) {381 const u = "username" in entry ? entry.username : undefined;382 const url = "url" in entry ? entry.url : undefined;383 if (existingKeys.has(keyOf(entry.title, url, u))) dupes += 1;384 }385 return dupes;386 }387388 async function handleImport() {389 const currentPreview = preview();390 if (!currentPreview) return;391392 if (needsBackupPassword() && !backupPassword().trim()) {393 setError("Enter the backup password used for this export.");394 return;395 }396397 if (needsMasterPassword()) {398 if (masterPassword().length < 8) {399 setError("Master password must be at least 8 characters.");400 return;401 }402 if (!masterPasswordReady()) {403 setError("Master passwords do not match.");404 return;405 }406 }407408 if (!isOnboarding && mergeMode() === "replace" && !awaitingReplaceConfirm()) {409 setAwaitingReplaceConfirm(true);410 return;411 }412413 setImporting(true);414 setError(null);415 setDuplicateWarning(0);416 setImportProgress(null);417418 try {419 if (currentPreview.entries.length === 0) {420 throw new Error("NO_ENTRIES_IMPORTED");421 }422423 if (needsMasterPassword()) {424 const createResponse = await sendMessage({425 type: "CREATE_VAULT",426 payload: { password: masterPassword() },427 });428 if (!(createResponse as { ok: boolean }).ok) {429 throw new Error("CREATE_VAULT_FAILED");430 }431 }432433 if (!isOnboarding && mergeMode() === "replace") {434 const clearRes = await sendMessage({ type: "CLEAR_VAULT_ENTRIES" });435 if (!(clearRes as { ok: boolean }).ok) {436 throw new Error("CLEAR_VAULT_FAILED");437 }438 }439440 let dupes = 0;441 if (!isOnboarding && mergeMode() === "add") {442 dupes = await countDuplicates(currentPreview.entries);443 }444445 let imported = 0;446 setImportProgress({ current: 0, total: currentPreview.entries.length });447448 const batchRes = await sendMessage({449 type: "BATCH_ADD_ENTRIES",450 payload: { entries: currentPreview.entries },451 });452453 if ((batchRes as { ok: boolean }).ok) {454 imported = ((batchRes as { ok: boolean; data?: VaultEntry[] }).data ?? []).length;455 setImportProgress({ current: imported, total: currentPreview.entries.length });456 } else {457 for (const entry of currentPreview.entries) {458 const response = await sendMessage({459 type: "ADD_ENTRY",460 payload: entry,461 });462 if ((response as { ok: boolean }).ok) imported += 1;463 setImportProgress({ current: imported, total: currentPreview.entries.length });464 }465 }466467 if (imported === 0) {468 throw new Error("NO_ENTRIES_IMPORTED");469 }470471 if (currentPreview.licenseKey) {472 await sendMessage({ type: "ACTIVATE_LICENSE", payload: { key: currentPreview.licenseKey } }).catch(() => {});473 }474475 setDuplicateWarning(dupes);476 await props.onImport();477 } catch (err) {478 const message = err instanceof Error ? err.message : String(err);479 setError(getImportErrorCopy(message));480 setImporting(false);481 setImportProgress(null);482 return;483 }484485 setImporting(false);486 setImportProgress(null);487 }488489 return (490 <>491 <SubviewHeader title="backup file" onBack={props.onBack} mode={props.mode} />492 <div class="peach-settings-body">493 <Show when={isOnboarding && props.replacesExistingVault}>494 <div class="peach-settings-group">495 <div class="peach-restore-warning">496 Restoring now replaces the locked local vault on this device with497 a fresh one from your backup.498 </div>499 </div>500 </Show>501 <div class="peach-settings-group peach-export-card peach-export-card--primary">502 <p class="peach-settings-label">choose a file</p>503 <p class="peach-export-card-copy">504 Bitwarden, 1Password, Proton Pass, Chrome / Edge / Safari CSV, FIDO505 credential-exchange passkeys, or a Peach JSON backup.506 </p>507508 <div class="peach-provider-chips">509 <span class="peach-provider-chip">Peach JSON</span>510 <span class="peach-provider-chip">FIDO passkeys</span>511 <span class="peach-provider-chip">Proton Pass</span>512 <span class="peach-provider-chip">Bitwarden</span>513 <span class="peach-provider-chip">1Password</span>514 <span class="peach-provider-chip">Browser CSV</span>515 </div>516517 <input518 class="peach-restore-file"519 type="file"520 accept=".csv,.json"521 onChange={(event) => void handleFileSelect(event)}522 />523 </div>524525 <Show when={needsBackupPassword()}>526 <div class="peach-settings-group">527 <p class="peach-settings-label">unlock backup</p>528 <p class="peach-export-card-copy">529 Enter the password used when you created this encrypted Peach JSON530 backup.531 </p>532 <input533 class="peach-restore-input"534 type="password"535 placeholder="backup password"536 value={backupPassword()}537 onInput={(event) =>538 setBackupPassword((event.target as HTMLInputElement).value)539 }540 onBlur={() => { if (backupPassword().trim()) void handleUnlockBackup(); }}541 onKeyDown={(event) => {542 if (event.key === "Enter") {543 event.preventDefault();544 void handleUnlockBackup();545 }546 }}547 />548 <div class="peach-settings-actions">549 <button550 class="peach-btn peach-btn--subtle peach-btn--sm"551 onClick={() => void handleUnlockBackup()}552 disabled={!backupPassword().trim()}553 >554 Decrypt →555 </button>556 </div>557 </div>558 </Show>559560 <Show when={preview() && !needsBackupPassword() && preview()!.format !== "unknown"}>561 <div class="peach-settings-group">562 <p class="peach-settings-label">{preview()!.label}</p>563 <div class="peach-import-summary">564 Found <strong>{preview()!.entries.length}</strong>{" "}565 entr{preview()!.entries.length === 1 ? "y" : "ies"}566 {preview()!.skippedItems > 0567 ? `. ${preview()!.skippedItems} skipped because no password was present.`568 : "."}569 </div>570571 <div class="peach-import-preview">572 {preview()!.entries.slice(0, 8).map((entry) => (573 <div class="peach-import-preview-row">574 <span class="peach-import-preview-title">{entry.title}</span>575 <span class="peach-import-preview-meta">576 {entry.type === "passkey"577 ? `${entry.userName || "passkey"} • ${entry.rpId || "site"}`578 : entry.type === "note"579 ? "secure note"580 : entry.username || entry.url || "password item"}581 </span>582 </div>583 ))}584 <Show when={preview()!.entries.length > 8}>585 <div class="peach-import-preview-more">586 ...and {preview()!.entries.length - 8} more587 </div>588 </Show>589 </div>590 </div>591 </Show>592593 <Show when={!isOnboarding && preview() && !needsBackupPassword() && preview()!.format !== "unknown" && preview()!.entries.length > 0}>594 <div class="peach-settings-group">595 <p class="peach-settings-label">merge or replace</p>596 <label class="peach-import-choice">597 <input598 type="radio"599 name="merge-mode"600 value="add"601 checked={mergeMode() === "add"}602 onChange={() => { setMergeMode("add"); setAwaitingReplaceConfirm(false); }}603 />604 <span class="peach-import-choice-main">605 <span class="peach-import-choice-label">Add to existing vault</span>606 <span class="peach-import-choice-sub">607 This will add {preview()!.entries.length} entries to your existing {existingCount()}.608 </span>609 </span>610 </label>611 <label class="peach-import-choice">612 <input613 type="radio"614 name="merge-mode"615 value="replace"616 checked={mergeMode() === "replace"}617 onChange={() => setMergeMode("replace")}618 />619 <span class="peach-import-choice-main">620 <span class="peach-import-choice-label">Replace vault</span>621 <span class="peach-import-choice-sub">622 This will replace your {existingCount()} entries with {preview()!.entries.length} imported ones.623 </span>624 </span>625 </label>626 <Show when={awaitingReplaceConfirm()}>627 <div class="peach-restore-warning" style="margin-top:6px">628 Replace forever? This cannot be undone.629 </div>630 </Show>631 </div>632 </Show>633634 <Show when={needsMasterPassword() && preview() && !needsBackupPassword() && preview()!.format !== "unknown"}>635 <div class="peach-settings-group">636 <p class="peach-settings-label">new master password</p>637 <p class="peach-export-card-copy">638 This password unlocks the Peach vault created on this device after639 import.640 </p>641 <div class="peach-auth-grid">642 <input643 class="peach-restore-input"644 type="password"645 placeholder="new master password"646 value={masterPassword()}647 onInput={(event) =>648 setMasterPassword((event.target as HTMLInputElement).value)649 }650 />651 <input652 class="peach-restore-input"653 type="password"654 placeholder="confirm master password"655 value={masterConfirm()}656 onInput={(event) =>657 setMasterConfirm((event.target as HTMLInputElement).value)658 }659 />660 </div>661 </div>662 </Show>663664 <Show when={error()}>665 <div class="peach-settings-group">666 <div class="peach-restore-error">{error()}</div>667 </div>668 </Show>669670 <Show when={importing() && importProgress()}>671 <div class="peach-settings-group">672 <div class="peach-import-summary">673 Importing... [{importProgress()!.current} / {importProgress()!.total}]674 </div>675 </div>676 </Show>677678 <Show when={duplicateWarning() > 0 && !importing()}>679 <div class="peach-settings-group">680 <div class="peach-export-card-copy">681 {duplicateWarning()} {duplicateWarning() === 1 ? "entry appears" : "entries appear"} to be duplicates of existing ones. They were still imported — you can review and delete them in your vault.682 </div>683 </div>684 </Show>685686 <Show when={preview() && !needsBackupPassword() && preview()!.format !== "unknown"}>687 <div class="peach-settings-group">688 <div class="peach-settings-actions">689 <button690 class="peach-btn peach-btn--subtle peach-btn--sm"691 onClick={() => void handleImport()}692 disabled={693 importing() ||694 preview()!.entries.length === 0 ||695 (needsMasterPassword() && !masterPasswordReady())696 }697 >698 {importing()699 ? "importing..."700 : isOnboarding701 ? `Create vault + import ${preview()!.entries.length}`702 : mergeMode() === "replace"703 ? awaitingReplaceConfirm()704 ? "Yes, replace forever"705 : `Replace vault with ${preview()!.entries.length}`706 : `Import ${preview()!.entries.length}`}707 </button>708 </div>709 </div>710 </Show>711 </div>712 </>713 );714}715716717718719720function OverlayHeader(props: { title: string; onBack: () => void }) {721 return (722 <div class="peach-overlay-header">723 <button724 class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"725 onClick={props.onBack}726 aria-label="Back"727 >728 <svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">729 <path d="M6 1L1 6L6 11" />730 </svg>731 </button>732 <span class="peach-overlay-title">{props.title}</span>733 <div class="peach-overlay-spacer"></div>734 </div>735 );736}737738function SubviewHeader(props: {739 title: string;740 onBack: () => void;741 mode: "existing" | "onboarding";742}) {743 744 745 return (746 <Show747 when={props.mode === "onboarding"}748 fallback={<OverlayHeader title={props.title} onBack={props.onBack} />}749 >750 <div class="peach-lock-view-header">751 <PeachWordmark class="peach-wordmark-wrap" />752 <button class="peach-lock-restore-link" onClick={props.onBack}>753 ‹ back754 </button>755 </div>756 </Show>757 );758}759760function getImportErrorCopy(error: string): string {761 switch (error) {762 case "UNSUPPORTED_IMPORT_FORMAT":763 return "Unrecognised file format. Use CSV, Bitwarden JSON, Proton Pass JSON/CSV, or a Peach encrypted backup.";764 case "INVALID_EXPORT_PASSWORD":765 case "INVALID_PASSPHRASE":766 return "Wrong backup password. This is the password you chose when you created the backup.";767 case "INVALID_PEACH_EXPORT":768 return "This Peach backup file looks corrupted or incomplete.";769 case "INVALID_PEACHSCRIPT_CHARACTER":770 return "This doesn't look like PeachScript. Make sure you copied the entire backup string.";771 case "PAYLOAD_TOO_SHORT":772 return "PeachScript looks truncated. Copy the full backup string and try again.";773 case "BAD_MAGIC":774 case "UNSUPPORTED_VERSION":775 return "This PeachScript was created by a different version of Peach.";776 case "CHECKSUM_FAILED":777 return "PeachScript appears corrupted. Re-copy from the source device.";778 case "CREATE_VAULT_FAILED":779 return "Peach couldn't create a new local vault before importing.";780 case "CLEAR_VAULT_FAILED":781 return "Peach couldn't clear the existing vault before replacing it.";782 case "MASTER_PASSWORD_REQUIRED":783 return "Choose a master password for your new vault first.";784 case "RESTORE_FAILED":785 return "Restore failed. Your existing vault is unchanged.";786 case "NO_ENTRIES_IMPORTED":787 return "No importable password entries were found in that file.";788 default:789 return error;790 }791}