Skip to content

feat: read title files from a read-only app0.zar (ZArchive) layer - #5835

Merged
adriadam10 merged 5 commits into
boykopovar:mainfrom
AlexOfTheForest:feat/app0-zarchive
Oct 16, 2026
Merged

adriadam10 merged 5 commits into
boykopovar:mainfrom
AlexOfTheForest:feat/app0-zarchive

Conversation

@AlexOfTheForest

Copy link
Copy Markdown
Contributor

What

Closes #4848. Adds an opt-in, read-only app0.zar (ZArchive) beside app0/. A path missing from app0/ is looked up in the archive case-insensitively; files on disk take precedence. Without an archive nothing changes.

  • build: zstd v1.5.7 and ZArchive as submodules, built from source as static PIC libraries; their symbols are excluded from the .prx export table on Linux.
  • libc: archive reader, archived-descriptor table with shared positions, and a guard in ResolvePath_nid_no_patch. Any call that does not read the archive throws NotImplemented for an archive-only path instead of returning ENOENT. opendir of a directory present on disk and in the archive, fdopen of an archived descriptor and chdir into the archive also throw. .. through an archive-only directory resolves.
  • libkernel:
    • Open: sceKernelOpen/open give an archived file a read-only null-device descriptor.
    • Reads: read, pread, readv, preadv and AIO reads are served from the archive.
    • Other descriptor calls: lseek, fstat and FIONREAD are served from the archive. dup, dup2 and F_DUPFD share the position.
    • Path calls: sceKernelStat and sceKernelCheckReachability see archived paths.
    • Errors: opening for writing fails with EROFS, O_CREAT|O_EXCL with EEXIST, and O_DIRECTORY on a file with ENOTDIR. Opening an archived or merged directory throws until getdents can list it.
  • Remaining gaps (fopen, access, mmap, record locks, directory listing) are recorded in docs/dev/debt/libc.md and libkernel.md; usage is in docs/user/USAGE.md.

Tested

Linux x86-64, GCC, full build with BUILD_TESTING=ON.

  • New tests (all passing): guest_app_archive_{present,absent,damaged,invalid} and guest_kernel_app_archive_{archive,plain}. They cover:
    • reads that cross 64 KiB block boundaries, plus vectored, positioned and AIO reads;
    • seeks and end of file;
    • shared positions after dup, F_DUPFD and close, and dup2 over an archived descriptor;
    • stat/fstat, the error codes, the refusals, a damaged block and an invalid archive;
    • disk precedence and unchanged behaviour without an archive.
  • Mutation check: removing the sceKernelRead hook makes the kernel test fail.
  • Full ctest: only environment failures (UDP/TCP sockets unavailable in the sandbox; shm EACCES check while running as root).
  • No title tested.

Checklist

  • Based on current main; no other open PR implements the same functions
  • One topic per PR; follow-ups go in a new PR
  • No comments in code except technical debt
  • Unimplemented paths throw (NotImplemented_nid_no_patch); silent stubs are listed under Silent stubs in their library's technical debt file
  • No notes, investigation .md files or images added to the repository (attach them to this PR)
  • New third-party code is a submodule built from source
  • Generic behaviour, not specific to one title
  • Depends on: none
  • Unblocks: none
  • GPUs tested: n/a
  • AI-assisted: yes

@adriadam10 adriadam10 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this, the layering and the tests are careful. One blocking issue before merge:

  • An archived file is a real null-device descriptor (core/libs/prx/libkernel/File/src/ArchivedFile.cpp:23), and only read/seek/stat/FIONREAD/AIO/dup check for it. Every other descriptor call still runs on the null device: mmap_nid_postfix (DirectMemory/PosixMemory.cpp:92, takes the fd straight to the host mmap), fchmod, fchown, futimes, flock, ftruncate, fsync/fdatasync (File/src/Stdio.cpp ~288-311, 693, 736, 903, 913). They either succeed silently or fail with an unrelated errno, and fchmod/futimes act on the host's /dev/null. That contradicts "any call that does not read the archive throws NotImplemented" in the description and the exact-or-throw rule (titles commonly mmap their assets).
  • Fix: for an archived descriptor these calls throw NotImplemented (as fdopen already does), and the debt line in docs/dev/debt/libkernel.md lists them. Add a test in GuestKernelAppArchive.cpp that calls each one on an archived fd.

Also note: the Windows paths (NUL placeholder, linking zarchive into libc) are not exercised by the PR CI, so the full Build dispatch will be the evidence for them.

@AlexOfTheForest

Copy link
Copy Markdown
Contributor Author

Thanks for this, the layering and the tests are careful. One blocking issue before merge:

  • An archived file is a real null-device descriptor (core/libs/prx/libkernel/File/src/ArchivedFile.cpp:23), and only read/seek/stat/FIONREAD/AIO/dup check for it. Every other descriptor call still runs on the null device: mmap_nid_postfix (DirectMemory/PosixMemory.cpp:92, takes the fd straight to the host mmap), fchmod, fchown, futimes, flock, ftruncate, fsync/fdatasync (File/src/Stdio.cpp ~288-311, 693, 736, 903, 913). They either succeed silently or fail with an unrelated errno, and fchmod/futimes act on the host's /dev/null. That contradicts "any call that does not read the archive throws NotImplemented" in the description and the exact-or-throw rule (titles commonly mmap their assets).
  • Fix: for an archived descriptor these calls throw NotImplemented (as fdopen already does), and the debt line in docs/dev/debt/libkernel.md lists them. Add a test in GuestKernelAppArchive.cpp that calls each one on an archived fd.

Also note: the Windows paths (NUL placeholder, linking zarchive into libc) are not exercised by the PR CI, so the full Build dispatch will be the evidence for them.

Thanks for the review now it should be fixed

@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 14, 2026
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 14, 2026
Build zstd v1.5.7 and ZArchive from source as static, position-independent libraries that are not part of the default target set. Their symbols stay out of the dynamic export table of the library that links them.
…lsewhere

A ZArchive named app0.zar beside app0/ is a read-only layer under it. libc opens it at the first path resolution, looks paths missing from app0/ up case-insensitively, reads files across compressed blocks and keeps the archived descriptor table with shared positions.

Resolving an archive-only path through a call that does not read the archive throws instead of failing with ENOENT, as do opendir of a directory present in both places, fdopen of an archived descriptor and chdir into the archive. A path through .. in an archive-only directory resolves.
sceKernelOpen and open give an archived file a read-only null-device descriptor. Reads, positioned, vectored and AIO reads, seeks, fstat and FIONREAD are served from the archive; dup, dup2 and F_DUPFD share the position. sceKernelStat and sceKernelCheckReachability see archived paths.

Writing opens fail with EROFS, O_CREAT|O_EXCL with EEXIST and O_DIRECTORY on a file with ENOTDIR. Opening an archived directory or one merged with the disk throws until getdents can list it.
flock, fstatfs, ftruncate, fsync, fdatasync, getdirentries, fchmod, futimes and mmap on an archived descriptor acted on its null device and succeeded or failed with an unrelated errno. They now throw NotImplemented, each covered by the kernel archive test; mmap is noexcept, so its refusal is checked through the terminate handler.

@adriadam10 adriadam10 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rebased onto current main: the only real conflict was File/src/Stdio.cpp in TransferIovecs, where main added File::LockPositionedTransfer(d). Kept both, the archived read first and the lock after it. No other change to the PR. guest_app_archive* and guest_kernel_app_archive* pass on the rebased tree. Merging once the full Build (Windows) is green.

@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 16, 2026
@adriadam10
adriadam10 merged commit 9158f03 into boykopovar:main Oct 16, 2026
14 checks passed
@JJenkx JJenkx mentioned this pull request Oct 16, 2026
11 tasks done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: Zarchive Support

2 participants