ReviewOS

also looking at this

craft-native/craft

feat(android): downloadFile and saveFile in Zig — ratchet 69 → 67

#176
Merged glennmichael123 wants to merge feat/android-files into main
6 files +477 -3
9 days ago

Neither needs the main thread, a listener, or an Activity result — the download is queued with the system's own manager and the save is ordinary file I/O — so both are served whole rather than in halves.

saveFile can report a path it never wrote

The shim's data-URL branch has no else:

if (parts.size == 2) {
    file.writeBytes(Base64.decode(parts[1], Base64.DEFAULT))
}
// ...then resolve(file.absolutePath) regardless

String.split(",") splits on every comma, so parts.size == 2 means "exactly one comma in the whole string". data:text/plain,a,b — a legal data URL — gives three parts, nothing is written, and the resolve still hands the page a path to a file that may not exist.

Base64 output contains no commas, so the common case is safe. It is the plain-text and malformed data URLs that fail silently.

Reproduced rather than corrected, because the reply is what a page acts on. The skipped case is named Plan.nothing rather than left as an absent else, and has a test that says what it does. Filed as #175 with a suggested fix.

The decoding is Java's

android.util.Base64.decode rather than a decoder here, for the reason bridge_android_notifications hashes ids in Java: what matters is not decoding base64 but decoding it the way the shim does, including which inputs it throws on. A decoder here would be a second implementation whose disagreements only ever show up on a device.

Four constants are read through JNI rather than written down — Base64.DEFAULT, VISIBILITY_VISIBLE_NOTIFY_COMPLETED, DIRECTORY_DOWNLOADS, DIRECTORY_DOCUMENTS. None of them throw when wrong: the first decodes with a different alphabet, the second silently hides the download from the user, and the last two write to a different directory. Same asymmetry that made addContact read its MIME types.

The stream closes on the error path too

A FileOutputStream left open holds its file descriptor until the GC gets to it, which on a device is long enough to matter. The close is a defer, and its method id is resolved before the write — which is what lets it be one, since a defer cannot return.

Testing

zig build test:android passes with the ratchet at 67. The mutation fired: dropping the second-comma check makes data:text/plain,a,b decode a,b as base64 instead of writing nothing, and the named test catches it. Both libcraft.so targets still build with no NDK.

6 changed files on the files tab, with 0 review threads.

Sign in to comment on this pull request.