Skip to content

refactor(load): unify legacy and opossum file loading paths#3167

Open
abraemer wants to merge 14 commits into
feat/adm-zip-migrationfrom
refactor/unify-legacy-opossum-loading
Open

refactor(load): unify legacy and opossum file loading paths#3167
abraemer wants to merge 14 commits into
feat/adm-zip-migrationfrom
refactor/unify-legacy-opossum-loading

Conversation

@abraemer

@abraemer abraemer commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Summary

After the fflate-to-adm-zip migration (#3164), the loading code still branched on file format throughout the entire stack. Every layer — from listeners through the DB process to loadFile.ts — had isOpossumFileFormat checks that selected different parsing paths, different save targets, and different state fields depending on whether the input was a .opossum file or a legacy JSON input.

This PR eliminates that dual-path complexity by ensuring every load produces the same uniform result — a LoadedArchive containing an AdmZip instance, parsed input, and parsed output — regardless of whether the source was a .opossum file or a legacy JSON input. It also simplifies the save path so that loadFile passes the output data it already has in hand directly to writeOpossumFile, avoiding a pointless round-trip through the DB.

Stacked on top of #3164 (adm-zip migration).

Problem

Before this PR, the import flow for legacy files was:

  1. importFileConvertAndLoadListenerconvertToOpossum() writes a .opossum file to disk
  2. initializeGlobalBackendState(opossumFilePath, true) → opens it as .opossum
  3. loadFile() → re-reads the file it just wrote, branches on isOpossumFileFormat()
  4. Downstream code branches on format for save paths, state fields, etc.

This was wasteful (write + immediate re-read) and needlessly complex: the listener already knew the format but threw that information away, forcing loadFile to re-derive it from the file extension.

Meanwhile, the "direct legacy load" path in loadFile.ts (the else branch of isOpossumFileFormat) was unreachable from the UI and broken — saving would throw Cannot save: no input file loaded because storedOpossumZip was undefined.

Additionally, saveFile covertly depended on global DB state via getSaveFileArgs(), even in the initial load path where the output data was already in hand — forcing a pointless DB round-trip.

Approach

Unified loading

Two format-specific loader functions produce a uniform LoadedArchive type:

  • loadOpossumFile(path) — opens AdmZip, extracts and parses input.json/output.json
  • loadLegacyFile(path) — reads raw JSON (.json/.json.gz), parses it, reads sidecar _attributions.json if present, then constructs a new in-memory AdmZip with both entries packed in

From that point on, all downstream code is format-agnostic: always an AdmZip, always an opossumFilePath for saves.

The listener layer explicitly picks the right loader (it already knows the format), so loadFile() no longer needs to infer it.

Explicit save paths

loadFile now adds output.json to the in-memory zip when it creates output from scratch (the null-output case), then passes resolvedOutputData directly to writeOpossumFile — no round-trip through the DB. The user-initiated save path in dbProcess.ts explicitly calls buildOpossumOutputFile (which reads from the DB) then writeOpossumFile. The global-state dependency is visible at each call site, not hidden inside a wrapper function.

Changes

Core loading

  • parseFile.ts: Replace parseOpossumFile / parseInputJsonFile / parseOutputJsonFile with loadOpossumFile / loadLegacyFile returning uniform LoadedArchive
  • loadFile.ts: Remove all isOpossumFileFormat branching; accepts LoadedArchive + opossumFilePath. When creating output from scratch, adds output.json to the zip and writes via writeOpossumFile directly (no DB round-trip)

IPC layer

  • dbProcess.ts: Split LoadFileMessage into LoadOpossumFileMessage (format: "opossum") and LoadLegacyFileMessage (format: "legacy"); routes to correct loader, passes uniform result to loadFile. Save handler calls buildOpossumOutputFile + writeOpossumFile explicitly
  • dbProcessClient.ts: Split loadFile() into loadOpossumFile() and loadLegacyFile(); saveFile() takes explicit (projectId, opossumFilePath) instead of SaveFileParams
  • importFromFile.ts: Split into loadOpossumFileFromPath() and loadLegacyFileFromPath()

Listeners

  • listeners.ts: importFileConvertAndLoadListener no longer calls convertToOpossum() (disk write + re-read) for legacy files; calls loadLegacyFileFromPath() directly. ScanCode/OWASP still use convertToOpossum. saveFileListener simplified — no more attributionFilePath/inputFileChecksum. initializeGlobalBackendState simplified — no isOpossumFormat boolean.

Save path

  • Deleted saveFile.tsSaveFileParams and persistOutputFile removed; callers use writeOpossumFile directly
  • buildOpossumOutputFile.ts (renamed from getSaveFileArgs.ts): exports buildOpossumOutputFile (assembles OpossumOutputFile from DB state) and getPreferredOverOriginIds (for its own test); querySaveFileArgs is private
  • writeOpossumFile in write-file.ts: always uses zip.updateFile(OUTPUT_FILE_NAME, ...) — the entry is guaranteed to exist because loadFile adds it when creating output

Types & state

  • types.ts: Remove resourceFilePath/attributionFilePath from GlobalBackendState; remove ParsedOpossumInputAndOutput (replaced by LoadedArchive)
  • getLoadedFile.ts: Remove getLoadedFileType() (dead code); simplify getLoadedFilePath() and isFileLoaded()

Deleted files

  • enums.ts (only contained LoadedFileFormat enum)
  • isOpossumFileFormat.ts
  • opossumArchive.ts (already inlined in previous commit)
  • saveFile.ts (replaced by direct writeOpossumFile calls)

Test plan

  • parseFile.test.ts — 12 tests pass (updated to test loadOpossumFile/loadLegacyFile; added tests for sidecar output reading, .json.gz, error handling on sidecar)
  • loadFile.test.ts — updated to use new two-step flow (load archive → loadFile); tests that output.json is written correctly for legacy input
  • buildOpossumOutputFile.test.ts — simplified (only tests buildOpossumOutputFile, not the deleted wrapper)
  • getLoadedFile.test.ts — simplified (removed LoadedFileFormat.Json tests)
  • globalBackendState.test.ts — updated (removed resourceFilePath/attributionFilePath)
  • listeners.test.ts / get-save-file-listener.test.ts / importFromFile.test.ts / errorHandling.test.ts — all pass with updated mocks
  • opossum-file.test.ts — updated to use loadOpossumFile instead of parseOpossumFile
  • TypeScript type check passes (tsc --noEmit)
  • Knip (unused export check) passes
  • Manual test: open a .opossum file
  • Manual test: import a legacy .json file via the import dialog

abraemer added 2 commits June 17, 2026 15:59
After the fflate-to-adm-zip migration (PR #3164), the loading code still
branched on file format throughout the stack. This commit eliminates that
dual-path complexity by ensuring every load produces the same uniform
result — a LoadedArchive containing an AdmZip instance, parsed input,
and parsed output — regardless of whether the source was a .opossum file
or a legacy JSON input.

Key changes:

- parseFile.ts: replace parseOpossumFile / parseInputJsonFile /
  parseOutputJsonFile with two format-specific loaders that return a
  uniform LoadedArchive type:
  • loadOpossumFile(path) — opens AdmZip, extracts input.json/output.json
  • loadLegacyFile(path) — reads raw JSON, parses it, reads sidecar
    _attributions.json if present, then constructs a new in-memory
    AdmZip with input.json packed in

- loadFile.ts: remove all isOpossumFileFormat branching; now takes a
  LoadedArchive + opossumFilePath (format-agnostic)

- dbProcess.ts: split LoadFileMessage into LoadOpossumFileMessage and
  LoadLegacyFileMessage, route to correct loader, pass uniform result
  to loadFile

- dbProcessClient.ts: split loadFile() into loadOpossumFile() and
  loadLegacyFile()

- importFromFile.ts: split into loadOpossumFileFromPath() and
  loadLegacyFileFromPath()

- listeners.ts: importFileConvertAndLoadListener no longer calls
  convertToOpossum() (disk write + re-read); it calls
  loadLegacyFileFromPath() directly.  saveFileListener simplified —
  no more attributionFilePath/inputFileChecksum.
  initializeGlobalBackendState simplified — no isOpossumFormat boolean.

- saveFile.ts: remove attributionFilePath branch; always write via
  writeOpossumFile

- types.ts: remove resourceFilePath/attributionFilePath from
  GlobalBackendState; remove ParsedOpossumInputAndOutput (replaced by
  LoadedArchive)

- getLoadedFile.ts: remove getLoadedFileType() (dead code); simplify
  getLoadedFilePath() and isFileLoaded()

- Delete enums.ts (only contained LoadedFileFormat), isOpossumFileFormat.ts

Signed-off-by: Adrian Braemer <adrian.braemer@tngtech.com>
Comment thread src/ElectronBackend/api/saveFile.ts
Comment thread src/ElectronBackend/main/listeners.ts Outdated
Comment thread src/ElectronBackend/api/saveFile.ts Outdated
abraemer added 12 commits June 18, 2026 10:48
- Keep inputFileMD5Checksum in save output (always undefined, with comment)
- Branch importFileConvertAndLoadListener on fileType: legacy uses
  loadLegacyFileFromPath, ScanCode/OWASP keep convertToOpossum
- Split saveFile into buildOpossumOutputFile (pure, testable) + saveFile
- Fix Prettier formatting and FileType import
- Remove unused imports (loadLegacyFileFromPath, LoadFileError,
  getFilePathWithAppendix, fs) flagged by strict TS
- Fix outputEntry possibly-null by using non-null assertion
- Fix writeOpossumFile: use addFile when output.json doesn't exist yet
  (happens when saving an in-memory zip from loadLegacyFile that only
  contains input.json)
- Unexport OPOSSUM_FILE_EXTENSION (only used internally in write-file-utils)
- Unexport LoadFileError (only used internally in loadFile.ts)
- Remove FileNotFoundError interface (never imported anywhere)
…date it

loadLegacyFile now adds output.json to the in-memory zip when a
sidecar _attributions.json exists. writeOpossumFile still needs the
getEntry guard because a freshly-opened .opossum file (or a legacy
file without sidecar output) may not have the entry yet — the first
saveFile call creates it via addFile, subsequent saves use updateFile.
…mplify writeOpossumFile

loadFile now adds the output.json entry to the in-memory zip when it
creates output from scratch (null-output case). This guarantees the zip
always has both entries after loadFile returns, so writeOpossumFile can
unconditionally use updateFile — no getEntry guard needed.
SaveFileParams was a wrapper only used to bundle two args that callers
already had separately. Replace with explicit (projectId, opossumFilePath, zip)
params and rename saveFile -> persistOutputFile to clarify its role.
…ctly

persistOutputFile was just buildOpossumOutputFile + writeOpossumFile.
The load path already has the output data in hand (resolvedOutputData)
so it can pass it directly to writeOpossumFile, avoiding a pointless
round-trip through the DB via getSaveFileArgs. The save path in
dbProcess now calls buildOpossumOutputFile + writeOpossumFile
explicitly, making the global-state dependency visible.
- File now exports buildOpossumOutputFile (main) and
  getPreferredOverOriginIds (for its own test)
- Internal getSaveFileArgs renamed to querySaveFileArgs (private)
- Deleted saveFile.ts (was empty after earlier refactor)
@abraemer abraemer marked this pull request as ready for review June 18, 2026 15:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant