Skip to content

feat: add archive support (zip, tar, 7z)#63

Merged
leszek3737 merged 4 commits into
mainfrom
zip
May 27, 2026
Merged

feat: add archive support (zip, tar, 7z)#63
leszek3737 merged 4 commits into
mainfrom
zip

Conversation

@leszek3737
Copy link
Copy Markdown
Owner

@leszek3737 leszek3737 commented May 27, 2026

Summary

Add core archive support (ZIP, TAR variants, 7z) with integrated UI dialogs, viewer preview, and batch operations.

New Features:

  • Support listing, extracting, and creating archives via a new archive operations module for ZIP, TAR (gz/bz2/xz/zst), and 7z formats.
  • Allow archive extraction and creation to run as background batch jobs with byte-level progress reporting and cancellation.
  • Add archive extract/create dialogs and an archive operations picker, including automatic format detection from filenames/extensions.
  • Enable opening archives in the built-in viewer as a formatted text listing instead of a hex dump, accessible via Enter/F3.

Enhancements:

  • Extend input handling, dialog rendering, and keybindings to integrate archive workflows (F7 conditional extract, F12 archive menu, Enter/F3 preview archives).
  • Refine text input handling in dialogs by sharing a common helper, improving consistency across dialog types.

Build:

  • Add compression and archive-related crates (zip, tar, flate2, zstd, ruzstd, sevenz-rust, bzip2, lzma-rs) to Cargo.toml.

Documentation:

  • Document archive support, formats, keybindings, and operations in the README, including new archive-related shortcuts and dependencies.

- Core archive module: list, extract, create for ZIP, TAR (GZ/BZ2/XZ/ZST), 7Z
- Security: path traversal prevention, symlink skip, OOM protection (temp files for XZ), setuid stripping
- UI: F7 extract/create dialog, F12 archive menu, Enter/F3 preview archives in viewer
- Viewer: archives displayed as formatted text listing (name, size, date) instead of hex
- Batch operations with progress and cancellation
- Dependencies: zip 8.6, bzip2 0.6, sevenz-rust 0.6, tar 0.4, flate2, zstd, lzma-rs
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented May 27, 2026

Reviewer's Guide

Implements a new archive subsystem (ZIP, TAR* and 7z) with list/extract/create operations, wires it into the batch job engine and UI (dialogs, pickers, keybindings, viewer), and documents the feature and new dependencies, including safety measures against path traversal, symlinks, and unsafe permissions.

Sequence diagram for archive extraction via F7 dialog

sequenceDiagram
    actor User
    participant NormalInput as handle_f7_key
    participant AppState
    participant Dialog as handle_archive_extract_dialog
    participant Batch as execute_batch_with_byte_progress
    participant Ops as batch_extract_archive
    participant Archive as archive::extract::extract_archive

    User->>NormalInput: press_F7
    NormalInput->>AppState: show_archive_dialog()
    AppState->>AppState: state.mode = DialogKind::ArchiveExtract

    User->>Dialog: Enter (confirm extract)
    Dialog->>AppState: PendingAction::ExtractArchive
    Dialog->>Batch: start_confirmed_action()
    Batch->>Batch: execute_batch_with_byte_progress()
    Batch->>Ops: batch_extract_archive()
    Ops->>Archive: archive::extract::extract_archive(source, dest, progress_tx, cancel)
    Archive-->>Ops: Result<(), ArchiveError>
    Ops-->>Batch: BatchReport
    Batch-->>AppState: update status/progress
Loading

Sequence diagram for archive preview in viewer

sequenceDiagram
    actor User
    participant NormalInput as handle_enter_key
    participant ViewerLoader as ViewerState::open_background
    participant ViewerState as ViewerState::open
    participant ArchiveList as archive::list::list_archive

    User->>NormalInput: Enter on archive
    NormalInput->>ViewerLoader: ViewerState::open_background(path)
    ViewerLoader->>ViewerState: open(path)
    ViewerState->>ViewerState: should_open_as_text(...)
    ViewerState->>ViewerState: open_as_archive_listing(path, file_size)
    ViewerState->>ViewerState: format_archive_listing(path)
    ViewerState->>ArchiveList: list_archive(path)
    ArchiveList-->>ViewerState: Vec<ArchiveEntry>
    ViewerState-->>ViewerLoader: ViewerState { view_mode = Text }
    ViewerLoader-->>User: archive listing displayed
Loading

File-Level Changes

Change Details Files
Add core archive abstraction and format-specific handlers (ZIP, TAR family, 7z) with detection, listing, extraction, and creation APIs, including safety protections.
  • Introduce ops::archive module with ArchiveFormat, ArchiveEntry and ArchiveError types and format auto-detection from headers/extensions.
  • Implement list/extract/create routing functions that delegate to format-specific implementations.
  • Add ZIP backend using the zip crate to list and extract archives with symlink skipping, safe path resolution, and archive creation using temporary files and recursive directory handling.
  • Add TAR backend using the tar crate plus flate2/bzip2/zstd/lzma-rs to support tar, tar.gz/bz2/xz/zst for list, safe extract with path traversal checks and setuid stripping, and tar creation (including XZ via temp files).
  • Add 7z backend using sevenz-rust to list and extract archives with cancel-aware streaming, path sanitization, and explicit unsupported create operation.
src/ops/archive/mod.rs
src/ops/archive/list.rs
src/ops/archive/extract.rs
src/ops/archive/create.rs
src/ops/archive/zip.rs
src/ops/archive/tar.rs
src/ops/archive/sevenz.rs
Integrate archive operations into batch engine and PendingAction model with progress reporting and cancellation.
  • Extend PendingAction with ExtractArchive and CreateArchive variants, including archive format in CreateArchive.
  • Map new PendingAction variants to human-readable labels via ops::helpers::action_label and adjust tests accordingly.
  • Add batch_extract_archive and batch_create_archive functions that spawn worker threads running archive::extract::extract_archive and archive::create::create_archive, forwarding byte deltas over mpsc channels to existing progress reporting and honoring cancel tokens.
  • Wire new batch handlers into execute_batch_with_byte_progress and ensure cancellation/worker-failure cases are surfaced correctly in BatchReport.
src/app/types/modes.rs
src/ops/helpers.rs
src/ops/batch.rs
Add archive-aware dialogs, pickers, and input handling so users can extract/create archives through F7/F12 and specialized dialogs.
  • Extend app DialogKind and PickerKind with ArchiveExtract, ArchiveCreate, and ArchiveMenu variants carrying source/destination and entry metadata.
  • Implement archive_format_from_path helper and dialog text-edit helper to share input editing across dialogs.
  • Add handle_archive_extract_dialog and handle_archive_create_dialog to validate paths, resolve user paths, infer archive format from filename, set PendingAction (ExtractArchive/CreateArchive), and trigger background jobs.
  • Wire new dialog handlers into handle_dialog and skip overwrite-confirm logic for archive actions.
  • Introduce Archive menu list picker with Extract/Create options, including behavior when an archive is under cursor or when multiple files are selected, and render it via render_list_picker_overlay.
  • Update key handling (F7, F12, Enter, F3 semantics via other changes) to open archive dialogs or the archive menu when appropriate.
src/app/types/dialogs.rs
src/input/dialogs.rs
src/input/pickers.rs
src/input/normal.rs
src/render.rs
src/render_dialog_map.rs
src/app/keymap.rs
src/ui/dialogs/mod.rs
src/ui/dialogs/archive.rs
Make the viewer archive-aware so Enter/F3 on archives open a formatted listing instead of hex.
  • Modify ViewerState::open to detect archive files via file_type::is_archive and attempt to open them as text listings before falling back to standard text/hex modes.
  • Add ViewerState::open_as_archive_listing and format_archive_listing helpers that call ops::archive::list::list_archive and render a simple table with size, modified time, and name (directories marked with trailing slash and size).
  • Adjust normal-mode Enter handling to open directories as before but preview archive files in the viewer by starting a background ViewerLoader and switching to Viewing mode.
src/ui/viewer/open.rs
src/input/mode_dispatch.rs
src/input/normal.rs
Update documentation, keybindings, and dependencies to reflect archive support and new keyboard operations.
  • Document archive support in README, including supported formats, read/write matrix, keyboard shortcuts (Enter/F3 preview, F7 extract, F12 archive menu), and behavior of extract/create dialogs and background jobs.
  • Update README key tables to describe new semantics for Enter/F3/F7 and add F12 mapping.
  • Extend KEYBINDINGS to describe F7 as Mkdir/ArchiveExtract and add F12 ArchiveMenu entry.
  • Add new dependencies for archive handling: zip, tar, flate2, zstd, ruzstd, sevenz-rust, bzip2, lzma-rs in Cargo.toml and mention them in README dependency table; update Cargo.lock accordingly.
README.md
src/app/keymap.rs
Cargo.toml
Cargo.lock

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • In archive_format_from_path the extension handling is narrower than elsewhere (e.g. detect_format) and the README (e.g. .tbz is supported by detection/docs but not accepted for creation), so it would be good to align all three to the same set of extensions to avoid user-visible inconsistencies.
  • The temp file names for TAR+XZ (lc-xz-tar-<pid> and lc-xz-decompress-<pid>) are fixed per process and not per operation; consider incorporating a random suffix or using a proper temp file API to avoid collisions between concurrent archive operations and to reduce the risk of races.
  • Creating a .7z archive currently goes through ArchiveFormat::SevenZ and ends in create_7z returning UnsupportedFormat; it may be clearer to either prevent .7z from being offered as a creatable format in the dialog or surface a more specific error so users understand that 7z writing is not implemented.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `archive_format_from_path` the extension handling is narrower than elsewhere (e.g. `detect_format`) and the README (e.g. `.tbz` is supported by detection/docs but not accepted for creation), so it would be good to align all three to the same set of extensions to avoid user-visible inconsistencies.
- The temp file names for TAR+XZ (`lc-xz-tar-<pid>` and `lc-xz-decompress-<pid>`) are fixed per process and not per operation; consider incorporating a random suffix or using a proper temp file API to avoid collisions between concurrent archive operations and to reduce the risk of races.
- Creating a `.7z` archive currently goes through `ArchiveFormat::SevenZ` and ends in `create_7z` returning `UnsupportedFormat`; it may be clearer to either prevent `.7z` from being offered as a creatable format in the dialog or surface a more specific error so users understand that 7z writing is not implemented.

## Individual Comments

### Comment 1
<location path="src/input/dialogs.rs" line_range="496-505" />
<code_context>
+fn handle_archive_extract_dialog(
</code_context>
<issue_to_address>
**issue (bug_risk):** Archive extract dialog ignores button selection and always treats Enter as "OK".

This dialog relies on `state.dialog_selection` to visually switch between `[ OK ]` and `[ Cancel ]`, but `handle_archive_extract_dialog` never updates `dialog_selection` on arrow/tab keys and always treats `KeyCode::Enter` as "OK". That makes `[ Cancel ]` effectively unreachable except via `Esc`. Please update `dialog_selection` on left/right/Tab and branch on the selected button when handling Enter so that `Cancel` actually cancels, consistent with other dialogs.
</issue_to_address>

### Comment 2
<location path="src/input/dialogs.rs" line_range="537-533" />
<code_context>
+fn handle_archive_create_dialog(
</code_context>
<issue_to_address>
**issue (bug_risk):** Archive create dialog has the same selection/Enter behavior issue as the extract dialog.

As with the extract dialog, `handle_archive_create_dialog` ignores `state.dialog_selection` on `KeyCode::Enter`, so `[ Cancel ]` can only be triggered with `Esc`. Please align this with the other dialogs so Enter activates the currently selected button and the action matches the highlighted option.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/input/dialogs.rs
Comment thread src/input/dialogs.rs
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces comprehensive archive support to Libre Commander, enabling users to browse, extract, and create various archive formats (ZIP, TAR/GZ/BZ2/XZ/ZST, and 7Z) directly from the dual-panel interface. Key review feedback focuses on critical bugs and security vulnerabilities: a compilation error in the ZIP date-time conversion where is_multiple_of is used on a primitive integer, malformed ZIP path separators on Windows, and potential path traversal vulnerabilities if absolute paths are present in archive entries. Additionally, using fs::remove_dir_all on extraction failure could lead to accidental data loss of pre-existing user files, and concurrent operations could trigger race conditions or leak temporary files on disk due to non-unique temporary filenames.

Comment thread src/ops/archive/zip.rs
Comment thread src/ops/archive/zip.rs Outdated
Comment thread src/ops/archive/zip.rs
Comment thread src/ops/archive/sevenz.rs Outdated
Comment thread src/ops/archive/tar.rs
Comment thread src/ops/archive/tar.rs Outdated
Comment thread src/ops/archive/sevenz.rs
Comment thread src/ops/archive/tar.rs
Comment thread src/input/normal.rs
Comment thread src/input/normal.rs
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 27, 2026

Greptile Summary

Adds a complete archive subsystem — ZIP, TAR (gz/bz2/xz/zst), and 7Z — with list, extract, and create operations, F7/F12 key bindings, an extract/create dialog, and archive-listing in the viewer.

  • New src/ops/archive/ module covers format detection (magic bytes + extension), path-traversal prevention, setuid stripping, cancellable extraction with partial-failure cleanup, and background batch execution.
  • UI changes: F7 now doubles as "extract archive" when cursor is on an archive file, F12 opens an archive menu, Enter/F3 renders archive contents as a text listing in the viewer.
  • Several correctness issues need attention before merge: the extract-dialog listing runs synchronously on the UI thread (blocking for large .tar.xz); the extraction rollback can silently delete pre-existing directories; temp files for XZ share the same PID-only name and will corrupt data if two XZ operations overlap; and #[allow] lint suppressions are used in four places against the project's explicit style rules.

Confidence Score: 3/5

Not safe to merge as-is: the extraction rollback deletes pre-existing user directories, the archive dialog freezes the TUI for large .tar.xz files, and concurrent XZ operations corrupt each other's temp files.

Three independent defects affect data integrity or responsiveness on the changed paths. The extraction cleanup calls remove_dir_all on directories that may have pre-dated the operation, which is silent data loss. The synchronous list_archive call on the key-handler thread will freeze the UI for seconds on compressed archives. PID-only temp file naming means two overlapping XZ operations write to the same path and corrupt data.

src/ops/archive/tar.rs, src/ops/archive/zip.rs, and src/ops/archive/sevenz.rs for the cleanup/rollback logic; src/input/normal.rs for the blocking listing call.

Important Files Changed

Filename Overview
src/ops/archive/mod.rs New archive module: format enum, ArchiveEntry struct, ArchiveError, and magic-byte+extension detect_format. Solid error plumbing; no issues found here.
src/ops/archive/tar.rs TAR list/extract/create with path-traversal prevention and setuid stripping; two bugs found: cleanup removes pre-existing directories on failure, and PID-only temp filenames collide on concurrent XZ operations.
src/ops/archive/zip.rs ZIP list/extract/create with traversal checks and symlink skip; manual date arithmetic can underflow on day=0 and the same pre-existing directory cleanup bug exists in extract_zip.
src/ops/archive/sevenz.rs 7Z list/extract with traversal checks and cancel support; create_7z is a stub returning UnsupportedFormat. Same pre-existing directory cleanup issue as tar/zip.
src/input/normal.rs F7/F12 key handling and archive dialog launch; show_archive_dialog calls list_archive synchronously on the input thread, blocking the TUI for potentially many seconds on .tar.xz files.
src/ops/batch.rs batch_extract_archive and batch_create_archive added; both correctly run the archiver on a background thread with progress reporting and cancellation. No issues found.
src/ui/dialogs/archive.rs New extract/create dialog rendering with Unicode-aware input field; uses forbidden #[allow(clippy::too_many_arguments)] instead of refactoring.
src/ui/viewer/open.rs Archive detection and listing in viewer; correctly uses the background loader path so list_archive runs off the main thread here. No issues.
src/input/dialogs.rs Extract and create dialog input handlers; adds #[allow(clippy::too_many_lines)] instead of splitting handle_input_action.
src/render_dialog_map.rs Maps app-level DialogKind to UI DialogKind; uses #[allow(clippy::too_many_lines)] rather than splitting to_ui_dialog.

Sequence Diagram

sequenceDiagram
    participant User
    participant InputHandler as input/normal.rs
    participant Batch as ops/batch.rs
    participant Archive as ops/archive/*
    participant TempFS as Temp Filesystem
    participant UI as ui/dialogs + viewer

    User->>InputHandler: F7 (on archive) / F12
    InputHandler->>Archive: list_archive() [BLOCKING on main thread]
    Archive->>TempFS: decompress .xz to PID-named temp file
    TempFS-->>Archive: entries
    Archive-->>InputHandler: "Vec<ArchiveEntry>"
    InputHandler->>UI: show ArchiveExtract/Create dialog

    User->>UI: confirm extract/create
    UI->>Batch: PendingAction::ExtractArchive / CreateArchive
    Batch->>Batch: thread::scope (background thread)
    Batch->>Archive: extract_archive / create_archive
    Archive->>TempFS: write to PID-named temp (xz only)
    Archive-->>Batch: Result
    Batch-->>UI: BatchReport + progress updates

    Note over InputHandler,Archive: list_archive blocks UI thread for .tar.xz
    Note over Archive,TempFS: PID-only temp name collides on concurrent ops
Loading

Comments Outside Diff (3)

  1. src/ops/archive/tar.rs, line 559-567 (link)

    P1 Cleanup deletes pre-existing directories on extraction failure

    extracted_paths accumulates every directory touched by create_dir_all, including ones that already existed before extraction started. If a later entry fails (or the operation is cancelled), the cleanup calls remove_dir_all on those directories, wiping any user files that pre-dated this extraction.

    Concrete scenario: if dest/docs/ exists with user files and the archive also contains a docs/ entry, a failure on any later archive entry causes remove_dir_all("dest/docs/") to silently delete everything inside. The same pattern exists in extract_zip and extract_7z.

  2. src/ops/archive/tar.rs, line 611 (link)

    P1 Temp file name derived solely from PID collides on concurrent operations

    lc-xz-tar-{pid} and lc-xz-decompress-{pid} (in wrap_decompress) are determined only by the process ID. If the user triggers a second XZ archive operation while one is in progress (e.g. extract while a create is running), both threads write to the same path, corrupting each other's data silently. The same collision exists in wrap_decompress for listing and extracting.

    Use a unique suffix per operation — std::time::Instant, a counter, or tempfile::NamedTempFile — to guarantee uniqueness.

  3. src/ops/archive/zip.rs, line 982-1005 (link)

    P2 Manual date arithmetic can underflow on day=0 in malformed zip entries

    let day = dt.day() as u64; — DOS dates allow a day value of 0 (malformed archives). day - 1 is evaluated on a u64, so in release mode it wraps to u64::MAX. That propagates into days_since_epoch * 24 * 3600 + …, and the resulting Duration::from_secs call will panic in debug mode and produce a wildly wrong timestamp in release mode. Consider clamping day to day.max(1) or replacing the whole function with an existing well-tested crate.

Reviews (1): Last reviewed commit: "feat: add archive support (zip, tar, 7z)" | Re-trigger Greptile

Comment thread src/input/normal.rs
Comment thread src/ui/dialogs/archive.rs Outdated
- Fix OOM: XZ decompress/create use temp files instead of in-memory buffers
- Fix silent TarGz fallback for unsupported formats (.rar, .deb) — now errors
- Fix F5 inactive-panel archive branch — removed confusing behavior
- Add dialog selection support (Left/Right toggle, Enter checks selection)
- Change remove_dir_all to remove_dir in extraction cleanup (3 extractors)
- Add atomic temp counter for unique filenames (AtomicUsize)
- Add absolute path rejection in all 3 sanitize/validate functions
- Add !entry.is_dir() checks in F7/F12/Enter handlers
- Add Windows backslash normalization in zip create paths
- Improve 7z creation error message (InvalidArchive instead of UnsupportedFormat)
- Deduplicate apply_text_edit to use apply_dialog_text_edit
- Remove all #[allow(clippy::too_many_lines/arguments)] suppressions
- Remove dead has_non_dotdot check, restore accidentally deleted create_tar
@leszek3737 leszek3737 merged commit 0067979 into main May 27, 2026
5 checks passed
@leszek3737 leszek3737 deleted the zip branch May 27, 2026 23:38
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