Support 7z extraction, and fix .xz extraction which would have crashed at runtime

Commons Compress declares its LZMA/LZMA2 support against org.tukaani:xz as an *optional* dependency, so nothing pulled it in transitively and nothing failed at build time. The app nevertheless advertised .xz as extractable, so the first user to open one would have got NoClassDefFoundError: org/tukaani/xz/FilterOptions. XZCompressorInputStream references org/tukaani/xz/XZInputStream directly. The library is now declared, and a round-trip test covers the path that was silently missing.

That same library is what 7z needs, and Commons Compress already ships a pure-Java 7z reader, so 7z extraction costs no native code and no further dependency. The docs had assumed the format was off limits and said so in three places, including the customer-facing store copy.

7z is the only format here that cannot arrive through a stream: its header sits at the end of the file, so the reader has to seek. That is now a capability the backend advertises, StorageSource.openSeekable: the all-files backend answers with a real FileChannel, the SAF fallback with null, so folder mode reports needs-a-seekable-file instead of failing obscurely. Entry names go through the same ArchivePath.safeRelativePath zip-slip guard. 7z creation stays deferred.
This commit is contained in:
2026-10-15 08:47:37 +02:00
parent a461be2a66
commit ba92cf392d
13 changed files with 326 additions and 51 deletions
+8 -5
View File
@@ -25,7 +25,7 @@ holds length-checked listing copy. The screenshots are captured: 5 phone (1080×
| UI | Jetpack Compose, BOM 2026.06.01, Material 3 |
| Build | Gradle 9.3.1, Android Gradle Plugin 9.1.0 |
| Storage access | `MANAGE_EXTERNAL_STORAGE` ("All files access") with a Storage Access Framework fallback |
| Archives | Apache Commons Compress (pure Java — no NDK) |
| Archives | Apache Commons Compress + XZ for Java (both pure Java — no NDK) |
| SDK levels | `compileSdk`/`targetSdk` 36, `minSdk` 26 |
`targetSdk` is 36 because Google Play requires new apps and updates to target
@@ -141,10 +141,13 @@ UI.** Translated UI text has to be supplied in `values-<locale>/`.
- **"Open with" and Share only work in the all-files mode.** A SAF path is relative to the
granted tree rather than a filesystem path, so it cannot be handed to `FileProvider`.
- **RAR and 7z are refused**, not supported. The stated reason is only half right: RAR
genuinely needs native code (the Commons Compress jar this app ships contains no RAR
classes at all), but **7z does not** — `SevenZFile`/`SevenZOutputFile` are already inside
that same jar, so 7z read, and AES-256-encrypted 7z write, is an unpack away.
- **RAR, CAB, ISO and WIM are refused**, not supported — they need native code, which this
project deliberately does not ship. (The Commons Compress jar contains no RAR classes at
all.) **7z is different**: Commons Compress reads it in pure Java, so it *is* supported —
but the format needs a seekable channel rather than a stream, because its header lives at
the end of the file. All-files access can supply one; the SAF fallback cannot, so 7z
extraction reports that plainly in folder mode instead of half-working.
- **7z files can be opened but not created** — writing stays ZIP-only.
- **Archive extraction does not restore original timestamps or permissions**, and there is
no archive encryption or password support.
- **Cancellation happens between entries**, so a single very large file finishes before a