Skip to content

Commit bfb3b46

Browse files
committed
zip: add handling for malformed zips
1 parent b571308 commit bfb3b46

File tree

5 files changed

+558
-76
lines changed

5 files changed

+558
-76
lines changed

src/components/DropZone.tsx

Lines changed: 114 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,12 @@ interface SendSafelyModalProps {
7070
saveCredentials: (key: string, secret: string) => void;
7171
loadSendSafelyFile: (file: PackageFile) => void;
7272
setLoading: (loading: boolean) => void;
73+
loadingMessage: string;
7374
setLoadingMessage: (message: string) => void;
75+
recoveryEntriesCount: number | null;
76+
setRecoveryEntriesCount: (count: number | null) => void;
77+
fileLoading: boolean;
78+
setFileLoading: (loading: boolean) => void;
7479
waitForWorkers: () => Promise<import("../state/types").IWorkerManager>;
7580
}
7681

@@ -97,14 +102,19 @@ function SendSafelyModalContent({
97102
saveCredentials,
98103
loadSendSafelyFile,
99104
setLoading,
105+
loadingMessage,
100106
setLoadingMessage,
107+
recoveryEntriesCount,
108+
setRecoveryEntriesCount,
109+
fileLoading,
110+
setFileLoading,
111+
waitForWorkers,
101112
}: SendSafelyModalProps) {
102113
const [showOtherFiles, setShowOtherFiles] = useState(false);
103114
const [forceStep1, setForceStep1] = useState(false);
104115
const [showValidateButton, setShowValidateButton] = useState(true);
105116
const [showSaveButton, setShowSaveButton] = useState(false);
106117
const [credentialsSaved, setCredentialsSaved] = useState(false);
107-
const [fileLoading, setFileLoading] = useState(false);
108118
const [fileError, setFileError] = useState<string | null>(null);
109119

110120
// On input change: reset button states and optionally auto-validate
@@ -707,14 +717,16 @@ function SendSafelyModalContent({
707717
);
708718
try {
709719
await loadSendSafelyFile(file);
720+
// Note: Don't close modal here - it will be closed by:
721+
// 1. User clicking "Proceed" button if recovery warning is shown
722+
// 2. Normal flow via onLoadingStage if no recovery warning
710723
} catch (error) {
711724
setFileError(
712725
error instanceof Error
713726
? error.message
714727
: "Unknown error occurred",
715728
);
716729
setLoading(false);
717-
} finally {
718730
setFileLoading(false);
719731
}
720732
}
@@ -860,7 +872,7 @@ function SendSafelyModalContent({
860872
</div>
861873
</div>
862874

863-
{fileLoading && (
875+
{fileLoading && recoveryEntriesCount === null && (
864876
<div style={{ textAlign: "center", padding: "20px" }}>
865877
<span className="loading-spinner-small" />
866878
<div
@@ -870,7 +882,67 @@ function SendSafelyModalContent({
870882
color: "var(--text-muted)",
871883
}}
872884
>
873-
Downloading and decrypting file...
885+
{loadingMessage || "Loading ZIP file metadata..."}
886+
</div>
887+
</div>
888+
)}
889+
890+
{recoveryEntriesCount !== null && (
891+
<div style={{ padding: "20px" }}>
892+
<div
893+
style={{
894+
textAlign: "center",
895+
marginBottom: "16px",
896+
fontSize: "24px",
897+
}}
898+
>
899+
⚠️
900+
</div>
901+
<div
902+
style={{
903+
marginBottom: "12px",
904+
fontSize: "13px",
905+
fontWeight: "bold",
906+
color: "var(--accent-warning)",
907+
textAlign: "center",
908+
}}
909+
>
910+
Zip file appears malformed or truncated
911+
</div>
912+
<div
913+
style={{
914+
marginBottom: "20px",
915+
fontSize: "12px",
916+
color: "var(--text-muted)",
917+
textAlign: "center",
918+
}}
919+
>
920+
File listing for {recoveryEntriesCount.toLocaleString()}{" "}
921+
files was decoded, but could be incomplete
922+
</div>
923+
<div style={{ display: "flex", justifyContent: "center" }}>
924+
<button
925+
onClick={async () => {
926+
// User confirmed - clear recovery warning and tell ZIP worker to proceed
927+
setRecoveryEntriesCount(null);
928+
const workerManager = await waitForWorkers();
929+
await workerManager.proceedWithRecovery();
930+
// ZIP worker will emit initializeComplete and continue with normal flow
931+
// Modal will close when we transition to table-loading stage
932+
}}
933+
style={{
934+
padding: "10px 24px",
935+
backgroundColor: "var(--accent-primary)",
936+
color: "#fff",
937+
border: "none",
938+
borderRadius: "4px",
939+
cursor: "pointer",
940+
fontSize: "13px",
941+
fontWeight: "500",
942+
}}
943+
>
944+
Proceed
945+
</button>
874946
</div>
875947
</div>
876948
)}
@@ -948,6 +1020,21 @@ function DropZone() {
9481020
const [packageLoading, setPackageLoading] = useState(false);
9491021
const [packageError, setPackageError] = useState<string | null>(null);
9501022
const [modalDismissable, setModalDismissable] = useState(false);
1023+
const [fileLoading, setFileLoading] = useState(false);
1024+
1025+
// CD recovery warning state - shown in modal when recovery is used
1026+
const [recoveryEntriesCount, setRecoveryEntriesCount] = useState<number | null>(null);
1027+
1028+
// Single function to close the SendSafely modal
1029+
const closeSendSafelyModal = useCallback(() => {
1030+
setShowSendSafelyModal(false);
1031+
setValidationStatus(null);
1032+
setPackageInfo(null);
1033+
setPackageError(null);
1034+
setPackageLoading(false);
1035+
setFileLoading(false);
1036+
setRecoveryEntriesCount(null);
1037+
}, []);
9511038

9521039
// Debug page state
9531040
const [showDebugPage, setShowDebugPage] = useState(false);
@@ -1082,10 +1169,17 @@ function DropZone() {
10821169
onLoadingStage: (stage: string, message: string) => {
10831170
setLoadingMessage(message);
10841171

1085-
if (stage === "complete" || stage === "error") {
1172+
// Close modal when we move past ZIP loading phase
1173+
if (stage === "table-loading" || stage === "complete" || stage === "error") {
10861174
setLoading(false);
1175+
closeSendSafelyModal();
10871176
}
10881177
},
1178+
onCdScanningComplete: (entriesCount: number) => {
1179+
// ZIP recovery was used - show warning in modal
1180+
// Keep fileLoading = true so we stay in step 4
1181+
setRecoveryEntriesCount(entriesCount);
1182+
},
10891183
onSendStackFileToIframe: (path: string, content: string, name?: string) => {
10901184
// Detect mode based on file path
10911185
const isLabeled = path.includes("stacks_with_labels.txt");
@@ -1126,7 +1220,7 @@ function DropZone() {
11261220
// stackData should already be populated via ADD_STACK_FILE actions
11271221
dispatch({ type: "SET_STACKGAZER_READY", ready: true });
11281222
},
1129-
onFileList: (entries: ZipEntryMeta[]) => {
1223+
onFileList: (entries: ZipEntryMeta[], _totalFiles: number) => {
11301224
// Received file list
11311225

11321226
(window as unknown as { __zipReader: unknown }).__zipReader =
@@ -1240,12 +1334,7 @@ function DropZone() {
12401334
},
12411335
});
12421336

1243-
// Close the modal after starting the load
1244-
setShowSendSafelyModal(false);
1245-
setValidationStatus(null);
1246-
setPackageInfo(null);
1247-
setPackageError(null);
1248-
setPackageLoading(false);
1337+
// Don't close here - onLoadingStage will call closeSendSafelyModal when complete
12491338
};
12501339

12511340
const validateCredentials = async (
@@ -1893,12 +1982,8 @@ function DropZone() {
18931982
justifyContent: "center",
18941983
}}
18951984
onClick={() => {
1896-
if (modalDismissable) {
1897-
setShowSendSafelyModal(false);
1898-
setValidationStatus(null);
1899-
setPackageInfo(null);
1900-
setPackageError(null);
1901-
setPackageLoading(false);
1985+
if (modalDismissable && recoveryEntriesCount === null) {
1986+
closeSendSafelyModal();
19021987
}
19031988
}}
19041989
>
@@ -1960,20 +2045,20 @@ function DropZone() {
19602045
</button>
19612046
<button
19622047
onClick={() => {
1963-
setShowSendSafelyModal(false);
1964-
setValidationStatus(null);
1965-
setPackageInfo(null);
1966-
setPackageError(null);
1967-
setPackageLoading(false);
2048+
// Don't allow closing modal while recovery warning is shown
2049+
if (recoveryEntriesCount === null) {
2050+
closeSendSafelyModal();
2051+
}
19682052
}}
19692053
style={{
19702054
background: "transparent",
19712055
border: "none",
19722056
color: "var(--text-secondary)",
19732057
fontSize: "18px",
1974-
cursor: "pointer",
2058+
cursor: recoveryEntriesCount === null ? "pointer" : "not-allowed",
19752059
padding: "0 4px",
19762060
lineHeight: "1",
2061+
opacity: recoveryEntriesCount === null ? 1 : 0.5,
19772062
}}
19782063
>
19792064
×
@@ -2162,7 +2247,12 @@ function DropZone() {
21622247
validateCredentials={validateCredentials}
21632248
loadSendSafelyFile={loadSendSafelyFile}
21642249
setLoading={setLoading}
2250+
loadingMessage={loadingMessage}
21652251
setLoadingMessage={setLoadingMessage}
2252+
recoveryEntriesCount={recoveryEntriesCount}
2253+
setRecoveryEntriesCount={setRecoveryEntriesCount}
2254+
fileLoading={fileLoading}
2255+
setFileLoading={setFileLoading}
21662256
waitForWorkers={waitForWorkers}
21672257
/>
21682258
)}

src/services/WorkerManager.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,11 @@ export class WorkerManager implements IWorkerManager {
9999
await this.sendMessage({ type: "init" });
100100
}
101101

102+
async proceedWithRecovery(): Promise<void> {
103+
// User confirmed they want to proceed with recovered ZIP entries
104+
await this.sendMessage({ type: "proceedWithRecovery" });
105+
}
106+
102107
async loadSingleStackFile(filePath: string): Promise<void> {
103108
// Trigger loading of a single stack file
104109
await this.sendMessage({ type: "loadSingleStackFile", filePath });
@@ -431,6 +436,12 @@ export class WorkerManager implements IWorkerManager {
431436
);
432437
break;
433438

439+
case "cdScanningComplete":
440+
this.options.onCdScanningComplete?.(
441+
message.entriesCount as number,
442+
);
443+
break;
444+
434445
case "sendStackFileToIframe":
435446
this.options.onSendStackFileToIframe?.(
436447
message.path as string,

src/state/types.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ export interface IWorkerManager {
202202
loadZipFile(file: File): Promise<ZipEntryMeta[]>;
203203
loadZipDataFromSendSafely(config: SendSafelyConfig): Promise<ZipEntryMeta[]>;
204204
initializeWorkers(): Promise<void>;
205+
proceedWithRecovery(): Promise<void>;
205206
destroy(): void;
206207

207208
// Database operations
@@ -248,7 +249,11 @@ export interface IWorkerManager {
248249
export interface IWorkerManagerCallbacks {
249250
// Stage progression callbacks
250251
onLoadingStage?: (stage: string, message: string) => void;
251-
onFileList?: (entries: ZipEntryMeta[], totalFiles: number) => void;
252+
onFileList?: (
253+
entries: ZipEntryMeta[],
254+
totalFiles: number,
255+
) => void;
256+
onCdScanningComplete?: (entriesCount: number) => void;
252257
onTableAdded?: (table: TableData) => void;
253258
onSendStackFileToIframe?: (path: string, content: string, name?: string) => void;
254259
onStackProcessingComplete?: (stackFilesCount: number) => void;

0 commit comments

Comments
 (0)