Skip to content

Commit b69fc78

Browse files
committed
archive: support generic extraction in drawers
1 parent 07d0192 commit b69fc78

7 files changed

Lines changed: 123 additions & 137 deletions

File tree

app/src/main/java/com/kyhsgeekcode/Util.kt

Lines changed: 40 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -115,54 +115,59 @@ suspend fun extract(
115115
publisher: (Long, Long) -> Unit = { current, total -> }
116116
) =
117117
withContext(Dispatchers.IO) {
118-
Log.v("extract", "File:${from.path}")
119-
var archi: ArchiveInputStream? = null
120-
val totalSize = from.length()
121-
try {
122-
archi =
123-
ArchiveStreamFactory().createArchiveInputStream(BufferedInputStream(from.inputStream()))
124-
var entry: ArchiveEntry?
118+
extractSupportedArchive(from, toDir) { current, total ->
119+
publisher(current, total)
120+
}
121+
}
125122

123+
@Throws(IOException::class, SecurityException::class)
124+
fun extractSupportedArchive(
125+
from: File,
126+
toDir: File,
127+
publisher: (Long, Long) -> Unit = { _, _ -> }
128+
) {
129+
val totalSize = from.length()
130+
try {
131+
ArchiveStreamFactory().createArchiveInputStream(BufferedInputStream(from.inputStream())).use { archi ->
132+
var entry: ArchiveEntry?
126133
while (archi.nextEntry.also { entry = it } != null) {
127-
if (entry!!.name == "")
128-
continue
129-
if (!archi.canReadEntryData(entry)) {
130-
// log something?
131-
Log.e("Extract archive", "Cannot read entry data")
134+
val archiveEntry = entry ?: continue
135+
if (archiveEntry.name.isBlank()) continue
136+
if (!archi.canReadEntryData(archiveEntry)) {
132137
continue
133138
}
134-
val f = toDir.resolve(entry?.name!!)
135-
if (entry!!.isDirectory) {
136-
if (!f.isDirectory && !f.mkdirs()) {
137-
throw IOException("failed to create directory $f")
139+
140+
val outputFile = toDir.resolve(archiveEntry.name)
141+
val canonicalPath = outputFile.canonicalPath
142+
if (!canonicalPath.startsWith(toDir.canonicalPath)) {
143+
throw SecurityException(
144+
"The archive file may have a Zip Path Traversal Vulnerability." +
145+
"Is the archive file trusted?"
146+
)
147+
}
148+
149+
if (archiveEntry.isDirectory) {
150+
if (!outputFile.isDirectory && !outputFile.mkdirs()) {
151+
throw IOException("failed to create directory $outputFile")
138152
}
139153
} else {
140-
val parent = f.parentFile
141-
if (!parent.isDirectory && !parent.mkdirs()) {
154+
val parent = outputFile.parentFile
155+
if (parent != null && !parent.isDirectory && !parent.mkdirs()) {
142156
throw IOException("failed to create directory $parent")
143157
}
144-
if (!f.canonicalPath.startsWith(toDir.canonicalPath)) {
145-
throw SecurityException(
146-
"The zip/apk file may have a Zip Path Traversal Vulnerability." +
147-
"Is the zip/apk file trusted?"
148-
)
158+
outputFile.outputStream().use { output ->
159+
IOUtils.copy(archi, output)
149160
}
150-
val o = f.outputStream()
151-
IOUtils.copy(archi, o)
152-
o.close()
153-
}
154-
withContext(Dispatchers.Main) {
155-
publisher(archi.bytesRead, totalSize)
156161
}
162+
publisher(archi.bytesRead, totalSize)
157163
}
158-
} catch (e: ArchiveException) {
159-
Log.e("Extract archive", "error inflating", e)
160-
} catch (e: ZipException) {
161-
Log.e("Extract archive", "error inflating", e)
162-
} finally {
163-
archi?.close()
164164
}
165+
} catch (e: ArchiveException) {
166+
throw IOException("error inflating archive", e)
167+
} catch (e: ZipException) {
168+
throw IOException("error inflating archive", e)
165169
}
170+
}
166171

167172
fun String.toValidFileName(): String {
168173
return this.replace(Regex("[\\\\/:*?\"<>|]"), "")

app/src/main/java/com/kyhsgeekcode/disassembler/FileDrawerListAdapter.kt

Lines changed: 3 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,15 @@ import com.kyhsgeekcode.disassembler.project.ProjectDataStorage
1717
import com.kyhsgeekcode.disassembler.project.ProjectManager
1818
import com.kyhsgeekcode.disassembler.project.models.ProjectModel
1919
import com.kyhsgeekcode.disassembler.project.models.ProjectType
20+
import com.kyhsgeekcode.extractSupportedArchive
2021
import com.kyhsgeekcode.getDrawable
2122
import org.jf.baksmali.Main
2223
import splitties.init.appCtx
2324
import java.io.File
24-
import java.io.FileInputStream
25-
import java.io.FileOutputStream
2625
import java.io.IOException
2726
import java.nio.ByteBuffer
2827
import java.nio.ByteOrder
2928
import java.util.*
30-
import java.util.zip.ZipEntry
31-
import java.util.zip.ZipInputStream
3229
import kotlin.experimental.and
3330
import kotlin.math.roundToInt
3431

@@ -103,36 +100,9 @@ class FileDrawerListAdapter(val progressHandler: ProgressHandler) {
103100
targetDirectory.mkdirs()
104101
val total = File(path).length() * 2
105102
progressHandler.publishProgress(0, total.toInt())
106-
var read = 0
107103
try {
108-
val zi = ZipInputStream(FileInputStream(path))
109-
var entry: ZipEntry? = null
110-
val buffer = ByteArray(2048)
111-
while (zi.nextEntry?.also { entry = it } != null) {
112-
val outfile = File(targetDirectory, entry!!.name)
113-
val canonicalPath = outfile.canonicalPath
114-
if (!canonicalPath.startsWith(targetDirectory.canonicalPath)) {
115-
throw SecurityException(
116-
"The file may have a Zip Path Traversal Vulnerability." +
117-
"Is the file trusted?"
118-
)
119-
}
120-
outfile.parentFile.mkdirs()
121-
var output: FileOutputStream? = null
122-
try {
123-
if (entry!!.name == "")
124-
continue
125-
Log.d(TAG, "entry: $entry, outfile:$outfile")
126-
output = FileOutputStream(outfile)
127-
var len = 0
128-
while (zi.read(buffer).also { len = it } > 0) {
129-
output.write(buffer, 0, len)
130-
}
131-
read += len
132-
} finally { // we must always close the output file
133-
output?.close()
134-
}
135-
progressHandler.publishProgress(read)
104+
extractSupportedArchive(File(path), targetDirectory) { current, totalBytes ->
105+
progressHandler.publishProgress(current.toInt(), totalBytes.toInt())
136106
}
137107
progressHandler.finishProgress()
138108
return getSubObjects(FileDrawerListItem(targetDirectory, initialLevel))

app/src/main/java/com/kyhsgeekcode/disassembler/FileDrawerListItem.kt

Lines changed: 3 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import com.kyhsgeekcode.disassembler.project.ProjectDataStorage
1414
import com.kyhsgeekcode.disassembler.project.ProjectManager
1515
import com.kyhsgeekcode.disassembler.project.models.ProjectModel
1616
import com.kyhsgeekcode.disassembler.project.models.ProjectType
17+
import com.kyhsgeekcode.extractSupportedArchive
1718
import com.kyhsgeekcode.filechooser.model.getValueFromTypeKindAndBytes
1819
import com.kyhsgeekcode.getDrawable
1920
import com.kyhsgeekcode.isArchive
@@ -22,8 +23,6 @@ import org.jf.baksmali.Main
2223
import timber.log.Timber
2324
import java.io.*
2425
import java.util.*
25-
import java.util.zip.ZipEntry
26-
import java.util.zip.ZipInputStream
2726

2827
class FileDrawerListItem {
2928
var caption: String
@@ -301,36 +300,9 @@ class FileDrawerListItem {
301300
targetDirectory.mkdirs()
302301
val total = File(path).length() * 2
303302
progressHandler(0, total.toInt())
304-
var read = 0
305303
try {
306-
val zi = ZipInputStream(FileInputStream(path))
307-
var entry: ZipEntry? = null
308-
val buffer = ByteArray(2048)
309-
while (zi.nextEntry?.also { entry = it } != null) {
310-
val outfile = File(targetDirectory, entry!!.name)
311-
val canonicalPath = outfile.canonicalPath
312-
if (!canonicalPath.startsWith(targetDirectory.canonicalPath)) {
313-
throw SecurityException(
314-
"The file may have a Zip Path Traversal Vulnerability." +
315-
"Is the file trusted?"
316-
)
317-
}
318-
outfile.parentFile.mkdirs()
319-
var output: FileOutputStream? = null
320-
try {
321-
if (entry!!.name == "")
322-
continue
323-
Timber.d("entry: " + entry + ", outfile:" + outfile)
324-
output = FileOutputStream(outfile)
325-
var len = 0
326-
while (zi.read(buffer).also { len = it } > 0) {
327-
output.write(buffer, 0, len)
328-
}
329-
read += len
330-
} finally { // we must always close the output file
331-
output?.close()
332-
}
333-
progressHandler(read, 100)
304+
extractSupportedArchive(File(path), targetDirectory) { current, totalBytes ->
305+
progressHandler(current.toInt(), totalBytes.toInt())
334306
}
335307
finishHandler()
336308
return FileDrawerListItem(targetDirectory, initialLevel).getSubObjects()

app/src/main/java/com/kyhsgeekcode/disassembler/ui/FileDrawerTree.kt

Lines changed: 2 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,16 @@ import com.kyhsgeekcode.disassembler.project.ProjectManager
1111
import com.kyhsgeekcode.disassembler.project.models.ProjectModel
1212
import com.kyhsgeekcode.disassembler.project.models.ProjectType
1313
import com.kyhsgeekcode.disassembler.ui.components.TreeNode
14+
import com.kyhsgeekcode.extractSupportedArchive
1415
import com.kyhsgeekcode.filechooser.model.getValueFromTypeKindAndBytes
1516
import com.kyhsgeekcode.getDrawable
1617
import com.kyhsgeekcode.isArchive
1718
import org.boris.pecoff4j.io.PEParser
1819
import org.jf.baksmali.Main
1920
import timber.log.Timber
2021
import java.io.File
21-
import java.io.FileInputStream
22-
import java.io.FileOutputStream
2322
import java.io.IOException
2423
import java.util.*
25-
import java.util.zip.ZipEntry
26-
import java.util.zip.ZipInputStream
2724

2825
// TODO: Cache children before invalidating
2926
class FileDrawerTreeItem : TreeNode<FileDrawerTreeItem> {
@@ -214,40 +211,8 @@ class FileDrawerTreeItem : TreeNode<FileDrawerTreeItem> {
214211
// appCtx.filesDir.resolve("extracted").resolve()
215212
targetDirectory.deleteRecursively()
216213
targetDirectory.mkdirs()
217-
val total = File(path).length() * 2
218-
// progressHandler(0, total.toInt())
219-
var read = 0
220214
try {
221-
val zi = ZipInputStream(FileInputStream(path))
222-
var entry: ZipEntry? = null
223-
val buffer = ByteArray(2048)
224-
while (zi.nextEntry?.also { entry = it } != null) {
225-
val outfile = File(targetDirectory, entry!!.name)
226-
val canonicalPath = outfile.canonicalPath
227-
if (!canonicalPath.startsWith(targetDirectory.canonicalPath)) {
228-
throw SecurityException(
229-
"The file may have a Zip Path Traversal Vulnerability." +
230-
"Is the file trusted?"
231-
)
232-
}
233-
outfile.parentFile.mkdirs()
234-
var output: FileOutputStream? = null
235-
try {
236-
if (entry!!.name == "")
237-
continue
238-
Timber.d("entry: " + entry + ", outfile:" + outfile)
239-
output = FileOutputStream(outfile)
240-
var len = 0
241-
while (zi.read(buffer).also { len = it } > 0) {
242-
output.write(buffer, 0, len)
243-
}
244-
read += len
245-
} finally { // we must always close the output file
246-
output?.close()
247-
}
248-
// progressHandler(read, 100)
249-
}
250-
// finishHandler()
215+
extractSupportedArchive(File(path), targetDirectory)
251216
return FileDrawerTreeItem(targetDirectory, initialLevel).getChildren()
252217
} catch (e: IOException) {
253218
Log.e("FileAdapter", "", e)
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package com.kyhsgeekcode.disassembler
2+
3+
import com.kyhsgeekcode.extractSupportedArchive
4+
import org.apache.commons.compress.archivers.ar.ArArchiveEntry
5+
import org.apache.commons.compress.archivers.ar.ArArchiveOutputStream
6+
import org.junit.jupiter.api.Assertions.assertEquals
7+
import org.junit.jupiter.api.Assertions.assertTrue
8+
import org.junit.jupiter.api.Test
9+
import org.junit.jupiter.api.io.TempDir
10+
import java.io.FileOutputStream
11+
import java.nio.file.Path
12+
import java.util.zip.ZipEntry
13+
import java.util.zip.ZipOutputStream
14+
15+
class ArchiveExtractionTest {
16+
@TempDir
17+
lateinit var tempDir: Path
18+
19+
@Test
20+
fun `extractSupportedArchive extracts zip archives`() {
21+
val archive = tempDir.resolve("sample.zip")
22+
ZipOutputStream(FileOutputStream(archive.toFile())).use { zip ->
23+
zip.putNextEntry(ZipEntry("nested/readme.txt"))
24+
zip.write("hello zip".encodeToByteArray())
25+
zip.closeEntry()
26+
}
27+
val outputDir = tempDir.resolve("zip-out").toFile()
28+
29+
extractSupportedArchive(archive.toFile(), outputDir)
30+
31+
assertEquals(
32+
"hello zip",
33+
outputDir.resolve("nested/readme.txt").readText()
34+
)
35+
}
36+
37+
@Test
38+
fun `extractSupportedArchive extracts ar archives`() {
39+
val archive = tempDir.resolve("sample.ar")
40+
ArArchiveOutputStream(FileOutputStream(archive.toFile())).use { ar ->
41+
val content = "hello ar".encodeToByteArray()
42+
ar.putArchiveEntry(ArArchiveEntry("readme.txt", content.size.toLong()))
43+
ar.write(content)
44+
ar.closeArchiveEntry()
45+
}
46+
val outputDir = tempDir.resolve("ar-out").toFile()
47+
48+
extractSupportedArchive(archive.toFile(), outputDir)
49+
50+
assertEquals(
51+
"hello ar",
52+
outputDir.resolve("readme.txt").readText()
53+
)
54+
}
55+
56+
@Test
57+
fun `extractSupportedArchive rejects path traversal entries`() {
58+
val archive = tempDir.resolve("evil.zip")
59+
ZipOutputStream(FileOutputStream(archive.toFile())).use { zip ->
60+
zip.putNextEntry(ZipEntry("../escape.txt"))
61+
zip.write("nope".encodeToByteArray())
62+
zip.closeEntry()
63+
}
64+
val outputDir = tempDir.resolve("evil-out").toFile()
65+
66+
val error = org.junit.jupiter.api.assertThrows<SecurityException> {
67+
extractSupportedArchive(archive.toFile(), outputDir)
68+
}
69+
70+
assertTrue(error.message!!.contains("Path Traversal"))
71+
}
72+
}

docs/maintenance/backlog-triage.ko.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
| `.so`/ELF/autosetup | `#514`, `#543`, `#576`, `#137` | `covered-by-open-pr` | `#728`에서 64-bit ELF machine type 매핑과 override autosetup 재적용 경로를 먼저 수정했다 | `#728` 병합 후 실제 `.so` 샘플로 재검증하고 남는 parser 문제만 분리 |
4646
| crash report 저신호 묶음 | `#716`, `#672`, `#512`, `#508`, `#507`, `#490`, `#438`, `#376`, `#280` | `needs-repro` | 제목만으로는 원인 판단이 어렵고 재현 자료가 부족하다 | 공통 템플릿으로 추가 정보 요청 후 재현 안 되면 정리 |
4747
| SWF 요청 중복 | `#721`, `#112` | `planned-fast-follow` | 같은 방향의 기능 요청이다 | 최신 요청 `#721` 중심으로 정리하고 하나는 중복 처리 검토 |
48-
| 포맷 확장 요청 | `#120`, `#116`, `#124`, `#129` | `planned-fast-follow` | 유효한 확장 요청이지만 기준선 복구 후가 맞다 | 포맷별 난이도와 수요를 다시 평가 |
48+
| 포맷 확장 요청 | `#120`, `#116`, `#124`, `#129` | `planned-fast-follow` | `#129``#728`에서 generic archive extraction으로 먼저 흡수했고, 나머지는 기준선 복구 후가 맞다 | `#129``#728` 병합 후 정리하고 나머지는 포맷별 난이도와 수요를 다시 평가 |
4949
| export/저장 유틸 | `#123`, `#159`, `#720` | `covered-by-open-pr` | `#728`에서 project ZIP export, detail `.txt` 저장, import 파일명 정규화/테스트를 함께 정리했다 | `#728` 병합 후 실제 기기에서 export/save 동작 확인하고 정리 |
5050
| 재컴파일/대형 기능 요청 | `#529`, `#706` | `obsolete-or-policy-invalid` | 유지보수 범위를 넘어서는 별도 제품 수준 요구에 가깝다 | 현재 유지보수 스코프에서는 보류 또는 종료 후보 |
5151
| 모호한 기능 요청 | `#717`, `#710`, `#596`, `#582`, `#532`, `#491`, `#425`, `#162`, `#158` | `obsolete-or-policy-invalid` | 설명이 너무 넓거나 현재 제품 방향과 맞지 않는 항목이 많다 | 구체화 요청 후 근거 없으면 정리 |

0 commit comments

Comments
 (0)