Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,18 @@ class MainActivityActionViewProjectArchiveIntentFlowTest {
assertEquals("ArchiveFixture", ProjectManager.currentProject?.name)
assertTrue(ProjectManager.currentProject?.sourceFilePath?.endsWith("sourceFilePath") == true)
}

@Test
fun actionViewProjectArchive_survivesRecreate() {
composeRule.waitUntil(timeoutMillis = PROJECT_OPEN_TIMEOUT_MS) {
ProjectManager.currentProject?.name == "ArchiveFixture"
}

composeRule.activityRule.scenario.recreate()

composeRule.waitUntil(timeoutMillis = PROJECT_OPEN_TIMEOUT_MS) {
ProjectManager.currentProject?.name == "ArchiveFixture"
}
composeRule.onNodeWithTag(MainTestTags.EXPORT_PROJECT_BUTTON).assertExists()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.kyhsgeekcode.disassembler

import android.content.ComponentName
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import androidx.test.espresso.intent.Intents.intending
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.kyhsgeekcode.disassembler.ui.MainTestTags
import com.kyhsgeekcode.filechooser.NewFileChooserActivity
import org.junit.Rule
import org.junit.Test
import org.junit.rules.RuleChain
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class MainActivityAdvancedImportCancelFlowTest {
private val projectCleanupRule = ProjectStateCleanupRule()
private val preferenceRule = PowerUserModePreferenceRule(powerUserModeEnabled = true)
private val intentsRule = InstrumentationIntentsRule()
private val composeRule = createAndroidComposeRule<MainActivity>()

@get:Rule
val rules: RuleChain = RuleChain.outerRule(projectCleanupRule)
.around(preferenceRule)
.around(intentsRule)
.around(composeRule)

@Test
fun advancedImportCancel_keepsPowerUserEntryPointsVisible() {
intending(
hasComponent(
ComponentName(
composeRule.activity,
NewFileChooserActivity::class.java
)
)
).respondWith(createCanceledActivityResult())

composeRule.onNodeWithTag(MainTestTags.IMPORT_ADVANCED_BUTTON).performClick()
composeRule.waitForIdle()

composeRule.onNodeWithTag(MainTestTags.IMPORT_SAF_BUTTON).assertExists()
composeRule.onNodeWithTag(MainTestTags.IMPORT_ADVANCED_BUTTON).assertExists()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package com.kyhsgeekcode.disassembler

import android.content.Intent
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onAllNodesWithTag
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import androidx.test.espresso.intent.Intents.intending
import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.kyhsgeekcode.disassembler.ui.MainTestTags
import org.hamcrest.CoreMatchers.allOf
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.junit.rules.RuleChain
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class MainActivityBinaryDetailExportFlowTest {
private val projectCleanupRule = ProjectStateCleanupRule()
private val preferenceRule = PowerUserModePreferenceRule(powerUserModeEnabled = false)
private val intentsRule = InstrumentationIntentsRule()
private val composeRule = createAndroidComposeRule<MainActivity>()

@get:Rule
val rules: RuleChain = RuleChain.outerRule(projectCleanupRule)
.around(preferenceRule)
.around(intentsRule)
.around(composeRule)

@Test
fun saveDetailsResult_writesTextDocument() {
stubSafImport("detail-export-source.apk")
val (outputFile, createDocumentResult) = createCreateDocumentResult("binary-details.txt")
intending(allOf(hasAction(Intent.ACTION_CREATE_DOCUMENT))).respondWith(createDocumentResult)

openProjectAndDetailTab()
composeRule.onNodeWithTag(MainTestTags.SAVE_DETAILS_BUTTON).performClick()

composeRule.waitUntil(timeoutMillis = 5_000) {
outputFile.length() > 0L
}

assertTrue(outputFile.readText().contains("File Size:"))
}

@Test
fun saveDetailsCancel_keepsProjectOpen() {
stubSafImport("detail-export-cancel-source.apk")
intending(allOf(hasAction(Intent.ACTION_CREATE_DOCUMENT))).respondWith(createCanceledActivityResult())

openProjectAndDetailTab()
composeRule.onNodeWithTag(MainTestTags.SAVE_DETAILS_BUTTON).performClick()
composeRule.waitForIdle()

composeRule.onNodeWithTag(MainTestTags.SAVE_DETAILS_BUTTON).assertExists()
composeRule.onNodeWithTag(MainTestTags.EXPORT_PROJECT_BUTTON).assertExists()
}

private fun stubSafImport(displayName: String) {
intending(
allOf(
hasAction(Intent.ACTION_OPEN_DOCUMENT)
)
).respondWith(
createOpenDocumentResult(
displayName = displayName,
content = "apk-content".encodeToByteArray()
)
)
}

private fun openProjectAndDetailTab() {
composeRule.onNodeWithTag(MainTestTags.IMPORT_SAF_BUTTON).performClick()
composeRule.waitUntil(timeoutMillis = 5_000) {
composeRule.onAllNodesWithTag(MainTestTags.EXPORT_PROJECT_BUTTON)
.fetchSemanticsNodes().isNotEmpty()
}
composeRule.onNodeWithTag(MainTestTags.BINARY_TAB_DETAIL).performClick()
composeRule.onNodeWithTag(MainTestTags.SAVE_DETAILS_BUTTON).assertExists()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.kyhsgeekcode.disassembler

import android.content.Intent
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import androidx.test.espresso.intent.Intents.intending
import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.kyhsgeekcode.disassembler.ui.MainTestTags
import org.hamcrest.CoreMatchers.allOf
import org.junit.Rule
import org.junit.Test
import org.junit.rules.RuleChain
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class MainActivitySafImportCancelFlowTest {
private val projectCleanupRule = ProjectStateCleanupRule()
private val preferenceRule = PowerUserModePreferenceRule(powerUserModeEnabled = false)
private val intentsRule = InstrumentationIntentsRule()
private val composeRule = createAndroidComposeRule<MainActivity>()

@get:Rule
val rules: RuleChain = RuleChain.outerRule(projectCleanupRule)
.around(preferenceRule)
.around(intentsRule)
.around(composeRule)

@Test
fun safImportCancel_keepsStandardEntryPointVisible() {
intending(
allOf(
hasAction(Intent.ACTION_OPEN_DOCUMENT)
)
).respondWith(createCanceledActivityResult())

composeRule.onNodeWithTag(MainTestTags.IMPORT_SAF_BUTTON).performClick()
composeRule.waitForIdle()

composeRule.onNodeWithTag(MainTestTags.IMPORT_SAF_BUTTON).assertExists()
composeRule.onNodeWithTag(MainTestTags.IMPORT_ADVANCED_BUTTON).assertDoesNotExist()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@ abstract class AbstractFile : Closeable {
}

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)
Comment on lines 23 to 33
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

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.

builder.append(appCtx.getString(R.string.FoffsCS))
.append(java.lang.Long.toHexString(codeSectionBase))
Expand Down Expand Up @@ -74,9 +74,48 @@ abstract class AbstractFile : Closeable {
@JvmField
var path = ""

open fun getBinaryContents(): ByteArray = fileContents

open fun getBinaryLength(): Long = getBinaryContents().size.toLong()

companion object {
private const val TAG = "AbstractFile"

@JvmStatic
internal fun readFileContentsForParsing(
file: File,
readerFactory: (File) -> BinaryRangeReader = ::FileChannelBinaryRangeReader
): ByteArray {
return readerFactory(file).use { reader ->
reader.readFully()
}
}

@JvmStatic
internal fun detectBinaryContainerFormat(
file: File,
readerFactory: (File) -> BinaryRangeReader = ::FileChannelBinaryRangeReader
): BinaryContainerFormat {
val header = readerFactory(file).use { reader ->
reader.read(offset = 0, length = 4)
}
if (header.size >= 4 &&
header[0] == 0x7F.toByte() &&
header[1] == 'E'.code.toByte() &&
header[2] == 'L'.code.toByte() &&
header[3] == 'F'.code.toByte()
) {
return BinaryContainerFormat.ELF
}
if (header.size >= 2 &&
header[0] == 'M'.code.toByte() &&
header[1] == 'Z'.code.toByte()
) {
return BinaryContainerFormat.PE
}
return BinaryContainerFormat.RAW
}

@JvmStatic
@Throws(IOException::class)
fun createInstance(file: File): AbstractFile {
Expand All @@ -89,8 +128,8 @@ abstract class AbstractFile : Closeable {
// 그리고 AfterReadFully 함수는 없어질지도 모른다!
// 그러면 중복코드도 사라짐
// 행복회로
val content = file.readBytes()
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 {
val facileReflector = Facile.load(file.path)
Expand All @@ -109,30 +148,45 @@ abstract class AbstractFile : Closeable {
} catch (e: SizeMismatchException) {
e.printStackTrace()
}
} else {
return try {
ElfFile(file, content)
} catch (e: Exception) { // not an elf file. try PE parser
Timber.d(e, "Fail elfutil")
}
return when (detectBinaryContainerFormat(file)) {
BinaryContainerFormat.ELF -> {
try {
ElfFile(file, filec = null, deferredContentLoader = BinaryContentLoader {
readFileContentsForParsing(file)
})
} catch (e: Exception) {
Timber.d(e, "Fail elfutil")
RawFile(file, filecontent = null) {
readFileContentsForParsing(file)
}
}
}

BinaryContainerFormat.PE -> {
val content = readFileContentsForParsing(file)
try {
PEFile(file, content)
PEFile(file, content, BinaryContentLoader {
readFileContentsForParsing(file)
})
} catch (f: NotThisFormatException) {
Timber.e(f, "Not this format exception")
RawFile(file, content)
// AllowRawSetup();
// failed to parse the file. please setup manually.
} catch (f: RuntimeException) { // AlertError("Failed to parse the file. Please setup manually. Sending an error report, the file being analyzed can be attached.", f);
} catch (f: RuntimeException) {
Timber.e(f, "Not this format exception")
RawFile(file, content)
// AllowRawSetup();
} catch (g: Exception) { // AlertError("Unexpected exception: failed to parse the file. please setup manually.", g);
} catch (g: Exception) {
Timber.e(g, "What the exception")
RawFile(file, content)
// AllowRawSetup();
}
}

BinaryContainerFormat.RAW -> {
RawFile(file, filecontent = null) {
readFileContentsForParsing(file)
}
}
}
return RawFile(file, content)
// return null
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.kyhsgeekcode.disassembler.files

enum class BinaryContainerFormat {
ELF,
PE,
RAW,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.kyhsgeekcode.disassembler.files

fun interface BinaryContentLoader {
fun load(): ByteArray
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.kyhsgeekcode.disassembler.files

import java.io.Closeable

interface BinaryRangeReader : Closeable {
val size: Long

fun read(offset: Long, length: Int): ByteArray

fun readFully(): ByteArray
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.kyhsgeekcode.disassembler.files

import java.io.File

class DeferredFileBackedBinaryContent(
private val file: File,
initialContent: ByteArray? = null,
private val loader: BinaryContentLoader? = null,
) {
private var loadedContent: ByteArray? = initialContent

fun contents(): ByteArray {
val existing = loadedContent
if (existing != null) {
return existing
}
val loaded = loader?.load() ?: file.readBytes()
loadedContent = loaded
return loaded
}

fun length(): Long {
return loadedContent?.size?.toLong() ?: file.length()
}
}
Loading
Loading