Skip to content

Commit aab6397

Browse files
committed
stability: bound string search scans for large files
1 parent b445252 commit aab6397

2 files changed

Lines changed: 107 additions & 6 deletions

File tree

app/src/main/java/com/kyhsgeekcode/disassembler/ui/tabs/StringTab.kt

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,43 @@ import timber.log.Timber
2929
private const val MAX_RENDERED_STRING_RESULTS = 5_000
3030
private const val MAX_RENDERED_STRING_CHARS = 4_096
3131
private const val MAX_RENDERED_STRING_TOTAL_CHARS = 32_768
32+
internal const val MAX_SEARCHED_STRING_BYTES = 4 * 1024 * 1024
33+
34+
data class StringSearchInput(
35+
val bytes: ByteArray,
36+
val originalSize: Long,
37+
val isTruncated: Boolean
38+
)
39+
40+
internal fun buildStringSearchInput(
41+
previewBytes: ByteArray,
42+
originalSize: Long,
43+
maxBytes: Int = MAX_SEARCHED_STRING_BYTES
44+
): StringSearchInput {
45+
return StringSearchInput(
46+
bytes = previewBytes,
47+
originalSize = originalSize,
48+
isTruncated = originalSize > maxBytes
49+
)
50+
}
51+
52+
internal fun buildStringSearchNotice(
53+
input: StringSearchInput,
54+
resultsTruncated: Boolean
55+
): String? {
56+
val parts = mutableListOf<String>()
57+
if (input.isTruncated) {
58+
parts += "Searching strings in first ${input.bytes.size} bytes of ${input.originalSize} bytes"
59+
}
60+
if (resultsTruncated) {
61+
parts += "Showing first $MAX_RENDERED_STRING_RESULTS results."
62+
}
63+
return when {
64+
parts.isEmpty() -> null
65+
parts.size == 1 -> parts.single()
66+
else -> parts.joinToString(". ")
67+
}
68+
}
3269

3370
internal fun clipFoundStringForRendering(
3471
result: FoundString,
@@ -88,11 +125,20 @@ class StringTabData(val data: TabKind.FoundString) : PreparedTabData() {
88125
val isDone = _isDone as StateFlow<Boolean>
89126
private val _isTruncated = MutableStateFlow(false)
90127
val isTruncated = _isTruncated as StateFlow<Boolean>
128+
private val _notice = MutableStateFlow<String?>(null)
129+
val notice = _notice as StateFlow<String?>
91130
lateinit var analyzer: Analyzer
92131
override suspend fun prepare() {
93-
val bytes = ProjectDataStorage.getFileContent(data.relPath)
132+
val fileSize = ProjectDataStorage.resolveToRead(data.relPath)?.length() ?: 0L
133+
val input = buildStringSearchInput(
134+
previewBytes = ProjectDataStorage.getFileContentPreview(
135+
data.relPath,
136+
MAX_SEARCHED_STRING_BYTES
137+
),
138+
originalSize = fileSize
139+
)
94140
Timber.d("Given relPath: ${data.relPath}")
95-
analyzer = Analyzer(bytes)
141+
analyzer = Analyzer(input.bytes)
96142
val accumulator = StringSearchResultAccumulator(MAX_RENDERED_STRING_RESULTS)
97143
analyzer.searchStrings(data.range.first, data.range.last) { p, t, fs ->
98144
fs?.let {
@@ -103,6 +149,7 @@ class StringTabData(val data: TabKind.FoundString) : PreparedTabData() {
103149
}
104150
if (p == t) { // done
105151
_isTruncated.value = accumulator.isTruncated
152+
_notice.value = buildStringSearchNotice(input, accumulator.isTruncated)
106153
_isDone.value = true
107154
}
108155
}
@@ -116,15 +163,13 @@ fun StringTab(data: TabData, viewModel: MainViewModel) {
116163
val preparedTabData: StringTabData = viewModel.getTabData(data)
117164
val strings = preparedTabData.strings
118165
val isDone = preparedTabData.isDone.collectAsState()
119-
val isTruncated = preparedTabData.isTruncated.collectAsState()
166+
val notice = preparedTabData.notice.collectAsState()
120167
Column {
121168
Row {
122169
if (!isDone.value) {
123170
Icon(imageVector = Icons.Default.MoreVert, contentDescription = "Searching...")
124171
}
125-
if (isTruncated.value) {
126-
Text("Showing first $MAX_RENDERED_STRING_RESULTS results")
127-
}
172+
notice.value?.let { Text(it) }
128173
}
129174
TableView(
130175
titles = listOf("Offset" to 100.dp, "Length" to 50.dp, "String" to 800.dp),
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package com.kyhsgeekcode.disassembler.ui.tabs
2+
3+
import kotlin.test.Test
4+
import kotlin.test.assertEquals
5+
import kotlin.test.assertFalse
6+
import kotlin.test.assertTrue
7+
8+
class StringSearchInputPolicyTest {
9+
@Test
10+
fun `buildStringSearchInput uses full content when file is within limit`() {
11+
val input = buildStringSearchInput(
12+
previewBytes = "hello".encodeToByteArray(),
13+
originalSize = 5,
14+
maxBytes = 8
15+
)
16+
17+
assertEquals("hello", input.bytes.decodeToString())
18+
assertFalse(input.isTruncated)
19+
assertEquals(5, input.originalSize)
20+
}
21+
22+
@Test
23+
fun `buildStringSearchInput marks truncated scans when file exceeds limit`() {
24+
val input = buildStringSearchInput(
25+
previewBytes = "abcd".encodeToByteArray(),
26+
originalSize = 10,
27+
maxBytes = 4
28+
)
29+
30+
assertEquals("abcd", input.bytes.decodeToString())
31+
assertTrue(input.isTruncated)
32+
assertEquals(10, input.originalSize)
33+
}
34+
35+
@Test
36+
fun `buildStringSearchNotice describes bounded scans`() {
37+
assertEquals(
38+
"Searching strings in first 4 bytes of 10 bytes",
39+
buildStringSearchNotice(
40+
StringSearchInput(bytes = "abcd".encodeToByteArray(), originalSize = 10, isTruncated = true),
41+
resultsTruncated = false
42+
)
43+
)
44+
}
45+
46+
@Test
47+
fun `buildStringSearchNotice combines scan and result truncation`() {
48+
assertEquals(
49+
"Searching strings in first 4 bytes of 10 bytes. Showing first 5000 results.",
50+
buildStringSearchNotice(
51+
StringSearchInput(bytes = "abcd".encodeToByteArray(), originalSize = 10, isTruncated = true),
52+
resultsTruncated = true
53+
)
54+
)
55+
}
56+
}

0 commit comments

Comments
 (0)