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
+20
View File
@@ -12,6 +12,13 @@ upload needs a **higher `versionCode`** than every previous upload.
### Fixed
- **`.xz` extraction would have crashed on a real device.** Commons Compress declares its
LZMA/LZMA2 support — the `.xz` compressor and the 7z format — against `org.tukaani:xz` as
an **optional** dependency. Optional means it is not pulled in transitively, so nothing
warned at build time, and the app went on advertising `.xz` as extractable. The first user
to open one would have got `NoClassDefFoundError: org/tukaani/xz/FilterOptions`. The
library is now declared, and a round-trip test covers the path that was silently missing.
- **The storage-usage screen stayed blank for ~45–60 s** on a real 55 GB volume, which
reads as a hang even though the work was off the main thread. `UsageAnalysis` now passes
the `FileEntry` down the recursion instead of re-`stat`ing every node — the parent's
@@ -31,6 +38,19 @@ upload needs a **higher `versionCode`** than every previous upload.
### Added
- **7z extraction.** Commons Compress already contained a pure-Java 7z reader
(`SevenZFile`), so this needed no new native code and no library beyond the XZ one the
`.xz` fix above required — the docs had simply assumed the format was off-limits and said
so in two places. The one real constraint is that 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 can advertise, `StorageSource.openSeekable`:
the all-files backend answers with a real `FileChannel`, and the SAF fallback answers
`null`, so a 7z in folder mode is refused with "needs a seekable file, which this storage
mode cannot provide" rather than failing obscurely. Entry names go through the same
`ArchivePath.safeRelativePath` zip-slip guard as every other format, with a test that
plants `../evil.txt` in a real 7z and asserts it lands nowhere. 7z *creation* stays
deferred, so ZIP is still the only writable format.
- **A two-pane layout for large screens.** A window at or above Material's "expanded"
breakpoint — 840 dp, which a 10-inch tablet in landscape at 1280 dp clears — now splits
into a master pane and a detail pane. Tapping a row highlights it, and the second pane