Skip to content

Fix CBR preview: replace internal RAR parser with UnRAR SDK dynamic loading#5

Open
Copilot wants to merge 14 commits intomainfrom
copilot/fix-cbr-files-preview
Open

Fix CBR preview: replace internal RAR parser with UnRAR SDK dynamic loading#5
Copilot wants to merge 14 commits intomainfrom
copilot/fix-cbr-files-preview

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 7, 2026

  • Create dll.hpp — standard RARLAB UnRAR SDK header
  • Rewrite RarArchive.cpp — UnRAR SDK dynamic loading (RAR4 + RAR5 support)
  • Update ComicTooltipExt.vcxproj — add dll.hpp to project ClInclude items
  • Update README.md — document unrar.dll runtime dependency
  • Fix dll.hpp — remove literal backtick characters corrupting #pragma once and #include <windows.h>
  • Fix RarArchive.cpp Load() race condition — delay hDll publication until all function pointers resolved
  • Fix HeaderFileName() off-by-one: resize to wlen, call MultiByteToWideChar, then pop_back() trailing L'\0'
  • Fix Load() DLL path: use GetModuleHandleEx(FROM_ADDRESS) + GetModuleFileNameW to build full path to unrar.dll
  • Fix error propagation: check pfnProcessFileW return values; treat non-ERAR_END_ARCHIVE as error
  • Fix README Security section: replace "no external dependencies" with runtime dependency caveat
  • Fix Load() init-once race: serialize with std::once_flag/std::call_once; only one thread ever calls LoadLibraryW
  • Fix unbounded allocation: cap per-entry reserve() and callback accumulation at MAX_ENTRY_BYTES / MAX_TOTAL_BYTES (64 MB each)
  • Fix ParseRarHeader(): return true for a valid empty archive when ret == ERAR_END_ARCHIVE
  • Fix dll.hpp #pragma pack: replace bare pack(1)/pack() with scoped pack(push,1)/pack(pop)
  • Fix 32-bit size truncation in ParseRarHeader(): combine UnpSizeHigh/UnpSize and PackSizeHigh/PackSize into 64-bit values before the MAX_ENTRY_BYTES check
  • Fix partial-data exposure: ReadRarFileHeader() clears data before returning false; ExtractFile() checks the return value and returns an empty vector on failure
Original prompt

This section details on the original issue you should resolve

<issue_title>Not working for cbr files</issue_title>
<issue_description>cbr files do not show preview images in the panel... they show blank pages (with navigation)</issue_description>

<agent_instructions>Replace the non-functional RarArchive RAR parser with the UnRAR SDK to enable
proper RAR4 and RAR5 decompression, while keeping the existing public interface
unchanged.

Task 1.1 – Add UnRAR SDK Dependencies:

  • Add the UnRAR SDK header file (dll.hpp) from the RARLAB unrarsrc package to
    the project
  • Configure linking against unrar.dll via import library or dynamic loading
    (LoadLibrary/GetProcAddress)
  • If using dynamic loading, add a helper in RarArchive.cpp that loads the DLL
    and resolves function pointers at runtime, allowing graceful degradation if
    unrar.dll is missing

Task 1.2 – Rewrite RarArchive File Listing:

  • In RarArchive.cpp, replace ParseRarHeader() with UnRAR SDK calls
  • Use RAROpenArchiveEx with RAR_OM_LIST mode to open the archive
  • Loop with RARReadHeaderEx to enumerate all entries and populate m_fileList
    with RarFileInfo structs (filename, sizes, compression method, etc.)
  • Remove the RAR5 early-exit stub (line 167); the UnRAR SDK handles RAR4 and
    RAR5 transparently
  • Call RARCloseArchive after enumeration
  • Update Open() to use the new implementation, preserving the same return
    semantics (return true if any files are found)

Task 1.3 – Rewrite RarArchive File Extraction:

  • Rewrite or replace ReadRarFileHeader() with a new internal method using
    UnRAR SDK extraction
  • Open the archive with RAR_OM_EXTRACT mode, seek to the target file with
    RARReadHeaderEx, and extract using RARProcessFile
  • Use the UnRAR callback mechanism (UCM_PROCESSDATA) to accumulate
    decompressed bytes into the output std::vector<BYTE>
  • Update ExtractFile() to call the new extraction method and return
    decompressed image data
  • Ensure ExtractFileToTemp() continues to work by writing the decompressed
    bytes to the temp file

Task 1.4 – Update Build and Deployment Configuration:

  • Add unrar.dll to the project's output directory or installer package
  • Update build scripts or project files to include UnRAR headers and library
    references
  • If using dynamic loading, handle the case where unrar.dll is absent by
    returning false from Open() with an empty file list, mirroring current
    failure behavior</agent_instructions>

Comments on the Issue (you are @copilot in this section)

@theotocopulitos @coderabbitai plan

✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Summary by CodeRabbit

  • New Features

    • Added runtime CBR (RAR) support via optional UnRAR DLL; supports RAR4 and RAR5 and improved archive listing/extraction.
    • Clarified ZIP handling to explicitly support stored and deflated entries.
  • Documentation

    • Updated README with troubleshooting for blank CBR pages and guidance to place the UnRAR DLL next to the extension to enable previews.
  • Bug Fixes

    • When the UnRAR DLL is missing, CBR preview gracefully falls back to filename/size tooltips instead of failing.

…oading

Co-authored-by: theotocopulitos <1540135+theotocopulitos@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix preview images for cbr files Fix CBR preview: replace internal RAR parser with UnRAR SDK dynamic loading Mar 7, 2026
@theotocopulitos
Copy link
Copy Markdown
Owner

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 7, 2026

✅ Actions performed

Full review triggered.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 7, 2026

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ce851b96-7386-4e7f-a266-a9c13a09f5e9

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds runtime UnRAR DLL integration: new dll.hpp UnRAR API header, RarArchive refactored to dynamically load/use unrar.dll for listing/extraction (RAR4/RAR5), project file exposes the header, and README documents runtime dependency and troubleshooting.

Changes

Cohort / File(s) Summary
UnRAR API header
dll.hpp
New public header declaring UnRAR binary-compatible structs, error codes, open/process flags, callback IDs, and function-pointer typedefs for dynamic DLL usage.
RAR archive handling
RarArchive.cpp
Replaces low-level signature parsing with a dynamic UnRAR loader (UnRarDll), adds ExtractionContext and UnRarCallback, implements ParseRarHeader via RAR_OM_LIST and ReadRarFileHeader via RAR_OM_EXTRACT, adds destructor, and handles missing DLL gracefully.
Project config
ComicTooltipExt.vcxproj
Adds dll.hpp to the public header (ClInclude) so the UnRAR API is exposed by the project.
Docs / Troubleshooting
README.md
Documents custom ZIP parser (.cbz) and runtime UnRAR dependency for .cbr, RAR4/RAR5 support via unrar.dll, placement/troubleshooting guidance, and behavior when DLL is absent.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant RarArchive
    participant UnRarDll
    participant UnrarDLL

    Client->>RarArchive: Open(filename)
    RarArchive->>UnRarDll: EnsureLoaded()
    UnRarDll->>UnrarDLL: LoadLibrary / GetProcAddresses
    alt DLL loaded
        RarArchive->>UnrarDLL: RAROpenArchiveEx(mode=LIST)
        UnrarDLL-->>RarArchive: archive handle / success
        RarArchive->>UnrarDLL: RARReadHeaderEx / iterate
        UnrarDLL-->>RarArchive: RARHeaderDataEx entries
        Client->>RarArchive: ReadRarFileHeader(target)
        RarArchive->>UnrarDLL: RAROpenArchiveEx(mode=EXTRACT)
        RarArchive->>UnrarDLL: RARProcessFileW(EXTRACT, callback)
        UnrarDLL->>RarArchive: UnRarCallback(data chunks)
        RarArchive-->>Client: file bytes
        RarArchive->>UnrarDLL: RARCloseArchive()
    else DLL missing
        RarArchive-->>Client: empty listing / graceful fallback
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Add page browser to comic preview pane #2: Adds RarArchive usage (m_pRar) in the comic preview UI and thus directly depends on the RarArchive API changes and new destructor/ReadRarFileHeader introduced here.

Poem

🐰
I hopped through bytes and DLL light,
Loaded UnRAR late at night,
CBRs now bloom from bytes once still,
Pages dance — a rabbit's thrill! 📚✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 41.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: replacing the internal RAR parser with UnRAR SDK dynamic loading to fix CBR preview functionality.
Linked Issues check ✅ Passed The PR addresses issue #4 by implementing UnRAR SDK dynamic loading to restore CBR file preview functionality, supporting RAR4/RAR5 with graceful degradation when unrar.dll is absent.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing CBR preview functionality: dll.hpp adds UnRAR API definitions, RarArchive.cpp implements dynamic UnRAR loading, README documents the runtime dependency, and vcxproj includes the header.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

📋 Issue Planner

Built with CodeRabbit's Coding Plans for faster development and fewer bugs.

View plan used: #4

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch copilot/fix-cbr-files-preview

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Comment @coderabbitai help to get the list of available commands and usage tips.

@theotocopulitos
Copy link
Copy Markdown
Owner

@coderabbitai help

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 7, 2026

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

CodeRabbit commands

These commands are invoked using PR/Issue comments.

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai evaluate custom pre-merge check --instructions <custom-checks-instructions> --name <custom-checks-title> [--mode <error|warning>] to validate the custom pre-merge checks instructions. Defaults to error when --mode is omitted.
    • @coderabbitai ignore pre-merge checks to override pre-merge checks and get an approval on PR.
    • @coderabbitai run pre-merge checks to run pre-merge checks on the pull request.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve to resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai generate configuration to create a PR that adds the current resolved configuration as .coderabbit.yaml (or show it if already present).
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit configuration file (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, support, documentation and community

  • Visit our status page to check the current availability of CodeRabbit.
  • Create a ticket on our support page for assistance with any issues or questions.
  • Visit our documentation site for detailed information on how to use CodeRabbit.
  • Join our Discord community to connect with other users and get help from the community.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (1)
dll.hpp (1)

71-128: Preserve the caller's packing state.

Use #pragma pack(push, 1) / #pragma pack(pop) here. The current #pragma pack(1) / #pragma pack() pair resets global packing instead of restoring the includer's previous state, which is risky in a public ABI header.

Safer packing wrapper
-#pragma pack(1)
+#pragma pack(push, 1)
 ...
-#pragma pack()
+#pragma pack(pop)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dll.hpp` around lines 71 - 128, The header currently forces global packing
with `#pragma` pack(1)/#pragma pack(), which can break an includer's packing;
change to a scoped packing pair by replacing that sequence with `#pragma`
pack(push, 1) before the definitions of RARHeaderDataEx and RAROpenArchiveDataEx
and `#pragma` pack(pop) after them so the caller's previous packing state is
preserved while keeping these structs packed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@dll.hpp`:
- Around line 7-8: dll.hpp currently uses Win32 types (CALLBACK, UINT, LPARAM,
HANDLE, PASCAL) but doesn't include their declarations; make the header
self-contained by adding the appropriate Windows type declarations—e.g., wrap an
`#include` <windows.h> (or the narrower <windef.h> / <winnt.h>) in an `#ifdef`
_WIN32 guard at the top of dll.hpp so symbols referenced in the callback and API
prototypes (search for CALLBACK, UINT, LPARAM, HANDLE, PASCAL in the file) are
defined when the header is included without relying on pch.h.

In `@RarArchive.cpp`:
- Around line 24-55: The bug is that Load() publishes hDll early allowing other
threads to see a non-null hDll before the function pointers (pfnOpenArchiveEx,
pfnCloseArchive, pfnReadHeaderEx, pfnProcessFileW, pfnSetCallback) are
initialized; fix by delaying publication of hDll: call ::LoadLibraryW into a
local HMODULE (e.g. tmpH), resolve all GetProcAddress results into local
function-pointer variables (or the existing pfn* members only after success), if
any lookup fails call ::FreeLibrary(tmpH) and return false, and only after all
pointers are valid assign hDll = tmpH (or use an atomic store) so other threads
never see a non-null hDll with uninitialized pfn* values; also ensure you
clear/reset the pfn* members on failure as currently done.
- Around line 105-109: HeaderFileName() currently computes wlen with
MultiByteToWideChar which returns the count including the terminating NUL, but
then resizes name to wlen - 1 and calls MultiByteToWideChar again with buffer
size wlen, allowing a one-wchar overflow; fix by allocating enough space and
removing the NUL afterwards: resize name to wlen (not wlen - 1) before calling
::MultiByteToWideChar(CP_ACP, 0, hdr.FileName, -1, &name[0], wlen) and then, if
the last character is L'\0', pop it (or resize to wlen - 1) so name contains the
string without the terminating NUL; reference HeaderFileName(), name,
hdr.FileName and the MultiByteToWideChar calls when making the change.
- Around line 28-29: The code loads unrar.dll via LoadLibraryW(L"unrar.dll")
which searches the process DLL paths instead of the extension's directory;
change the logic around hDll so you first call GetModuleFileName for the current
module (the ComicTooltipExt DLL), compute the directory and append
L"\\unrar.dll" to form a full path, and then call LoadLibraryW with that full
path (use the result to set hDll and keep the existing !hDll check). Ensure you
use the module handle for the extension when calling GetModuleFileName so the
lookup locates unrar.dll placed next to ComicTooltipExt.dll.
- Around line 284-303: The loop in ParseRarHeader and the similar logic in
ReadRarFileHeader currently ignore g_unrar.pfnProcessFileW return values and
derive overall success from m_fileList.empty()/data.empty(), which can hide
partial reads or SDK errors (e.g., password-protected or corrupt archives);
update ParseRarHeader() to check the return code of g_unrar.pfnProcessFileW
after each header iteration and break/return a failure on any non-success
status, use the final g_unrar.pfnReadHeaderEx return code (ret) to determine why
the loop ended (and treat non-ERAR_END_ARCHIVE as an error), and apply the same
pattern in ReadRarFileHeader() for operations that use RAR_TEST so success is
keyed off the SDK return codes rather than non-empty data; ensure errors from
the UnRAR SDK are propagated up (do not convert partial data into success) and
preserve correct handling for truly empty but valid archives.

In `@README.md`:
- Around line 74-75: Update the README statement that currently claims "no
external dependencies" to reflect the runtime dependency on the RARLAB UnRAR DLL
(unrar.dll) for CBR support: replace the absolute claim with a caveat like "no
compile-time dependencies; CBR preview requires the RARLAB UnRAR DLL (unrar.dll)
at runtime — otherwise CBRs degrade to filename/size tooltips" and apply the
same change where the duplicate claim appears (the other occurrence of "no
external dependencies"). Ensure the wording also mentions that the DLL is loaded
dynamically and that absence triggers graceful degradation.

---

Nitpick comments:
In `@dll.hpp`:
- Around line 71-128: The header currently forces global packing with `#pragma`
pack(1)/#pragma pack(), which can break an includer's packing; change to a
scoped packing pair by replacing that sequence with `#pragma` pack(push, 1) before
the definitions of RARHeaderDataEx and RAROpenArchiveDataEx and `#pragma`
pack(pop) after them so the caller's previous packing state is preserved while
keeping these structs packed.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 11bb2273-18eb-48f5-b3c0-16568c574bf9

📥 Commits

Reviewing files that changed from the base of the PR and between 00eba19 and 148767d.

📒 Files selected for processing (4)
  • ComicTooltipExt.vcxproj
  • README.md
  • RarArchive.cpp
  • dll.hpp

Comment thread dll.hpp
Comment thread RarArchive.cpp Outdated
Comment thread RarArchive.cpp
Comment thread RarArchive.cpp Outdated
Comment thread README.md
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
@theotocopulitos
Copy link
Copy Markdown
Owner

@copilot

Verify each finding against the current code and only fix it if needed.

Inline comments:
In @dll.hpp:

  • Around line 7-8: dll.hpp currently uses Win32 types (CALLBACK, UINT, LPARAM,
    HANDLE, PASCAL) but doesn't include their declarations; make the header
    self-contained by adding the appropriate Windows type declarations—e.g., wrap an
    #include <windows.h> (or the narrower <windef.h> / <winnt.h>) in an #ifdef
    _WIN32 guard at the top of dll.hpp so symbols referenced in the callback and API
    prototypes (search for CALLBACK, UINT, LPARAM, HANDLE, PASCAL in the file) are
    defined when the header is included without relying on pch.h.

In @RarArchive.cpp:

  • Around line 24-55: The bug is that Load() publishes hDll early allowing other
    threads to see a non-null hDll before the function pointers (pfnOpenArchiveEx,
    pfnCloseArchive, pfnReadHeaderEx, pfnProcessFileW, pfnSetCallback) are
    initialized; fix by delaying publication of hDll: call ::LoadLibraryW into a
    local HMODULE (e.g. tmpH), resolve all GetProcAddress results into local
    function-pointer variables (or the existing pfn* members only after success), if
    any lookup fails call ::FreeLibrary(tmpH) and return false, and only after all
    pointers are valid assign hDll = tmpH (or use an atomic store) so other threads
    never see a non-null hDll with uninitialized pfn* values; also ensure you
    clear/reset the pfn* members on failure as currently done.
  • Around line 105-109: HeaderFileName() currently computes wlen with
    MultiByteToWideChar which returns the count including the terminating NUL, but
    then resizes name to wlen - 1 and calls MultiByteToWideChar again with buffer
    size wlen, allowing a one-wchar overflow; fix by allocating enough space and
    removing the NUL afterwards: resize name to wlen (not wlen - 1) before calling
    ::MultiByteToWideChar(CP_ACP, 0, hdr.FileName, -1, &name[0], wlen) and then, if
    the last character is L'\0', pop it (or resize to wlen - 1) so name contains the
    string without the terminating NUL; reference HeaderFileName(), name,
    hdr.FileName and the MultiByteToWideChar calls when making the change.
  • Around line 28-29: The code loads unrar.dll via LoadLibraryW(L"unrar.dll")
    which searches the process DLL paths instead of the extension's directory;
    change the logic around hDll so you first call GetModuleFileName for the current
    module (the ComicTooltipExt DLL), compute the directory and append
    L"\unrar.dll" to form a full path, and then call LoadLibraryW with that full
    path (use the result to set hDll and keep the existing !hDll check). Ensure you
    use the module handle for the extension when calling GetModuleFileName so the
    lookup locates unrar.dll placed next to ComicTooltipExt.dll.
  • Around line 284-303: The loop in ParseRarHeader and the similar logic in
    ReadRarFileHeader currently ignore g_unrar.pfnProcessFileW return values and
    derive overall success from m_fileList.empty()/data.empty(), which can hide
    partial reads or SDK errors (e.g., password-protected or corrupt archives);
    update ParseRarHeader() to check the return code of g_unrar.pfnProcessFileW
    after each header iteration and break/return a failure on any non-success
    status, use the final g_unrar.pfnReadHeaderEx return code (ret) to determine why
    the loop ended (and treat non-ERAR_END_ARCHIVE as an error), and apply the same
    pattern in ReadRarFileHeader() for operations that use RAR_TEST so success is
    keyed off the SDK return codes rather than non-empty data; ensure errors from
    the UnRAR SDK are propagated up (do not convert partial data into success) and
    preserve correct handling for truly empty but valid archives.

In @README.md:

  • Around line 74-75: Update the README statement that currently claims "no
    external dependencies" to reflect the runtime dependency on the RARLAB UnRAR DLL
    (unrar.dll) for CBR support: replace the absolute claim with a caveat like "no
    compile-time dependencies; CBR preview requires the RARLAB UnRAR DLL (unrar.dll)
    at runtime — otherwise CBRs degrade to filename/size tooltips" and apply the
    same change where the duplicate claim appears (the other occurrence of "no
    external dependencies"). Ensure the wording also mentions that the DLL is loaded
    dynamically and that absence triggers graceful degradation.

Nitpick comments:
In @dll.hpp:

  • Around line 71-128: The header currently forces global packing with #pragma
    pack(1)/#pragma pack(), which can break an includer's packing; change to a
    scoped packing pair by replacing that sequence with #pragma pack(push, 1) before
    the definitions of RARHeaderDataEx and RAROpenArchiveDataEx and #pragma
    pack(pop) after them so the caller's previous packing state is preserved while
    keeping these structs packed.

…e <windows.h>

Co-authored-by: theotocopulitos <1540135+theotocopulitos@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 7, 2026

@copilot

Verify each finding against the current code and only fix it if needed.
...

Fixed in 0cbc2db. dll.hpp had literal backtick characters wrapping both the #pragma once guard and the #include <windows.h> directive (e.g. `#pragma` once), which made both non-functional preprocessor directives and left Win32 types undeclared. The backticks have been removed so the header is now correctly self-contained.

@theotocopulitos
Copy link
Copy Markdown
Owner

@copilot

Verify each finding against the current code and only fix it if needed.

Inline comments:

In @RarArchive.cpp:

Around line 24-55: The bug is that Load() publishes hDll early allowing other
threads to see a non-null hDll before the function pointers (pfnOpenArchiveEx,
pfnCloseArchive, pfnReadHeaderEx, pfnProcessFileW, pfnSetCallback) are
initialized; fix by delaying publication of hDll: call ::LoadLibraryW into a
local HMODULE (e.g. tmpH), resolve all GetProcAddress results into local
function-pointer variables (or the existing pfn* members only after success), if
any lookup fails call ::FreeLibrary(tmpH) and return false, and only after all
pointers are valid assign hDll = tmpH (or use an atomic store) so other threads
never see a non-null hDll with uninitialized pfn* values; also ensure you
clear/reset the pfn* members on failure as currently done.

…re resolved

Co-authored-by: theotocopulitos <1540135+theotocopulitos@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 7, 2026

@copilot

Verify each finding against the current code and only fix it if needed.
...

Fixed in 62b2557. Load() now calls ::LoadLibraryW into a local tmpH and resolves all five GetProcAddress results into local variables. Only after every pointer is confirmed non-null are they written to the struct members (pfnOpenArchiveEx … pfnSetCallback), then hDll = tmpH is assigned last. Any thread checking if (hDll) can only see a non-null handle once all function pointers are already valid. On any lookup failure ::FreeLibrary(tmpH) is called and false is returned without touching the struct members.

@theotocopulitos
Copy link
Copy Markdown
Owner

@copilot

Verify each finding against the current code and only fix it if needed.

In @RarArchive.cpp:

Around line 105-109: HeaderFileName() currently computes wlen with
MultiByteToWideChar which returns the count including the terminating NUL, but
then resizes name to wlen - 1 and calls MultiByteToWideChar again with buffer
size wlen, allowing a one-wchar overflow; fix by allocating enough space and
removing the NUL afterwards: resize name to wlen (not wlen - 1) before calling
::MultiByteToWideChar(CP_ACP, 0, hdr.FileName, -1, &name[0], wlen) and then, if
the last character is L'\0', pop it (or resize to wlen - 1) so name contains the
string without the terminating NUL; reference HeaderFileName(), name,
hdr.FileName and the MultiByteToWideChar calls when making the change.
Around line 28-29: The code loads unrar.dll via LoadLibraryW(L"unrar.dll")
which searches the process DLL paths instead of the extension's directory;
change the logic around hDll so you first call GetModuleFileName for the current
module (the ComicTooltipExt DLL), compute the directory and append
L"\unrar.dll" to form a full path, and then call LoadLibraryW with that full
path (use the result to set hDll and keep the existing !hDll check). Ensure you
use the module handle for the extension when calling GetModuleFileName so the
lookup locates unrar.dll placed next to ComicTooltipExt.dll.

Around line 284-303: The loop in ParseRarHeader and the similar logic in
ReadRarFileHeader currently ignore g_unrar.pfnProcessFileW return values and
derive overall success from m_fileList.empty()/data.empty(), which can hide
partial reads or SDK errors (e.g., password-protected or corrupt archives);
update ParseRarHeader() to check the return code of g_unrar.pfnProcessFileW
after each header iteration and break/return a failure on any non-success
status, use the final g_unrar.pfnReadHeaderEx return code (ret) to determine why
the loop ended (and treat non-ERAR_END_ARCHIVE as an error), and apply the same
pattern in ReadRarFileHeader() for operations that use RAR_TEST so success is
keyed off the SDK return codes rather than non-empty data; ensure errors from
the UnRAR SDK are propagated up (do not convert partial data into success) and
preserve correct handling for truly empty but valid archives.

…SDK error propagation

Co-authored-by: theotocopulitos <1540135+theotocopulitos@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 7, 2026

@copilot

Verify each finding against the current code and only fix it if needed.
...

Fixed in d56ca67. Three changes made:

  1. HeaderFileName() buffer sizing — resize to wlen (not wlen-1) before the second MultiByteToWideChar call, then pop_back() the trailing L'\0', eliminating the one-wchar write beyond the allocated range.

  2. Load() DLL pathGetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS) using UnRarCallback's address resolves the extension DLL's own HMODULE; GetModuleFileNameW then gives its full path, from which the filename portion is replaced with unrar.dll. Arithmetic safety guards (offset > 0, bounds check) and a wcscpy_s return-value check are in place; any failure clears dllPath[0] to trigger the bare-name fallback.

  3. SDK error propagation — both loops now capture the pfnProcessFileW return value and break on non-ERAR_SUCCESS. After each loop, ParseRarHeader() checks that ret == ERAR_END_ARCHIVE (clearing m_fileList and returning false otherwise). ReadRarFileHeader() keys success on procRet == ERAR_SUCCESS from the RAR_TEST call instead of !data.empty().

@theotocopulitos
Copy link
Copy Markdown
Owner

@copilot
In @README.md:

Around line 74-75: Update the README statement that currently claims "no
external dependencies" to reflect the runtime dependency on the RARLAB UnRAR DLL
(unrar.dll) for CBR support: replace the absolute claim with a caveat like "no
compile-time dependencies; CBR preview requires the RARLAB UnRAR DLL (unrar.dll)
at runtime — otherwise CBRs degrade to filename/size tooltips" and apply the
same change where the duplicate claim appears (the other occurrence of "no
external dependencies"). Ensure the wording also mentions that the DLL is loaded
dynamically and that absence triggers graceful degradation.

Co-authored-by: theotocopulitos <1540135+theotocopulitos@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 7, 2026

@copilot
In @README.md:

...

Fixed in 8f45abb. The Security section's "No network access or external dependencies" line has been updated to:

No network access; no compile-time external dependencies. CBR preview requires the RARLAB UnRAR DLL (unrar.dll) at runtime — it is loaded dynamically, and if absent CBRs degrade gracefully to filename/size tooltips.

The archive-support bullet at line 74 already correctly described the UnRAR SDK dependency, so no change was needed there.

@theotocopulitos
Copy link
Copy Markdown
Owner

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 7, 2026

✅ Actions performed

Full review triggered.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
RarArchive.cpp (1)

349-356: ⚠️ Potential issue | 🟡 Minor

Don't collapse valid empty archives into corruption.

After the ret != ERAR_END_ARCHIVE check, the SDK has already reported a clean end-of-archive. return !m_fileList.empty() still makes an empty but valid RAR fail Open().

♻️ Suggested fix
-    return !m_fileList.empty();
+    return true;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@RarArchive.cpp` around lines 349 - 356, The current logic treats an empty but
properly-terminated archive as a failure because after checking if (ret !=
ERAR_END_ARCHIVE) it returns !m_fileList.empty(); change this so that when ret
== ERAR_END_ARCHIVE the function returns true (a valid, possibly-empty archive)
and only returns false when ret != ERAR_END_ARCHIVE; update the code around the
ERAR_END_ARCHIVE check (the ret variable and m_fileList handling in
RarArchive::Open()) to avoid collapsing valid empty archives into an error.
🧹 Nitpick comments (1)
dll.hpp (1)

72-129: Use #pragma pack(push, 1) and #pragma pack(pop) for defensive consistency.

While the risk here is limited (dll.hpp is only directly included in RarArchive.cpp and no structs follow the final #pragma pack() reset), using scoped packing follows the UnRAR SDK best practice and prevents accidental layout issues if the header is later included elsewhere or extended. The suggested change is straightforward and costs nothing:

♻️ Suggested fix
-#pragma pack(1)
+#pragma pack(push, 1)
 
 struct RARHeaderDataEx
 {
-#pragma pack()
+#pragma pack(pop)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dll.hpp` around lines 72 - 129, Replace the unscoped packing pragmas with
scoped ones: wrap the packed struct definitions for RARHeaderDataEx and
RAROpenArchiveDataEx with `#pragma` pack(push, 1) before the structs and `#pragma`
pack(pop) after them to ensure packing is restored safely; update the existing
bare `#pragma` pack(1) and final `#pragma` pack() to use these push/pop pragmas so
the struct layouts remain 1-byte aligned without risking accidental global
packing changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@RarArchive.cpp`:
- Around line 28-93: The Load() method has a race: the unguarded if (hDll)
return true; and subsequent assignments to hDll and pfn* can be executed
concurrently causing double LoadLibraryW calls and torn publishes; fix by
serializing initialization (e.g. add a std::once_flag and use std::call_once or
use Windows INIT_ONCE) around the whole initialization body in Load() so only
one thread performs LoadLibraryW/GetProcAddress and assigns pfnOpenArchiveEx,
pfnCloseArchive, pfnReadHeaderEx, pfnProcessFileW, pfnSetCallback and hDll;
ensure any temporary module (tmpH) is freed on early failure and keep the public
check of hDll only after call_once completes to preserve the intended atomic
publish.
- Around line 107-124: The extraction currently allows unbounded allocation from
archive-controlled sizes: before calling data.reserve(fileInfo.FileSize) (the
code that handles per-entry allocation) check fileInfo.FileSize against a
defined MAX_ENTRY_SIZE and abort processing of that entry if it exceeds the cap;
similarly, extend ExtractionContext with a cumulative size counter and enforce a
MAX_TOTAL_BUFFER (or reuse the same MAX_ENTRY_SIZE) inside UnRarCallback to
refuse/appended data when ctx->currentSize + size > MAX_TOTAL_BUFFER, returning
a failure status (and cleaning up) instead of appending; update places using
ExtractionContext and UnRarCallback to handle the error return so oversized
entries are rejected early and cannot exhaust memory.

---

Duplicate comments:
In `@RarArchive.cpp`:
- Around line 349-356: The current logic treats an empty but properly-terminated
archive as a failure because after checking if (ret != ERAR_END_ARCHIVE) it
returns !m_fileList.empty(); change this so that when ret == ERAR_END_ARCHIVE
the function returns true (a valid, possibly-empty archive) and only returns
false when ret != ERAR_END_ARCHIVE; update the code around the ERAR_END_ARCHIVE
check (the ret variable and m_fileList handling in RarArchive::Open()) to avoid
collapsing valid empty archives into an error.

---

Nitpick comments:
In `@dll.hpp`:
- Around line 72-129: Replace the unscoped packing pragmas with scoped ones:
wrap the packed struct definitions for RARHeaderDataEx and RAROpenArchiveDataEx
with `#pragma` pack(push, 1) before the structs and `#pragma` pack(pop) after them
to ensure packing is restored safely; update the existing bare `#pragma` pack(1)
and final `#pragma` pack() to use these push/pop pragmas so the struct layouts
remain 1-byte aligned without risking accidental global packing changes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4799e25f-59e5-4cd7-93bd-2c19ba5b52d4

📥 Commits

Reviewing files that changed from the base of the PR and between 00eba19 and 8f45abb.

📒 Files selected for processing (4)
  • ComicTooltipExt.vcxproj
  • README.md
  • RarArchive.cpp
  • dll.hpp

Comment thread RarArchive.cpp
Comment on lines +28 to +93
bool Load()
{
if (hDll) return true;

// Build a full path to unrar.dll placed alongside ComicTooltipExt.dll
// so we never accidentally load an unrar.dll from an arbitrary location
// on the process DLL search path.
wchar_t dllPath[MAX_PATH] = {};
HMODULE hSelf = NULL;
if (::GetModuleHandleExW(
GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
reinterpret_cast<LPCWSTR>(UnRarCallback), // address inside this DLL
&hSelf) && hSelf)
{
DWORD len = ::GetModuleFileNameW(hSelf, dllPath, MAX_PATH);
if (len > 0 && len < MAX_PATH)
{
wchar_t* lastSlash = wcsrchr(dllPath, L'\\');
if (lastSlash)
{
// Replace filename portion with unrar.dll.
// Guard against arithmetic underflow before computing remaining.
ptrdiff_t offset = lastSlash + 1 - dllPath;
if (offset > 0 && static_cast<size_t>(offset) <= MAX_PATH)
{
size_t remaining = MAX_PATH - static_cast<size_t>(offset);
if (wcscpy_s(lastSlash + 1, remaining, L"unrar.dll") != 0)
dllPath[0] = L'\0'; // on failure fall back to bare name
}
}
}
}

HMODULE tmpH = ::LoadLibraryW(dllPath[0] ? dllPath : L"unrar.dll");
if (!tmpH) return false;

PFN_RAROpenArchiveEx tmpOpenArchiveEx = reinterpret_cast<PFN_RAROpenArchiveEx>(
::GetProcAddress(tmpH, "RAROpenArchiveEx"));
PFN_RARCloseArchive tmpCloseArchive = reinterpret_cast<PFN_RARCloseArchive>(
::GetProcAddress(tmpH, "RARCloseArchive"));
PFN_RARReadHeaderEx tmpReadHeaderEx = reinterpret_cast<PFN_RARReadHeaderEx>(
::GetProcAddress(tmpH, "RARReadHeaderEx"));
PFN_RARProcessFileW tmpProcessFileW = reinterpret_cast<PFN_RARProcessFileW>(
::GetProcAddress(tmpH, "RARProcessFileW"));
PFN_RARSetCallback tmpSetCallback = reinterpret_cast<PFN_RARSetCallback>(
::GetProcAddress(tmpH, "RARSetCallback"));

if (!tmpOpenArchiveEx || !tmpCloseArchive || !tmpReadHeaderEx ||
!tmpProcessFileW || !tmpSetCallback)
{
::FreeLibrary(tmpH);
return false;
}

// All pointers resolved — publish atomically: hDll becomes non-null
// only after every pfn* member is valid, so other threads can never
// observe a non-null hDll with uninitialized function pointers.
pfnOpenArchiveEx = tmpOpenArchiveEx;
pfnCloseArchive = tmpCloseArchive;
pfnReadHeaderEx = tmpReadHeaderEx;
pfnProcessFileW = tmpProcessFileW;
pfnSetCallback = tmpSetCallback;
hDll = tmpH;
return true;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "RarArchive.cpp" -o -name "RarArchive.h" | head -20

Repository: theotocopulitos/comicInfoTooltip

Length of output: 107


🏁 Script executed:

rg -l "RarArchive" --type cpp --type h | head -20

Repository: theotocopulitos/comicInfoTooltip

Length of output: 169


🏁 Script executed:

fd -e cpp -e h | grep -i rar

Repository: theotocopulitos/comicInfoTooltip

Length of output: 103


🏁 Script executed:

cat -n RarArchive.h

Repository: theotocopulitos/comicInfoTooltip

Length of output: 2142


🏁 Script executed:

cat -n RarArchive.cpp

Repository: theotocopulitos/comicInfoTooltip

Length of output: 18633


🏁 Script executed:

# Verify if there are any synchronization mechanisms I may have missed
rg -A 5 -B 5 "INIT_ONCE|call_once|mutex|critical|lock" RarArchive.cpp

Repository: theotocopulitos/comicInfoTooltip

Length of output: 58


🏁 Script executed:

# Check for volatile or atomic usage on hDll/pfn*
rg "volatile|atomic" RarArchive.cpp

Repository: theotocopulitos/comicInfoTooltip

Length of output: 152


🏁 Script executed:

# Check where g_unrar.Load() is called from
rg -B 3 "g_unrar\.Load()" RarArchive.cpp

Repository: theotocopulitos/comicInfoTooltip

Length of output: 285


Add synchronization to Load() — it has a data race.

The if (hDll) return true; check at line 30 is unguarded. Two threads can both pass this check, call LoadLibraryW() twice (leaking a refcount), and race to write hDll and the pfn* pointers without synchronization. The comment claiming "atomic" publication is incorrect—plain member assignments have no synchronicity guarantee. In the multi-threaded context of Explorer/prevhost.exe, wrap the entire initialization in INIT_ONCE (Windows) or std::call_once (C++11).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@RarArchive.cpp` around lines 28 - 93, The Load() method has a race: the
unguarded if (hDll) return true; and subsequent assignments to hDll and pfn* can
be executed concurrently causing double LoadLibraryW calls and torn publishes;
fix by serializing initialization (e.g. add a std::once_flag and use
std::call_once or use Windows INIT_ONCE) around the whole initialization body in
Load() so only one thread performs LoadLibraryW/GetProcAddress and assigns
pfnOpenArchiveEx, pfnCloseArchive, pfnReadHeaderEx, pfnProcessFileW,
pfnSetCallback and hDll; ensure any temporary module (tmpH) is freed on early
failure and keep the public check of hDll only after call_once completes to
preserve the intended atomic publish.

Comment thread RarArchive.cpp
Comment on lines +107 to +124
struct ExtractionContext
{
std::vector<BYTE>* pData = nullptr;
};

static int CALLBACK UnRarCallback(UINT msg, LPARAM userData, LPARAM p1, LPARAM p2)
{
if (msg == UCM_PROCESSDATA)
{
ExtractionContext* ctx = reinterpret_cast<ExtractionContext*>(userData);
if (ctx && ctx->pData)
{
const BYTE* ptr = reinterpret_cast<const BYTE*>(p1);
size_t size = static_cast<size_t>(p2);
ctx->pData->insert(ctx->pData->end(), ptr, ptr + size);
}
}
return 1;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "RarArchive.cpp" -type f

Repository: theotocopulitos/comicInfoTooltip

Length of output: 92


🏁 Script executed:

cat -n RarArchive.cpp

Repository: theotocopulitos/comicInfoTooltip

Length of output: 18633


🏁 Script executed:

rg -i "max|limit|size.*cap" RarArchive.cpp

Repository: theotocopulitos/comicInfoTooltip

Length of output: 505


🏁 Script executed:

sed -n '363,412p' RarArchive.cpp | head -50

Repository: theotocopulitos/comicInfoTooltip

Length of output: 1901


Implement hard caps on buffered entry size to prevent resource exhaustion.

The extraction flow at lines 369–370 and the callback at line 121 buffer entire RAR entries without size validation. fileInfo.FileSize is populated from the archive header's UnpSize (archive-controlled), and data.reserve(fileInfo.FileSize) can allocate gigabytes based on attacker input. The callback then appends chunks unbounded via vector::insert(). A crafted CBR can force multi-GB allocation attempts, causing out-of-memory conditions in the preview host. Reject oversized entries before processing and enforce a maximum cumulative buffer size in the callback.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@RarArchive.cpp` around lines 107 - 124, The extraction currently allows
unbounded allocation from archive-controlled sizes: before calling
data.reserve(fileInfo.FileSize) (the code that handles per-entry allocation)
check fileInfo.FileSize against a defined MAX_ENTRY_SIZE and abort processing of
that entry if it exceeds the cap; similarly, extend ExtractionContext with a
cumulative size counter and enforce a MAX_TOTAL_BUFFER (or reuse the same
MAX_ENTRY_SIZE) inside UnRarCallback to refuse/appended data when
ctx->currentSize + size > MAX_TOTAL_BUFFER, returning a failure status (and
cleaning up) instead of appending; update places using ExtractionContext and
UnRarCallback to handle the error return so oversized entries are rejected early
and cannot exhaust memory.

@theotocopulitos
Copy link
Copy Markdown
Owner

@copilot

Verify each finding against the current code and only fix it if needed.

Inline comments:
In @RarArchive.cpp:

  • Around line 28-93: The Load() method has a race: the unguarded if (hDll)
    return true; and subsequent assignments to hDll and pfn* can be executed
    concurrently causing double LoadLibraryW calls and torn publishes; fix by
    serializing initialization (e.g. add a std::once_flag and use std::call_once or
    use Windows INIT_ONCE) around the whole initialization body in Load() so only
    one thread performs LoadLibraryW/GetProcAddress and assigns pfnOpenArchiveEx,
    pfnCloseArchive, pfnReadHeaderEx, pfnProcessFileW, pfnSetCallback and hDll;
    ensure any temporary module (tmpH) is freed on early failure and keep the public
    check of hDll only after call_once completes to preserve the intended atomic
    publish.
  • Around line 107-124: The extraction currently allows unbounded allocation from
    archive-controlled sizes: before calling data.reserve(fileInfo.FileSize) (the
    code that handles per-entry allocation) check fileInfo.FileSize against a
    defined MAX_ENTRY_SIZE and abort processing of that entry if it exceeds the cap;
    similarly, extend ExtractionContext with a cumulative size counter and enforce a
    MAX_TOTAL_BUFFER (or reuse the same MAX_ENTRY_SIZE) inside UnRarCallback to
    refuse/appended data when ctx->currentSize + size > MAX_TOTAL_BUFFER, returning
    a failure status (and cleaning up) instead of appending; update places using
    ExtractionContext and UnRarCallback to handle the error return so oversized
    entries are rejected early and cannot exhaust memory.

Duplicate comments:
In @RarArchive.cpp:

  • Around line 349-356: The current logic treats an empty but properly-terminated
    archive as a failure because after checking if (ret != ERAR_END_ARCHIVE) it
    returns !m_fileList.empty(); change this so that when ret == ERAR_END_ARCHIVE
    the function returns true (a valid, possibly-empty archive) and only returns
    false when ret != ERAR_END_ARCHIVE; update the code around the ERAR_END_ARCHIVE
    check (the ret variable and m_fileList handling in RarArchive::Open()) to avoid
    collapsing valid empty archives into an error.

Nitpick comments:
In @dll.hpp:

  • Around line 72-129: Replace the unscoped packing pragmas with scoped ones:
    wrap the packed struct definitions for RARHeaderDataEx and RAROpenArchiveDataEx
    with #pragma pack(push, 1) before the structs and #pragma pack(pop) after them
    to ensure packing is restored safely; update the existing bare #pragma pack(1)
    and final #pragma pack() to use these push/pop pragmas so the struct layouts
    remain 1-byte aligned without risking accidental global packing changes.

…, scoped pack pragmas

Co-authored-by: theotocopulitos <1540135+theotocopulitos@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 7, 2026

@copilot

Verify each finding against the current code and only fix it if needed.
...

Fixed in 1b423d0. Four changes made:

  1. Load() init-once race — moved the initialization body into a private DoLoad() and wrapped the public Load() in std::call_once with a std::once_flag member. Only one thread ever calls LoadLibraryW/GetProcAddress; all concurrent callers block until the first completes and then read the cached initResult.

  2. Unbounded allocation — added MAX_ENTRY_BYTES and MAX_TOTAL_BYTES (64 MB each, with a comment explaining why they're identical). ReadRarFileHeader() rejects any entry whose FileSize header exceeds the cap before calling reserve(). ExtractionContext now tracks a currentSize counter and a limitReached flag; UnRarCallback checks currentSize + size > MAX_TOTAL_BYTES and returns -1 (aborting the extraction) if exceeded — the -1 propagates as a non-ERAR_SUCCESS from pfnProcessFileW, setting extractOk = false.

  3. Empty-archive false negativeParseRarHeader() now returns true unconditionally when ret == ERAR_END_ARCHIVE; a valid but empty archive is no longer treated as a failure.

  4. Scoped pack pragmas#pragma pack(1) / #pragma pack() in dll.hpp replaced with #pragma pack(push, 1) / #pragma pack(pop) so the 1-byte alignment scope is explicitly bounded.

@theotocopulitos theotocopulitos marked this pull request as ready for review March 7, 2026 20:10
Copilot AI review requested due to automatic review settings March 7, 2026 20:10
@qodo-code-review
Copy link
Copy Markdown

Review Summary by Qodo

Fix CBR preview by replacing internal RAR parser with UnRAR SDK

🐞 Bug fix ✨ Enhancement

Grey Divider

Walkthroughs

Description
• Replace internal RAR parser with UnRAR SDK dynamic loading for RAR4/RAR5 support
• Implement thread-safe DLL loading with allocation safety caps (64 MB limits)
• Add callback-based file extraction with decompressed data accumulation
• Update project configuration and documentation for unrar.dll runtime dependency
Diagram
flowchart LR
  A["CBR File"] -->|"Open with UnRAR SDK"| B["RAROpenArchiveEx"]
  B -->|"List/Extract"| C["RARReadHeaderEx"]
  C -->|"Process entries"| D["RARProcessFileW"]
  D -->|"Decompress callback"| E["UnRarCallback"]
  E -->|"Accumulate data"| F["ExtractionContext"]
  F -->|"Return file list/data"| G["RarArchive"]
  H["unrar.dll"] -.->|"Dynamic load"| B
Loading

Grey Divider

File Changes

1. RarArchive.cpp 🐞 Bug fix +286/-128

Replace RAR parser with UnRAR SDK dynamic loading

• Replaced manual RAR4 binary parsing with UnRAR SDK calls via dynamic DLL loading
• Implemented thread-safe UnRarDll struct with std::once_flag to ensure single-threaded
 initialization
• Added UnRarCallback function to accumulate decompressed data with safety caps (MAX_ENTRY_BYTES
 and MAX_TOTAL_BYTES at 64 MB each)
• Rewrote ParseRarHeader() to use RAROpenArchiveEx + RARReadHeaderEx for both RAR4 and RAR5
 support
• Rewrote ReadRarFileHeader() to extract files via RARProcessFileW with RAR_TEST mode and
 callback-based decompression
• Added HeaderFileName() helper to convert RAR header filenames (ANSI or Unicode) to normalized
 wide strings
• Added safety checks for file handle validity and proper error propagation from SDK calls

RarArchive.cpp


2. dll.hpp ✨ Enhancement +141/-0

Add UnRAR SDK header with API declarations

• New file containing RARLAB UnRAR SDK header declarations for binary compatibility
• Defines error codes (ERAR_SUCCESS, ERAR_END_ARCHIVE, etc.) and operation modes (RAR_OM_LIST,
 RAR_OM_EXTRACT)
• Declares callback message IDs (UCM_PROCESSDATA, UCM_CHANGEVOLUME, etc.) for decompression
 notifications
• Defines packed structures RARHeaderDataEx and RAROpenArchiveDataEx with 1-byte alignment via
 #pragma pack(push,1)/#pragma pack(pop)
• Declares function pointer typedefs for all UnRAR SDK entry points (RAROpenArchiveEx,
 RARCloseArchive, RARReadHeaderEx, RARProcessFileW, RARSetCallback)

dll.hpp


3. ComicTooltipExt.vcxproj ⚙️ Configuration changes +1/-0

Add dll.hpp to project includes

• Added dll.hpp to the ClInclude project item group to include the new UnRAR SDK header in the
 build

ComicTooltipExt.vcxproj


View more (1)
4. README.md 📝 Documentation +4/-3

Document unrar.dll runtime dependency and setup

• Added troubleshooting entry documenting unrar.dll requirement and download location from
 rarlab.com
• Updated Archive support section to clarify UnRAR SDK usage for RAR4/RAR5 via dynamic DLL loading
 with graceful degradation
• Updated Security section to note runtime (not compile-time) dependency on unrar.dll and graceful
 fallback behavior
• Updated Dependencies section to replace "no external libraries" with explicit unrar.dll runtime
 requirement and placement instructions

README.md


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented Mar 7, 2026

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Action required

1. DLL search-order fallback🐞 Bug ⛨ Security
Description
When full-path construction fails, the code falls back to LoadLibraryW(L"unrar.dll"), which can load
an attacker-controlled DLL via the default search order (working dir/PATH), contradicting the stated
intent to avoid that.
Code

RarArchive.cpp[R54-85]

+        // Build a full path to unrar.dll placed alongside ComicTooltipExt.dll
+        // so we never accidentally load an unrar.dll from an arbitrary location
+        // on the process DLL search path.
+        wchar_t dllPath[MAX_PATH] = {};
+        HMODULE hSelf = NULL;
+        if (::GetModuleHandleExW(
+                GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
+                GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
+                reinterpret_cast<LPCWSTR>(UnRarCallback),   // address inside this DLL
+                &hSelf) && hSelf)
+        {
+            DWORD len = ::GetModuleFileNameW(hSelf, dllPath, MAX_PATH);
+            if (len > 0 && len < MAX_PATH)
+            {
+                wchar_t* lastSlash = wcsrchr(dllPath, L'\\');
+                if (lastSlash)
+                {
+                    // Replace filename portion with unrar.dll.
+                    // Guard against arithmetic underflow before computing remaining.
+                    ptrdiff_t offset = lastSlash + 1 - dllPath;
+                    if (offset > 0 && static_cast<size_t>(offset) <= MAX_PATH)
+                    {
+                        size_t remaining = MAX_PATH - static_cast<size_t>(offset);
+                        if (wcscpy_s(lastSlash + 1, remaining, L"unrar.dll") != 0)
+                            dllPath[0] = L'\0';  // on failure fall back to bare name
+                    }
+                }
+            }
+        }
+
+        HMODULE tmpH = ::LoadLibraryW(dllPath[0] ? dllPath : L"unrar.dll");
+        if (!tmpH) return false;
Evidence
The loader explicitly states it wants to avoid loading from the process DLL search path, but then
falls back to loading by bare name, which uses that search path.

RarArchive.cpp[54-56]
RarArchive.cpp[84-85]
Best Practice: Microsoft Docs - Dynamic-link library search order / LoadLibrary

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`UnRarDll::DoLoad()` falls back to `LoadLibraryW(L&amp;quot;unrar.dll&amp;quot;)` when it can’t build a full path. This reintroduces DLL search-order loading and undermines the security intent stated in the comment.
### Issue Context
This code runs inside Explorer/prevhost via a shell extension, so loading an unexpected DLL is high impact.
### Fix Focus Areas
- RarArchive.cpp[52-86]
### Notes
- Prefer: build an absolute path to `unrar.dll` adjacent to `ComicTooltipExt.dll` and only attempt to load that.
- If you must support a fallback, use `LoadLibraryExW` with safe search flags and/or `AddDllDirectory`/`SetDefaultDllDirectories` to avoid current-directory/PATH injection.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. MAX_PATH truncation load🐞 Bug ⛯ Reliability
Description
If GetModuleFileNameW truncates (returns MAX_PATH), dllPath may not be NUL-terminated, yet the code
can still pass it to LoadLibraryW, risking undefined behavior or attempting to load a truncated
path.
Code

RarArchive.cpp[R65-85]

+            DWORD len = ::GetModuleFileNameW(hSelf, dllPath, MAX_PATH);
+            if (len > 0 && len < MAX_PATH)
+            {
+                wchar_t* lastSlash = wcsrchr(dllPath, L'\\');
+                if (lastSlash)
+                {
+                    // Replace filename portion with unrar.dll.
+                    // Guard against arithmetic underflow before computing remaining.
+                    ptrdiff_t offset = lastSlash + 1 - dllPath;
+                    if (offset > 0 && static_cast<size_t>(offset) <= MAX_PATH)
+                    {
+                        size_t remaining = MAX_PATH - static_cast<size_t>(offset);
+                        if (wcscpy_s(lastSlash + 1, remaining, L"unrar.dll") != 0)
+                            dllPath[0] = L'\0';  // on failure fall back to bare name
+                    }
+                }
+            }
+        }
+
+        HMODULE tmpH = ::LoadLibraryW(dllPath[0] ? dllPath : L"unrar.dll");
+        if (!tmpH) return false;
Evidence
The code only enters the path-editing block when len < MAX_PATH, but it still uses dllPath when
dllPath[0] is non-zero. On truncation, GetModuleFileNameW can fill the buffer without
NUL-termination, leaving dllPath[0] non-zero while the buffer is not a valid C-string.

RarArchive.cpp[65-67]
RarArchive.cpp[84-85]
Best Practice: Microsoft Docs - GetModuleFileNameW

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`dllPath` is used as a C-string in `LoadLibraryW`, but `GetModuleFileNameW` truncation (`len &amp;gt;= MAX_PATH`) can yield a non-terminated buffer.
### Issue Context
Even if rare, this is a classic MAX_PATH edge case that can cause intermittent load failures or memory overreads.
### Fix Focus Areas
- RarArchive.cpp[57-86]
### Suggested approach
- If `len == 0` or `len &amp;gt;= MAX_PATH`, clear `dllPath` and fail closed (or use a robust path-building approach).
- Consider replacing the fixed buffer with a `std::wstring` grow/retry loop to reliably retrieve the full module path.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Callback size overflow 🐞 Bug ⛨ Security
Description
The decompression callback uses ctx->currentSize + size without overflow-safe math and casts
LPARAM p2 to size_t, which can turn negative values into huge sizes and potentially bypass the
MAX_TOTAL_BYTES cap via wraparound.
Code

RarArchive.cpp[R138-148]

+            const BYTE* ptr  = reinterpret_cast<const BYTE*>(p1);
+            size_t      size = static_cast<size_t>(p2);
+            if (ctx->currentSize + size > MAX_TOTAL_BYTES)
+            {
+                // Refuse oversized data and signal an error to UnRAR so the
+                // extraction is aborted rather than filling memory unboundedly.
+                ctx->limitReached = true;
+                return -1;
+            }
+            ctx->pData->insert(ctx->pData->end(), ptr, ptr + size);
+            ctx->currentSize += size;
Evidence
The code performs an unchecked addition on attacker-influenced chunk sizes and then uses that size
to form a pointer range for vector insertion. If size is unexpectedly large (including via
signed→unsigned conversion), currentSize + size can wrap and defeat the cap check.

RarArchive.cpp[131-148]
Best Practice: C/C++ secure coding best practices (integer overflow checks)

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`UnRarCallback` computes `currentSize + size` and compares it to `MAX_TOTAL_BYTES`, but the addition can overflow. Also, `p2` is signed (`LPARAM`) and is cast directly to `size_t`.
### Issue Context
This callback processes archive-controlled data sizes. Even if the SDK normally provides sane sizes, hardening here prevents edge-case crashes and makes the cap enforcement robust.
### Fix Focus Areas
- RarArchive.cpp[131-152]
### Recommended changes
- Treat `p2 &amp;lt;= 0` as `size = 0` (or abort) before casting.
- Replace `currentSize + size &amp;gt; MAX_TOTAL_BYTES` with `size &amp;gt; (MAX_TOTAL_BYTES - currentSize)`.
- Optionally verify `ptr != nullptr` when `size &amp;gt; 0` before inserting.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

Comment thread RarArchive.cpp
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR replaces the custom CBR (RAR) parsing/extraction logic with runtime dynamic loading of the RARLAB UnRAR SDK (unrar.dll) to enable proper RAR4/RAR5 support in the shell extension’s tooltip/preview pipeline.

Changes:

  • Added dll.hpp with UnRAR SDK declarations and updated the VS project to include it.
  • Rewrote RarArchive.cpp to enumerate/extract entries via UnRAR (RAROpenArchiveEx/RARReadHeaderEx/RARProcessFileW) with init-once loading and extraction size caps.
  • Updated README.md to document the runtime dependency and troubleshooting guidance for blank CBR previews.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

File Description
dll.hpp Adds UnRAR SDK struct/function-pointer declarations for dynamic loading.
RarArchive.cpp Implements dynamic DLL loading, archive listing, and extraction via UnRAR callbacks.
README.md Documents unrar.dll runtime requirement and behavior when absent.
ComicTooltipExt.vcxproj Adds dll.hpp to the project includes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread RarArchive.cpp Outdated
Comment on lines +357 to +360
// RarFileInfo.FileSize is DWORD (32-bit); images in comic archives
// are never ≥ 4 GB so storing the lower 32 bits of UnpSize is safe.
fi.FileSize = hdr.UnpSize;
fi.CompressedSize = hdr.PackSize;
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

Sizes are truncated to 32-bit (hdr.UnpSize/hdr.PackSize) and UnpSizeHigh/PackSizeHigh are ignored. This can misreport entry sizes and bypass the MAX_ENTRY_BYTES pre-check when the high dword is non-zero. Consider combining high/low into a 64-bit size and rejecting entries whose true uncompressed size exceeds the safety cap.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

@copilot apply changes based on this feedback

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in bdf7f97. In ParseRarHeader(), hdr.UnpSize and hdr.PackSize are now combined with their high-dword counterparts before the size cap is applied:

uint64_t unpSize64  = (static_cast<uint64_t>(hdr.UnpSizeHigh) << 32) | hdr.UnpSize;
uint64_t packSize64 = (static_cast<uint64_t>(hdr.PackSizeHigh) << 32) | hdr.PackSize;

Entries where unpSize64 > MAX_ENTRY_BYTES are skipped (not added to m_fileList). Only after the 64-bit check passes are the low 32 bits written to fi.FileSize and fi.CompressedSize — at that point truncation is safe since the value is guaranteed ≤ 64 MB.

Comment thread RarArchive.cpp Outdated
Comment thread RarArchive.cpp Outdated
Comment thread RarArchive.cpp
Comment on lines +429 to +433
// RAR_TEST decompresses into the callback; nothing is written to disk.
// Key success off the SDK return code, not data.empty(), so that a
// legitimately zero-byte entry is handled correctly.
int procRet = g_unrar.pfnProcessFileW(hArc, RAR_TEST, NULL, NULL);
found = true;
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

ReadRarFileHeader() can return false after partially appending bytes via the callback (e.g., when MAX_TOTAL_BYTES is exceeded). Since ExtractFile() ignores the boolean return, callers may still receive truncated/invalid image data. Clear data on failure and/or make ExtractFile() return an empty vector when ReadRarFileHeader() fails.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

@copilot apply changes based on this feedback

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 323109d. Two changes:

  1. ReadRarFileHeader() now calls data.clear() before returning false, ensuring no partial bytes survive an error (including mid-stream MAX_TOTAL_BYTES exhaustion).
  2. ExtractFile() now checks the ReadRarFileHeader() boolean return and calls result.clear() on failure, so callers always receive either a complete, valid buffer or an empty vector.

theotocopulitos and others added 2 commits March 7, 2026 23:13
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…heck in ParseRarHeader()

Co-authored-by: theotocopulitos <1540135+theotocopulitos@users.noreply.github.com>
… empty vector on error

Co-authored-by: theotocopulitos <1540135+theotocopulitos@users.noreply.github.com>
…rt UTF-16/UTF-8 BOMs in ComicInfo.xml; match ComicInfo.xml case-insensitively by basename only
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.

Not working for cbr files

3 participants