Skip to content

fix: truncate long image filenames when saving to prevent ENAMETOOLONG - #1324

Open
ibrahim-iqbal wants to merge 2 commits into
ReadYouApp:mainfrom
ibrahim-iqbal:fix/image-download-filename-toolong-1315
Open

ibrahim-iqbal wants to merge 2 commits into
ReadYouApp:mainfrom
ibrahim-iqbal:fix/image-download-filename-toolong-1315

Conversation

@ibrahim-iqbal

Copy link
Copy Markdown

Fixes #1315.

`URLUtil.guessFileName()` derives the save-target name from the URL when the response doesn't carry an unambiguous `Content-Disposition`. For image URLs with long titles / hashed paths, that name can exceed the 255-byte filename limit ext4/f2fs enforces on Android. `ContentResolver.openFileDescriptor` then blows up with:

```
java.io.FileNotFoundException: open failed: ENAMETOOLONG (File name too long)
at me.ash.reader.infrastructure.android.AndroidImageDownloader.downloadImage2.invokeSuspend(AndroidImageDownloader.kt:181)
```

The tap-to-save flow is a 100% crash on any image whose guessed name goes past the limit — reproducer in #1315.

Added a small `sanitizeFileName` helper that caps the name at 200 chars (headroom under the 255-byte limit for typical single-byte scripts) while preserving the file extension so the saved file still opens as an image. Applied at the single call site right after `URLUtil.guessFileName` so both the Android Q+ MediaStore path and the pre-Q direct-file path get the sanitized name.

Test plan

Would need a device to verify end-to-end, which I don't have set up for this repo. Logically covered:

  • Short name: `image.jpg` (9 chars) → unchanged, early return.
  • Long name with valid extension: 300-char base + `.jpg` → truncated to 196 chars + `.jpg` = 200 chars total, still valid image.
  • Long name with no extension: 300 chars → truncated to 200 chars.
  • Long name with pseudo-extension (`.thing-that-is-long`, 19 chars): treated as no extension, kept, truncated to 200 chars total.

Happy to add a unit test if you'd like — didn't see an existing test file for AndroidImageDownloader in the app module, so didn't want to seed a new one without your steer.

URLUtil.guessFileName() can produce a filename longer than the 255-byte
limit ext4/f2fs enforces on Android, so saving an image with a long
title or URL-derived name crashes with

    java.io.FileNotFoundException: open failed: ENAMETOOLONG

on ContentResolver.openFileDescriptor. Sanitize the name to at most
200 chars while preserving the file extension so the resulting file
still opens as an image.

Fixes ReadYouApp#1315.
conradlyn added a commit to conradlyn/ReadYou that referenced this pull request Sep 24, 2026
Cherry-picked from upstream open PRs. All of them touch files this fork has
never modified, so they apply cleanly and will keep merging cleanly if upstream
lands them later.

  ReadYouApp#1322 browser-like User-Agent to bypass WAF bot blocking; BestIconFinder is
        now failure-tolerant, tries https for bare domains, and caps icon
        candidates at 4; searchFeed reports HTTP status in its errors
  ReadYouApp#1323 proper charset detection when parsing full content
        (HTTP header -> BOM -> <meta> -> UTF-8), replacing the old peek+Jsoup
  ReadYouApp#1324 truncate over-long image filenames to avoid ENAMETOOLONG
  ReadYouApp#1320 reset the intent package when opening a link fails
  ReadYouApp#1317 keep the trailing slash when saving a greader/FreshRSS server URL

Deviation on purpose: ReadYouApp#1322 also shipped two tests that perform real network
requests to phoronix.com. Those are dropped -- a unit test that depends on a
third party's availability and anti-bot policy would make CI flaky. The other
tests from both PRs (4 charset cases, 3 UA cases, 1 local RSS parse) are kept.
ENAMETOOLONG is a limit on encoded bytes (NAME_MAX = 255 on ext4/f2fs),
not on Kotlin/UTF-16 character count. A filename guessed from feed
content can be in any script, so the original .take(200) could still
leave a filename well over 255 bytes for CJK, Cyrillic, emoji, etc. —
the exact crash this PR set out to prevent, just for non-ASCII names.

Walk the string one whole code point at a time (never split a
surrogate pair) and stop once the UTF-8 byte budget is used up. Added
AndroidImageDownloaderTest covering ASCII, multi-byte, emoji and
no-extension cases; there was no existing test for this class.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Crash when saving image: java.io.FileNotFoundException: open failed: ENAMETOOLONG

1 participant