Skip to content

Commit 92134e2

Browse files
committed
stability: guard stale project entries
1 parent 310fb45 commit 92134e2

8 files changed

Lines changed: 76 additions & 10 deletions

File tree

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,13 @@ class FileDrawerListAdapter(val progressHandler: ProgressHandler) {
9191
}
9292
DrawerItemType.ARCHIVE, DrawerItemType.APK -> {
9393
val path = item.tag as String
94+
val relPath = ProjectManager.getRelPathOrNull(path)
95+
if (relPath == null) {
96+
items.add(FileDrawerListItem("Failed to resolve project path", newLevel))
97+
return items
98+
}
9499
val targetDirectory =
95-
ProjectDataStorage.resolveToWrite(ProjectManager.getRelPath(path), true)
100+
ProjectDataStorage.resolveToWrite(relPath, true)
96101
Log.d(TAG, "Target directory $targetDirectory")
97102
// File(File(appCtx.filesDir, "/extracted/"), File(path).name + "/")
98103
// appCtx.filesDir.resolve("extracted").resolve()
@@ -114,8 +119,13 @@ class FileDrawerListAdapter(val progressHandler: ProgressHandler) {
114119
DrawerItemType.DEX -> {
115120
progressHandler.startProgress()
116121
val filename = item.tag as String
122+
val relPath = ProjectManager.getRelPathOrNull(filename)
123+
if (relPath == null) {
124+
items.add(FileDrawerListItem("Failed to resolve project path", newLevel))
125+
return items
126+
}
117127
val targetDirectory =
118-
ProjectDataStorage.resolveToWrite(ProjectManager.getRelPath(filename), true)
128+
ProjectDataStorage.resolveToWrite(relPath, true)
119129
// val targetDirectory = File(File(appCtx.filesDir, "/dex-decompiled/"), File(filename).name + "/")
120130
targetDirectory.mkdirs()
121131
// run backsmali

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -291,8 +291,13 @@ class FileDrawerListItem {
291291
}
292292
DrawerItemType.ARCHIVE, DrawerItemType.APK -> {
293293
val path = tag as String
294+
val relPath = ProjectManager.getRelPathOrNull(path)
295+
if (relPath == null) {
296+
items.add(FileDrawerListItem("Failed to resolve project path", newLevel))
297+
return items
298+
}
294299
val targetDirectory =
295-
ProjectDataStorage.resolveToWrite(ProjectManager.getRelPath(path), true)
300+
ProjectDataStorage.resolveToWrite(relPath, true)
296301
Timber.d("Target directory $targetDirectory")
297302
// File(File(appCtx.filesDir, "/extracted/"), File(path).name + "/")
298303
// appCtx.filesDir.resolve("extracted").resolve()
@@ -314,8 +319,13 @@ class FileDrawerListItem {
314319
DrawerItemType.DEX -> {
315320
startHandler()
316321
val filename = tag as String
322+
val relPath = ProjectManager.getRelPathOrNull(filename)
323+
if (relPath == null) {
324+
items.add(FileDrawerListItem("Failed to resolve project path", newLevel))
325+
return items
326+
}
317327
val targetDirectory =
318-
ProjectDataStorage.resolveToWrite(ProjectManager.getRelPath(filename), true)
328+
ProjectDataStorage.resolveToWrite(relPath, true)
319329
// val targetDirectory = File(File(appCtx.filesDir, "/dex-decompiled/"), File(filename).name + "/")
320330
targetDirectory.mkdirs()
321331
Main.main(arrayOf("d", "-o", targetDirectory.absolutePath, filename))

app/src/main/java/com/kyhsgeekcode/disassembler/project/ProjectManager.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,16 @@ object ProjectManager {
267267
return computeProjectRelativePath(currentProject!!, path)
268268
}
269269

270+
fun getRelPathOrNull(path: String): String? {
271+
val project = currentProject ?: return null
272+
return computeProjectRelativePathOrNull(project, path)
273+
.also {
274+
if (it == null) {
275+
Logger.e(TAG, "Failed to resolve project-relative path for $path")
276+
}
277+
}
278+
}
279+
270280
// fun getRelPathFromGen(path: String): String {
271281
// val rootPath = getGenerated("").absolutePath
272282
// val relPath: String
@@ -364,6 +374,10 @@ fun computeProjectRelativePath(projectModel: ProjectModel, path: String): String
364374
)
365375
}
366376

377+
fun computeProjectRelativePathOrNull(projectModel: ProjectModel, path: String): String? {
378+
return runCatching { computeProjectRelativePath(projectModel, path) }.getOrNull()
379+
}
380+
367381
private fun substringWithoutLeadingSlash(path: String, prefix: String): String {
368382
val sub = path.substring(prefix.length)
369383
if (sub.isNotEmpty() && sub[0] == '/') {

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,8 +204,13 @@ class FileDrawerTreeItem : TreeNode<FileDrawerTreeItem> {
204204
}
205205
DrawerItemType.ARCHIVE, DrawerItemType.APK -> {
206206
val path = tag as String
207+
val relPath = ProjectManager.getRelPathOrNull(path)
208+
if (relPath == null) {
209+
items.add(FileDrawerTreeItem("Failed to resolve project path", newLevel))
210+
return items
211+
}
207212
val targetDirectory =
208-
ProjectDataStorage.resolveToWrite(ProjectManager.getRelPath(path), true)
213+
ProjectDataStorage.resolveToWrite(relPath, true)
209214
Timber.d("Target directory $targetDirectory")
210215
// File(File(appCtx.filesDir, "/extracted/"), File(path).name + "/")
211216
// appCtx.filesDir.resolve("extracted").resolve()
@@ -222,8 +227,13 @@ class FileDrawerTreeItem : TreeNode<FileDrawerTreeItem> {
222227
DrawerItemType.DEX -> {
223228
// startHandler()
224229
val filename = tag as String
230+
val relPath = ProjectManager.getRelPathOrNull(filename)
231+
if (relPath == null) {
232+
items.add(FileDrawerTreeItem("Failed to resolve project path", newLevel))
233+
return items
234+
}
225235
val targetDirectory =
226-
ProjectDataStorage.resolveToWrite(ProjectManager.getRelPath(filename), true)
236+
ProjectDataStorage.resolveToWrite(relPath, true)
227237
// val targetDirectory = File(File(appCtx.filesDir, "/dex-decompiled/"), File(filename).name + "/")
228238
targetDirectory.mkdirs()
229239
Main.main(arrayOf("d", "-o", targetDirectory.absolutePath, filename))

app/src/main/java/com/kyhsgeekcode/disassembler/viewmodel/MainViewModel.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -538,7 +538,8 @@ private fun createTabData(item: FileDrawerTreeItem): TabData {
538538
// Log.d(TAG, "rootPath:${rootPath}")
539539
Timber.d("absPath:$abspath")
540540
val ext = File(abspath).extension.lowercase(Locale.getDefault())
541-
val relPath: String = ProjectManager.getRelPath(abspath)
541+
val relPath = ProjectManager.getRelPathOrNull(abspath)
542+
?: return buildUnavailablePathTabData(item.caption, abspath)
542543
// if (abspath.length > rootPath.length)
543544
// relPath = abspath.substring(rootPath.length+2)
544545
// else
@@ -577,6 +578,15 @@ private fun createTabData(item: FileDrawerTreeItem): TabData {
577578
return TabData(title, tabkind)
578579
}
579580

581+
private fun buildUnavailablePathTabData(caption: String, abspath: String): TabData {
582+
val key = "unavailable:${abspath.hashCode()}"
583+
ProjectDataStorage.putFileContent(
584+
key,
585+
"The selected entry is no longer inside the current project:\n$abspath".encodeToByteArray()
586+
)
587+
return TabData("$caption unavailable", TabKind.Text(key))
588+
}
589+
580590
fun fileItemTypeToProjectType(fileItem: FileItem): String {
581591
if (fileItem is FileItemApp)
582592
return ProjectType.APK

app/src/test/java/com/kyhsgeekcode/disassembler/ProjectManagerTest.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.kyhsgeekcode.disassembler
22

33
import com.kyhsgeekcode.disassembler.project.computeProjectRelativePath
4+
import com.kyhsgeekcode.disassembler.project.computeProjectRelativePathOrNull
45
import com.kyhsgeekcode.disassembler.project.importedProjectInfoFile
56
import com.kyhsgeekcode.disassembler.project.relocateImportedProjectModel
67
import com.kyhsgeekcode.disassembler.project.models.ProjectSourceDescriptor
@@ -56,6 +57,13 @@ class ProjectManagerTest {
5657
}
5758
}
5859

60+
@Test
61+
fun `computeProjectRelativePathOrNull returns null for paths outside project boundaries`() {
62+
val project = projectModelFor("sample")
63+
64+
assertEquals(null, computeProjectRelativePathOrNull(project, "outside-project/classes.dex"))
65+
}
66+
5967
@Test
6068
fun `resolvedSourceDescriptor falls back to legacy file path`() {
6169
val project = projectModelFor("sample")

docs/maintenance/backlog-triage.ko.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
# 백로그 1차 분류표
22

3-
이 문서는 2026-03-20 기준으로 열려 있는 GitHub PR 2개, 이슈 43개를 유지보수 재가동 관점에서 다시 묶은 결과다.
3+
이 문서는 2026-03-20 기준으로 열려 있는 GitHub PR 1개, 이슈 42개를 유지보수 재가동 관점에서 다시 묶은 결과다.
44
상태 이름은 `docs/maintenance/issue-triage.md`의 공통 분류를 따른다.
55

66
## 요약
77

88
| 항목 | 수량 | 판단 |
99
| --- | --- | --- |
1010
| 오픈 PR | 1 | 오래된 자동 bump PR과 `#728`에 흡수된 PR은 정리했고, `#726`도 현재 릴리스 워크플로에 superseded 되었다 |
11-
| 오픈 이슈 | 43 | 표면상 43개지만, 실제 작업 묶음은 대략 15~20개 수준으로 수렴한다 |
11+
| 오픈 이슈 | 42 | 표면상 42개지만, 실제 작업 묶음은 대략 15~20개 수준으로 수렴한다 |
1212
| 즉시 닫기/대체 가능 후보 | 감소 | SWF 중복 이슈와 구형 Android 지원 이슈까지 포함해 일부 정리했다 |
1313
| 즉시 구현 후보 | 소수 | 릴리스 파이프라인, storage 정책 마감, crash 재현 클러스터가 우선이다 |
1414

@@ -26,6 +26,7 @@
2626
| PR `#704`, `#701`, `#699`, `#695`, `#693`, `#692`, `#677`, `#637`, `#615`, `#565` | 닫음 | 오래된 자동 bump/alpha 제안으로 현재 유지보수 기준선보다 뒤처짐 |
2727
| 이슈 `#112` | 닫음 | SWF 요청은 `#721`로 통합 |
2828
| 이슈 `#221` | 닫음 | 현재 유지보수 방향은 최신 Android 대응이며 Android 4.4 지원 복구는 범위 밖 |
29+
| 이슈 `#280` | 닫음 | API 23에서 죽던 color picker dialog 경로는 현재 설정 화면에서 이미 제거되어 더 이상 활성 코드 경로가 아님 |
2930

3031
## 오픈 이슈 클러스터
3132

@@ -38,7 +39,8 @@
3839
| 회전/상태 복원 크래시 | `#160` | `covered-by-open-pr` | `#728`에서 Activity 재생성 시 외부 import intent 재처리를 막는 1차 가드를 넣었다 | `#728` 병합 후 실제 회전 회귀를 확인하고 정리 |
3940
| `.so`/ELF/autosetup | `#514`, `#543`, `#576`, `#137` | `covered-by-open-pr` | `#728`에서 64-bit ELF machine type 매핑과 override autosetup 재적용 경로를 먼저 수정했다 | `#728` 병합 후 실제 `.so` 샘플로 재검증하고 남는 parser 문제만 분리 |
4041
| 구형 Android archive 감지 크래시 | `#507`, `#508` | `covered-by-open-pr` | `#728`에서 archive 확장자 fast path와 `NoClassDefFoundError` 방어를 넣어 구형 Android의 Commons Compress 감지 크래시 경로를 우회했다 | `#728` 병합 후 Android 6~7 계열에서 archive chooser와 open 경로를 재확인하고 정리 |
41-
| crash report 저신호 묶음 | `#716`, `#672`, `#512`, `#490`, `#438`, `#376`, `#280` | `needs-repro` | 제목만으로는 원인 판단이 어렵고 재현 자료가 부족하다 | 공통 템플릿으로 추가 정보 요청 후 재현 안 되면 정리 |
42+
| project-relative path assertion 크래시 | `#512` | `covered-by-open-pr` | `#728`에서 drawer/tab 경로가 project 바깥 항목을 열려다 `getRelPath()` 예외로 죽지 않도록 null-safe guard를 넣었다 | `#728` 병합 후 stale project/drawer entry에서 실패 메시지로만 처리되는지 확인하고 정리 |
43+
| crash report 저신호 묶음 | `#716`, `#672`, `#490`, `#438`, `#376` | `needs-repro` | 제목만으로는 원인 판단이 어렵고 재현 자료가 부족하다 | 공통 템플릿으로 추가 정보 요청 후 재현 안 되면 정리 |
4244
| SWF 요청 | `#721` | `planned-fast-follow` | 모바일 SWF 확장/디컴파일 요구는 남아 있지만 추적 스레드는 하나로 줄었다 | 기준선 병합 후 포맷 확장 우선순위에서 다시 평가 |
4345
| 포맷 확장 요청 | `#120`, `#116`, `#124`, `#129` | `planned-fast-follow` | `#129``#728`에서 generic archive extraction으로 먼저 흡수했고, 나머지는 기준선 복구 후가 맞다 | `#129``#728` 병합 후 정리하고 나머지는 포맷별 난이도와 수요를 다시 평가 |
4446
| export/저장 유틸 | `#123`, `#159`, `#720` | `covered-by-open-pr` | `#728`에서 project ZIP export, detail `.txt` 저장, import 파일명 정규화/테스트를 함께 정리했다 | `#728` 병합 후 실제 기기에서 export/save 동작 확인하고 정리 |

docs/maintenance/implementation-log.ko.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
| 이슈 `#95` project archive / 기존 project reopen 연결 | chooser의 `Open as project` 흐름이 기존 project 디렉터리와 exported project ZIP을 실제로 열도록 연결하고, project zip은 `project_info.json` 존재 여부로 판별하게 바꿨다 | 이전에는 `openProject` 플래그가 state에만 저장되고 실질 동작이 없어서 reopen 경계가 끊겨 있었는데, 이제 기존 project 디렉터리는 바로 열고 exported project ZIP은 바로 import할 수 있다 | `app/src/main/java/com/kyhsgeekcode/disassembler/project/ProjectManager.kt`, `app/src/main/java/com/kyhsgeekcode/disassembler/viewmodel/MainViewModel.kt`, `app/src/main/java/com/kyhsgeekcode/filechooser/model/FileItem.kt`, `app/src/test/java/com/kyhsgeekcode/disassembler/viewmodel/ProjectOpenActionTest.kt` | 완료 |
4545
| 이슈 `#129` `.ar` archive 지원 | 공용 archive extractor를 추가하고, Compose/legacy drawer의 archive 확장 경로를 ZIP 전용 구현에서 generic archive extractor로 교체했다 | archive 판정은 되는데 실제 확장은 ZIP만 되던 불일치를 제거해서 `.ar` 같은 지원 가능한 archive도 실제로 탐색 가능하게 만들었다 | `app/src/main/java/com/kyhsgeekcode/Util.kt`, `app/src/main/java/com/kyhsgeekcode/disassembler/ui/FileDrawerTree.kt`, `app/src/main/java/com/kyhsgeekcode/disassembler/FileDrawerListItem.kt`, `app/src/main/java/com/kyhsgeekcode/disassembler/FileDrawerListAdapter.kt`, `app/src/test/java/com/kyhsgeekcode/disassembler/ArchiveExtractionTest.kt` | 완료 |
4646
| 이슈 `#507`, `#508` 구형 Android archive 감지 크래시 | archive 판정에 확장자 fast path를 추가하고, Commons Compress 탐지/추출 경로가 `NoClassDefFoundError`를 앱 크래시로 터뜨리지 않도록 방어했다. ZIP 추출기는 첫 엔트리 뒤 스트림을 닫던 버그도 같이 수정했다 | Android 6~7 계열에서 `ArchiveStreamFactory.detect(...)`가 내부적으로 사용할 수 없는 클래스를 건드리며 죽던 경로를 우회했고, ZIP/APK/JAR/AAR는 안전한 내장 경로로 처리하도록 바꿔 chooser와 archive open 경계를 안정화했다 | `app/src/main/java/com/kyhsgeekcode/FileExtensions.kt`, `app/src/main/java/com/kyhsgeekcode/Util.kt`, `app/src/test/java/com/kyhsgeekcode/ArchiveDetectionTest.kt`, `app/src/test/java/com/kyhsgeekcode/disassembler/ArchiveExtractionTest.kt` | 완료 |
47+
| 이슈 `#512` project-relative path assertion 크래시 | drawer와 탭 생성 경로가 더 이상 `getRelPath()` 예외를 그대로 올리지 않고, project 경계 밖 항목은 실패 메시지로만 처리하도록 가드했다 | 오래된 project 구조나 stale drawer entry 때문에 relative path 계산이 실패해도 앱이 바로 죽지 않고, invalid entry를 열지 못했다는 정보만 보여주도록 바꿔 project reopen/drawer 경계를 안정화했다 | `app/src/main/java/com/kyhsgeekcode/disassembler/project/ProjectManager.kt`, `app/src/main/java/com/kyhsgeekcode/disassembler/FileDrawerListItem.kt`, `app/src/main/java/com/kyhsgeekcode/disassembler/FileDrawerListAdapter.kt`, `app/src/main/java/com/kyhsgeekcode/disassembler/ui/FileDrawerTree.kt`, `app/src/main/java/com/kyhsgeekcode/disassembler/viewmodel/MainViewModel.kt`, `app/src/test/java/com/kyhsgeekcode/disassembler/ProjectManagerTest.kt` | 완료 |
4748
| 프로젝트 경로/파일명 회귀 방지 | 프로젝트 상대경로 계산과 import 파일명 정규화를 pure helper로 분리 | 단위 테스트가 가능하도록 로직을 분리하고 경계 케이스를 줄였다 | `app/src/main/java/com/kyhsgeekcode/disassembler/project/ProjectManager.kt`, `app/src/main/java/com/kyhsgeekcode/disassembler/viewmodel/MainViewModel.kt` | 완료 |
4849
| 회귀 테스트 부재 | `ProjectManager`, 저장소 권한, Hex 레이아웃, import 파일명 테스트 추가 | 최소한의 유지보수 안전망을 확보했다 | `app/src/test/java/com/kyhsgeekcode/disassembler/ProjectManagerTest.kt`, `app/src/test/java/com/kyhsgeekcode/disassembler/PermissionUtilsTest.kt`, `app/src/test/java/com/kyhsgeekcode/disassembler/ui/components/HexViewLayoutTest.kt`, `app/src/test/java/com/kyhsgeekcode/disassembler/viewmodel/ImportedFileNameTest.kt` | 완료 |
4950

@@ -80,6 +81,7 @@
8081
| details export file name 테스트 | 통과 | detail 저장 파일명이 원본 파일명 기반으로 생성되고 금지 문자를 제거하는지 확인 |
8182
| generic archive extraction 테스트 | 통과 | ZIP과 `.ar`가 같은 extractor로 풀리고 path traversal 항목은 거부되는지 확인 |
8283
| archive detection 회귀 테스트 | 통과 | 지원 확장자는 fast path로 archive로 인식하고, ZIP/APK/JAR/AAR 경로는 구형 Android에서도 Commons Compress 탐지 크래시 없이 처리하는 기반을 고정 |
84+
| project-relative path null-safe helper 테스트 | 통과 | project 경계 밖 경로는 예외 대신 `null`을 반환하고, UI 호출부가 이를 흡수할 수 있는 계약을 고정 |
8385
| workflow YAML 파싱 | 통과 | `.github/workflows/ci.yml`, `.github/workflows/release.yml` 모두 Ruby YAML 파서 기준 확인 |
8486

8587
## 다음 웨이브 후보

0 commit comments

Comments
 (0)