Skip to content

Reduce retained binary memory with range-reader groundwork#742

Open
yhs0602 wants to merge 5 commits intomasterfrom
codex/binary-range-reader
Open

Reduce retained binary memory with range-reader groundwork#742
yhs0602 wants to merge 5 commits intomasterfrom
codex/binary-range-reader

Conversation

@yhs0602
Copy link
Owner

@yhs0602 yhs0602 commented Mar 22, 2026

Summary

  • add a FileChannel-backed binary range reader abstraction for future large-file refactors
  • move AbstractFile.createInstance() behind a reader bridge instead of calling File.readBytes() directly
  • lazy-load raw and ELF binary contents, and stop retaining PE parsing bytes after object construction

What Changed

  • add BinaryRangeReader and FileChannelBinaryRangeReader
  • add DeferredFileBackedBinaryContent and a shared loader interface
  • sniff binary container headers before deciding how aggressively to read content
  • keep RawFile and ElfFile contents deferred until a view actually needs bytes
  • parse PEFile with a temporary byte array but avoid keeping that array attached to the object afterward

Test Coverage

  • add unit tests for the range reader bridge and deferred file-backed content helper
  • add unit tests for header sniffing and raw lazy-load behavior

Verification

  • JAVA_HOME=$(/usr/libexec/java_home -v 17) ANDROID_HOME=$HOME/Library/Android/sdk ./gradlew testDebugUnitTest assembleDebug

Summary by CodeRabbit

  • New Features

    • Automatic detection of binary file types (ELF, PE, RAW) when opening files.
  • Performance Improvements

    • Deferred (lazy) binary content loading to reduce memory use for large files.
  • Bug Fixes

    • Corrected displayed file size in binary details for large files.
  • Tests

    • Added extensive tests for format detection, lazy loading, range reading, and related UI flows.

@coderabbitai
Copy link

coderabbitai bot commented Mar 22, 2026

📝 Walkthrough

Walkthrough

Adds deferred binary-content loading and container-format detection: new reader/loader abstractions and range-based file reads; refactors file factories and file classes (ELF/PE/RAW) to support lazy loading, and updates UI and tests to use the new lazy APIs.

Changes

Cohort / File(s) Summary
Core deferred-loading primitives
app/src/main/java/com/kyhsgeekcode/disassembler/files/BinaryRangeReader.kt, app/src/main/java/com/kyhsgeekcode/disassembler/files/BinaryContentLoader.kt, app/src/main/java/com/kyhsgeekcode/disassembler/files/BinaryContainerFormat.kt, app/src/main/java/com/kyhsgeekcode/disassembler/files/FileChannelBinaryRangeReader.kt, app/src/main/java/com/kyhsgeekcode/disassembler/files/DeferredFileBackedBinaryContent.kt
New interfaces/enums and concrete FileChannel reader plus a deferred file-backed content holder that caches initial or lazily-loaded bytes and exposes length without forcing load.
File factory & detection
app/src/main/java/com/kyhsgeekcode/disassembler/files/AbstractFile.kt
Introduced getBinaryContents()/getBinaryLength() APIs, readFileContentsForParsing() and detectBinaryContainerFormat() helpers; createInstance() now detects container format and constructs ELF/PE/RAW with deferred loaders instead of unconditional eager reads.
Format-specific files (deferred support)
app/src/main/java/com/kyhsgeekcode/disassembler/files/ElfFile.kt, app/src/main/java/com/kyhsgeekcode/disassembler/files/RawFile.kt, app/src/main/java/com/kyhsgeekcode/disassembler/files/PEFile.java
Constructors updated to accept optional initial content and optional deferred loaders; implementations override lazy getBinaryContents()/getBinaryLength() and defer populating internal buffers until needed.
Disassembly UI adaptation
app/src/main/java/com/kyhsgeekcode/disassembler/ui/tabs/BinaryDisasmTab.kt
Caches binary payload lazily and uses getBinaryLength() for address validation and pagination, replacing direct fileContents access.
UI test tags & binary detail tab
app/src/main/java/com/kyhsgeekcode/disassembler/ui/MainTestTags.kt, app/src/main/java/com/kyhsgeekcode/disassembler/ui/tabs/BinaryDetailTab.kt, app/src/main/java/com/kyhsgeekcode/disassembler/ui/tabs/BinaryTab.kt
Added test tag constants and applied testTag to Binary Detail tab and Save Details button to support new instrumented tests.
Tests — reader, detection, lazy behavior, and android flows
app/src/test/java/.../AbstractFileReaderBridgeTest.kt, .../BinaryContainerFormatDetectionTest.kt, .../DeferredFileBackedBinaryContentTest.kt, .../FileChannelBinaryRangeReaderTest.kt, .../RawFileLazyContentTest.kt, app/src/androidTest/java/.../*.kt
New unit tests cover range-reader behavior, container-format detection, deferred content caching/length semantics, and several Android instrumented flows for import/export and UI resilience.
sequenceDiagram
    participant Caller
    participant AbstractFile.Companion as Factory
    participant FileSystem as BinaryRangeReader
    participant Parser as ELF/PE/RAW
    Caller->>Factory: createInstance(file)
    Factory->>FileSystem: detectBinaryContainerFormat(file)
    FileSystem-->>Factory: ELF / PE / RAW
    Factory->>Parser: instantiate appropriate File (may pass loader)
    alt PE needs parsing bytes
        Factory->>FileSystem: readFileContentsForParsing(file)
        FileSystem-->>Factory: parsing bytes
        Factory->>Parser: provide bytes (or deferred loader)
    end
    Parser-->>Caller: AbstractFile instance (deferred or preloaded)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I nibble bytes with careful paws, I fetch but do not bite,
I peek at headers, sleep on loads, and wake to read just right.
ELF, PE, or raw I sniff—then bring only what you need,
Light-footed files hop home again, content fetched on speed.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.94% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: introducing range-reader infrastructure to enable lazy loading and reduce retained binary memory, which is the core refactor across all file types.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/binary-range-reader

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Tip

CodeRabbit can generate a title for your PR based on the changes.

Add @coderabbitai placeholder anywhere in the title of your PR and CodeRabbit will replace it with a title based on the changes in the PR. You can change the placeholder by changing the reviews.auto_title_placeholder setting.

@yhs0602
Copy link
Owner Author

yhs0602 commented Mar 22, 2026

Added another instrumentation-focused commit on top of the range-reader work:

  • cancel-path coverage for SAF import and advanced import
  • binary detail export success/cancel coverage
  • recreate coverage for ACTION_VIEW project-archive import
  • stable test tags for the detail tab and save-details action

Verification:

  • JAVA_HOME=$(/usr/libexec/java_home -v 17) ANDROID_HOME=$HOME/Library/Android/sdk ./gradlew testDebugUnitTest assembleDebug assembleDebugAndroidTest

Copy link

@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: 5

🧹 Nitpick comments (1)
app/src/main/java/com/kyhsgeekcode/disassembler/files/AbstractFile.kt (1)

131-133: Drop the unused eager read in the Unity DLL path.

Line 132 reads the entire file into memory, but nothing in this branch consumes content. That partially undoes the PR's memory savings on one of the larger binaries this code handles.

♻️ Proposed cleanup
             if (file.path.endsWith("assets/bin/Data/Managed/Assembly-CSharp.dll")) { // Unity C# dll file
-                val content = readFileContentsForParsing(file)
                 Logger.v(TAG, "Found C# unity dll")
                 try {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/kyhsgeekcode/disassembler/files/AbstractFile.kt` around
lines 131 - 133, The branch that checks
file.path.endsWith("assets/bin/Data/Managed/Assembly-CSharp.dll") eagerly calls
readFileContentsForParsing(file) and assigns it to content but never uses it,
negating memory savings; remove the unused call and variable so the branch only
logs via Logger.v(TAG, "Found C# unity dll") (or any remaining necessary logic)
and rely on lazy parsing where needed, ensuring no other code in the
Assembly-CSharp.dll branch depends on the removed content variable or call.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/main/java/com/kyhsgeekcode/disassembler/files/AbstractFile.kt`:
- Around line 23-33: The toString() method incorrectly treats an uninitialized
deferred property fileContents as "not configured" and returns a placeholder;
remove that early ::fileContents.isInitialized check and let toString() always
emit available metadata (use getBinaryLength(), parsed fields) even when
fileContents is lazy-loaded; if RawFile still needs a special prefix, gate only
that prefix on the concrete type (this is RawFile) rather than on fileContents
initialization, ensuring ElfFile/PEFile and others show size/address info
regardless of fileContents state.

In
`@app/src/main/java/com/kyhsgeekcode/disassembler/files/FileChannelBinaryRangeReader.kt`:
- Around line 15-35: The read(offset: Long, length: Int) implementation assumes
channel.read(buffer, offset) fills the buffer; change it to handle short reads
by looping on channel.read until the buffer has no remaining space or EOF (-1)
is returned, accumulating the total bytes read and stopping when reached
readableLength; then return only the bytes actually read (use buffer.position()
or similar) so partial reads don't leave zero-padded data. Update readFully() to
rely on the corrected read(...) behavior for large reads and ensure you respect
the readableLength computed in read(offset,length) and break the loop on -1 to
avoid infinite loops.

In `@app/src/main/java/com/kyhsgeekcode/disassembler/files/PEFile.java`:
- Around line 36-48: The constructor PEFile(File file, byte[] filec,
BinaryContentLoader deferredContentLoader) sets contentLoaded = true when
deferredContentLoader is null but never assigns fileContents, leaving
getBinaryContents()/getBinaryLength() to read a null buffer; fix by initializing
the instance field fileContents with parsingBytes (or a copy) before setting
contentLoaded when deferredContentLoader == null (inside the same constructor),
ensuring DeferredFileBackedBinaryContent is created as before and then
fileContents is populated so getBinaryContents()/getBinaryLength() are safe.

In `@app/src/main/java/com/kyhsgeekcode/disassembler/files/RawFile.kt`:
- Around line 10-15: The constructor sets contentLoaded = filecontent != null
but never initializes the superclass lateinit fileContents, so calls to
getBinaryContents() or getBinaryLength() will crash on the eager-content path;
modify the RawFile constructor to assign fileContents when filecontent is
non-null (e.g., wrap or extract the initialContent into the same structure
DeferredFileBackedBinaryContent expects or set fileContents directly) and ensure
binaryContent and DeferredFileBackedBinaryContent initialization uses that same
initialized fileContents state; update the constructor code paths around
contentLoaded, binaryContent, and initialization of fileContents so both eager
(filecontent != null) and deferred (deferredContentLoader) cases populate
fileContents consistently.

In `@app/src/main/java/com/kyhsgeekcode/disassembler/ui/tabs/BinaryDisasmTab.kt`:
- Around line 159-160: The isValidAddress function currently checks addresses
against a mix of file offsets and virtual base; update it to validate jump
targets against the mapped virtual-address range by ensuring address >=
file.codeVirtAddr and address < file.codeVirtAddr + file.getBinaryLength() (end
exclusive) so jumpto() and loadMore() cannot receive unmapped addresses; adjust
the check in isValidAddress accordingly and ensure any callers (e.g., jumpto(),
loadMore()) rely on this updated predicate.

---

Nitpick comments:
In `@app/src/main/java/com/kyhsgeekcode/disassembler/files/AbstractFile.kt`:
- Around line 131-133: The branch that checks
file.path.endsWith("assets/bin/Data/Managed/Assembly-CSharp.dll") eagerly calls
readFileContentsForParsing(file) and assigns it to content but never uses it,
negating memory savings; remove the unused call and variable so the branch only
logs via Logger.v(TAG, "Found C# unity dll") (or any remaining necessary logic)
and rely on lazy parsing where needed, ensuring no other code in the
Assembly-CSharp.dll branch depends on the removed content variable or call.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 18960237-4633-4b97-80bc-572e9c8feb41

📥 Commits

Reviewing files that changed from the base of the PR and between dde36d9 and 658b1ba.

📒 Files selected for processing (15)
  • app/src/main/java/com/kyhsgeekcode/disassembler/files/AbstractFile.kt
  • app/src/main/java/com/kyhsgeekcode/disassembler/files/BinaryContainerFormat.kt
  • app/src/main/java/com/kyhsgeekcode/disassembler/files/BinaryContentLoader.kt
  • app/src/main/java/com/kyhsgeekcode/disassembler/files/BinaryRangeReader.kt
  • app/src/main/java/com/kyhsgeekcode/disassembler/files/DeferredFileBackedBinaryContent.kt
  • app/src/main/java/com/kyhsgeekcode/disassembler/files/ElfFile.kt
  • app/src/main/java/com/kyhsgeekcode/disassembler/files/FileChannelBinaryRangeReader.kt
  • app/src/main/java/com/kyhsgeekcode/disassembler/files/PEFile.java
  • app/src/main/java/com/kyhsgeekcode/disassembler/files/RawFile.kt
  • app/src/main/java/com/kyhsgeekcode/disassembler/ui/tabs/BinaryDisasmTab.kt
  • app/src/test/java/com/kyhsgeekcode/disassembler/files/AbstractFileReaderBridgeTest.kt
  • app/src/test/java/com/kyhsgeekcode/disassembler/files/BinaryContainerFormatDetectionTest.kt
  • app/src/test/java/com/kyhsgeekcode/disassembler/files/DeferredFileBackedBinaryContentTest.kt
  • app/src/test/java/com/kyhsgeekcode/disassembler/files/FileChannelBinaryRangeReaderTest.kt
  • app/src/test/java/com/kyhsgeekcode/disassembler/files/RawFileLazyContentTest.kt

Comment on lines 23 to 33
override fun toString(): String {
if (fileContents == null) {
if (!::fileContents.isInitialized) {
return "The file has not been configured. You should setup manually in the first page before you can see the details."
}
val builder = StringBuilder(
if (this is RawFile) "The file has not been configured. You should setup manually in the first page before you can see the details." +
System.lineSeparator() else ""
)
builder.append(/*R.getString(R.string.FileSize)*/"File Size:")
.append(Integer.toHexString(fileContents.size))
.append(java.lang.Long.toHexString(getBinaryLength()))
.append(ls)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't treat deferred bytes as "not configured".

Line 24 now returns the placeholder string whenever fileContents is still lazy. After this PR, RawFile, ElfFile, and PEFile intentionally leave fileContents uninitialized until bytes are requested, so toString() skips the size/address metadata even though getBinaryLength() and the parsed fields are already available.

🩹 Proposed fix
     override fun toString(): String {
-        if (!::fileContents.isInitialized) {
-            return "The file has not been configured. You should setup manually in the first page before you can see the details."
-        }
         val builder = StringBuilder(
             if (this is RawFile) "The file has not been configured. You should setup manually in the first page before you can see the details." +
                     System.lineSeparator() else ""
         )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/kyhsgeekcode/disassembler/files/AbstractFile.kt` around
lines 23 - 33, The toString() method incorrectly treats an uninitialized
deferred property fileContents as "not configured" and returns a placeholder;
remove that early ::fileContents.isInitialized check and let toString() always
emit available metadata (use getBinaryLength(), parsed fields) even when
fileContents is lazy-loaded; if RawFile still needs a special prefix, gate only
that prefix on the concrete type (this is RawFile) rather than on fileContents
initialization, ensuring ElfFile/PEFile and others show size/address info
regardless of fileContents state.

Comment on lines +15 to +35
override fun read(offset: Long, length: Int): ByteArray {
require(offset >= 0) { "offset must be non-negative" }
require(length >= 0) { "length must be non-negative" }
if (length == 0 || offset >= size) {
return ByteArray(0)
}
val readableLength = min(length.toLong(), size - offset).toInt()
val buffer = ByteBuffer.allocate(readableLength)
channel.read(buffer, offset)
return buffer.array()
}

override fun readFully(): ByteArray {
if (size == 0L) {
return ByteArray(0)
}
require(size <= Int.MAX_VALUE) {
"File is too large to fit into a ByteArray: $size bytes"
}
return read(offset = 0, length = size.toInt())
}
Copy link

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

🌐 Web query:

Does java.nio.channels.FileChannel.read(ByteBuffer, long) guarantee that a single call fills the ByteBuffer for regular files, or can it legally return fewer bytes than requested?

💡 Result:

No, java.nio.channels.FileChannel.read(ByteBuffer, long) does not guarantee that a single call fills the ByteBuffer, even for regular files. It can legally return fewer bytes than requested.

Citations:


Handle short reads instead of assuming one FileChannel.read fills the buffer.

The channel.read(buffer, offset) call at line 23 ignores the returned byte count. Since FileChannel.read() can legally return fewer bytes than requested, even for regular files, a short read leaves the buffer partially uninitialized (zero-padded). This corrupts readFully() at line 34, which inherits the same defect.

Use a loop to read until the buffer is full or EOF:

Proposed fix
     override fun read(offset: Long, length: Int): ByteArray {
         require(offset >= 0) { "offset must be non-negative" }
         require(length >= 0) { "length must be non-negative" }
         if (length == 0 || offset >= size) {
             return ByteArray(0)
         }
         val readableLength = min(length.toLong(), size - offset).toInt()
         val buffer = ByteBuffer.allocate(readableLength)
-        channel.read(buffer, offset)
-        return buffer.array()
+        var position = offset
+        while (buffer.hasRemaining()) {
+            val bytesRead = channel.read(buffer, position)
+            if (bytesRead <= 0) {
+                break
+            }
+            position += bytesRead
+        }
+        return buffer.array().copyOf(buffer.position())
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
override fun read(offset: Long, length: Int): ByteArray {
require(offset >= 0) { "offset must be non-negative" }
require(length >= 0) { "length must be non-negative" }
if (length == 0 || offset >= size) {
return ByteArray(0)
}
val readableLength = min(length.toLong(), size - offset).toInt()
val buffer = ByteBuffer.allocate(readableLength)
channel.read(buffer, offset)
return buffer.array()
}
override fun readFully(): ByteArray {
if (size == 0L) {
return ByteArray(0)
}
require(size <= Int.MAX_VALUE) {
"File is too large to fit into a ByteArray: $size bytes"
}
return read(offset = 0, length = size.toInt())
}
override fun read(offset: Long, length: Int): ByteArray {
require(offset >= 0) { "offset must be non-negative" }
require(length >= 0) { "length must be non-negative" }
if (length == 0 || offset >= size) {
return ByteArray(0)
}
val readableLength = min(length.toLong(), size - offset).toInt()
val buffer = ByteBuffer.allocate(readableLength)
var position = offset
while (buffer.hasRemaining()) {
val bytesRead = channel.read(buffer, position)
if (bytesRead <= 0) {
break
}
position += bytesRead
}
return buffer.array().copyOf(buffer.position())
}
override fun readFully(): ByteArray {
if (size == 0L) {
return ByteArray(0)
}
require(size <= Int.MAX_VALUE) {
"File is too large to fit into a ByteArray: $size bytes"
}
return read(offset = 0, length = size.toInt())
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/kyhsgeekcode/disassembler/files/FileChannelBinaryRangeReader.kt`
around lines 15 - 35, The read(offset: Long, length: Int) implementation assumes
channel.read(buffer, offset) fills the buffer; change it to handle short reads
by looping on channel.read until the buffer has no remaining space or EOF (-1)
is returned, accumulating the total bytes read and stopping when reached
readableLength; then return only the bytes actually read (use buffer.position()
or similar) so partial reads don't leave zero-padded data. Update readFully() to
rely on the corrected read(...) behavior for large reads and ensure you respect
the readableLength computed in read(offset,length) and break the loop on -1 to
avoid infinite loops.

Comment on lines 36 to +48
public PEFile(File file, byte[] filec) throws IOException, NotThisFormatException {
this(file, filec, null);
}

public PEFile(File file, byte[] filec, BinaryContentLoader deferredContentLoader) throws IOException, NotThisFormatException {
path = file.getPath();
byte[] parsingBytes = filec != null ? filec : loadBinaryContents(file, deferredContentLoader);
binaryContent = new DeferredFileBackedBinaryContent(
file,
deferredContentLoader == null ? parsingBytes : null,
deferredContentLoader
);
contentLoaded = deferredContentLoader == null;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Initialize fileContents before marking the PE payload as loaded.

When deferredContentLoader is null, Line 48 sets contentLoaded = true, but no value is assigned to fileContents. Any caller that still uses the two-arg constructor on Line 36 will then hit an uninitialized/null buffer in getBinaryContents() and getBinaryLength().

🐛 Proposed fix
     byte[] parsingBytes = filec != null ? filec : loadBinaryContents(file, deferredContentLoader);
     binaryContent = new DeferredFileBackedBinaryContent(
             file,
             deferredContentLoader == null ? parsingBytes : null,
             deferredContentLoader
     );
-    contentLoaded = deferredContentLoader == null;
+    if (deferredContentLoader == null) {
+        fileContents = parsingBytes;
+        contentLoaded = true;
+    } else {
+        contentLoaded = false;
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/kyhsgeekcode/disassembler/files/PEFile.java` around
lines 36 - 48, The constructor PEFile(File file, byte[] filec,
BinaryContentLoader deferredContentLoader) sets contentLoaded = true when
deferredContentLoader is null but never assigns fileContents, leaving
getBinaryContents()/getBinaryLength() to read a null buffer; fix by initializing
the instance field fileContents with parsingBytes (or a copy) before setting
contentLoaded when deferredContentLoader == null (inside the same constructor),
ensuring DeferredFileBackedBinaryContent is created as before and then
fileContents is populated so getBinaryContents()/getBinaryLength() are safe.

Comment on lines +10 to +15
private var contentLoaded = filecontent != null
private val binaryContent = DeferredFileBackedBinaryContent(
file = file,
initialContent = filecontent,
loader = deferredContentLoader?.let { BinaryContentLoader { it() } },
)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Initialize fileContents on the eager-content path.

contentLoaded becomes true as soon as filecontent is non-null, but this constructor never assigns the superclass fileContents. The first getBinaryContents() or getBinaryLength() call on that path will therefore hit an uninitialized lateinit property and throw.

Suggested fix
     init {
         path = file.path
+        if (filecontent != null) {
+            fileContents = filecontent
+        }
     }

Also applies to: 17-30

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

In `@app/src/main/java/com/kyhsgeekcode/disassembler/files/RawFile.kt` around
lines 10 - 15, The constructor sets contentLoaded = filecontent != null but
never initializes the superclass lateinit fileContents, so calls to
getBinaryContents() or getBinaryLength() will crash on the eager-content path;
modify the RawFile constructor to assign fileContents when filecontent is
non-null (e.g., wrap or extract the initialContent into the same structure
DeferredFileBackedBinaryContent expects or set fileContents directly) and ensure
binaryContent and DeferredFileBackedBinaryContent initialization uses that same
initialized fileContents state; update the constructor code paths around
contentLoaded, binaryContent, and initialization of fileContents so both eager
(filecontent != null) and deferred (deferredContentLoader) cases populate
fileContents consistently.

Comment on lines 159 to +160
private fun isValidAddress(address: Long): Boolean {
return if (address > file.fileContents.size + file.codeVirtAddr) false else address >= 0
return if (address > file.getBinaryLength() + file.codeVirtAddr) false else address >= 0
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validate jump targets against the mapped virtual-address range.

Now that the upper bound is in virtual-address space, the lower bound has to move to file.codeVirtAddr as well, and the end should be exclusive. As written, this still accepts addresses below the image base and codeVirtAddr + getBinaryLength(), so jumpto() can hand loadMore() an unmapped address.

Suggested fix
     private fun isValidAddress(address: Long): Boolean {
-        return if (address > file.getBinaryLength() + file.codeVirtAddr) false else address >= 0
+        val start = file.codeVirtAddr
+        val endExclusive = start + file.getBinaryLength()
+        return address >= start && address < endExclusive
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private fun isValidAddress(address: Long): Boolean {
return if (address > file.fileContents.size + file.codeVirtAddr) false else address >= 0
return if (address > file.getBinaryLength() + file.codeVirtAddr) false else address >= 0
private fun isValidAddress(address: Long): Boolean {
val start = file.codeVirtAddr
val endExclusive = start + file.getBinaryLength()
return address >= start && address < endExclusive
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/kyhsgeekcode/disassembler/ui/tabs/BinaryDisasmTab.kt`
around lines 159 - 160, The isValidAddress function currently checks addresses
against a mix of file offsets and virtual base; update it to validate jump
targets against the mapped virtual-address range by ensuring address >=
file.codeVirtAddr and address < file.codeVirtAddr + file.getBinaryLength() (end
exclusive) so jumpto() and loadMore() cannot receive unmapped addresses; adjust
the check in isValidAddress accordingly and ensure any callers (e.g., jumpto(),
loadMore()) rely on this updated predicate.

Copy link

@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.

🧹 Nitpick comments (2)
app/src/main/java/com/kyhsgeekcode/disassembler/ui/tabs/BinaryTab.kt (1)

240-245: Prefer explicit stable tags over ::class.simpleName fallback.

At Line 243, binary_tab_${tabKind::class.simpleName} is brittle for test selectors (renames/obfuscation). Consider a fixed mapping per tab kind.

♻️ Suggested stable mapping
 private fun binaryTabTestTag(tabKind: BinaryTabKind): String {
     return when (tabKind) {
-        is BinaryTabKind.BinaryDetail -> com.kyhsgeekcode.disassembler.ui.MainTestTags.BINARY_TAB_DETAIL
-        else -> "binary_tab_${tabKind::class.simpleName}"
+        is BinaryTabKind.BinaryDetail -> com.kyhsgeekcode.disassembler.ui.MainTestTags.BINARY_TAB_DETAIL
+        is BinaryTabKind.BinaryOverview -> "binary_tab_overview"
+        is BinaryTabKind.BinaryImportSymbol -> "binary_tab_import_symbol"
+        is BinaryTabKind.BinaryExportSymbol -> "binary_tab_export_symbol"
+        is BinaryTabKind.BinaryDisasm -> "binary_tab_disassembly"
+        is BinaryTabKind.BinaryString -> "binary_tab_strings"
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/kyhsgeekcode/disassembler/ui/tabs/BinaryTab.kt` around
lines 240 - 245, The binaryTabTestTag function uses a fragile fallback
"binary_tab_${tabKind::class.simpleName}" for test selectors; update
binaryTabTestTag to return explicit, stable tag strings for each BinaryTabKind
variant instead of relying on ::class.simpleName (handle
BinaryTabKind.BinaryDetail already, and add explicit cases for other variants of
BinaryTabKind or a sealed when-else that returns constants), and preferably
reference or add descriptive constants (e.g., in MainTestTags) so test selectors
remain stable across renames/obfuscation.
app/src/androidTest/java/com/kyhsgeekcode/disassembler/MainActivityActionViewProjectArchiveIntentFlowTest.kt (1)

54-62: Avoid singleton-coupled assertions in the recreate test.

Line 54 and Line 60 gate on ProjectManager.currentProject, but that field is process-singleton state (ProjectManager.currentProject in app/src/main/java/com/kyhsgeekcode/disassembler/project/ProjectManager.kt:35-59), so this can pass without validating true recreation restoration.

Use UI/state observable from the recreated activity as the primary readiness/assertion signal.

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

In
`@app/src/androidTest/java/com/kyhsgeekcode/disassembler/MainActivityActionViewProjectArchiveIntentFlowTest.kt`
around lines 54 - 62, The test currently uses process-singleton state
ProjectManager.currentProject around
composeRule.activityRule.scenario.recreate(), which can hide failures; replace
those waits that reference ProjectManager.currentProject with UI/state
observations from the recreated activity instead (keep the recreate call
composeRule.activityRule.scenario.recreate() as-is). Concretely, change the two
composeRule.waitUntil blocks that check ProjectManager.currentProject to
wait/assert on a Compose UI node that reflects the opened project (e.g., use
composeRule.waitUntil or
composeRule.onNodeWithText("ArchiveFixture").assertExists() or check a semantics
tag like "projectName" via
composeRule.onNodeWithTag("projectName").assertExists()) so the test verifies
the recreated Activity/Compose state rather than the singleton ProjectManager.
Ensure both pre- and post-recreate readiness/assertions use the UI-based check.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@app/src/androidTest/java/com/kyhsgeekcode/disassembler/MainActivityActionViewProjectArchiveIntentFlowTest.kt`:
- Around line 54-62: The test currently uses process-singleton state
ProjectManager.currentProject around
composeRule.activityRule.scenario.recreate(), which can hide failures; replace
those waits that reference ProjectManager.currentProject with UI/state
observations from the recreated activity instead (keep the recreate call
composeRule.activityRule.scenario.recreate() as-is). Concretely, change the two
composeRule.waitUntil blocks that check ProjectManager.currentProject to
wait/assert on a Compose UI node that reflects the opened project (e.g., use
composeRule.waitUntil or
composeRule.onNodeWithText("ArchiveFixture").assertExists() or check a semantics
tag like "projectName" via
composeRule.onNodeWithTag("projectName").assertExists()) so the test verifies
the recreated Activity/Compose state rather than the singleton ProjectManager.
Ensure both pre- and post-recreate readiness/assertions use the UI-based check.

In `@app/src/main/java/com/kyhsgeekcode/disassembler/ui/tabs/BinaryTab.kt`:
- Around line 240-245: The binaryTabTestTag function uses a fragile fallback
"binary_tab_${tabKind::class.simpleName}" for test selectors; update
binaryTabTestTag to return explicit, stable tag strings for each BinaryTabKind
variant instead of relying on ::class.simpleName (handle
BinaryTabKind.BinaryDetail already, and add explicit cases for other variants of
BinaryTabKind or a sealed when-else that returns constants), and preferably
reference or add descriptive constants (e.g., in MainTestTags) so test selectors
remain stable across renames/obfuscation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4d4d1e33-6dc0-46d6-80e5-3b06076439da

📥 Commits

Reviewing files that changed from the base of the PR and between 658b1ba and 9dcda8b.

📒 Files selected for processing (7)
  • app/src/androidTest/java/com/kyhsgeekcode/disassembler/MainActivityActionViewProjectArchiveIntentFlowTest.kt
  • app/src/androidTest/java/com/kyhsgeekcode/disassembler/MainActivityAdvancedImportCancelFlowTest.kt
  • app/src/androidTest/java/com/kyhsgeekcode/disassembler/MainActivityBinaryDetailExportFlowTest.kt
  • app/src/androidTest/java/com/kyhsgeekcode/disassembler/MainActivitySafImportCancelFlowTest.kt
  • app/src/main/java/com/kyhsgeekcode/disassembler/ui/MainTestTags.kt
  • app/src/main/java/com/kyhsgeekcode/disassembler/ui/tabs/BinaryDetailTab.kt
  • app/src/main/java/com/kyhsgeekcode/disassembler/ui/tabs/BinaryTab.kt
✅ Files skipped from review due to trivial changes (1)
  • app/src/main/java/com/kyhsgeekcode/disassembler/ui/MainTestTags.kt

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