115 Commits
Author SHA1 Message Date
Seregon 1a0b8a46c1 Merge pull request #16 from NotSoTypicalGamer/main
fix: preserve native install/uninstall error codes
2026-10-17 19:07:19 +02:00
NotSoTypicalGamer 1aa1ab1c2d games/ps5: fix install error-code parsing 2026-10-17 09:30:45 -04:00
NotSoTypicalGamer 91452c150a games/ps5: fix uninstall error-code parsing
Skip the space after ERROR in sscanf and preserve the signed native
error code instead of negating it. Previously a native failure parsed
as 0 and was negated into a positive value that api_games_uninstall
treated as success.
2026-10-17 09:27:37 -04:00
Seregon 83d78ef958 Merge pull request #15 from NotSoTypicalGamer/main
Add native PKG uninstall through the installer helper (zhttp)
2026-10-17 15:21:57 +02:00
NotSoTypicalGamer baaee914ac games/ps5: native uninstall through the installer helper
zhttp can now uninstall a title natively, reusing the same embedded
helper that drives installs. Three changes make it work on 13.60:

- install_helper.c escalates to the SYSTEM_PROCESS auth id
  (0x4800000000000006) by writing cr_sceAuthID at ucred+0x58 via
  kernel_copyin; the SDKs
2026-10-16 21:09:07 -04:00
seregonwar 8c3302284b Harden console startup and sendfile fallback
Treat `instance.pid` as a control identity instead of a hard lock by authenticating shutdown requests with the saved token, ignoring stale/reused/malformed identities, and only blocking when a verified payload (or authenticated prior instance) is still alive. Add PS4/PS5 `sendfile` safety gating that uses buffered `pread`+`send` on incompatible or unknown mount types (exFAT/FAT/nullfs/PFS, or mount lookup failure) while keeping zero-copy on safe filesystems, and wire in README/Makefile updates plus new host-side tests and stubs for both paths.
2026-10-05 17:45:10 +02:00
seregonwar e19c68c3a2 fix(build): keep the web-less builds clean under newer clang
The macOS/Linux ftp jobs rejected pal_dns_filter.c with
-Wunused-but-set-global: g_dns_stop is only read by the console resolver
loops, so on hosts it was written for nothing.  The control port carried the
same kind of dead state (g_control_thread_started, and a thread id only ever
passed to pthread_create) because the thread is detached and never joined.
Guard the DNS stop flag with the console builds and drop the unused control
port variables; an audit of every release configuration on preprocessed
sources now reports no set-but-unread file-scope statics.
2026-09-30 20:52:32 +02:00
seregonwar 037aaa9974 fix(build): compile the non-web variants again
The PS5 ftp build (ENABLE_ZHTTPD=0) did not compile: main.c evaluated
ENABLE_ZHTTPD before defaulting it (-Wundef) and the PS5 shutdown helpers sat
inside the web-only block even though the main loop and the control port rely
on them.  Default the switch at the top of the file, keep the shutdown helpers
in every PS5 build, and let the Makefile define ENABLE_ZHTTPD=0 explicitly so
no translation unit ever sees the macro undefined.
2026-09-30 20:42:12 +02:00
seregonwar 7099a9d214 fix(games): read PS5 title names from the appmeta param.json
Console-format PS5 packages (magic 0x7F "FIH") hold no readable param.sfo, so
native PS5 titles kept showing their title id twice while PS4 ones resolved.
Fall back to the system metadata when the package cannot be parsed:
/user/appmeta/<TITLE>/param.json and then /system_data/priv/appmeta, whose
localizedParameters carry the display name as plain UTF-8 (first entry = the
default language).  The format is pinned by a fixture in test_http_games.
2026-09-30 19:23:46 +02:00
seregonwar da573e05a8 build(ps5): do not build or embed the install helper when the installer is off
With ENABLE_PKG_INSTALL=0 the helper was still compiled and its 115 KB blob
linked into the payload even though no code referenced it.  Gate the helper
build and blob on the flag so release artifacts carry the installer neither in
code nor in data.
2026-09-30 19:18:44 +02:00
seregonwar cb7ce7c715 perf(copy): deeper parallel-read copy pipeline and server-side move
- copy.c: replace the 2x4 MB ping-pong with a 4-slot ring of 8 MB chunks
  (PS4: 1 MB), filled by concurrent pread() readers (2 on PS5, 1 on PS4)
  and drained by a single in-order writer. The progress callback now runs
  without holding the pipeline mutex, and mmap pressure degrades to fewer
  slots/smaller chunks before falling back to the serial copy.
- Fix a source fd leak on the pre-flight ENOSPC path.
- /api/copy?move=1: rename() when source and destination share a
  filesystem, otherwise copy and remove the source entries (keep_src=0).
  Moves merging into an existing folder rename file by file.
- web: moves use the server-side mode instead of copy + recursive delete.
2026-09-30 19:15:10 +02:00
seregonwar 181e3d509b feat(ui): hide the package installer behind the build flag
Builds without ENABLE_PKG_INSTALL now report it in /api/status
(features.pkg_install) and the web interface hides every installer entry point
— the Games action, the file explorer item and the progress pollers — instead
of offering actions that only answer 409.  The release workflow already builds
with the flag off, so this release ships without the remote PKG installer.
2026-09-30 18:00:07 +02:00
seregonwar 15114b8af1 test(http): cover archive extraction and the system endpoints
Extend the HTTP suite with the archive extraction fixtures (good, traversal
and symlink cases) and the system endpoints, so the split APIs keep their
behavior pinned.
2026-09-30 17:51:39 +02:00
seregonwar b8a230e348 web: browser interface updates and documentation
Bring the file manager, transfers, settings and games views in line with the
split HTTP APIs (dump jobs, copy/extract progress, DNS filter and system
status), refresh the styling and document the API, pkg install flow and
architecture, together with the repository issue and pull request templates.
2026-09-30 17:51:29 +02:00
seregonwar a9d4e1e666 build(ps4): PacBrew portlibs for the URL downloader, PIE payloads, CI wiring
tools/fetch_pacbrew_ps4.sh unpacks the pinned PacBrew portlibs (libcurl,
mbedTLS, zlib) with a Mozilla CA bundle; the Makefile detects that prefix and
enables ENABLE_LIBCURL for PS4, bridging the three libc symbols the OpenOrbis
build expects.  Console payloads now always link as PIE (GoldHEN's payloader
rejects ET_EXEC), which is what made the plain `make deploy` flow work, and
the release workflow installs the same portlibs for PS4 zhttp artifacts.
2026-09-30 17:51:29 +02:00
seregonwar dfb358dc9d fix(games): read the title name from single-package (app.pkg) installs
Titles delivered as one package keep no sce_sys/param.sfo on disk, so the
Games view fell back to the title id and showed the content id twice.  Read
the param.sfo entry from app.pkg when the loose file is missing (magic probe,
encrypted entries skipped), cover CONTENT_ID/CATEGORY lookups too, and let the
web UI hide the id line when it would repeat the name.  Regression test builds
a minimal package image and checks the catalog reports the SFO title.
2026-09-30 17:51:09 +02:00
seregonwar 09b7cbff2a fix(console): survive an aborted download instead of dying on SIGPIPE
The PS4 entry point never ignored SIGPIPE (the shared handler is compiled out
there), so a browser cancelling a download killed the payload with signal 13.
Ignore SIGPIPE on PS4 like PS5 and POSIX do, and set SO_NOSIGPIPE on the HTTP
listener and client sockets (FTP sockets follow through pal_socket_configure)
so a vanished peer only breaks its own connection.  test_http_server aborts a
streamed download mid-flight and checks the server keeps answering.
2026-09-30 17:50:57 +02:00
seregonwar 7589664a6d feat(platform): volumes, disc, DNS filter, PS5 offsets and instance control
Add removable-volume detection that ignores empty slots, disc eject/state
helpers, the userland DNS filter, PS5 firmware offset lookup with the optional
kernel net filter, and a control port a new payload uses to retire the
previous instance.
2026-09-30 17:50:57 +02:00
seregonwar 7121e13861 feat(games): catalog, launch, install and app.db repair
List installed titles with icons, start them through LncUtil (with the
no-logged-in-user guard), install and uninstall packages, and repair missing
home screen entries through the shell's app.db.  PS5 package installs are
driven by an embedded helper delivered to elfldr on loopback.
2026-09-30 17:50:57 +02:00
seregonwar fbfaa68021 feat(dump): decrypted game dumps with cancel support
Dump an installed title's running sandbox (already decrypted by the kernel)
to the browser as a streamed ZIP or to console storage as a file tree or ZIP.
The job launches the title when needed, reports entries/size/source through
the status endpoint, honours POST /api/dump/cancel and keeps the decrypt flag
configurable.  Documented in docs/game-dumping.md, covered by test_dump_job.
2026-09-30 17:50:57 +02:00
seregonwar 50bbecb776 feat(archive): streaming ZIP writer, archive paths and builtin unzip
Add a ZIP64 writer that streams entries straight into the HTTP response (and
decrypts SELF containers on consoles), archive path validation helpers and a
reworked builtin unzip, all covered by fixtures and unit tests.
2026-09-30 17:50:45 +02:00
seregonwar fb1646ce2b refactor(io): shared file I/O layer and FTP instance plumbing
Move copy, sendfile and directory helpers behind one platform layer, let the
FTP server run beside the HTTP stack with its own instance bookkeeping, and
keep the transfer command on the shared pipeline.
2026-09-30 17:50:45 +02:00
seregonwar b8f15aed5a feat(transfer): unified download manager with curl, NFS and torrent backends
One manager owns remote downloads: libcurl for HTTP/HTTPS/FTP (with ranged
parallel fetches), libnfs for nfs:// URLs and an optional libtorrent engine
for magnet links.  Jobs survive payload re-injections, support pause, cancel
and retry, and report per-job progress through the HTTP API.
2026-09-30 17:50:45 +02:00
seregonwar f19ac02d6f refactor(http): split the server into listener, stream, upload and json modules
The HTTP stack had grown into a single file that owned sockets, parsing,
uploads and every API domain.  Split it into focused modules (listener,
response stream, upload, json helpers) with the domain handlers next to the
APIs they serve, and cover framing, streaming and uploads with unit tests.
2026-09-30 17:50:36 +02:00
Seregon 2db0f1014f web: redesign the browser interface
Replace the embedded UI with a file manager: sidebar, details table, keyboard shortcuts, uploads, copy/move and extraction. Games, System, Transfers and Settings use the same components. The previous UI is archived in web/legacy/ and left out of the embedded resources.

The restyle is flat: donation banner, credits with the maintainer photo, and no gradients or card shadows. JavaScript stays ES5.
2026-09-29 12:53:34 +02:00
seregonwar a042eae480 refactor: modularize and harden file I/O 2026-09-09 18:13:06 +02:00
seregonwar a62b2ad2b2 refactor: modularize remaining FTP commands 2026-09-09 16:57:12 +02:00
seregonwar 5c2b18776b refactor: isolate FTP transfer commands 2026-09-09 16:32:34 +02:00
seregonwar 95bfcfcb83 refactor: split FTP control and listing commands 2026-09-09 16:28:05 +02:00
seregonwar 7745a1ff66 refactor: modularize HTTP games and routing 2026-09-09 16:23:12 +02:00
seregonwar be789e857e refactor: isolate HTTP system API and unify route matching 2026-09-09 14:18:13 +02:00
seregonwar 13dc390800 refactor: isolate HTTP process API and harden pid parsing 2026-09-09 14:13:20 +02:00
seregonwar 439499a117 refactor: isolate HTTP filesystem API 2026-09-09 14:08:36 +02:00
seregonwar 3eb3d0f115 refactor: split transfer archive and static HTTP handlers 2026-09-09 13:58:40 +02:00
seregonwar efa7b01d20 refactor: extract shared HTTP API primitives 2026-09-09 13:50:38 +02:00
seregonwar 5c9ff1dcae refactor: organize source tree by subsystem 2026-09-09 13:27:56 +02:00
seregonwar d0f6dd707d feat: expose ftp and nfs transfers in web downloader 2026-09-09 13:27:56 +02:00
seregonwar 1ac381b85d feat: replace custom download stack with curl and libnfs 2026-09-09 12:30:02 +02:00
Seregon c680efc605 Add HTTP /api/notify endpoint for custom PS4/PS5 system toasts.
Expose pal_notification_send_ex over ZHTTP so local automation (e.g. Home Assistant) can push console notifications without a dedicated payload.
2026-08-04 20:41:57 +02:00
Seregon 36845fd82a Add rest-mode resilience and fix CHMOD/copy regressions.
Survive console Rest Mode by recreating FTP/HTTP listeners with backoff and exposing daemon instance_id; implement real SITE CHMOD (#9) and fix atomic-copy temp paths that broke dashboard moves (#7).
2026-07-28 16:27:43 +02:00
Seregon 7bcc5c5f75 Update zftpd performance metrics in README
Updated performance metrics for zftpd to reflect improved speeds.
2026-07-10 11:50:12 +02:00
Seregon 272b6cb7b4 Update README.md
typo fix
2026-06-30 11:47:01 +02:00
Seregon e59fc3facb Update pal_filesystem_psx.c
Added KERNEL_ADDRESS_DATA_BASE for fw up to 13.xx
2026-06-30 11:26:39 +02:00
Seregon bf0206ae35 Merge pull request #8 from aydencharles/fix/list-flag-curlftpfs
fix: strip LIST -a flag for curlftpfs compatibility
2026-06-25 14:56:28 +02:00
Hexwell 6a7a25cb3d fix: handle LIST -a flag for curlftpfs compatibility
curlftpfs sends 'LIST -a' by default.  zftpd treated '-a' as a
path argument, causing 550 errors and empty directory listings
on FUSE-based FTP mounts (e.g. fnOS remote mount).

Add ftp_path_has_list_flag() / ftp_path_skip_list_flag() to
ftp_path.c with stat()-based flag-vs-path disambiguation.
Only well-known flags (-a/-l/-la/-al) are recognised; unknown
flag patterns pass through as paths to preserve access to files
whose names start with '-'.
2026-06-25 19:43:09 +08:00
Seregon e36bd23258 Update README.md 2026-06-15 09:44:50 +02:00
seregonwar 1ec37c6ee7 docs: fix ftp_config.h comment — PS4/PS5 default port is 2120, not 2122 2026-06-14 18:59:31 +02:00
seregonwar 8f15f5f3b0 ci: platform-aware release packaging — .elf/.bin for PS4/PS5, .app for macOS
- PS4/PS5: upload .elf and .bin files directly (no tar.gz) for payload injection
- macOS: create .app bundle with Info.plist (extracts VERSION from ftp_config.h)
- Linux: keep standard tar.gz with BUILD_INFO
- Upload step: use dist/* to handle all artifact types
- Release assets: collect .elf, .bin, and .tar.gz files
- Fix Info.plist heredoc: remove single quotes so VERSION expands correctly
- Fix sha256sum: remove || true (if ls guard already handles missing files)
2026-06-14 18:38:20 +02:00
seregonwar 829f25914e ci: remove Linux zhttp build from release matrix, keep only standard Linux 2026-06-14 18:26:58 +02:00
seregonwar caa8529736 fix: remove stray closing paren after rss cast in api_processes 2026-06-14 18:20:44 +02:00
seregonwar 37063409c9 fix: resolve 7 GCC Linux warnings in http_api.c
- scanf: remove length modifier from suppressed %*lu fields (gnu_scanf)
- sign-conversion: cast rss before unsigned multiplication, not after
- api_copy: bound both dir and base parts in %s/%s snprintf
- title_id: use %.63s for ent->d_name in 64-byte buffer
- api_extract: explicit NUL termination after strncpy
- api_dl_start: explicit NUL termination after strncpy
- strict-overflow: use int64_t for tv_usec += deadline arithmetic
2026-06-14 18:05:27 +02:00
seregonwar d6b8336ea4 fix: suppress -Wunused-result on write() with explicit ssize_t capture
Linux _FORTIFY_SOURCE=2 declares write() with __attribute__((warn_unused_result)).
Casting to (void) does not suppress this on GCC. Store the return value in
a ssize_t variable before discarding to satisfy the checker.
2026-06-14 17:56:15 +02:00
seregonwar 6f5704116b feat: add epoll event loop for Linux, conditional build in Makefile
Linux does not have kqueue (<sys/event.h>). Add src/event_loop_epoll.c
implementing the same event_loop.h interface using epoll_create1/epoll_ctl/
epoll_wait with EPOLLRDHUP for EV_EOF parity.

Makefile now selects event_loop_epoll.c on TARGET=linux and
 event_loop_kqueue.c on all other platforms via EVENT_LOOP_SRC variable.
2026-06-14 17:53:29 +02:00
seregonwar 7f8de06f05 fix: use %.*s in cmd_RETR diagnostic log to avoid -Wformat-truncation
GCC traces short_name back to resolved[FTP_PATH_MAX] (4096 bytes) even after
strrchr truncation, so replace %s with %.*s bounded to fit diag[320].
2026-06-14 17:48:33 +02:00
seregonwar 69171bd323 Update pal_fileio.c 2026-06-14 17:45:38 +02:00
seregonwar 4f02c2306a fix: resolve -Werror=format-truncation warnings on Linux (GCC)
Address all snprintf calls where format strings with %s referencing
FTP_PATH_MAX (4096) buffers could overflow smaller output buffers:

- Pattern A: cmd_PWD, cmd_MLST, cmd_MKD — truncate path with %.*s
  to fit FTP_REPLY_BUFFER_SIZE (1024)
- Pattern B: all '%s/%s' path concatenations limited with %.*s
  to prevent combined length exceeding FTP_PATH_MAX
- Pattern C: mlsx_format_line — limit display_name in 512-byte buffer
- Pattern D: pal_fileio tmp_path — same truncation logic as cmd_STOR

Also fixes:
- cmd_RETR diagnostic log: use basename instead of full path
- cmd_STOR tmp_path: overflow-safe size_t arithmetic
- cmd_OPTS: guard strncmp with len >= 4 to avoid -Wstring-compare

Files: ftp_commands.c, ftp_path.c, pal_fileio.c, http_server.c, http_api.c
2026-06-14 17:43:08 +02:00
seregonwar e38d178570 fix: resolve linux release warnings 2026-06-14 17:20:38 +02:00
seregonwar a479f2926c fix: bound file path diagnostics 2026-06-14 17:15:17 +02:00
seregonwar 1e9e3919ac fix: terminate pal curl source with newline 2026-06-14 17:07:54 +02:00
Seregon 72217eb69b Delete external/sJson-main.zip 2026-06-14 17:05:55 +02:00
seregonwar ed5cb8a425 fix: vendor miniz for builtin unzip 2026-06-14 17:01:43 +02:00
seregonwar b1e778e5df fix: unblock release builds 2026-06-14 16:50:34 +02:00
seregonwar f26e09ef2d ci: build console SDKs for releases 2026-06-14 16:45:02 +02:00
seregonwar 59dba12664 feat: modernize web UI and release builds 2026-06-14 16:08:54 +02:00
seregonwar f95e723a63 fix: Fixed the ZIP file unpacking system 2026-06-13 19:45:34 +02:00
seregonwar c41605ba09 perf: zero-copy sendfile on every file system, mmap pipeline, no fallback, EAGAIN HTTP retry
- sendfile_safe/VFS_CAP_SENDFILE are always enabled (whitelist/blacklist removed)
- mmap(MAP_ANONYMOUS) for pipeline copy buffers → bypasses buddy arena fragmentation
- removed pread+send_all fallback in http_server and pal_sendfile → sendfile or error
- cmd_RETR only sendfile with EAGAIN retry (removed read() cooldown fallback)
- HTTP download: EAGAIN retry for(;;) capped at 16 attempts, chunk size 512KB→2MB
- removed posix_fadvise(DONTNEED) from dst_fd, cmd_STOR, cmd_APPE (PS5 synchronous flush)
- buddy arena 8MB→16MB, dead fstatfs code removed, comment cleanup
2026-06-11 21:58:39 +02:00
seregonwar b98c8797e9 API fix 2026-06-11 19:27:36 +02:00
seregonwar 6c24f9a2c3 Update ftp_commands.c 2026-05-29 14:05:14 +02:00
seregonwar 7a5dd533dd Evict page cache after STOR/APPE writes
Call posix_fadvise(..., POSIX_FADV_DONTNEED) after successful STOR and APPE operations to evict written pages from the kernel page cache and avoid RAM exhaustion / throughput degradation when many small files are written. The calls are guarded by POSIX_FADV_DONTNEED and explicitly exclude PS4 (whose libc lacks posix_fadvise). Also extend the <fcntl.h> include to FreeBSD so posix_fadvise is available on FreeBSD/PS5 as well as Linux. This mirrors the eviction behavior already used in pal_fileio.c.
2026-05-29 12:53:40 +02:00
seregonwar 19b5fc123d Add resilient accept and auto-recreate listener
Introduce pal_resilient_accept to handle accept() failures by recreating, rebinding and relistening the server socket (with retries and logs), and send a notification when the server resumes. Add public header and implementation (include/pal_resilient_server.h, src/pal_resilient_server.c), wire it into the server accept loop (src/ftp_server.c) replacing the direct PAL_ACCEPT call, and add the new source to the build (Makefile). Also enable FTP_PORT_ALLOW_FOREIGN_IP in test builds and update tests/test_security.c to use 127.0.0.1 instead of 10.0.0.2.
2026-05-25 21:50:29 +02:00
seregonwar f2c0d8b6dd Merge branch 'main' of https://github.com/seregonwar/zftpd 2026-05-25 11:52:44 +02:00
Seregon dcc5eb16b9 Fix formatting of contributor acknowledgment in README 2026-05-24 21:24:54 +02:00
Seregon 4e35aa3a06 Update README.md 2026-05-24 21:24:35 +02:00
seregonwar b572c90c3e Update README.md 2026-05-24 21:11:23 +02:00
seregonwar 6be5efbe89 Sanitize downloads, support /api/file/get alias
Add a browser-safe /api/file/get alias for file downloads (some browsers/extensions block /api/download). Improve downloader robustness by: validating URL schemes (reject magnet links, restrict HTTPS on PS4/PS5 builds), sanitizing extracted filenames, and returning a clear error when a URL scheme is unsupported. Fix progress reporting for completed downloads with unknown total size and escape name/url/error fields in JSON status responses. Also include <strings.h> and enable ENABLE_LIBCURL by default when ENABLE_ZHTTPD is set in the Makefile. Update web/js/api.js to call the new alias and provide better fetch error handling that surfaces server messages when available.
2026-05-21 17:27:21 +02:00
seregonwar 47fc292f3f Improve PS4/PS5 app launch handling and symbol loading
Add named SCE error/app-id constants and replace magic numbers in launch logic; implement launch_result_is_success and psx_user_id_is_valid helpers. Add PS4-specific weak imports and a ps4_load_prx_symbol helper to resolve PRX exports (falls back to syscalls/dlsym) and prefer LncUtil/SystemService symbols when available. Harden launch flow: try multiple param flags, use LNC_SKIP_SYSTEM_UPDATE_CHECK on PS4, avoid unsafe SystemService/LoadExec fallbacks on PS4 to prevent ShellUI crashes, and add extra diagnostics via launch_diag_log. Also update user lookup to use foreground/initial/login fallbacks and validate user IDs. Makefile: add Sce* libraries for PS4, enable zhttpd for ps4/ps5, and simplify ENABLE_MCP defaulting to 0. .gitignore: remove the '*.txt' ignore line and add /external and log.txt entries.
2026-05-21 14:27:28 +02:00
Seregon 30a10bac19 Add MCP module, MLST improvements, and tests
improve FTP MLST/MLSD handling.

- Add external/sJson-main.zip and update .gitignore to ignore external/sJson-main, mcp, and logs.
- src/ftp_commands.c: add send_control_raw, MLST/MLSD formatting helpers (mlsx_format_line, mlsx_format_modify_time, ftp_path_to_client_path), implement MLST to return RFC3659-style facts on the control channel, and set session state on authentication. Update FEAT advertisement for MLST facts.
- src/ftp_session.c: update session state transitions when opening/closing data connections.
- tests/test_mlst_ascii.c: new integration tests for MLST reply formatting and ASCII STOR behavior.

These changes improve MLST compliance and path handling, and add tests to validate MLST/ASCII behaviors.
2026-04-13 09:27:31 +02:00
Seregon bda9ed4d49 Detect external sysent hooks and add error
Prevent installing the net filter when another payload has already hooked the syscall table. Adds is_external_hook_installed() which reads sysent[SYS_CONNECT].sy_call, determines whether the handler points into DMAP (indicating an external hook), logs a warning, and aborts installation with a new error code PS5_NET_FILTER_ERR_EXTERNAL_HOOK. Updates ps5_net_filter_strerror mapping and header (also switches header license notice to MIT).
2026-04-09 16:35:56 +02:00
Seregon 0a5c6bec97 Fix JMP patch offsets and add non-sleep checks
Adjust hook trampoline encoding and patch offsets for connect/sendto hooks and add thread sleep-safety checks.

- Update g_hook_connect_code/g_hook_sendto_code layout and comments to reflect JMP QWORD PTR [RIP+0] (FF 25 00 00 00 00) followed by an 8-byte absolute address. Clarify layout and explain why the 8-byte slot must be patched after the 6-byte JMP. Fix incorrect offsets that previously wrote into the rel32 field: HOOK_CONNECT_JMP_PATCH_OFFSET changed from 18 to 22 and HOOK_SENDTO_JMP_PATCH_OFFSET changed from 10 to 14, preventing wild jumps / SIGILL caused by corrupted instruction encoding.

- In hook_sys_connect and hook_sys_sendto, add checks of td_critnest and td_locks (at thread offsets 0x1CC and 0x1D0 on FreeBSD/PS5) to detect when the current thread cannot sleep. If either value is non-zero, skip copyin()/filtering and fall back to the original handler to avoid sleeping while holding non-sleepable locks (which can panic INVARIANTS).

These changes improve stability and correctness when installing trampolines and when handling calls from threads that cannot sleep.
2026-04-09 15:43:23 +02:00
Seregon 38e7b7f710 Update kstuff offsets; remove SIGPIPE ignore
Remove SIGPIPE suppression in main.c and refresh PS5 kernel offset data in ps5_net_filter.c. The kstuff source URL was updated to EchoStretch/kstuff and the firmware table was replaced with verified kstuff offsets, adding many firmware entries and correcting addresses for multiple versions.
2026-04-08 11:42:28 +02:00
Seregon c4fa18da8c feat(games): add games management view and API integration
- Introduced a new games management view styled like PS4's XMB.
- Added functionality for installing, reinstalling, and uninstalling games.
- Implemented API endpoints for managing installed games and their visibility.
- Included UI elements for game actions such as launch, repair, and refresh.
- Enhanced the dashboard to support game launching from installed apps.
- Created a dedicated CSS file for games view styling.
2026-03-30 15:45:39 +02:00
Seregon f2e5a7cf6e feat: enhance API error handling and response formatting for launch and fan endpoints 2026-03-30 11:17:36 +02:00
Seregon 031c55ae01 feat: improve PS5 pager address resolution added support firmware up to 12.70 2026-03-30 11:14:14 +02:00
Seregon a6c0901737 feat: Update PS5 IP address handling to bind on 0.0.0.0 for improved accessibility 2026-03-30 11:00:27 +02:00
Seregon 26f506ede7 feat: Add PS4/PS5 game launch functionality with a new API endpoint and integrated dashboard UI. 2026-03-22 23:03:44 +01:00
Seregon f28c6982e5 feat: Implement a comprehensive web UI redesign, add exFAT/fpkg and package unpacker functionality, and integrate libarchive/libcurl support. 2026-03-22 22:58:29 +01:00
Seregon e3c7b848d8 feat: Optimize PS5/PS4 transfer throughput with larger buffers and reduced syscall overhead
- ftp_config: bump version to 1.5.0; change PS4/PS5 default port from 2121 to 2120; add FTP_TCP_DATA_SNDBUF (4 MB on PS5/PS4 to bypass OrbisOS auto-tuning clamp, 0 elsewhere); reduce FTP_SENDFILE_EAGAIN_SLEEP_US from 1ms to 0.5ms for LAN RTT; increase FTP_RETR_SENDFILE_CHUNK from 512 KB to 2 MB to reduce syscall boundary overhead on NVMe
- http_config: add HTTP_DOWNLOAD_PREAD_CHUNK (2 MB) for pread() path
2026-03-20 14:40:01 +01:00
Seregon ae72ea0856 Merge branch 'main' of https://github.com/seregonwar/zftpd 2026-03-14 13:31:35 +01:00
Seregon 897bb6e9e0 feat: update FTP default port for PS4/PS5 to 2121U 2026-03-14 13:31:21 +01:00
Seregon f62cfa637b Delete web.zip 2026-03-13 15:47:59 +01:00
Seregon 8f089a8620 fix: Add time/entry budget limits to directory size calculation with partial result indicator and concurrency control
- http_api: refactor http_dir_size_recursive() to enforce 200ms timeout (DIR_SIZE_TIMEOUT_MS), 10,000 entry limit (DIR_SIZE_MAX_ENTRIES), and reduce max depth from 32 to 8 levels; introduce dir_size_ctx_t context with deadline/entries/partial tracking and dir_size_exceeded() helper that checks gettimeofday() every 64 entries to minimize syscall overhead; split logic into dir_size_walk
2026-03-11 16:48:09 +01:00
Seregon b861a5f28e feat: Add PS5 kernel-mode network filter with pause/resume for file copy operations and bump version to 1.4.0
- Makefile: add ps5-hook-blob target to compile src/ps5_net_filter_hook.c with kernel-safe flags (-fno-stack-protector, -mno-red-zone, -mcmodel=large), extract .text.hook_connect/.text.hook_sendto sections via objcopy, and generate ps5_net_filter_hook_blob.h byte-array header; conditionally include src/ps5_net_filter.c in PS5 build
- ftp_config: bump RELEASE_VERSION from 1.3.1 to 1.4.0
- http
2026-03-11 16:21:40 +01:00
Seregon 98290297d3 feat: Refactor theme selector to use native dropdown on touch devices and scope theme dropdown checkbox visibility
- http_resources: regenerate index.html from 298837 to 301077 bytes to scope .td-ck opacity rule to .theme-dd context (change to display:block), add .theme-switcher container with flexbox layout, add .theme-select-mobile <select> element with 120px width and custom styling, and hide .theme-btn while showing .theme-select-mobile on touch devices via @media (hover: none) and (pointer: coarse)
2026-03-11 13:24:32 +01:00
Seregon 7d16c2e071 feat: Add mobile-friendly theme selector and restore sendto modal default to current directory
- web/index.html: add .theme-select-mobile dropdown with 160px width, 8px padding, and focus outline; hide theme-btn and show theme-select-mobile on touch devices via @media (hover: none) and (pointer: coarse); scope .td-ck opacity rule to .theme-dd context
- web/index.html: implement buildThemeSelect() to populate <select> with THEMES array and sync value with current theme; call buildThemeSelect() in
2026-03-11 13:05:25 +01:00
Seregon f517e30fe2 feat: Add graceful FTP server shutdown with session pool cleanup and TCP buffer reset API
- ftp_server: implement bounded-wait shutdown sequence in ftp_server_stop() that closes listen_fd to unblock accept(), calls shutdown(SHUT_RDWR) on active session ctrl_fd to interrupt recv(), and waits up to 5s for active_sessions drain; expose ftp_server_release_session() for session threads to decrement active_sessions and reset slot state at exit
- ftp_session: call ftp_server_release_session() at end of f
2026-03-11 12:53:08 +01:00
Seregon 2c0485ecd4 feat: Add post-delete verification to prevent silent failures and implement localStorage caching for directory listings
- http_api: add stat() verification after delete operations to detect silent failures (e.g., permission denied, I/O errors) and return HTTP 500 with descriptive error message if path still exists after deletion
- http_resources: regenerate index.html from 291354 to 297322 bytes to add localStorage-based directory listing cache with getCacheKey()/getCachedListing() helpers and _
2026-03-10 22:29:04 +01:00
Seregon cca57cf4a3 feat: Add separate file/folder upload inputs, increase HTTP upload chunk size to 512 KB, and add NULL-safety checks for api_download response creation
- http_config: increase HTTP_UPLOAD_CHUNK_SIZE from 256 KB to 512 KB per active upload
- http_api: add NULL-check after http_response_create() in api_download to prevent SIGSEGV when response pool is exhausted; close fd and return NULL to avoid file-descriptor leak; add NULL-check after http_response_finalize() to handle buffer-full condition and
2026-03-10 16:26:04 +01:00
Seregon 89e0bf880d Regenerate HTTP resources with upload stats display and close button for drop zone overlay
- http_resources: update index.html from 287201 to 291067 bytes to add .drop-close button styles (absolute positioning, 28x28px, hover effects), .drop-card relative positioning, and .d-stats section with .d-stat-row/.d-stat-label/.d-stat-val for displaying upload statistics in drop zone overlay
2026-03-10 01:37:48 +01:00
Seregon c57138885b feat: Add lazy directory size calculation API endpoint and disable POSIX_FADV_DONTNEED on PS4
- http_api: add GET /api/dirsize?path=<dir> endpoint that recursively sums st_blocks*512 for all regular files under a directory with 32-level depth limit; expose http_dir_size_recursive() in header; trigger lazy size fetch for visible directories after listing render via _dirSizeToken cancellation mechanism
- http_api: force PS5 disk stats to always report /user partition instead of largest mount to match
2026-03-09 23:45:13 +01:00
Seregon b0331f0f02 feat: Add sendfile safety check for PS4/PS5 filesystems, implement recursive directory deletion, and optimize cache eviction across all platforms
- http_response: add HTTP_STATUS_409_CONFLICT and sendfile_safe flag with detailed documentation on PS4/PS5 kernel panic risk from sendfile(2) on exfatfs/msdosfs/nullfs/pfsmnt/pfs vnodes
- http_api: implement fstatfs-based filesystem detection in api_download to set sendfile_safe flag; add recursive directory deletion with opt-in ?recursive=1 parameter and
2026-03-09 20:26:12 +01:00
Seregon 1c36750341 feat: Improve HTTP upload streaming performance with dedicated buffers and socket tuning, and enhance FTP sendfile error handling for fatal storage errors. 2026-03-09 15:38:01 +01:00
Seregon 3baa416f65 feat: improve sendfile performance by enabling TCP auto-tuning and optimizing chunk sizes for downloads. 2026-03-09 00:57:33 +01:00
Seregon 0ec02a62a3 feat: Implement zero-copy file transfer for HTTP responses and optimize file copy performance on PS5/PS4 by using direct large writes and extending sequential read-ahead hints. 2026-03-08 18:25:53 +01:00
Seregon dffbe9cbdc Add HTTP copy progress error reporting and regenerate resources
Expose error_code/error_errno in copy progress API and capture errno in copy operations for better diagnostics.

- http_api: add error_code/error_errno atomics to copy_progress_t, populate them in copy_thread_fn via pal_file_copy_recursive_ex, and include in api_copy_progress JSON response; initialize new fields in api_copy
- ftp_commands: switch ftp_copy_thread_func to pal_file_copy_recursive_ex with out_errno parameter to capture OS
2026-03-08 01:12:56 +01:00
Seregon 86b3b5ead9 Fix STOR zero-window stalls on PS4/PS5 by forcing single-buffer mode and conditional SO_RCVBUF bump
Root-cause the ~800 KB abort point in STOR double-buffer mode: setsockopt(SO_RCVBUF) on an already-accepted socket is silently capped to ~256 KB on OrbisOS, regardless of the listening socket's pre-bind value. With two 256 KB app buffers + 256 KB kernel buffer ≈ 768 KB stall point, the TCP window drops to zero during PFS write latency, triggering FileZilla's 20 s data-inactivity timeout.

- ftp_commands
2026-03-07 22:48:01 +01:00
Seregon 644b78776e Add PLATFORM_PS4/PLATFORM_PS5 defines and fix passive mode binding/select issues
Unify platform detection by adding PLATFORM_PS4/PLATFORM_PS5 alongside existing PS4/PS5 defines across ftp_config.h and http_config.h. Extend FTP_TCP_RCVBUF to 4 MB on PS4 (same as PS5) to absorb PFS write latency. Increase HTTP_SENDFILE_CHUNK_SIZE to 1 MB to reduce syscall overhead.

- ftp_commands: serialize O_CREAT opens on PS4/PS5 with g_pfs_create_mtx to prevent inode-allocation lock contention (prevents >20s open
2026-03-07 22:17:23 +01:00
Seregon 83d26bef15 Update README.md 2026-03-04 22:29:39 +01:00
Seregon 8afd8f2f03 Update README.md 2026-03-04 17:47:59 +01:00
Seregon e022e9e5e5 Update README.md 2026-03-04 17:45:09 +01:00
Seregon 89711f5bc1 Add CPFR/CPTO/COPY commands and cross-device copy support
Implement async server-side file copy via CPFR/CPTO/COPY commands with cross-device fallback and PS5 PFS optimizations.

- ftp_commands: add cmd_CPFR/cmd_CPTO/cmd_COPY handlers with async copy thread, pthread-based background copy, progress tracking, and RNTO fallback to async copy for cross-device moves; add copy_thread_cleanup and copy_worker with recursive copy/move logic; integrate copy state checks in session cleanup and command dispatch
2026-03-04 17:43:26 +01:00
Seregon e340f6fb0f Refactor FTP RETR sendfile with cooldown retry and rewrite HTTP parser without strtok/strncpy
Improve FTP sendfile robustness on PS4/PS5 exFAT and eliminate unsafe string functions in HTTP parser.

- ftp_commands: implement sendfile burst/cooldown/retry loop to work around PS4/PS5 exFAT page cache stalls (~28 MB bursts); after sendfile stalls, run 1 MB read() cooldown to release page pressure then retry sendfile; if retry fails immediately, fall back to read() permanently; sync file offset between
2026-03-03 22:10:14 +01:00
Seregon 8676fb32ec Harden HTTP/FTP security and path confinement
Add multiple security and robustness fixes across HTTP and FTP subsystems:

- HTTP API: introduce http_api_set_root/http_api_get_root and http_validate_and_confine to enforce filesystem confinement; validate and use canonical 'safe' paths in list/download/stats/create endpoints.
- HTTP server: accept a root_path on creation, store/propagate it to API, enforce upload confinement to the configured root, and switch connection counters and in-use flag to atomic types for thread-safety.
- CSRF: make http_csrf_init return int, fail hard if entropy is unavailable or conversion fails (disables uploads), track initialization with a flag and check it in validation; main logs when CSRF init fails.
- FTP: add static assert and safer logic in ftp_buffer_pool to prevent undefined behavior for large FTP_MAX_SESSIONS bitmasks; reformat buffer declaration and fix acquire/release logic.
- FTP commands: check snprintf truncation in MKD reply and add PORT IP validation against control connection (configurable via FTP_PORT_ALLOW_FOREIGN_IP).
- Build/test: update Makefile test target to build test_http_query using LIB_OBJECTS.

These changes close several path traversal, symlink, CSRF predictability, race/atomic and buffer-mask issues and improve overall robustness.
2026-03-02 02:00:55 +01:00
Seregon fb0005e8fb Enhance file I/O, cross-device moves, and error reporting
Bump release to 1.3.1 and improve several I/O and error handling behaviors.

- ftp_commands: Provide detailed human-readable 550 messages for RNTO errors so clients (FileZilla/WinSCP) show actionable reasons instead of a generic "Rename failed.".
- pal_fileio: Add headers, logging and a max move recursion depth. Fix sendfile to treat EINTR as a partial send (avoid re-sending already transmitted bytes). Replace large static thread-local copy buffer with heap allocation to reduce memory footprint and fix temp-filename overflow by placing a short temp name in the destination directory.
- pal_fileio: Improve errno handling with logging and proper error mapping, free temporary buffers on all paths, and adjust close() behavior to account for differing EINTR semantics on FreeBSD/PS consoles vs Linux (prevents leaked fds on PS5).
- pal_fileio: Implement robust EXDEV fallback: recursive cross-device move for directories (pal_move_cross_device_r) with safe rollback semantics (only remove dst if created by this operation), recursive cleanup, path-length/depth checks, and safer pal_file_rename delegation.
- pal_notification: fix formatting/newline around include.

These changes fix several robustness bugs (temp name overflow, potential data loss on rollback, fd leaks on interrupted close, and retransmit on interrupted sendfile), add logging for diagnostics, and improve client-visible error messages.
2026-03-02 01:23:56 +01:00
Seregon a0986aabe0 Fix typos and update formatting in README.md 2026-03-01 16:08:51 +01:00
Seregon 87665b72a7 Add HTTP resources API and refresh web assets
Add a new include/http_resources.h header that defines a http_resource_t descriptor and http_resource_get() API for pre-compressed HTTP resources (with MIT license header). Regenerate/modify the embedded web UI asset in src/http_resources.c (index.html blob updated, size adjusted) to reflect updated front-end JS/SVG/content. Expand README.md with a clearer "Build & commands" section describing make targets, useful Makefile variables and example execution flags.
2026-02-28 22:43:28 +01:00
253 changed files with 67652 additions and 128394 deletions

No files matched your search

+116
View File
@@ -0,0 +1,116 @@
name: Bug report
description: Report a reproducible zftpd problem.
title: "[Bug]: "
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to report a bug. zftpd is a homebrew FTP + web explorer payload for jailbroken PS4/PS5 consoles: keep reports focused on the payload, the FTP/HTTP servers and the web interface itself.
- type: dropdown
id: component
attributes:
label: Component
description: Which area is affected?
options:
- Payload - PS5
- Payload - PS4
- FTP server
- Web UI - Files
- Web UI - Games
- Web UI - Transfers
- Web UI - Settings
- HTTP API
- Download queue / resume
- ZIP archive / bulk download
- Game dumping (decrypted or raw)
- Protected file decryption (.self)
- DNS filter
- Net filter (kernel, PS5)
- Launch / disc handling
- Mounts / external storage
- Build / release
- Documentation
validations:
required: true
- type: input
id: version
attributes:
label: zftpd version or commit
description: Release tag, commit SHA, or branch name — do not report against an unknown build.
placeholder: "v1.6.0, main@abcdef0, or local branch name"
validations:
required: true
- type: textarea
id: environment
attributes:
label: Environment
description: The exact setup matters more than anything else in this project.
placeholder: |
Console model:
Firmware:
Jailbreak / payload loader (e.g. elfldr on port 9021, GoldHEN):
Payload SDK used to build (e.g. PacBrew v0.40.2):
Payload build (release/debug, ENABLE_ZHTTPD):
Client OS and browser:
validations:
required: true
- type: textarea
id: steps
attributes:
label: Reproduction steps
description: List the shortest reliable path that triggers the issue.
placeholder: |
1. Load the payload with ...
2. Open http://<console-ip>:8888 ...
3. Start a transfer / dump / launch ...
4. Observe ...
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected behavior
description: What should zftpd have done instead?
validations:
required: true
- type: textarea
id: actual
attributes:
label: Actual behavior
description: What happened? Include exact errors, HTTP status codes, screenshots, or console output if available.
validations:
required: true
- type: textarea
id: logs
attributes:
label: Logs and diagnostics
description: Paste the payload console output, `GET /api/status`, one of the `/api/debug/*` endpoints, or the browser console log when relevant.
render: text
- type: textarea
id: verification
attributes:
label: Checks already tried
description: Mark or describe anything you already ran.
value: |
- [ ] `make test`
- [ ] Release build for the affected target (PS4 / PS5)
- [ ] Reloaded the web interface (hard reload, no cache)
- [ ] Reproduced on a clean checkout
- [ ] Reproduced after reinjecting the payload
- type: checkboxes
id: scope
attributes:
label: Use scope
description: zftpd is a file management and transfer tool for consoles you own.
options:
- label: This report is about zftpd itself, not a request for piracy, online cheating or help with content I do not own.
required: true
+11
View File
@@ -0,0 +1,11 @@
blank_issues_enabled: false
contact_links:
- name: Build and installation guide
url: https://github.com/seregonwar/zftpd/blob/main/README.md
about: Check the documented build commands and how to load the payload before opening an issue.
- name: Technical documentation
url: https://github.com/seregonwar/zftpd/tree/main/docs
about: HTTP API reference, architecture notes and the game dumping specification.
- name: Support the project
url: https://www.seregonwar.com/donations
about: Donations are optional, but they keep the payloads coming.
+113
View File
@@ -0,0 +1,113 @@
name: Console crash or payload failure
description: Report a PS4/PS5 hard crash, kernel panic, or unsafe payload behaviour.
title: "[Crash]: "
body:
- type: markdown
attributes:
value: |
Use this form for console hangs, kernel panics, payload exits, or any operation that leaves the console unstable. Crashes caused by the kernel-side features (net filter, protected file decryption, disc eject) are especially valuable: include the debug endpoints output.
- type: dropdown
id: platform
attributes:
label: Platform
options:
- PS5
- PS4
validations:
required: true
- type: input
id: firmware
attributes:
label: Firmware / environment
placeholder: "Console firmware, jailbreak and payload loader"
validations:
required: true
- type: input
id: build
attributes:
label: zftpd build
description: Release tag, commit SHA, branch, and whether it is a release or a local build.
placeholder: "v1.6.0, main@abcdef0, local release build"
validations:
required: true
- type: dropdown
id: operation
attributes:
label: Operation that triggered the crash
options:
- Payload startup
- FTP connection / login
- FTP transfer (upload or download)
- HTTP download (single file)
- Download queue / resume
- Bulk download (ZIP)
- Game dump - to this PC
- Game dump - to console storage
- Game launch
- Disc eject
- Mounts / external storage
- DNS filter toggle
- Net filter installation (PS5)
- Settings change
- Other
validations:
required: true
- type: textarea
id: target
attributes:
label: Target and paths
description: Include the title ID, the paths involved, the destination (PC or console), the transfer size and the protocol used.
placeholder: |
Title ID / content:
Source path:
Destination:
Transfer size:
Protocol (FTP / HTTP / web UI):
- type: textarea
id: steps
attributes:
label: Exact reproduction path
description: Start from payload injection and include every action taken in the web interface or FTP client.
placeholder: |
1. Inject the payload with ...
2. Open the web interface ...
3. Start ...
4. Console crashes / payload exits after ...
validations:
required: true
- type: textarea
id: crash_behavior
attributes:
label: Crash behaviour
description: Describe the visible console state, the payload output, and whether the console reboots.
placeholder: |
Console state:
Payload log:
Reboot required:
Frequency (always / sometimes / once):
validations:
required: true
- type: textarea
id: diagnostics
attributes:
label: Diagnostics
description: Paste the payload console output, plus `GET /api/status`, `GET /api/debug/kernel` and `GET /api/dump/status` when the crash involved decryption or dumping.
render: text
- type: checkboxes
id: safety
attributes:
label: Safety checks
options:
- label: I can reproduce this on my own console and I am willing to retest with a diagnostic build.
required: true
- label: I did not use zftpd against online services, multiplayer sessions, or content I do not own.
required: true
@@ -0,0 +1,74 @@
name: Feature request
description: Propose a zftpd capability or workflow improvement.
title: "[Feature]: "
body:
- type: markdown
attributes:
value: |
Feature requests should describe a workflow you actually run: transferring files, managing a console's storage, dumping a title you own, or automating the payload through its HTTP API.
- type: dropdown
id: area
attributes:
label: Area
options:
- FTP server
- HTTP API
- Web UI - Files
- Web UI - Games
- Web UI - Transfers
- Web UI - Settings
- Download queue / resume
- ZIP archive / bulk download
- Game dumping
- Protected file decryption (.self)
- DNS filter
- Net filter (kernel, PS5)
- Launch / disc handling
- Mounts / external storage
- Build / release
- Documentation
validations:
required: true
- type: textarea
id: workflow
attributes:
label: Workflow or problem
description: What are you trying to do, and where does zftpd slow you down today?
placeholder: "I want to ... because ..."
validations:
required: true
- type: textarea
id: proposal
attributes:
label: Proposed behavior
description: Describe the user-facing behaviour, the endpoint or FTP command, and how it should appear in the interface.
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives considered
description: Mention existing workarounds, other payloads or tools, and why they are not enough.
- type: textarea
id: verification
attributes:
label: How should it be verified?
description: List the expected host tests, manual console checks and documentation updates.
placeholder: |
- Host test:
- PS4/PS5 manual check:
- HTTP API or FTP level check:
- Documentation:
- type: checkboxes
id: scope
attributes:
label: Use scope
options:
- label: This request is about managing files on a console I own, not about piracy, online cheating or bypassing services.
required: true
+41
View File
@@ -0,0 +1,41 @@
## Summary
-
## Type of change
- [ ] FTP server / protocol
- [ ] HTTP API
- [ ] Web interface (Files, Games, Transfers, Settings)
- [ ] Transfer engine (queue, resume, curl backend)
- [ ] Archive / ZIP handling
- [ ] Game dumping / protected file decryption
- [ ] DNS filter
- [ ] Net filter / kernel side
- [ ] Platform layer (PS4 / PS5 / PAL)
- [ ] Build / release
- [ ] Documentation
- [ ] Bug fix
## Risk surface
- Affected platform(s):
- Affected endpoints, screens, or FTP commands:
- Console safety considerations (does this touch the kernel, decryption or the filesystem?):
## Verification
- [ ] `make test`
- [ ] PS4 release build
- [ ] PS5 release build
- [ ] Manual console check
- [ ] Web interface checked with a hard reload
- [ ] Not applicable, because:
## Notes for reviewers
-
## Scope confirmation
- [ ] This change supports file management, transfers and backups on consoles the user owns.
+331
View File
@@ -0,0 +1,331 @@
name: Release
on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
tag:
description: "Release tag to create/update, for example v1.6.0"
required: false
type: string
draft:
description: "Create the GitHub release as draft"
required: false
default: false
type: boolean
prerelease:
description: "Mark the GitHub release as prerelease"
required: false
default: false
type: boolean
permissions:
contents: write
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
jobs:
build:
name: ${{ matrix.platform }} / ${{ matrix.variant }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- platform: linux
os: ubuntu-latest
zhttpd: "0"
variant: ftp
- platform: macos
os: macos-latest
zhttpd: "0"
variant: ftp
- platform: macos
os: macos-latest
zhttpd: "1"
variant: zhttp
- platform: ps4
os: ubuntu-latest
zhttpd: "0"
variant: ftp
- platform: ps4
os: ubuntu-latest
zhttpd: "1"
variant: zhttp
- platform: ps5
os: ubuntu-latest
zhttpd: "0"
variant: ftp
- platform: ps5
os: ubuntu-latest
zhttpd: "1"
variant: zhttp
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: recursive
- name: Install Linux build dependencies
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y \
build-essential \
binutils \
clang \
cmake \
curl \
gcc \
libarchive-dev \
libcurl4-openssl-dev \
lld \
llvm \
make \
pkg-config \
python3 \
xxd
- name: Install macOS build dependencies
if: runner.os == 'macOS'
run: |
brew update
brew install libarchive llvm || true
if [ -d /opt/homebrew/opt/llvm/bin ]; then
echo "/opt/homebrew/opt/llvm/bin" >> "$GITHUB_PATH"
fi
if [ -d /usr/local/opt/llvm/bin ]; then
echo "/usr/local/opt/llvm/bin" >> "$GITHUB_PATH"
fi
- name: Clone and build PS4 Payload SDK
if: matrix.platform == 'ps4'
run: |
set -euo pipefail
sdk_src="$RUNNER_TEMP/ps4-payload-sdk"
sdk_dst="$GITHUB_WORKSPACE/ps4-payload-sdk-install"
git clone --recursive https://github.com/ps4-payload-dev/sdk.git "$sdk_src"
make -C "$sdk_src" -j"$(nproc)"
make -C "$sdk_src" DESTDIR="$sdk_dst" install
test -f "$sdk_dst/toolchain/orbis.mk"
test -x "$sdk_dst/bin/orbis-clang"
echo "PS4_PAYLOAD_SDK=$sdk_dst" >> "$GITHUB_ENV"
- name: Install PacBrew PS4 portlibs (libcurl)
if: matrix.platform == 'ps4' && matrix.zhttpd == '1'
run: |
set -euo pipefail
bash tools/fetch_pacbrew_ps4.sh "$GITHUB_WORKSPACE/pacbrew-ps4-openorbis"
echo "PS4_PACBREW_ROOT=$GITHUB_WORKSPACE/pacbrew-ps4-openorbis" >> "$GITHUB_ENV"
- name: Install PS5 PacBrew SDK bundle
if: matrix.platform == 'ps5'
run: |
set -euo pipefail
pacbrew_version="v0.40.2"
pacbrew_sha256="a85f65de418a8e6a898c6c3e3c870d50fff7618a200e4dd59ea9692af6ecec4d"
archive="$RUNNER_TEMP/ps5-payload-dev-${pacbrew_version}.tar.gz"
root="$RUNNER_TEMP/ps5-pacbrew"
curl -fL --retry 3 \
-o "$archive" \
"https://github.com/ps5-payload-dev/pacbrew-repo/releases/download/${pacbrew_version}/ps5-payload-dev.tar.gz"
echo "${pacbrew_sha256} ${archive}" | sha256sum -c -
mkdir -p "$root"
tar -xzf "$archive" -C "$root"
sdk_dst="$root/opt/ps5-payload-sdk"
test -f "$sdk_dst/toolchain/prospero.mk"
test -x "$sdk_dst/bin/prospero-clang"
test -x "$sdk_dst/bin/prospero-pkg-config"
"$sdk_dst/bin/prospero-pkg-config" --exists libcurl
"$sdk_dst/bin/prospero-pkg-config" --exists libnfs
test -f "$sdk_dst/target/user/homebrew/etc/ca-bundle.crt"
echo "PS5_PAYLOAD_SDK=$sdk_dst" >> "$GITHUB_ENV"
- name: Build PS5 BitTorrent engine
if: matrix.platform == 'ps5' && matrix.zhttpd == '1'
run: |
set -euo pipefail
sudo apt-get install -y libboost-dev
bash tools/build_ps5_libtorrent.sh "$PS5_PAYLOAD_SDK"
- name: Build release variant
run: |
set -euo pipefail
if [ "${{ matrix.platform }}" = "ps4" ]; then
make \
PS4_PAYLOAD_SDK="${PS4_PAYLOAD_SDK}" \
TARGET="${{ matrix.platform }}" \
BUILD_TYPE=release \
ENABLE_ZHTTPD="${{ matrix.zhttpd }}" \
ENABLE_MCP=0 \
ENABLE_PKG_INSTALL=0 \
clean all
elif [ "${{ matrix.platform }}" = "ps5" ]; then
make \
PS5_PAYLOAD_SDK="${PS5_PAYLOAD_SDK}" \
TARGET="${{ matrix.platform }}" \
BUILD_TYPE=release \
ENABLE_ZHTTPD="${{ matrix.zhttpd }}" \
ENABLE_MCP=0 \
ENABLE_PKG_INSTALL=0 \
ENABLE_LIBTORRENT="${{ matrix.zhttpd }}" \
clean all
else
make \
TARGET="${{ matrix.platform }}" \
BUILD_TYPE=release \
ENABLE_ZHTTPD="${{ matrix.zhttpd }}" \
ENABLE_MCP=0 \
ENABLE_PKG_INSTALL=0 \
clean all
fi
- name: Package artifact
run: |
set -euo pipefail
variant_dir="build/${{ matrix.platform }}/release"
if [ "${{ matrix.zhttpd }}" = "1" ]; then
variant_dir="${variant_dir}-zhttp"
fi
mkdir -p dist
if [ "${{ matrix.platform }}" = "ps4" ] || [ "${{ matrix.platform }}" = "ps5" ]; then
# PS4/PS5: upload .elf and .bin directly (ready for payload injection)
find "$variant_dir" -maxdepth 1 \( -name '*.elf' -o -name '*.bin' \) -print -exec cp {} dist/ \;
ls -la dist/
elif [ "${{ matrix.platform }}" = "macos" ]; then
# macOS: bundle into .app
elf=$(find "$variant_dir" -maxdepth 1 -name 'zftpd-*' ! -name '*.bin' ! -name '*.tar.gz' -type f | head -1)
if [ -z "$elf" ]; then
echo "ERROR: no ELF binary found in $variant_dir"
ls -la "$variant_dir"
exit 1
fi
# Extract version from ftp_config.h (same method as Makefile)
VERSION=$(grep -E 'define[[:space:]]+RELEASE_VERSION' include/ftp/ftp_config.h | head -1 | cut -d'"' -f2)
app_dir="dist/${elf##*/}.app"
mkdir -p "${app_dir}/Contents/MacOS"
cp "$elf" "${app_dir}/Contents/MacOS/zftpd"
chmod +x "${app_dir}/Contents/MacOS/zftpd"
cat > "${app_dir}/Contents/Info.plist" << PLISTEOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>zftpd</string>
<key>CFBundleIdentifier</key>
<string>com.zftpd.server</string>
<key>CFBundleName</key>
<string>zftpd</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>${VERSION:-0.0.0}</string>
<key>CFBundleVersion</key>
<string>${VERSION:-0.0.0}</string>
<key>LSMinimumSystemVersion</key>
<string>10.15</string>
<key>NSHighResolutionCapable</key>
<true/>
</dict>
</plist>
PLISTEOF
tar -C dist -czf "dist/${elf##*/}.app.tar.gz" "${elf##*/}.app"
rm -rf "${app_dir}"
ls -la dist/
else
# Linux: standard tar.gz with BUILD_INFO
pkg_dir="dist/${{ matrix.platform }}-${{ matrix.variant }}"
mkdir -p "$pkg_dir"
find "$variant_dir" -maxdepth 1 -type f -print -exec cp {} "$pkg_dir/" \;
cat > "$pkg_dir/BUILD_INFO.txt" <<EOF
project=zftpd
platform=${{ matrix.platform }}
zhttpd=${{ matrix.zhttpd }}
mcp=0
pkg_install=0
build_type=release
ref=${{ github.ref_name }}
sha=${{ github.sha }}
EOF
tar -C dist -czf "dist/zftpd-${{ matrix.platform }}-${{ matrix.variant }}.tar.gz" "${{ matrix.platform }}-${{ matrix.variant }}"
rm -rf "$pkg_dir"
ls -la dist/
fi
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: zftpd-${{ matrix.platform }}-${{ matrix.variant }}
path: dist/*
if-no-files-found: error
retention-days: 14
release:
name: Publish GitHub Release
runs-on: ubuntu-latest
needs: build
if: startsWith(github.ref, 'refs/tags/') || (github.event_name == 'workflow_dispatch' && inputs.tag != '')
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Download artifacts
uses: actions/download-artifact@v4
with:
path: release-artifacts
merge-multiple: true
- name: Prepare release assets
run: |
set -euo pipefail
mkdir -p release-assets
find release-artifacts -type f \( -name '*.tar.gz' -o -name '*.elf' -o -name '*.bin' \) -exec cp {} release-assets/ \;
cd release-assets
if ls *.tar.gz *.elf *.bin >/dev/null 2>&1; then
sha256sum *.tar.gz *.elf *.bin > SHA256SUMS.txt 2>/dev/null
fi
ls -lh
- name: Create or update release
env:
GH_TOKEN: ${{ github.token }}
INPUT_TAG: ${{ inputs.tag }}
INPUT_DRAFT: ${{ inputs.draft }}
INPUT_PRERELEASE: ${{ inputs.prerelease }}
run: |
set -euo pipefail
tag="${GITHUB_REF_NAME}"
if [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ]; then
tag="${INPUT_TAG}"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
if ! git rev-parse -q --verify "refs/tags/${tag}" >/dev/null; then
git tag "${tag}" "${GITHUB_SHA}"
git push origin "refs/tags/${tag}"
fi
fi
flags=(--title "zftpd ${tag}" --generate-notes)
if [ "${INPUT_DRAFT}" = "true" ]; then
flags+=(--draft)
fi
if [ "${INPUT_PRERELEASE}" = "true" ]; then
flags+=(--prerelease)
fi
if gh release view "${tag}" >/dev/null 2>&1; then
gh release upload "${tag}" release-assets/* --clobber
else
gh release create "${tag}" release-assets/* "${flags[@]}"
fi
+23 -1
View File
@@ -56,6 +56,7 @@ dkms.conf
.DS_Store
/external/ps4-payload-sdk
/external/ps5-payload-sdk
/external/pacbrew
/external/ftpsrv-master
/build
/external/ps3libraries-master
@@ -68,7 +69,7 @@ ftpd.log
/zFrame
*.plist
test_mc_ping.py
*.txt
/backup
docs/ps4_ps5_syscall_report.md
@@ -77,3 +78,24 @@ docs/ps4_ps5_syscall_report.md
*.pyc
docs/investigation_report.md
/external/ftpsrv-1.15-ng-beta8
/zftpd-1.3.0
CHANGELOG.md
/zftpd-patched
zftpd-patched (1).zip
/external/libarchive-3.8.6-compiled
/external/libarchive-3.8.6
/reference
/external/curl
/external/Itemzflow-main
/external/PS4-fan-control-master
/external/ps5-fan-threshold-master
/external/sJson-main
/mcp
/logs
race
/external
log.txt
/git-patch
*.bak
/.commandcode
+638 -48
View File
@@ -4,7 +4,7 @@
# Project information
PROJECT := zftpd
VERSION := $(shell grep -E 'define[[:space:]]+RELEASE_VERSION' include/ftp_config.h | head -n 1 | cut -d'"' -f2)
VERSION := $(shell grep -E 'define[[:space:]]+RELEASE_VERSION' include/ftp/ftp_config.h | head -n 1 | cut -d'"' -f2)
ifeq ($(strip $(VERSION)),)
VERSION := 0.0.0
endif
@@ -67,7 +67,9 @@ ifeq ($(TARGET),ps4)
export LLVM_CONFIG := $(ORBIS_LLVM_CONFIG)
include $(PS4_PAYLOAD_SDK)/toolchain/orbis.mk
PLATFORM_DEFS := -DPLATFORM_PS4 -DPS4
PLATFORM_LIBS := -lkernel -lpthread
# SceSysmodule is loaded at runtime with dlopen: the payload SDK does not
# ship a linkable library for it.
PLATFORM_LIBS := -lkernel -lpthread -lSceSystemService -lSceUserService
PLATFORM_LDFLAGS :=
endif
@@ -109,8 +111,16 @@ endif
# Default to GCC if no target matched
CC ?= gcc
CXX ?= c++
PLATFORM_LIBS ?= -lpthread
# Event loop implementation — kqueue for BSD/macOS/PS4/PS5, epoll for Linux
ifeq ($(TARGET),linux)
EVENT_LOOP_SRC := src/runtime/event_loop_epoll.c
else
EVENT_LOOP_SRC := src/runtime/event_loop_kqueue.c
endif
#============================================================================
# COMPILER FLAGS (SAFETY-CRITICAL STANDARDS)
#============================================================================
@@ -173,14 +183,27 @@ ifneq ($(filter $(TARGET),ps4 ps5),)
CFLAGS += -fvisibility=hidden
endif
# PS5: disable aggressive function inlining. Without this flag the compiler
# inlines every static api_* handler into http_api_handle, creating a massive
# stack frame (~60-80 KB) that overflows the PS5 thread stack (SIGSEGV).
ifeq ($(TARGET),ps5)
CFLAGS += -fno-inline-functions
endif
# Include directories
CFLAGS += -I./include
CFLAGS += -I./include/ftp -I./include/http -I./include/platform
CFLAGS += -I./include/platform/ps5 -I./include/archive -I./include/runtime
#============================================================================
# LINKER FLAGS
#============================================================================
ifeq ($(HOST_OS),Linux)
# Console payloads are loaded by loaders (GoldHEN binloader, elfldr) that
# expect a position-independent executable, no matter which host built them.
ifneq ($(filter $(TARGET),ps4 ps5),)
LDFLAGS += -pie
else ifeq ($(HOST_OS),Linux)
LDFLAGS += -pie
endif
LDFLAGS += $(PLATFORM_LDFLAGS)
@@ -191,22 +214,52 @@ LIBS := $(PLATFORM_LIBS)
#============================================================================
# Source files
SOURCES := src/pal_network.c
SOURCES += src/pal_fileio.c
SOURCES += src/pal_alloc.c
SOURCES += src/pal_scratch.c
SOURCES += src/pal_notification.c
SOURCES += src/pal_filesystem.c
SOURCES += src/pal_filesystem_psx.c
SOURCES += src/ftp_path.c
SOURCES += src/ftp_server.c
SOURCES += src/ftp_session.c
SOURCES += src/ftp_protocol.c
SOURCES += src/ftp_commands.c
SOURCES += src/ftp_buffer_pool.c
SOURCES += src/ftp_log.c
SOURCES += src/ftp_crypto.c
SOURCES += src/main.c
SOURCES := src/platform/pal_network.c
SOURCES += src/platform/fileio/basic.c
SOURCES += src/platform/fileio/sendfile.c
SOURCES += src/platform/fileio/copy.c
SOURCES += src/platform/fileio/tree.c
SOURCES += src/platform/fileio/directory.c
SOURCES += src/platform/pal_alloc.c
SOURCES += src/platform/pal_scratch.c
SOURCES += src/platform/pal_notification.c
SOURCES += src/platform/pal_disc.c
SOURCES += src/platform/pal_volume.c
SOURCES += src/platform/pal_dns_filter.c
SOURCES += src/platform/pal_filesystem.c
SOURCES += src/platform/pal_filesystem_psx.c
SOURCES += src/ftp/ftp_path.c
SOURCES += src/ftp/ftp_server.c
SOURCES += src/ftp/ftp_session.c
SOURCES += src/ftp/ftp_protocol.c
SOURCES += src/ftp/commands/control.c
SOURCES += src/ftp/commands/listing.c
SOURCES += src/ftp/commands/transfer.c
SOURCES += src/ftp/commands/filesystem.c
SOURCES += src/ftp/commands/data_connection.c
SOURCES += src/ftp/commands/extensions.c
SOURCES += src/ftp/commands/information.c
SOURCES += src/ftp/commands/transfer_params.c
SOURCES += src/ftp/ftp_buffer_pool.c
SOURCES += src/ftp/ftp_log.c
SOURCES += src/ftp/ftp_crypto.c
SOURCES += src/app/main.c
SOURCES += src/app/instance_control.c
SOURCES += src/platform/pal_resilient_server.c
SOURCES += src/ftp/ftp_instance.c
# PS5 kernel syscall hook is opt-in until its firmware-specific install path
# has been validated on real hardware. The normal payload never loads it.
ENABLE_PS5_NET_FILTER ?= 0
ifeq ($(TARGET),ps5)
CFLAGS += -DENABLE_PS5_NET_FILTER=$(ENABLE_PS5_NET_FILTER)
endif
ifeq ($(TARGET),ps5)
SOURCES += src/platform/ps5/ps5_fw_offsets.c
ifeq ($(ENABLE_PS5_NET_FILTER),1)
SOURCES += src/platform/ps5/ps5_net_filter.c
endif
endif
#============================================================================
# ZHTTPD (Web File Explorer) — compile-time toggle
@@ -214,13 +267,16 @@ SOURCES += src/main.c
#============================================================================
ifneq ($(filter $(TARGET),ps4 ps5),)
ENABLE_ZHTTPD ?= 0
ENABLE_ZHTTPD ?= 1
else
ENABLE_ZHTTPD ?= 1
endif
# Artifact/build variants (e.g., zhttp)
ifeq ($(ENABLE_ZHTTPD),1)
TEST_BINS += $(BUILD_DIR)/tests/test_http_query
TEST_BINS += $(BUILD_DIR)/tests/test_http_confinement
TEST_BINS += $(BUILD_DIR)/tests/test_notify
VARIANT_TAG := zhttp
endif
@@ -245,34 +301,433 @@ STRIP ?= strip
ifeq ($(ENABLE_ZHTTPD),1)
CFLAGS += -DENABLE_ZHTTPD=1
CFLAGS += -DENABLE_WEB_UPLOAD=1
SOURCES += src/event_loop_kqueue.c
SOURCES += src/http_server.c
SOURCES += src/http_parser.c
SOURCES += src/http_response.c
SOURCES += src/http_api.c
SOURCES += src/http_csrf.c
SOURCES += src/http_resources.c
# PKG installation rides along with the web interface; override with =0.
ENABLE_PKG_INSTALL ?= 1
CFLAGS += -DENABLE_PKG_INSTALL=$(ENABLE_PKG_INSTALL)
ifeq ($(TARGET),ps5)
ifeq ($(ENABLE_PKG_INSTALL),1)
# The system installer cannot be driven from a payload process, so a
# small helper program is embedded here and delivered at runtime. With
# the installer disabled the helper is neither built nor embedded.
INSTALL_HELPER_ELF := $(BUILD_DIR)/install_helper.elf
INSTALL_HELPER_BLOB := $(BUILD_DIR)/generated/install_helper_blob.c
SOURCES += $(INSTALL_HELPER_BLOB)
CFLAGS += -DZFTPD_INSTALL_HELPER=1
endif
SOURCES += src/http/games/ps5_install_helper.c
endif
ENABLE_LIBCURL ?= 1
SOURCES += $(EVENT_LOOP_SRC)
SOURCES += src/http/http_server.c
SOURCES += src/http/http_listener.c
SOURCES += src/http/http_upload.c
SOURCES += src/http/http_parser.c
SOURCES += src/http/http_response.c
SOURCES += src/http/http_response_stream.c
SOURCES += src/http/http_json.c
SOURCES += src/http/http_api.c
SOURCES += src/http/http_api_common.c
SOURCES += src/http/http_api_files.c
SOURCES += src/http/http_api_process.c
SOURCES += src/http/http_api_system.c src/http/games/common.c src/http/games/psx_install.c src/http/games/psx_appdb.c src/http/games/psx_launch.c src/http/games/admin_api.c src/http/games/metadata_api.c src/http/games/catalog.c
SOURCES += src/http/http_api_transfer.c
SOURCES += src/http/http_static.c
SOURCES += src/http/http_api_archive.c
SOURCES += src/http/http_api_dump.c
SOURCES += src/http/http_csrf.c
WEB_RESOURCE_FILES := $(shell find web -path web/legacy -prune -o -type f -print | sort)
HTTP_RESOURCES_C := $(BUILD_DIR)/generated/http/http_resources.c
SOURCES += $(HTTP_RESOURCES_C)
SOURCES += src/archive/exfat_unpacker.c
SOURCES += src/archive/pkg_unpacker.c
SOURCES += src/archive/builtin_unzip.c
SOURCES += src/archive/zip_writer.c
SOURCES += src/archive/archive_path.c
SOURCES += src/transfer/transfer_manager.c
SOURCES += src/transfer/transfer_state.c
SOURCES += src/transfer/dump_job.c
else
# Keep the switch visible in every translation unit either way.
CFLAGS += -DENABLE_ZHTTPD=0
endif
# Object files
OBJECTS := $(patsubst src/%.c,$(OBJ_DIR)/%.o,$(SOURCES))
# NFS URL transfers are enabled by default for PS5 zhttp builds. Other targets
# can opt in explicitly with ENABLE_LIBNFS=1 when libnfs is installed.
ifeq ($(ENABLE_ZHTTPD),1)
ifeq ($(TARGET),ps5)
ENABLE_LIBNFS ?= 1
else
ENABLE_LIBNFS ?= 0
endif
else
ENABLE_LIBNFS := 0
endif
#============================================================================
# Enable with ENABLE_MCP=1
#============================================================================
ifeq ($(ENABLE_MCP),)
ENABLE_MCP ?= 0
endif
ifeq ($(ENABLE_MCP),1)
CFLAGS += -DENABLE_MCP=1
CFLAGS += -I./mcp/include
CFLAGS += -I./external/sJson-main/src
# JSON configuration for sJson
CFLAGS += -DJSON_MAX_DEPTH=32
CFLAGS += -DJSON_MAX_STRING_LEN=65536
CFLAGS += -DJSON_MAX_NODES=16384
SOURCES += mcp/src/mcp_protocol.c
SOURCES += mcp/src/mcp_server.c
SOURCES += mcp/src/mcp_handlers.c
SOURCES += external/sJson-main/src/sJson.c
SOURCES += $(EVENT_LOOP_SRC)
# Execution modules
SOURCES += mcp/src/mcp_execution/payload.c
SOURCES += mcp/src/mcp_execution/syscall_race.c
# Hunter modules (Zero-Day detection)
SOURCES += mcp/src/mcp_hunter/process_monitor.c
SOURCES += mcp/src/mcp_hunter/vuln_fuzzer.c
SOURCES += mcp/src/mcp_hunter/exploit_chain.c
SOURCES += mcp/src/mcp_hunter/jit_compiler.c
# Additional include path for hunter headers
CFLAGS += -I./mcp/src/mcp_hunter
$(info [INFO] MCP kernel analysis module enabled (with Zero-Day Hunter))
endif
#============================================================================
# OPTIONAL LIBRARIES — enable with ENABLE_LIBARCHIVE=1 / ENABLE_LIBCURL=1
#
# When enabled, the Makefile verifies that the required header is actually
# available. For desktop (gcc/clang) it uses a compiler probe. For
# PS4/PS5 cross-compilers the probe fails (missing sysroot headers),
# so we fall back to a simple file-existence check on bundled headers.
#============================================================================
# ── libarchive: zhttp only; auto-detect on desktop ─────────────────────
override ENABLE_LIBCURL ?= 0
ifeq ($(ENABLE_ZHTTPD),1)
ifeq ($(TARGET),ps5)
# Optional PacBrew library for non-ZIP formats; ZIP keeps the builtin reader.
override ENABLE_LIBARCHIVE ?= 0
ifeq ($(ENABLE_LIBARCHIVE),1)
PS5_PKG_CONFIG ?= $(PS5_PAYLOAD_SDK)/bin/prospero-pkg-config
_HAS_PS5_ARCHIVE := $(shell test -x $(PS5_PKG_CONFIG) && $(PS5_PKG_CONFIG) --exists libarchive && echo 1 || echo 0)
ifneq ($(_HAS_PS5_ARCHIVE),1)
$(error PS5 archive extraction requires PacBrew ps5-payload-libarchive)
endif
CFLAGS += -DENABLE_LIBARCHIVE=1 $(shell $(PS5_PKG_CONFIG) --cflags libarchive)
LIBS += $(shell $(PS5_PKG_CONFIG) --static --libs libarchive)
$(info [INFO] PS5 archive extraction: PacBrew libarchive)
endif
else ifeq ($(TARGET),ps4)
# PS4 builds use the builtin ZIP backend.
override ENABLE_LIBARCHIVE := 0
else
# Desktop (macOS/Linux): auto-detect unless user explicitly set it to 0
ifneq ($(ENABLE_LIBARCHIVE),0)
_HAS_ARCHIVE := 0
# 1) System installation
_SYS_ARCHIVE := $(shell echo '\#include <archive.h>' | $(CC) -xc -fsyntax-only - 2>/dev/null && echo 1 || echo 0)
ifeq ($(_SYS_ARCHIVE),1)
_HAS_ARCHIVE := 1
LIBS += -larchive
$(info [INFO] libarchive: using system installation)
endif
# 2) Homebrew keg (libarchive is keg-only on macOS)
ifeq ($(_HAS_ARCHIVE),0)
ifeq ($(HOST_OS),Darwin)
_BREW_ARCHIVE_PREFIX := $(shell brew --prefix libarchive 2>/dev/null)
ifneq ($(_BREW_ARCHIVE_PREFIX),)
_HB_ARCHIVE := $(shell echo '\#include <archive.h>' | $(CC) -xc -fsyntax-only -I$(_BREW_ARCHIVE_PREFIX)/include - 2>/dev/null && echo 1 || echo 0)
ifeq ($(_HB_ARCHIVE),1)
_HAS_ARCHIVE := 1
CFLAGS += -I$(_BREW_ARCHIVE_PREFIX)/include
LDFLAGS += -L$(_BREW_ARCHIVE_PREFIX)/lib
LIBS += -larchive
$(info [INFO] libarchive: using Homebrew keg $(_BREW_ARCHIVE_PREFIX))
endif
endif
endif
endif
# 3) Bundled headers fallback (external/libarchive-3.8.6)
ifeq ($(_HAS_ARCHIVE),0)
_BUNDLED_ARCHIVE_H := $(wildcard external/libarchive-3.8.6/libarchive/archive.h)
ifneq ($(_BUNDLED_ARCHIVE_H),)
_HAS_ARCHIVE := 1
CFLAGS += -I./external/libarchive-3.8.6/libarchive
ifeq ($(wildcard external/libarchive-3.8.6-compiled/.libs/libarchive.a),)
LIBS += -larchive
else
LIBS += external/libarchive-3.8.6-compiled/.libs/libarchive.a
endif
$(info [INFO] libarchive: using bundled headers (external/libarchive-3.8.6))
endif
endif
# 4) Not found — disable
ifeq ($(_HAS_ARCHIVE),0)
$(info [INFO] libarchive not found — extraction disabled (install: brew install libarchive))
override ENABLE_LIBARCHIVE := 0
else
override ENABLE_LIBARCHIVE := 1
CFLAGS += -DENABLE_LIBARCHIVE=1
endif
endif
endif
else
override ENABLE_LIBARCHIVE := 0
endif
# ── libcurl detection ─────────────────────────────────────────────────────
# zftpd deliberately uses external libcurl instead of maintaining an HTTP/TLS
# reimplementation. PS5 consumes the official PacBrew ps5-payload-curl port.
ifeq ($(ENABLE_LIBCURL),1)
ifeq ($(TARGET),ps5)
PS5_PKG_CONFIG ?= $(PS5_PAYLOAD_SDK)/bin/prospero-pkg-config
_HAS_PS5_CURL := $(shell test -x $(PS5_PKG_CONFIG) && $(PS5_PKG_CONFIG) --exists libcurl && echo 1 || echo 0)
ifneq ($(_HAS_PS5_CURL),1)
$(error PS5 zhttp requires PacBrew ps5-payload-curl)
endif
CFLAGS += -DENABLE_LIBCURL=1 $(shell $(PS5_PKG_CONFIG) --cflags libcurl)
LIBS += $(shell $(PS5_PKG_CONFIG) --libs libcurl)
PS5_CA_BUNDLE ?= $(PS5_PAYLOAD_SDK)/target/user/homebrew/etc/ca-bundle.crt
ifeq ($(wildcard $(PS5_CA_BUNDLE)),)
$(error PS5 libcurl CA bundle not found: $(PS5_CA_BUNDLE))
endif
GENERATED_CA_HEADER := $(BUILD_DIR)/generated/zftpd_ca_bundle.h
CFLAGS += -DZFTPD_EMBEDDED_CA_BUNDLE=1 -I$(BUILD_DIR)/generated
CURL_CA_BUNDLE_SRC := $(PS5_CA_BUNDLE)
$(info [INFO] URL downloads: external PacBrew libcurl + embedded CA bundle)
else ifeq ($(TARGET),ps4)
# PacBrew ps4-openorbis portlibs (libcurl + mbedTLS + zlib) as unpacked by
# tools/fetch_pacbrew_ps4.sh. Only the headers and the static libraries
# are used: the curl wrapper the package ships emits the OpenOrbis
# toolchain's own link flags, which do not belong in a payload-SDK link.
PS4_PACBREW_ROOT ?= $(shell for d in $(abspath external/pacbrew/ps4/openorbis) /opt/pacbrew/ps4/openorbis; do [ -f "$$d/usr/lib/libcurl.a" ] && { printf '%s' "$$d"; break; }; done)
ifneq ($(strip $(PS4_PACBREW_ROOT)),)
# The OpenOrbis build reaches errno through musl's __errno_location()
# and uses fnmatch()/execl(); the payload libc has none of them.
SOURCES += src/platform/ps4/ps4_pacbrew_compat.c
CFLAGS += -DENABLE_LIBCURL=1 -DCURL_STATICLIB -I$(PS4_PACBREW_ROOT)/usr/include
LIBS += -L$(PS4_PACBREW_ROOT)/usr/lib -lcurl -lmbedtls -lmbedx509 -lmbedcrypto -lz -lSceNet
# Same trust-store contract as PS5: HTTPS verification must not degrade
# silently, so the Mozilla bundle is embedded at build time.
PS4_CA_BUNDLE ?= $(PS4_PACBREW_ROOT)/usr/etc/ca-bundle.crt
ifeq ($(wildcard $(PS4_CA_BUNDLE)),)
$(error PS4 libcurl CA bundle not found: $(PS4_CA_BUNDLE) — re-run tools/fetch_pacbrew_ps4.sh or set PS4_CA_BUNDLE=<path>)
endif
GENERATED_CA_HEADER := $(BUILD_DIR)/generated/zftpd_ca_bundle.h
CFLAGS += -DZFTPD_EMBEDDED_CA_BUNDLE=1 -I$(BUILD_DIR)/generated
CURL_CA_BUNDLE_SRC := $(PS4_CA_BUNDLE)
$(info [INFO] URL downloads: PacBrew PS4 portlibs ($(PS4_PACBREW_ROOT)) + embedded CA bundle)
else
# A toolchain-provided pkg-config entry (or curl-config) still works.
# Probe the target environment only — a host libcurl would link code built
# for the wrong OS.
PS4_PKG_CONFIG ?= $(shell for c in $(PS4_PAYLOAD_SDK)/bin/orbis-pkg-config $(PS4_PAYLOAD_SDK)/bin/openorbis-pkg-config $(OPENORBIS)/bin/openorbis-pkg-config; do [ -x "$$c" ] && { printf '%s' "$$c"; break; }; done)
PS4_CURL_CONFIG ?= $(shell for c in $(PS4_PAYLOAD_SDK)/bin/orbis-curl-config $(PS4_PAYLOAD_SDK)/bin/curl-config $(PS4_PAYLOAD_SDK)/bin/openorbis-curl-config; do [ -x "$$c" ] && { printf '%s' "$$c"; break; }; done)
_HAS_PS4_CURL := $(shell if [ -n "$(strip $(PS4_PKG_CONFIG))" ] && $(PS4_PKG_CONFIG) --exists libcurl >/dev/null 2>&1; then echo 1; elif [ -n "$(strip $(PS4_CURL_CONFIG))" ]; then echo 1; fi)
ifneq ($(_HAS_PS4_CURL),1)
$(info [INFO] PS4 external libcurl not found — URL downloader disabled)
$(info [INFO] run tools/fetch_pacbrew_ps4.sh to install PacBrew ps4-openorbis-portlibs)
$(info [INFO] or point PS4_PKG_CONFIG / PS4_CURL_CONFIG at a target config tool)
override ENABLE_LIBCURL := 0
else
ifneq ($(strip $(PS4_PKG_CONFIG)),)
PS4_CURL_CFLAGS := $(shell $(PS4_PKG_CONFIG) --cflags libcurl)
PS4_CURL_LIBS := $(shell $(PS4_PKG_CONFIG) --static --libs libcurl)
else
PS4_CURL_CFLAGS := $(shell $(PS4_CURL_CONFIG) --cflags)
PS4_CURL_LIBS := $(shell $(PS4_CURL_CONFIG) --static-libs)
endif
CFLAGS += -DENABLE_LIBCURL=1 $(PS4_CURL_CFLAGS)
LIBS += $(PS4_CURL_LIBS)
# Same trust-store contract as PS5: the PacBrew curl package ships the
# Mozilla bundle, and HTTPS verification must not degrade silently.
PS4_CA_BUNDLE ?= $(PS4_PAYLOAD_SDK)/target/user/homebrew/etc/ca-bundle.crt
ifeq ($(wildcard $(PS4_CA_BUNDLE)),)
$(error PS4 libcurl CA bundle not found: $(PS4_CA_BUNDLE) — override with PS4_CA_BUNDLE=<path>)
endif
GENERATED_CA_HEADER := $(BUILD_DIR)/generated/zftpd_ca_bundle.h
CFLAGS += -DZFTPD_EMBEDDED_CA_BUNDLE=1 -I$(BUILD_DIR)/generated
CURL_CA_BUNDLE_SRC := $(PS4_CA_BUNDLE)
$(info [INFO] URL downloads: PacBrew PS4 libcurl + embedded CA bundle)
endif
endif
else
_HAS_CURL := $(shell echo '\#include <curl/curl.h>' | $(CC) -xc -fsyntax-only - 2>/dev/null && echo 1 || echo 0)
ifneq ($(_HAS_CURL),1)
$(info [INFO] libcurl headers not found — disabling ENABLE_LIBCURL)
override ENABLE_LIBCURL := 0
else
CFLAGS += -DENABLE_LIBCURL=1
LIBS += -lcurl
endif
endif
endif
ifeq ($(ENABLE_LIBCURL),1)
SOURCES += src/transfer/backend_curl.c
endif
# Native BitTorrent engine for magnet links. PS5 needs a libtorrent-rasterbar
# port installed into its payload SDK; host libraries must never be linked
# into a console payload.
ENABLE_LIBTORRENT ?= 0
ifeq ($(ENABLE_LIBTORRENT),1)
ifeq ($(TARGET),ps5)
PS5_PKG_CONFIG ?= $(PS5_PAYLOAD_SDK)/bin/prospero-pkg-config
ifneq ($(shell test -x $(PS5_PKG_CONFIG) && $(PS5_PKG_CONFIG) --exists libtorrent-rasterbar && echo 1),1)
$(error PS5 magnet downloads require a PS5 libtorrent-rasterbar port in the payload SDK)
endif
TORRENT_CFLAGS := $(shell $(PS5_PKG_CONFIG) --cflags libtorrent-rasterbar)
TORRENT_LIBS := $(shell $(PS5_PKG_CONFIG) --static --libs libtorrent-rasterbar)
else
ifneq ($(shell pkg-config --exists libtorrent-rasterbar 2>/dev/null && echo 1),1)
$(error ENABLE_LIBTORRENT=1 requires libtorrent-rasterbar development files)
endif
TORRENT_CFLAGS := $(shell pkg-config --cflags libtorrent-rasterbar)
TORRENT_LIBS := $(shell pkg-config --libs libtorrent-rasterbar)
ifeq ($(HOST_OS),Darwin)
TORRENT_BOOST_PREFIX := $(shell brew --prefix boost 2>/dev/null)
TORRENT_CFLAGS += -I$(TORRENT_BOOST_PREFIX)/include
TORRENT_OPENSSL_PREFIX := $(shell brew --prefix openssl@3 2>/dev/null)
TORRENT_CFLAGS += -I$(TORRENT_OPENSSL_PREFIX)/include
endif
endif
CFLAGS += -DENABLE_LIBTORRENT=1
TORRENT_CXXFLAGS := -std=c++17 -Wall -Wextra -Wpedantic -Werror -O2 -fPIE \
$(PLATFORM_DEFS) -I./include $(TORRENT_CFLAGS)
ifeq ($(TARGET),ps5)
TORRENT_CXXFLAGS += -fvisibility=hidden -fno-inline-functions
endif
LIBS += $(TORRENT_LIBS)
TORRENT_OBJECT := $(OBJ_DIR)/transfer/backend_torrent.o
CXX_LINK := $(CXX)
else
CXX_LINK := $(CC)
endif
# ── libnfs detection ──────────────────────────────────────────────────────
ifeq ($(ENABLE_LIBNFS),1)
ifeq ($(TARGET),ps5)
PS5_PKG_CONFIG ?= $(PS5_PAYLOAD_SDK)/bin/prospero-pkg-config
_HAS_LIBNFS := $(shell test -x $(PS5_PKG_CONFIG) && $(PS5_PKG_CONFIG) --exists libnfs && echo 1 || echo 0)
ifneq ($(_HAS_LIBNFS),1)
$(error PS5 NFS transfers require PacBrew ps5-payload-libnfs)
endif
CFLAGS += -DENABLE_LIBNFS=1 $(shell $(PS5_PKG_CONFIG) --cflags libnfs)
LIBS += $(shell $(PS5_PKG_CONFIG) --libs libnfs)
else
_HAS_LIBNFS := $(shell pkg-config --exists libnfs 2>/dev/null && echo 1 || echo 0)
ifneq ($(_HAS_LIBNFS),1)
$(error ENABLE_LIBNFS=1 requires libnfs development files)
endif
CFLAGS += -DENABLE_LIBNFS=1 $(shell pkg-config --cflags libnfs)
LIBS += $(shell pkg-config --static --libs libnfs)
endif
SOURCES += src/transfer/backend_nfs.c
endif
# Object files (handle both src/ and mcp/src/ paths)
OBJECTS := $(patsubst src/%.c,$(OBJ_DIR)/%.o,$(filter src/%.c,$(SOURCES)))
OBJECTS += $(patsubst mcp/src/%.c,$(OBJ_DIR)/mcp/%.o,$(filter mcp/src/%.c,$(SOURCES)))
OBJECTS += $(patsubst $(BUILD_DIR)/generated/%.c,$(OBJ_DIR)/generated/%.o,$(filter $(BUILD_DIR)/generated/%.c,$(SOURCES)))
OBJECTS += $(TORRENT_OBJECT)
# FFI Object files
FFI_SOURCES := ffi/c_core/pal_ffi.c
FFI_OBJECTS := $(patsubst ffi/%.c,$(OBJ_DIR)/ffi/%.o,$(FFI_SOURCES))
# Object files without main (for unit tests and ffi library)
LIB_OBJECTS := $(filter-out $(OBJ_DIR)/main.o,$(OBJECTS))
LIB_OBJECTS := $(filter-out $(OBJ_DIR)/app/main.o,$(OBJECTS))
# Dependency files
DEPENDS := $(patsubst $(OBJ_DIR)/%.o,$(DEP_DIR)/%.d,$(OBJECTS))
DEPENDS := $(patsubst $(OBJ_DIR)/%.o,$(DEP_DIR)/%.d,$(filter-out $(OBJ_DIR)/mcp/%.o,$(OBJECTS)))
DEPENDS += $(patsubst $(OBJ_DIR)/mcp/%.o,$(DEP_DIR)/mcp/%.d,$(filter $(OBJ_DIR)/mcp/%.o,$(OBJECTS)))
#============================================================================
# BUILD TARGETS
#============================================================================
.PHONY: all clean distclean install test help bin deploy deploy-i deploy-nc doctor-ps4
.PHONY: all-platforms release-all debug-all ffi ffi-java ffi-rust ffi-python
.PHONY: all-platforms release-all debug-all ffi ffi-java ffi-rust ffi-python resources
.PHONY: ps5-hook-blob
# ============================================================================
# PS5 NET FILTER HOOK — Kernel-safe compilation pipeline
#
# The hook functions (src/platform/ps5/ps5_net_filter_hook.c) run in ring-0 (kernel mode)
# and require special compiler flags that differ from the normal build.
#
# Pipeline:
# 1. Compile hook with kernel-safe flags → ps5_net_filter_hook.o
# 2. Extract .text.hook_connect and .text.hook_sendto sections → .bin
# 3. Generate C byte-array header → ps5_net_filter_hook_blob.h
#
# The blob header is included by ps5_net_filter.c to replace the placeholder
# byte arrays (g_hook_connect_code[], g_hook_sendto_code[]).
#
# Run manually before the PS5 build:
# make ps5-hook-blob
# ============================================================================
ifeq ($(TARGET),ps5)
HOOK_OBJ := $(OBJ_DIR)/ps5_net_filter_hook.o
HOOK_BIN := $(OBJ_DIR)/ps5_net_filter_hook.bin
HOOK_BLOB_H := $(BUILD_DIR)/generated/ps5/ps5_net_filter_hook_blob.h
# Kernel-safe compiler flags (MUST differ from normal CFLAGS)
HOOK_CFLAGS := \
-DPS5_HOOK_BUILD \
-DPLATFORM_PS5 \
-std=c11 \
-O2 \
-fno-stack-protector \
-mno-red-zone \
-fPIC \
-mcmodel=large \
-fno-plt \
-fno-common \
-fno-builtin \
-fno-exceptions \
-fomit-frame-pointer \
-I include/platform/ps5/
ps5-hook-blob: $(HOOK_BLOB_H)
@echo " [BLOB] $< generated ($(shell wc -c < $(HOOK_BIN) 2>/dev/null || echo '?') bytes)"
$(HOOK_BLOB_H): $(HOOK_BIN)
@echo " [XXD] $@"
@mkdir -p $(dir $@)
@xxd -i $< > $@
$(HOOK_BIN): $(HOOK_OBJ)
@echo " [OBJCOPY] $@"
@$(OBJCOPY) -O binary \
--only-section=.text.hook_connect \
--only-section=.text.hook_sendto \
$< $@
$(HOOK_OBJ): src/platform/ps5/ps5_net_filter_hook.c | $(OBJ_DIR)
@echo " [HOOK-CC] $<"
@$(CC) $(HOOK_CFLAGS) -c $< -o $@
endif # TARGET=ps5
resources: $(HTTP_RESOURCES_C)
$(HTTP_RESOURCES_C): tools/generate_resources.py $(WEB_RESOURCE_FILES)
@echo " [GEN] $@"
@mkdir -p $(dir $@)
@python3 tools/generate_resources.py > $@
.DEFAULT_GOAL := all
@@ -285,7 +740,7 @@ FFI_OUTPUT := $(BIN_DIR)/libzftpd_ffi.so
FFI_LDFLAGS := -shared
endif
$(BIN_DIR) $(OBJ_DIR) $(DEP_DIR) $(BUILD_DIR)/tests $(OBJ_DIR)/ffi/c_core:
$(BIN_DIR) $(OBJ_DIR) $(DEP_DIR) $(BUILD_DIR)/tests $(OBJ_DIR)/ffi/c_core $(OBJ_DIR)/mcp:
@mkdir -p $@
ifeq ($(filter $(TARGET),ps4 ps5),)
@@ -301,7 +756,7 @@ $(PROJECT): all
$(OUTPUT_ELF): $(OBJECTS) | $(BIN_DIR)
@echo " [LD] $@"
@mkdir -p $(BIN_DIR)
@$(CC) $(LDFLAGS) -o $@ $^ $(LIBS)
@$(CXX_LINK) $(LDFLAGS) -o $@ $^ $(LIBS)
@echo "Build complete: $(PROJECT) ($(TARGET), $(BUILD_TYPE))"
# FFI Build Targets
@@ -327,7 +782,7 @@ endif
$(FFI_OUTPUT): $(LIB_OBJECTS) $(FFI_OBJECTS) | $(BIN_DIR)
@echo " [LD] $@ (Shared Library)"
@mkdir -p $(BIN_DIR)
@$(CC) $(LDFLAGS) $(FFI_LDFLAGS) -fPIC -o $@ $(LIB_OBJECTS) $(FFI_OBJECTS) $(LIBS)
@$(CXX_LINK) $(LDFLAGS) $(FFI_LDFLAGS) -fPIC -o $@ $(LIB_OBJECTS) $(FFI_OBJECTS) $(LIBS)
@echo "FFI C-Core built: $@"
# Build all supported platforms (best-effort: includes only toolchains found on the host).
@@ -411,7 +866,10 @@ release-matrix:
for t in $(TARGETS_ALL); do \
for z in $(ZHTTP_VARIANTS); do \
echo "==> Building $$t (release, ENABLE_ZHTTPD=$$z)"; \
$(MAKE) TARGET=$$t BUILD_TYPE=release ENABLE_ZHTTPD=$$z clean all; \
$(MAKE) TARGET=$$t BUILD_TYPE=release ENABLE_ZHTTPD=$$z \
ENABLE_LIBARCHIVE=$(ENABLE_LIBARCHIVE) \
ENABLE_LIBCURL=$(ENABLE_LIBCURL) \
clean all; \
done; \
done
@@ -421,18 +879,44 @@ debug-all:
echo "==> Building $$t (debug)"; \
$(MAKE) TARGET=$$t BUILD_TYPE=debug clean all; \
done
# Generate the embedded trust store from PacBrew's Mozilla CA bundle (PS4/PS5).
ifneq ($(strip $(CURL_CA_BUNDLE_SRC)),)
$(OBJ_DIR)/transfer/backend_curl.o: $(GENERATED_CA_HEADER)
$(GENERATED_CA_HEADER): $(CURL_CA_BUNDLE_SRC) tools/embed_binary.py
@echo " [GEN] $@"
@python3 tools/embed_binary.py $< $@ zftpd_ca_bundle
endif
# Compile C source files
$(OBJ_DIR)/transfer/backend_torrent.o: src/transfer/backend_torrent.cpp | $(OBJ_DIR) $(DEP_DIR)
@echo " [CXX] $<"
@mkdir -p $(dir $@) $(dir $(DEP_DIR)/transfer/backend_torrent.d)
@$(CXX) $(TORRENT_CXXFLAGS) -MMD -MP -MF $(DEP_DIR)/transfer/backend_torrent.d -MT $@ -c $< -o $@
$(OBJ_DIR)/%.o: src/%.c | $(OBJ_DIR) $(DEP_DIR)
@echo " [CC] $<"
@mkdir -p $(dir $@) $(dir $(DEP_DIR)/$*.d)
@$(CC) $(CFLAGS) -MMD -MP -MF $(DEP_DIR)/$*.d -MT $@ -c $< -o $@
# Compile generated C sources without writing into the source tree.
$(OBJ_DIR)/generated/%.o: $(BUILD_DIR)/generated/%.c | $(OBJ_DIR) $(DEP_DIR)
@echo " [CC] $<"
@mkdir -p $(dir $@) $(dir $(DEP_DIR)/generated/$*.d)
@$(CC) $(CFLAGS) -MMD -MP -MF $(DEP_DIR)/generated/$*.d -MT $@ -c $< -o $@
# Compile FFI C source files (with -fPIC for shared library)
$(OBJ_DIR)/ffi/%.o: ffi/%.c | $(OBJ_DIR)/ffi/c_core
@echo " [CC] $< (FFI)"
@mkdir -p $(dir $@) $(dir $(DEP_DIR)/ffi/$*.d)
@$(CC) $(CFLAGS) -fPIC -MMD -MP -MF $(DEP_DIR)/ffi/$*.d -MT $@ -c $< -o $@
# Compile MCP C source files
$(OBJ_DIR)/mcp/%.o: mcp/src/%.c | $(OBJ_DIR)/mcp
@echo " [CC] $< (MCP)"
@mkdir -p $(dir $@) $(dir $(DEP_DIR)/mcp/$*.d)
@$(CC) $(CFLAGS) -MMD -MP -MF $(DEP_DIR)/mcp/$*.d -MT $@ -c $< -o $@
# Include dependency files
-include $(DEPENDS)
@@ -476,26 +960,71 @@ TEST_BINS += $(BUILD_DIR)/tests/test_path_security
TEST_BINS += $(BUILD_DIR)/tests/test_buffer_pool
TEST_BINS += $(BUILD_DIR)/tests/test_scratch
TEST_BINS += $(BUILD_DIR)/tests/test_alloc
TEST_BINS += $(BUILD_DIR)/tests/test_http_query
TEST_BINS += $(BUILD_DIR)/tests/test_mlst_ascii
TEST_BINS += $(BUILD_DIR)/tests/test_list_flag
TEST_BINS += $(BUILD_DIR)/tests/test_instance
TEST_BINS += $(BUILD_DIR)/tests/test_instance_control
TEST_BINS += $(BUILD_DIR)/tests/test_chmod
TEST_BINS += $(BUILD_DIR)/tests/test_copy_atomic
TEST_BINS += $(BUILD_DIR)/tests/test_ftp_commands
TEST_BINS += $(BUILD_DIR)/tests/test_fileio_tree
TEST_BINS += $(BUILD_DIR)/tests/test_fileio_basic
TEST_BINS += $(BUILD_DIR)/tests/test_sendfile_console
TEST_BINS += $(BUILD_DIR)/tests/test_ps5_fw_offsets
ifeq ($(ENABLE_ZHTTPD),1)
TEST_BINS += $(BUILD_DIR)/tests/test_transfer
TEST_BINS += $(BUILD_DIR)/tests/test_transfer_parallel
TEST_BINS += $(BUILD_DIR)/tests/test_transfer_state
TEST_BINS += $(BUILD_DIR)/tests/test_transfer_queue
TEST_BINS += $(BUILD_DIR)/tests/test_transfer_orphans
ifeq ($(ENABLE_LIBTORRENT),1)
TEST_BINS += $(BUILD_DIR)/tests/test_transfer_torrent
endif
TEST_BINS += $(BUILD_DIR)/tests/test_http_parser
TEST_BINS += $(BUILD_DIR)/tests/test_http_upload
TEST_BINS += $(BUILD_DIR)/tests/test_http_server
TEST_BINS += $(BUILD_DIR)/tests/test_http_response_stream
TEST_BINS += $(BUILD_DIR)/tests/test_http_api_common
TEST_BINS += $(BUILD_DIR)/tests/test_http_files
TEST_BINS += $(BUILD_DIR)/tests/test_http_process
TEST_BINS += $(BUILD_DIR)/tests/test_http_system
TEST_BINS += $(BUILD_DIR)/tests/test_http_games
TEST_BINS += $(BUILD_DIR)/tests/test_archive_path
TEST_BINS += $(BUILD_DIR)/tests/test_builtin_unzip
TEST_BINS += $(BUILD_DIR)/tests/test_zip_writer
TEST_BINS += $(BUILD_DIR)/tests/test_pal_volume
TEST_BINS += $(BUILD_DIR)/tests/test_http_archive
TEST_BINS += $(BUILD_DIR)/tests/test_dump_job
endif
ifeq ($(filter $(TARGET),linux macos),)
test: $(OUTPUT_BIN)
@echo "Tests skipped for TARGET=$(TARGET)"
else
test: $(OUTPUT_ELF) $(OUTPUT_BIN) $(TEST_BINS)
test: $(OUTPUT_ELF) $(TEST_BINS)
@echo "Running tests..."
@for t in $(TEST_BINS); do ./$$t; done
@set -e; for t in $(TEST_BINS); do ./$$t; done
endif
$(BUILD_DIR)/tests/test_http_query: tests/test_http_query.c src/http_api.c \
src/http_response.c src/http_parser.c src/http_csrf.c src/http_resources.c \
| $(BUILD_DIR)/tests
$(BUILD_DIR)/tests/test_instance_control: tests/test_instance_control.c src/app/instance_control.c tests/instance_control_stubs/sys/user.h tests/instance_control_stubs/sys/sysctl.h $(LIB_OBJECTS) | $(BUILD_DIR)/tests
@echo " [CC] $<"
@$(CC) $(CFLAGS) -DFTP_AUTH_DELAY=0 -o $@ $^ $(LDFLAGS) $(LIBS)
@$(CXX_LINK) $(CFLAGS) -Itests/instance_control_stubs -o $@ -x c $< -x none $(LIB_OBJECTS) $(LDFLAGS) $(LIBS)
$(BUILD_DIR)/tests/test_sendfile_console: tests/test_sendfile_console.c src/platform/fileio/sendfile.c tests/sendfile_stubs/sys/mount.h $(LIB_OBJECTS) | $(BUILD_DIR)/tests
@echo " [CC] $<"
@$(CXX_LINK) $(CFLAGS) -Itests/sendfile_stubs -o $@ -x c $< -x none $(LIB_OBJECTS) $(LDFLAGS) $(LIBS)
$(BUILD_DIR)/tests/test_http_query: tests/test_http_query.c $(LIB_OBJECTS) | $(BUILD_DIR)/tests
@echo " [CC] $<"
@$(CXX_LINK) $(CFLAGS) -DFTP_AUTH_DELAY=0 -DFTP_PORT_ALLOW_FOREIGN_IP=1 -o $@ -x c $< -x none $(LIB_OBJECTS) $(LDFLAGS) $(LIBS)
$(BUILD_DIR)/tests/test_transfer_torrent: tests/test_transfer_torrent.cpp $(TORRENT_OBJECT) | $(BUILD_DIR)/tests
@echo " [CXX] $<"
@$(CXX) $(TORRENT_CXXFLAGS) -o $@ $< $(TORRENT_OBJECT) $(LDFLAGS) $(TORRENT_LIBS) -lpthread
$(BUILD_DIR)/tests/%: tests/%.c $(LIB_OBJECTS) | $(BUILD_DIR)/tests
@echo " [CC] $<"
@$(CC) $(CFLAGS) -DFTP_AUTH_DELAY=0 -o $@ $< $(LIB_OBJECTS) $(LDFLAGS) $(LIBS)
@$(CXX_LINK) $(CFLAGS) -DFTP_AUTH_DELAY=0 -DFTP_PORT_ALLOW_FOREIGN_IP=1 -o $@ -x c $< -x none $(LIB_OBJECTS) $(LDFLAGS) $(LIBS)
bin: $(OUTPUT_BIN)
@@ -572,12 +1101,14 @@ help:
@echo " analyze - Run static analysis (requires clang)"
@echo " test - Run test suite"
@echo " help - Display this help message"
@echo " resources - Generate embedded web resources under build/.../generated"
@echo ""
@echo "Variables:"
@echo " TARGET - Target platform (linux, macos, ps3, ps4, ps5)"
@echo " Default: linux"
@echo " BUILD_TYPE - Build configuration (debug, release)"
@echo " Default: release"
@echo " TARGET - Target platform (linux, macos, ps3, ps4, ps5)"
@echo " BUILD_TYPE - Build configuration (debug, release)"
@echo " ENABLE_LIBARCHIVE - Enable archive extraction (0/1, requires libarchive)"
@echo " ENABLE_LIBCURL - Enable URL downloads (0/1, requires libcurl)"
@echo " WEB_DEPLOY_DIR - Deprecated (web UI is now embedded in binary)"
@echo ""
@echo "Examples:"
@echo " make # Build for Linux (release)"
@@ -585,11 +1116,15 @@ help:
@echo " make TARGET=ps5 # Build for PS5"
@echo " make BUILD_TYPE=debug # Build debug version"
@echo " make TARGET=ps4 BUILD_TYPE=debug # PS4 debug build"
@echo " make ENABLE_LIBARCHIVE=1 ENABLE_LIBCURL=1 # With extract + download"
@echo " make resources # Regenerate embedded web resources"
@echo ""
@echo "Current configuration:"
@echo " Target: $(TARGET)"
@echo " Build type: $(BUILD_TYPE)"
@echo " Compiler: $(CC)"
@echo " libarchive: $(ENABLE_LIBARCHIVE)"
@echo " libcurl: $(ENABLE_LIBCURL)"
@echo ""
#============================================================================
@@ -602,3 +1137,58 @@ compile_commands.json:
@echo "Compilation database generated."
.PHONY: compile_commands.json
#============================================================================
#============================================================================
# WEB DEPLOY — DEPRECATED
#
# All web assets are now embedded directly in the binary via
# http_resources.c (run make resources to regenerate).
# The web-deploy target is kept as a no-op for backward compatibility.
#============================================================================
web-deploy:
@echo " [WEB] web-deploy is deprecated — web UI is now embedded in the binary"
@echo " [WEB] Resources are generated automatically under build/.../generated"
@echo " [WEB] Done — $(shell find web -path web/legacy -prune -o \( -name '*.css' -o -name '*.js' \) -print | wc -l | tr -d ' ') files deployed"
#============================================================================
# Flag changes must invalidate object files.
#
# Objects do not record the flags they were built with, so toggling a feature
# (ENABLE_LIBTORRENT, ENABLE_PKG_INSTALL, ...) used to relink stale objects and
# fail with undefined symbols. The stamp is rewritten only when the flags
# actually change, and every object depends on it, so a real change rebuilds.
#============================================================================
FLAG_STAMP := $(OBJ_DIR)/.build-flags
.PHONY: FORCE
FORCE:
$(FLAG_STAMP): FORCE
@mkdir -p $(OBJ_DIR)
@printf '%s\n' '$(CFLAGS)' '$(LDFLAGS)' '$(LIBS)' > $@.tmp
@if ! cmp -s $@.tmp $@; then \
echo " [FLAGS] build flags changed — objects will be rebuilt"; \
mv -f $@.tmp $@; \
fi
@rm -f $@.tmp
$(OBJECTS) $(LIB_OBJECTS): $(FLAG_STAMP)
#============================================================================
# PS5 installer helper
#============================================================================
ifneq ($(INSTALL_HELPER_ELF),)
$(INSTALL_HELPER_ELF): src/platform/ps5/install_helper/install_helper.c | $(BIN_DIR)
@echo " [CC] $<"
@$(CXX_LINK) $(CFLAGS) -o $@ $< $(LDFLAGS) $(LIBS) -lSceIpmi -lSceAppInstUtil
$(INSTALL_HELPER_BLOB): $(INSTALL_HELPER_ELF) tools/gen_blob.py
@mkdir -p $(dir $@)
@python3 tools/gen_blob.py $< install_helper_elf $@
$(OBJ_DIR)/generated/install_helper_blob.o: $(INSTALL_HELPER_BLOB)
endif
+170 -11
View File
@@ -24,6 +24,13 @@ Runs anywhere POSIX runs. Saturates Gigabit. Ships a console payload too.
---
## Repository architecture
The C code is split into responsibility-based modules (`ftp`, `http`, `transfer`,
`archive`, `runtime`, `platform`, and `app`) rather than a flat `src/`/`include/`
namespace. Generated assets live under `build/`, not in the source tree. See
[`docs/architecture.md`](docs/architecture.md) for the dependency rules and layout.
## Overview
`zftpd` is a high-performance FTP server written in C11. It was designed around a single idea: **the data path should be as fast as the hardware allows**, with no unnecessary work anywhere between file and socket.
@@ -45,14 +52,14 @@ Philosophy
## ⚡ Performance
> `zftpd` saturates a full Gigabit Ethernet link — **~112 MB/s sustained** in both directions.
> `zftpd` saturates a full Gigabit Ethernet link — **~117 MB/s sustained** in both directions.
This is the physical ceiling of a 1 GbE connection. It is achieved out of the box, with no kernel tuning required.
```
Benchmark — single stream, wired 1 GbE, plain transfer
Download ████████████████████████████████████████████ 112 MB/s
Download ████████████████████████████████████████████ 117 MB/s
Upload █████████████████████████████████████████ 108 MB/s
────────
Physical ceiling (1 GbE) 125 MB/s
@@ -80,10 +87,15 @@ This is the physical ceiling of a 1 GbE connection. It is achieved out of the bo
**Transfer engine**
- `sendfile` zero-copy fast path (Linux · BSD · macOS)
- PS4/PS5 FTP and HTTP downloads use buffered reads on exFAT/FAT, PFS and
nullfs mounts, and when mount detection fails, to avoid incompatible vnode
pagers. Other filesystems retain the zero-copy path.
- Fallback to buffered I/O when encrypted
- Backpressure-aware send loop, EINTR-safe
- Upload resume: `REST` + `STOR`
- Append mode: `APPE`
- Server-side copy: `CPFR`/`CPTO`, `COPY` *(async background thread)*
- Cross-device move: `RNTO` fallback with async copy
- Transfer rate limiting via token bucket *(compile-time, opt-in)*
**Connection handling**
@@ -109,6 +121,7 @@ This is the physical ceiling of a 1 GbE connection. It is achieved out of the bo
**Platform extras**
- Linux, macOS, PS4, PS5
- On-screen IP/port notification on PS4 and PS5
- Rest Mode resilience — listener recreate + daemon `instance_id` *(see [docs/restmode.md](docs/restmode.md))*
- ZHTTP web file explorer *(compile-time, see [ZHTTP](#-zhttp))*
</td>
@@ -127,11 +140,12 @@ This is the physical ceiling of a 1 GbE connection. It is achieved out of the bo
| Directory listing | `LIST` `NLST` `MLSD` `MLST` |
| File transfer | `RETR` `STOR` `APPE` `REST` |
| File management | `DELE` `RMD` `MKD` `RNFR` `RNTO` |
| Server-side copy | `CPFR` `CPTO` `COPY` — async background thread |
| Data connection | `PORT` `PASV` `EPSV` |
| Metadata | `SIZE` `MDTM` `STAT` `SYST` `FEAT` `HELP` |
| Transfer parameters | `TYPE` `MODE` `STRU` |
| Negotiation | `OPTS` `CLNT` |
| Site extensions | `SITE CHMOD` |
| Site extensions | `SITE CHMOD` — change Unix permission bits |
| Encryption | `AUTH XCRYPT` — ChaCha20 with PSK *(opt-in)* |
</details>
@@ -202,10 +216,81 @@ To make these persistent, add them to `/etc/sysctl.conf`.
- Launch after the system is fully booted and the loader is ready. The on-screen notification will display the IP and port.
- For maximum throughput: direct cable from console to PC, static IPs, no router in between.
- Avoid initiating transfers while background downloads or system updates are active — the network stack is shared.
- If you see **"payload already loaded"**: a previous instance is still active. `zftpd` will attempt to terminate it and restart on the default port. If that fails, it tries up to 9 subsequent ports (`FTP_DEFAULT_PORT+1` … `+9`).
- If you see **"payload already loaded"**: a previous instance is still active. The new payload asks it to shut down on its control port (`127.0.0.1:28888`) and the previous instance runs its own teardown even when its threads are blocked, so it always releases the FTP, HTTP and MCP ports. Instances that predate the control port — or one wedged after accepting it — are identified by thread name and stopped, never signalling the loader's process group. If a payload still holds the console, the new instance stops instead of binding ports another instance owns.
</details>
</details>
<details>
<summary><b>PS5 — firmware-dependent transfer speed</b></summary>
<br/>
Transfer speed to the **internal storage** (`/data/...`) varies significantly across PS5 firmware versions. This is **not** a `zftpd` limitation — it is caused by Sony's kernel-level I/O driver improvements across firmware updates.
```
Upload speed to internal storage (wired 1 GbE, measured across multiple consoles)
FW 4.03 ████████ ~20 MB/s
FW 8.60 ████████████████████ ~50 MB/s
FW 9.00 ██████████████████████████████████ ~85 MB/s
FW 10.00 ████████████████████████████████████████████ ~113 MB/s
────────
Physical ceiling (1 GbE) 125 MB/s
```
**Why it happens:**
- The PS5 internal storage uses the PFS (PlayStation File System) with mandatory block-level encryption. Every `write()` syscall goes through the kernel's crypto + NVMe pipeline.
- Sony has incrementally improved this pipeline (write scheduling, page cache, NVMe queue depth) across firmware releases. On FW 10.00 the kernel saturates Gigabit.
- This is **not** related to PFS crypto cost alone — if it were, speeds would be constant across all firmware. The scaling pattern proves the bottleneck is kernel I/O scheduling, not encryption.
**External USB storage** (`/mnt/usb0/...`, typically exFAT) bypasses PFS entirely and consistently reaches **~113 MB/s** regardless of firmware version.
**What this means for users:**
- On older firmware (< 9.00), internal storage writes are kernel-limited. No FTP server (zftpd, ftpsrv, GoldHEN) can exceed these speeds — the limit is in the OS.
- For maximum speed on older firmware, transfer to **external USB-C storage** instead.
- On FW 9.00+ the internal storage speed approaches Gigabit saturation.
</details>
---
## 📦 Build & commands
**Make targets (host auto-detection, best-effort toolchains):**
- `make` — build default target (Linux on Linux host, macOS on macOS host) release.
- `make release-all` — release build per platform detected (macos, linux, ps3, ps4, ps5).
- `make debug-all` — debug build per platform detected.
- `make release-matrix` — release build per platform *and* variant `ENABLE_ZHTTPD=0/1`, producing ELF/BIN dove applicabile.
- `make TARGET=<platform> BUILD_TYPE=<release|debug> [ENABLE_ZHTTPD=0|1] clean all` — build singolo.
**Useful Makefile variables:**
- `TARGET`: `linux`, `macos`, `ps3`, `ps4`, `ps5` (auto su host). Case-insensitive.
- `BUILD_TYPE`: `release` (default), `debug`.
- `ENABLE_ZHTTPD`: `1` abilita web UI zhttp (default 0 su console, 1 su PC). Influenza naming: es. `zftpd-ps5-zhttp-v1.3.0.bin`.
- `ARTIFACT_PREFIX`: prefisso binari (default `zftpd`).
- `ffi_langs`: opzionale, per build dei binding FFI.
**Output naming (release):**
- ELF: `build/<target>/release[/ -zhttp]/zftpd-<platform-tag>[-zhttp]-v<version>.elf`
- BIN (console): `... .bin`
**Execution (binaries host):**
```
./build/macos/release/zftpd-macos-$(uname -m)-v1.3.0 -p <port> -d <root>
./build/linux/release/zftpd-linux-$(uname -m)-v1.3.0.elf -p <port> -d <root>
```
Supported options:
- `-p <PORT>` (default 2121)
- `-d <DIR>` root FTP
- `-h` help
---
## 📦 Build
@@ -219,8 +304,8 @@ Output artifacts are versioned and platform-tagged, placed in `build/<target>/<b
| Compiler | C11 — `gcc` or `clang` |
| Build system | `make` |
| `.bin` generation | `objcopy` (binutils or llvm-objcopy); PS4: `orbis-objcopy`; PS5: `prospero-objcopy` |
| PS4 | `PS4_PAYLOAD_SDK` set in environment |
| PS5 | `PS5_PAYLOAD_SDK` set in environment |
| PS4 | `PS4_PAYLOAD_SDK` set in environment; zhttp builds with downloads use the PacBrew PS4 portlibs, installed by `tools/fetch_pacbrew_ps4.sh` |
| PS5 | `PS5_PAYLOAD_SDK` set in environment; zhttp builds require the PacBrew SDK bundle with libcurl + libnfs |
### Commands
@@ -241,6 +326,61 @@ make TARGET=linux test
make TARGET=macos test
```
### Console web downloads
PS4 and PS5 zhttp builds use the maintained PacBrew ports of **libcurl**
(PS5 also **libnfs**). The downloader supports HTTP/HTTPS/FTP/FTPS and
`nfs://` NAS sources; HTTPS certificate verification stays enabled and the
PacBrew Mozilla CA bundle is embedded into the payload at build time.
See [`docs/dependencies.md`](docs/dependencies.md).
PS5 zhttp releases also accept `magnet:?` links. They use a native
libtorrent-rasterbar engine inside the payload; no companion computer is
required. For local PS5 builds, install a PS5 SDK, then run
`bash tools/build_ps5_libtorrent.sh "$PS5_PAYLOAD_SDK"` followed by
`make TARGET=ps5 ENABLE_LIBTORRENT=1 clean all`. The release workflow builds this
dependency automatically.
PS5 extracts ZIP archives with its built-in reader. To enable other formats
supported by libarchive, install PacBrew `ps5-payload-libarchive` in the PS5
SDK and build with `make TARGET=ps5 ENABLE_LIBARCHIVE=1`. ZIP files continue
to use the built-in reader in that build.
PS4 builds detect the PacBrew package through the target toolchain only
(`orbis-pkg-config` / `orbis-curl-config` inside `PS4_PAYLOAD_SDK`, or the
`OPENORBIS` environment), never through the host's libcurl. When the package
is missing the downloader is disabled at build time and the rest of zftpd is
unaffected; the same behaviour is available explicitly with
`ENABLE_LIBCURL=0`. Override `PS4_PKG_CONFIG`, `PS4_CURL_CONFIG` and
`PS4_CA_BUNDLE` to point at a custom install.
#### Download queue and resume
Links pasted into the Transfers view line up in a FIFO queue: `TRANSFER_MAX_CONCURRENT`
downloads run at a time (2 by default, override at build time with
`-DTRANSFER_MAX_CONCURRENT=<n>`) and the rest wait, showing their position.
Queued jobs can be held back or cancelled before they ever start; up to
`TRANSFER_MAX_ACTIVE` (16) jobs are tracked.
HTTP/FTP/NFS downloads write to `<name>.zftpd.part` and rename it after
`fsync()`. Magnet downloads write a `<name>.zftpd.part` directory containing
the torrent files; the directory is renamed after every piece is verified.
Download status, destination and progress are recorded
atomically in `/data/zftpd/transfers.state` (falling back to `/tmp/zftpd/`).
BitTorrent resume data is stored alongside that state file. On startup zftpd
restores the visible list and resumes unfinished jobs from the bytes already
on disk. Paused jobs stay paused, and failed
jobs remain visible for retry. The Transfers view can delete a saved partial
and its record, or use Start over to delete it and start the same link from
zero. Removing a completed record leaves the completed file untouched.
On PS5, `/data/zftpd/instance.pid` is a saved control identity, not a lock.
After a reboot its PID may belong to another process: startup only refuses
replacement for a verified zftpd payload or an instance that authenticated the
shutdown request. Stale or malformed identities are removed after that check.
`transfers.state` is retained for download recovery and never acts as an
instance lock.
### Artifacts
| Platform | Output |
@@ -280,11 +420,11 @@ On startup: `FTP: <ip>:<port>` notification.
## ⚙️ Configuration
All configuration is compile-time, in [`include/ftp_config.h`](include/ftp_config.h).
All configuration is compile-time, in [`include/ftp/ftp_config.h`](include/ftp/ftp_config.h).
| Macro | Default | Notes |
|---|---|---|
| `FTP_DEFAULT_PORT` | `2121` (POSIX) · `2122` (console) | Listening port |
| `FTP_DEFAULT_PORT` | `2121` (POSIX) · `2120` (console) | Listening port |
| `FTP_MAX_SESSIONS` | — | Maximum concurrent client sessions |
| `FTP_SESSION_TIMEOUT` | — | Idle session timeout |
| `FTP_TRANSFER_RATE_LIMIT_BPS` | *disabled* | Token-bucket average rate cap |
@@ -308,13 +448,32 @@ Upload support is enabled automatically alongside ZHTTP (`ENABLE_WEB_UPLOAD=1`).
> **Security:** ZHTTP has no authentication beyond network access. It is designed for local-network use. Do not expose it on a public interface.
After console Rest Mode, ZHTTP auto-reconnects via `/api/status` (see [docs/restmode.md](docs/restmode.md)).
Custom console toasts are available via `GET /api/notify?text=Hello` (useful for Home Assistant and similar local automation — see [docs/API.md](docs/API.md)).
The System view also drives the hardware: fan threshold, network-listener restart and **Blu-ray eject** (`POST /api/system/eject`, hidden on Digital Edition consoles). The eject path comes from [BD-EJ](https://github.com/seregonwar/BD-EJ).
Downloads and previews send the raw file bytes. Console **SELF containers** can be requested decrypted instead, exactly like the FTP path does: enable *Settings → Downloads → Decrypt protected files* (or pass `?decrypt=1` to `/api/file/get`). The kernel pager is swapped in for that transfer only, so every other file — and every other client — keeps getting the on-disk bytes untouched.
Selecting many items (folders included) offers **Download as ZIP**: the archive is built *while* it streams (`/api/archive/zip`), so nothing is written to the console and even multi-gigabyte selections need no free space.
---
## Acknowledgements
**hippie68** — PS4 FTP reference implementation
**John Törnblom** — PS5 payload framework
**PlayStation homebrew community** — testing and feedback
I would like to express our sincere thanks to:
- **hippie68** — for the PS4 FTP reference implementation
- **John Törnblom** — for the PS5 payload framework
- **Drakmor** — for the inspiration in the implementation of PFR / CPTO / COPY
- **The PlayStation homebrew community** — for testing, feedback, and ongoing support
---
## Contributors
Special thanks to **M///Class** for contributing to the project through testing and for the steady commitment shown in following its development.
---
+21
View File
@@ -0,0 +1,21 @@
#!/bin/bash
export LLVM_CONFIG=$(command -v llvm-config-15 || command -v llvm-config)
export LLVM_PREFIX=$(dirname $(dirname $LLVM_CONFIG))
export CC="${LLVM_PREFIX}/bin/clang"
export AR="${LLVM_PREFIX}/bin/llvm-ar"
export RANLIB="${LLVM_PREFIX}/bin/llvm-ranlib"
export CFLAGS="-target x86_64-scei-ps4 -I$(pwd)/external/ps4-payload-sdk/include"
export LDFLAGS="-target x86_64-scei-ps4 -L$(pwd)/external/ps4-payload-sdk/lib"
cd external/libarchive-3.8.6
mkdir -p build_ps4 && cd build_ps4
cmake -DCMAKE_C_COMPILER="$CC" \
-DCMAKE_AR="$AR" \
-DCMAKE_RANLIB="$RANLIB" \
-DCMAKE_SYSTEM_NAME=Generic \
-DCMAKE_C_FLAGS="$CFLAGS" \
-DENABLE_ZLIB=OFF -DENABLE_BZip2=OFF -DENABLE_LIBXML2=OFF \
-DENABLE_EXPAT=OFF -DENABLE_ZSTD=OFF -DENABLE_LZMA=OFF \
-DENABLE_CNG=OFF -DENABLE_OPENSSL=OFF \
-DBUILD_SHARED_LIBS=OFF -DBUILD_TESTING=OFF \
-DPOSIX_C_SOURCE=200112L ..
make -j4
+224 -1
View File
@@ -2,6 +2,24 @@
## Endpoints
### GET /api/status
Identity and build switches of the running daemon. The web interface polls it
for the connection state and to know which features the payload was compiled
with, so views can hide what the build does not have.
```json
{"ok":true,"version":"1.6.0","instance_id":"15114b8a2c31d9f0",
"start_monotonic_ns":463980095296000,"pid":74,"platform":"ps4",
"features":{"pkg_install":false}}
```
- `instance_id` changes when the payload is re-injected: clients treat it as a
restart and drop their server-side handles.
- `features.pkg_install` mirrors the `ENABLE_PKG_INSTALL` build flag. When it is
`false` the package installer endpoints answer `409 PKG installation is
disabled for this build` and the UI hides the installer entirely.
### GET /api/list
List directory contents.
@@ -36,6 +54,184 @@ List directory contents.
Download file (TODO).
### GET /api/mounts
Lists the removable volumes that are **really** connected (`/mnt/usb0`…`/mnt/usb7`,
`/mnt/ext0`, `/mnt/ext1`). The console creates those directories even with
nothing plugged in, so zftpd checks whether a filesystem is mounted on them
(different `st_dev` from the parent directory) and reports size and free space.
The file manager's Places list is built from this, so empty slots no longer
appear as drives.
```json
{"ok":true,"mounts":[{"path":"/mnt/usb0","name":"usb0","kind":"usb","total":32007397376,"free":12561055744}]}
```
### GET /api/archive/zip
Streams a ZIP archive of the selection — the bulk download used by the web UI
for "Download as ZIP". Entries are produced while the response is sent (no
temporary archive on the console), stored uncompressed, and folders are walked
recursively. ZIP64 records are emitted automatically for entries, offsets or
counts past the 32-bit limits, so multi-gigabyte selections stay valid.
The response announces the exact `Content-Length` (no chunked encoding), so
console browsers show progress and complete the download reliably.
**Query Parameters:**
- `path` (required, repeatable): file or folder, confined to the HTTP root.
- `name` (optional): download name, default `zftpd-<timestamp>.zip`.
Up to 64 paths per request; unreadable or unconfined entries are skipped, and
when the selection exceeds the entry limit the response carries
`X-Zftpd-Truncated: entry-limit`.
```text
GET /api/archive/zip?path=/data/games&path=/data/saves/config.bin&name=backup.zip
```
### GET /api/file/get
Streams a file to the client. By default the raw on-disk bytes are sent
(zero-copy `sendfile`), exactly like every other transfer path.
**Query Parameters:**
- `path` (required): file to send, confined to the HTTP root.
- `decrypt` (optional): `1` asks for the **decrypted** content when the file is
a SELF container (PS4/PS5). The kernel pager is swapped in for the transfer
(MAP_SELF), so the client receives the plain executable instead of the
encrypted container. Other files are always sent untouched, and the response
carries `X-Zftpd-Decrypted: self` plus chunked encoding.
The web UI exposes this as *Settings → Downloads → Decrypt protected files*,
which appends the flag to every download and preview link.
```text
GET /api/file/get?path=/mnt/disc/CUSA00001/sce_sys/eboot.bin&decrypt=1
```
### Game dump
Decrypted dump of an installed title, read from the running title's sandbox
mount (`/mnt/sandbox/<TITLEID>-app0[-patch0-union]` on PS4, `_000/app0` and
friends under `/mnt/sandbox` on PS5), where the kernel exposes the application
in the clear; SELF/SPRX entries are decrypted through the self pager while they
are read. There is no fallback to the encrypted `/user/app` image, so a title
that is not running is launched first and the job waits for its sandbox to
mount. One dump runs at a time.
```text
POST /api/dump/start
{"title_id":"CUSA00001","target":"download","format":"zip","decrypt":1}
{"title_id":"CUSA00001","target":"local","dest":"/mnt/usb0/homebrew","format":"files"}
GET /api/dump/status # state, source, entries, size, bytes_done
GET /api/dump?id=N # streams the prepared archive (Content-Length)
POST /api/dump/cancel # {"id":N} optional; the single slot is implied
```
`target` is `download` (default; a streamed ZIP the client then fetches with
`GET /api/dump?id=N`) or `local`. `format` is `zip` (default) or `files`
(console targets only — a streamed dump is always a ZIP). `decrypt` defaults
to `1`; `0` keeps the raw SELF containers.
`POST /api/dump/start` answers immediately with the slot id and the initial
state; `source`, `entries` and `size` become available through the status as
soon as the job worker has walked the source tree:
```json
{"ok":true,"id":1,"state":"launching","title_id":"CUSA00001","target":"download"}
```
`GET /api/dump/status` (the response the web UI polls):
```json
{"ok":true,"active":true,"id":1,"state":"running","title_id":"CUSA00001",
"source":"/mnt/sandbox/CUSA00001_000/app0","entries":1234,"size":32212254720,
"bytes_done":1073741824,"started":1790778983,"to_console":false,
"cancelled":false,"message":""}
```
`state` is `launching`, `running`, `ready` (streamed dump prepared, waiting for
the download), `done` or `failed`; `message` carries the failure reason
(`Cancelled` after a cancel request). A slot that stops advancing is retired by
its deadline and reported as `failed`, so a broken dump never blocks the next
one.
`POST /api/dump/cancel` stops the job — the local writer gives up at the next
chunk, a prepared archive is dropped, a streamed download ends early:
```json
{"ok":true,"id":1,"state":"running","cancelled":true}
```
### POST /api/system/eject
Ejects the Blu-ray disc from the console's optical drive (PS4/PS5). The drive
is a FreeBSD `cd(4)` device: zftpd sends `CDIOCALLOW` + `CDIOCEJECT` to
`/dev/cd0` and shows a toast on success.
```json
{"ok":true,"status":"ok","message":"Disc ejected","code":0}
```
Failures answer HTTP 200 with `ok:false` and the driver reason, so a client can
tell a Digital Edition console (`ENOENT`) from a busy drive (`EBUSY`):
```json
{"ok":false,"status":"error","message":"Drive busy: a disc game is running","code":-16}
```
### GET /api/system/disc
Reports whether the console has an optical drive, so the UI can hide the action
on Digital Edition consoles.
```json
{"ok":true,"present":true}
```
### GET|POST /api/notify
Show a custom system notification on PS4/PS5 (toast). On non-console builds the
message is forwarded to syslog. Designed for local-network automation such as
Home Assistant.
**Query Parameters:**
- `text` (required): Notification message (URL-encoded). Max 1023 bytes after decode.
- `icon` (optional): PS notification texture suffix, default `icon_system`.
Allowed characters: `[A-Za-z0-9_]`.
**Examples:**
```
GET /api/notify?text=Washing%20machine%20is%20done
GET /api/notify?text=Doorbell&icon=icon_system
```
**Home Assistant (`rest_command`):**
```yaml
rest_command:
ps5_notify:
url: "http://{{ states('sensor.ps5_ip') }}:2121/api/notify?text={{ text | urlencode }}"
method: GET
```
**Response:**
```json
{
"ok": true,
"text": "Washing machine is done",
"icon": "icon_system"
}
```
**Errors:**
- `400`: Missing/empty `text`, control characters, or invalid `icon`
- `405`: Method other than GET/POST
> Prefer **GET** for automation clients. POST is subject to CSRF validation when
> web upload support is enabled.
### Static Files
- `GET /` → `index.html`
@@ -44,7 +240,7 @@ Download file (TODO).
## Adding Custom Endpoints
Edit `zhttpd/src/http_api.c`:
Edit `src/http/http_api.c`:
```c
http_response_t* http_api_handle(const http_request_t *request) {
@@ -54,3 +250,30 @@ http_response_t* http_api_handle(const http_request_t *request) {
// ... existing handlers
}
```
## Package installation
`GET|POST /api/admin/games/install`
| Parameter | Meaning |
| --------- | ------- |
| `path` | Package already on console storage. |
| `url` | Package the console downloads by itself (http/https). |
| `dest` | `internal` (default) or `extended`. |
| `overwrite` | `1` replaces an existing installation of the same title. |
Exactly one of `path` and `url` is required. When the package is already
installed the request answers `409` with
`{"error":"already_installed","title_id":…,"can_overwrite":true}` and nothing is
changed; the caller repeats it with `overwrite=1` to replace the installation.
`GET /api/admin/games/install_status` reports the running task:
```json
{"ok":true,"active":true,"task_id":-1,"progress":42,"error":0,
"length":0,"transferred":0,"title_id":"","path":"https://…/package.pkg"}
```
On PS5 the installation is driven by a helper program that zftpd delivers to the
loader on loopback, because the system installer API is only usable from a
process the loader spawned. On PS4 the download service performs it directly.
+68
View File
@@ -0,0 +1,68 @@
# Architecture and repository layout
zftpd is organized by responsibility instead of by filename prefix. A module
owns its implementation and headers, while generated code is kept out of the
source tree.
```text
src/
├── app/ process entry point and composition
├── ftp/ FTP protocol, sessions, commands and data path
├── http/ HTTP server, REST API, parsing and CSRF
├── transfer/ remote-to-local transfer manager and protocol backends
├── archive/ ZIP, PKG and exFAT/container readers
├── runtime/ event-loop implementations
└── platform/ platform abstraction layer
└── ps5/ PS5-only networking/filter code
include/
├── ftp/
├── http/
├── transfer/
├── archive/
├── runtime/
└── platform/
└── ps5/
```
## Dependency direction
The intended dependency direction is deliberately one-way:
```text
app
├── ftp ───────────────> platform
└── http
├── transfer ──────> external protocol libraries / POSIX
├── archive
└── platform
runtime ───────────────> platform / operating-system event APIs
```
Lower-level modules must not import `app`. `platform` must not depend on FTP
or HTTP policy. Protocol-specific remote transfer code belongs in
`transfer/backends`, not in REST handlers or the PAL.
## Generated code
Generated files are build artifacts. In particular, the embedded web UI and
PS5 CA bundle are generated under `build/<target>/<variant>/generated/` and are
never committed under `src/`.
This keeps source diffs reviewable and prevents a small web change from
creating tens of thousands of lines of generated C changes.
## External dependencies
Do not reimplement mature network protocols inside zftpd. HTTP/HTTPS/FTP/FTPS
use libcurl and NFS uses libnfs. PS4 and PS5 consume the PacBrew ports. See
[`dependencies.md`](dependencies.md) for pinned versions and build details.
## Adding a feature
Choose the module by ownership, not by caller. A new FTP command belongs in
`ftp/`; an HTTP route belongs in `http/`; a new remote source protocol belongs
in `transfer/`; a console syscall or compatibility layer belongs in
`platform/`. If multiple modules need a facility, place it in the lowest
layer that can provide it without importing a higher-level policy module.
+90
View File
@@ -0,0 +1,90 @@
# External dependencies
zftpd keeps protocol implementations outside the project when a mature,
maintained library already exists. Console builds use the ports shipped by
the PacBrew ecosystem rather than local compatibility shims.
## PS4 zhttp transfer stack
| Component | Purpose | Source |
| --- | --- | --- |
| libcurl 7.80.0 | HTTP, HTTPS, FTP and FTPS transfers | PacBrew `ps4-openorbis-libcurl` (static, links `-lSceNet`) |
| mbedTLS 2.16.6 | TLS used by libcurl | PacBrew `ps4-openorbis-mbedtls` |
| zlib 1.3.1 | libcurl transfer dependency | PacBrew `ps4-openorbis-zlib` |
| Mozilla CA bundle | HTTPS trust store | downloaded by the fetch script and embedded at build time |
`tools/fetch_pacbrew_ps4.sh` installs the pinned PacBrew packages into
`external/pacbrew/ps4/openorbis` (versions and SHA-256 are pinned in the
script, and the packages come from the public PacBrew repository). No pacman
and no OpenOrbis toolchain are needed: the payload keeps being built with
`PS4_PAYLOAD_SDK`, and only the headers and the static libraries are used —
the `curl-config` wrapper inside the package emits the OpenOrbis toolchain's
own link flags (its linker script, `-nostdlib`, `-lSceLibcInternal`), which
must not leak into a payload link. The Makefile picks the prefix up
automatically (`PS4_PACBREW_ROOT`, defaulting to the path above and to
`/opt/pacbrew/ps4/openorbis`), and the release workflow installs the same set
for the published PS4 artifacts.
The OpenOrbis build of libcurl reaches `errno` through musl's
`__errno_location()` and uses `fnmatch()`/`execl()`, none of which the payload
libc exports; `src/platform/ps4/ps4_pacbrew_compat.c` bridges the three.
With no prefix installed the URL downloader is simply disabled at build time.
A toolchain-provided `orbis-pkg-config` / `orbis-curl-config` is still probed
as a fallback, and `PS4_PKG_CONFIG`, `PS4_CURL_CONFIG` and `PS4_CA_BUNDLE`
override the detection. The build never falls back to the host's libcurl,
which would link code built for the wrong OS.
## PS5 zhttp transfer stack
| Component | Purpose | Source |
| --- | --- | --- |
| libcurl 8.18.0 | HTTP, HTTPS, FTP and FTPS transfers | `ps5-payload-dev/pacbrew-repo` (`ps5-payload-curl`) |
| OpenSSL | TLS used by libcurl | PacBrew dependency of `ps5-payload-curl` |
| zlib / zstd / libpsl | libcurl transfer dependencies | PacBrew |
| libnfs 5.0.3 | Userspace NFS client | `ps5-payload-dev/pacbrew-repo` (`ps5-payload-libnfs`) |
| libtorrent-rasterbar 2.1.2 | Native BitTorrent magnet downloads | Cross-built from the pinned upstream source by `tools/build_ps5_libtorrent.sh` |
| libarchive (optional) | Non-ZIP archive extraction with `ENABLE_LIBARCHIVE=1` | `ps5-payload-dev/pacbrew-repo` (`ps5-payload-libarchive`) |
| Mozilla CA bundle | HTTPS trust store | generated by the PacBrew curl package and embedded at build time |
The release workflow pins PacBrew `v0.40.2` and verifies the published
`ps5-payload-dev.tar.gz` SHA-256 before using it. The SDK bundle is not
committed to this repository.
## Transfer architecture
`src/transfer/` owns network-to-file transfers. The HTTP API only validates
requests and destination paths, then delegates to the transfer manager.
Backends are selected by URL scheme:
- `http://`, `https://`, `ftp://`, `ftps://` -> libcurl
- `nfs://` -> libnfs
- `magnet:?` -> libtorrent-rasterbar in PS5 builds with `ENABLE_LIBTORRENT=1`
HTTP/FTP/NFS downloads are written to `<name>.zftpd.part`, support resume
where the remote protocol permits it, and are renamed after the transfer and
`fsync()` succeed. Magnet downloads use a partial directory with the same
suffix and store resume data in `/data/zftpd` (or `/tmp/zftpd` fallback).
The directory becomes visible under its final name only after libtorrent
verifies all pieces.
For new HTTP/HTTPS downloads of at least 128 MiB, libcurl first checks byte-range
support and a strong ETag, then measures one 32 MiB range. If that connection
is below 16 MB/s, four range requests run concurrently in 8 MiB chunks. Each
wave is verified and appended in order, so the partial file stays contiguous
and can still resume after interruption. An existing partial file can also use
four connections: before fetching its suffix, zftpd compares small regions at
the start, middle and end of the local prefix with the current remote file.
A mismatch stops the download without changing the partial file. This sampling
does not prove every byte of a legacy partial matches; new suffix ranges are
checked against the same strong ETag. Servers without range support or a strong
ETag continue with one connection.
NFS URLs use libnfs syntax, for example:
```text
nfs://192.168.1.20/export/games/game.pkg
```
The NAS must expose an NFS share reachable from the console. No kernel NFS
mount, FUSE layer or firmware-specific mount patch is required.
+99
View File
@@ -0,0 +1,99 @@
# Game dumping (spec)
Reimplementation of the app-dumper idea ([EchoStretch/ps5-app-dumper](https://github.com/EchoStretch/ps5-app-dumper),
itself based on [logic-68/pfsmnt-dumper](https://github.com/logic-68/pfsmnt-dumper)) inside zftpd,
built out of the modules zftpd already has.
## Confirmed requirements
1. **Decrypted output**: the dump contains the game's files in the clear, taken
from the running title's sandbox mount `/mnt/sandbox/<TITLEID>_000/app0`
("pfsmnt"), where the kernel already exposes the app decrypted. SELF/SPRX
inside the tree are decrypted through the self pager as they are read.
2. **Two destinations, one job**:
* **Browser**: streamed to the PC over HTTP (no temporary copy on the
console, works for dumps far larger than the free space).
* **Console**: written to any path the daemon can reach — USB (`/mnt/usbN`),
extended storage (`/mnt/extN`), internal (`/data/...`).
3. Nothing in the dump is modified: no FSELF re-signing, no SDK backport
(those change the payload; a transfer daemon hands out what the console
produced).
## Reuse map
| Need | Existing piece |
| --- | --- |
| List titles + names/icons | `src/http/games/catalog.c`, `/api/admin/games/installed` |
| Start the title so its sandbox mounts | `src/http/games/psx_launch.c`, `POST /api/admin/launch` |
| Walk a tree, stream it, ZIP64 for multi-GB | `src/archive/zip_writer.c` |
| Decrypt SELF/SPRX while reading | `psx_vfs_try_open_self()` / `psx_vfs_read()` (MAP_SELF) |
| Job progress, cancel, single slot | `src/transfer/dump_job.c`, `src/http/http_api_dump.c` |
| Prepared-job + short URL pattern | `/api/archive/zip` POST → `?id=N` |
| Progress UI | Transfers view (`web/js/transfers.js`) |
## API
```text
POST /api/dump/start
{"title_id":"PPSA01234","target":"download","format":"zip","decrypt":1}
{"title_id":"PPSA01234","target":"local","dest":"/mnt/usb0/homebrew","format":"files"}
→ {"ok":true,"id":N,"title_id":"...","target":"download","state":"launching"}
GET /api/dump?id=N # streams the prepared archive (Content-Length)
GET /api/dump/status # state, source, entries, size, bytes_done
POST /api/dump/cancel {"id":N} # id optional: one slot is implied
```
* The start reply is immediate: the title is launched (when needed) and the
source tree is walked by the job worker, so `source`, `entries` and `size`
arrive with the first `/api/dump/status` poll that finds the job ready.
`target:"download"` then fetches `GET /api/dump?id=N`; `target:"local"` runs
in the background and is observed through the status.
* `format:"zip"` — one archive (`<TITLEID>.zip`), streamed for downloads,
written file-by-file for local targets (atomic: `<name>.zftpd.part` then
rename, same convention as the downloader).
* `format:"files"` — directory tree, the reference layout: every file is copied
to `dest/<TITLEID>/` with the same walk, so a dump can be inspected without
extracting. Download targets are always ZIP.
* `decrypt` (default `1`) — SELF/SPRX entries are read through the self pager;
`0` keeps the raw containers.
* `source` is always the running title's sandbox mount: there is no fallback to
`/user/app/<TITLEID>`, because that path holds the *encrypted* image and the
result would not be a usable dump.
* `/api/dump/cancel` stops a running dump (the writer gives up at the next
chunk, a prepared archive is dropped, a streamed download ends early) and is
what the Transfers view cancels with.
## UI
* **Games view**: `Dump to this PC` streams the decrypted archive straight to
the browser; `Dump on the console…` picks a destination folder first and then
offers the format (files or one ZIP) and the decrypt toggle (on by default).
* **Transfers view**: the job appears with live rate/ETA and cancel, so a
50 GB dump is observable and interruptible.
## Auto-launch (confirmed)
The job launches the title through `POST /api/admin/launch` when `app0` is not
mounted yet and stays in state `waiting-for-title` until the sandbox appears
(bounded retry), then switches to `running`. Nothing else about the flow
changes: the dump itself starts only once the source path exists.
## SELF entries inside a ZIP (implementation note)
`zip_writer` emits each entry's size in its local header and in the central
directory, and the download mode announces the total length **before**
streaming (`zip_writer_total_size()`): the size of a SELF entry must therefore
be known at *walk* time, not at read time.
So `decrypt` support is split:
* during collection, files whose first bytes are a SELF magic are opened once
through `psx_vfs_try_open_self()` and recorded with the **decrypted** size
plus a `self` flag (a non-SELF or unreadable file keeps the raw size);
* while streaming, an entry flagged `self` is read with `psx_vfs_read()`
(re-opened lazily, which is what the VFS does anyway), everything else is
read with plain `pread()`.
The file-tree mode (`format:"files"`) needs none of this: it copies each file
as it goes, so it can decide per file at read time.
+7 -5
View File
@@ -10,7 +10,7 @@ This document summarizes the main internal facilities you can rely on when addin
- Prefer platform abstraction layers (`pal_*`) instead of direct syscalls.
## Error Types
- `ftp_error_t` in `include/ftp_types.h` covers FTP-domain errors.
- `ftp_error_t` in `include/ftp/ftp_types.h` covers FTP-domain errors.
- Use `FTP_OK` for success; return specific error enums for callers to translate into FTP replies/logs.
## Memory Management
@@ -150,8 +150,10 @@ ftp_log_session_event(session, "MYMODULE", FTP_OK, bytes_processed);
- Lightweight wrapper to display toast messages on consoles.
```c
#include "pal_notification.h"
pal_notify("zftpd", "Started on 192.168.0.10:2122");
pal_notification_send("Started on 192.168.0.10:2122");
pal_notification_send_ex("Doorbell", "icon_system");
```
- HTTP: `GET /api/notify?text=...[&icon=...]` (see [API.md](API.md)).
## Crypto (optional) (`ftp_crypto`)
- ChaCha20 PSK via `AUTH XCRYPT`; keep disabled unless explicitly required.
@@ -167,7 +169,7 @@ ftp_crypto_derive_key(key, nonce, out_key);
- Token-bucket per session in `ftp_commands.c` (`ftp_rate_limit_wait`); reuse pattern for new data paths.
## HTTP/ZHTTP Modules
- When `ENABLE_ZHTTPD=1`, HTTP server modules live in `src/http_*.c`; add endpoints in `http_api.c`.
- When `ENABLE_ZHTTPD=1`, HTTP server modules live in `src/http/http_*.c`; add endpoints in `http_api.c`.
```c
// http_api.c
if (strncmp(req->uri, "/api/custom", 11) == 0) {
@@ -212,8 +214,8 @@ ftp_error_t send_file(int data_fd, const char *path) {
```
## Where to Look
- Headers: `include/pal_*.h`, `include/ftp_*.h`
- Implementations: `src/pal_*`, `src/ftp_commands.c`, `src/http_*.c`, `src/ftp_crypto.c`
- Headers: `include/platform/pal_*.h`, `include/ftp/ftp_*.h`
- Implementations: `src/platform/pal_*`, `src/ftp/ftp_commands.c`, `src/http/http_*.c`, `src/ftp/ftp_crypto.c`
- The PAL layers are usable to build standalone apps (e.g., custom services like a lightweight game server or tools unrelated to FTP). Reuse `pal_*` for portability, `ftp_buffer_pool` for I/O buffers, and add your own protocol handlers atop the same evented model.
Keep modules small, defensive, and consistent with existing patterns.
+155
View File
@@ -0,0 +1,155 @@
# PKG installation
Install `.pkg` packages on PS4 and PS5 from zftpd itself: from the file explorer
when the selection is a package, and from the Games page with either a file on
the computer or one already on the console storage.
**Status:** the web installer accepts a PKG from the computer, console storage,
or an HTTP(S) link. Console files are served by zftpd with HTTP byte ranges.
The PS5 route launches an installer helper through elfldr and polls
`sceAppInstUtilGetInstallStatus`; PS4 uses BGFT. Host tests cover the HTTP
range path, but an actual PS5/PS4 installation still needs console validation.
The design notes below record the intended behavior and may include items that
are not yet implemented.
## Decisions
- **Destinations:** PS4 exposes internal and extended storage. The current PS5
installer API does not accept an explicit destination, so its extended
storage choice is disabled in the interface.
- **Enabled by default in ZHTTP builds**, like the web interface itself:
`ENABLE_PKG_INSTALL` must default to on whenever `ENABLE_ZHTTPD=1`, and stay
overridable with `ENABLE_PKG_INSTALL=0`. Verify a release build still links
before committing this default.
- **Off in the published releases**: the release workflow builds every artifact
with `ENABLE_PKG_INSTALL=0`. A build without the installer stubs its
endpoints (HTTP 409 `PKG installation is disabled for this build`) and the
web UI hides every entry point behind the flag the daemon reports in
`/api/status` (`features.pkg_install`): the *Install PKG* action in Games,
the *Install package* item in the file explorer, and the install progress
pollers.
- The package is **never read into memory**: it is served over HTTP on loopback
and the system installer pulls it from that URL.
## Existing code to start from
| Path | Role |
| ---- | ---- |
| `src/http/games/psx_install.c` | the current install path — read this first |
| `src/http/games/admin_api.c`, `common.c`, `games_internal.h` | endpoints and helpers around it |
| `src/http/http_upload.c` | streaming upload, reusable for "install from the computer" |
| `src/http/http_api_files.c` | file explorer API, where the `.pkg` action is detected |
| `Makefile`, `.github/workflows/release.yml` | the `ENABLE_PKG_INSTALL` flag |
## Flow
1. The package exists either on console storage (any path the user can browse) or
arrives by upload; uploads land in a temporary directory under
`/data/zftpd/pkg/` and are removed when the install finishes or fails.
2. zftpd serves the package over its own HTTP server (streamed, range-capable),
so the installer pulls the bytes instead of the payload buffering them.
3. The URL plus the chosen destination go to the platform installer:
- **PS4** — the PKG installer service path.
- **PS5** — the `libSceAppInstaller` API; check in the SDK which entry point
accepts both the title and the destination, and follow Itemzflow for the
exact call sequence.
4. Progress is polled until the installer reports success or failure; the result
is also pushed to the console notification area.
## API surface
- `POST /api/pkg/install` — `{"path" | "url", "destination": "internal"|"extended"}`.
Returns an install id.
- `POST /api/pkg/upload` — streaming upload followed by an install; the response
returns the same install id.
- `GET /api/pkg/status?id=N` — state, percentage, bytes, error text.
- `POST /api/pkg/cancel?id=N` — best effort, reported honestly when the system
installer cannot be interrupted.
## Interface
- **File explorer:** a single `.pkg` in the selection adds *Install package* to
the context menu (and to the actions menu). The entry must not appear for
other file types, and stays hidden for multi-selections that contain anything
else.
- **Games page:** an *Install PKG* action offering two sources — *from the
computer* (upload with progress) and *from console storage* (reuse the
existing folder picker) — plus the destination selector and the free space of
each destination next to it.
- Progress belongs in the **Transfers** view, next to downloads and dumps, not
in a toast that disappears.
## Work breakdown
1. Read `psx_install.c` and state what it already does on PS4 and PS5; keep what
works, delete what does not.
2. Platform layer: expose one `pal_pkg_install(path_or_url, destination)` plus a
`pal_pkg_status()` for both consoles, with `-ENOSYS` stubs on host builds.
3. Serving path: stream a package from an arbitrary path over HTTP, with range
support, reusing the existing response streaming.
4. Upload path: reuse `http_upload.c`, write into `/data/zftpd/pkg/`, clean up on
every exit path.
5. Endpoints: implement the four routes above and register them in the games API
dispatcher.
6. Destination handling: enumerate internal and extended storage, check free
space with `statvfs`, refuse the install when it does not fit.
7. Interface: context-menu entry in the file explorer, the Games page action
with both sources and the destination picker, progress row in Transfers.
8. `ENABLE_PKG_INSTALL` on by default for ZHTTP builds, then a full release build
for PS4 and PS5 to confirm the link.
9. Tests and docs: host tests for destination parsing and the free-space check,
plus updates to `docs/API.md` and the README feature list.
## PS5: how the installation has to run
What the platform requires:
- The install API is **`sceAppInstUtil*`** — `sceAppInstUtilInitialize`,
`sceAppInstUtilInstallByPackage(pkg_metadata_t *, pkg_info_t *, playgo_info_t *)`,
`sceAppInstUtilGetInstallStatus`, `sceAppInstUtilAppInstallAll`. The metadata
carries `uri`, `ex_uri`, `content_id`, `content_name`, `playgo_scenario_id`,
`icon_url`.
- **It cannot be called from a payload like zftpd.** The reference runs it in a
**separate helper process**: it embeds a small ELF, delivers it to **elfldr on
127.0.0.1:9021**, and then talks to that process over a loopback IPC socket
(the helper connects back to a listener the payload opened). That is why the
project carries `install_helper.c`, `install_helper_blob.S`,
`install_process.c` and `install_ipc.c`.
- `sceBgftService*` (what zftpd uses today) is the PS4-shaped path; on PS5 the
helper route is the one that installs.
### Consequence for zftpd
1. Keep the current BGFT path for PS4.
2. Add an embedded **installer helper ELF** for PS5, delivered to elfldr:9021
exactly like a payload, with a small IPC protocol (install request →
progress → result).
3. The helper pulls the package over HTTP from zftpd itself (we already serve
files over loopback), so nothing is copied twice: no temporary package on
console storage.
4. Progress: the helper reports through IPC, zftpd forwards it to
`/api/admin/games/install_status`, which the Transfers and Games views
already poll.
## Helper protocol (PS5)
zftpd delivers the embedded helper to the loader on `127.0.0.1:9021`, then talks
to it over a loopback socket. Requests are one line each:
| Request | Answer |
| ------- | ------ |
| `CHECK <dest> <path>` | `INSTALLED <title_id>` or `NEW` |
| `INSTALL <dest> <url> <overwrite>` | `STATE …`, `PROGRESS <pct> <bytes> <total>`, then `DONE 0` or `ERROR 0x…` |
The check exists because the title id and the installed state can only be read
from a process the loader spawned: the payload asking the same question leaves
the request hanging. When the helper answers `INSTALLED` and the caller did not
ask to overwrite, zftpd answers the interface with `needs_confirm`, which is
what produces the *Replace?* prompt in the Games and Transfers views.
## Interface
The Games action opens a dialog with a drop target (clickable as well), a
destination choice that defaults to the normal application install location, an
**Explorer** button to pick a package already on the console, and **Cancel** and
**Install** on the right.
+110
View File
@@ -0,0 +1,110 @@
# Rest Mode Resilience
zftpd survives PlayStation Rest Mode cycles by recreating dead listen sockets
and exposing a stable process identity so clients can reconnect cleanly.
There is **no** “enter rest mode” protocol command — Rest Mode is an environmental
event (sockets die / network stack drops / payload may be re-injected).
## Does activity resume after wake?
**Yes, if the payload process survives Rest Mode** (typical on PS4/PS5 when the
process is only suspended):
1. Existing FTP/HTTP TCP sessions die (the stack drops them)
2. zftpd detects the dead listen socket (`EBADF` / `ENOTSOCK` / liveness probe)
3. It recreates `bind`/`listen` with exponential backoff while the network is down
4. On success it shows an on-screen notification (`Server resumed` / `HTTP resumed`)
5. New client connections work again; FTP clients and ZHTTP reconnect
**No automatic resume if the payload was terminated** during Rest Mode — you must
re-inject zftpd. Clients then see a new `instance_id`.
## Scenarios
| Case | Payload process | Listen sockets | zftpd behaviour |
|------|-----------------|----------------|-----------------|
| Terminated | Dies | Gone | Requires re-inject; new `instance_id` |
| Suspended | Survives | Invalid (`EBADF` / `ENOTSOCK`) | Recreate listener with backoff |
| Suspended + stale FD | Survives | FD looks open, stack down | Liveness probe → recreate |
## Server (FTP)
`pal_resilient_accept()` (`src/platform/pal_resilient_server.c`):
1. Detects listener loss on `accept()` (`EBADF`, `ENOTSOCK`, `EINVAL`)
2. Periodically probes the listen FD (`getsockopt(SO_TYPE)` / `SO_ACCEPTCONN`)
3. Recreates `socket` → `SO_REUSEADDR` → `bind` → `listen` with backoff:
`500 → 1000 → 2000 → 4000 → 8000 → 10000 ms` (indefinite while running)
4. Every few failed attempts calls `pal_network_reinit()`
5. Sends an on-screen notification: `zftpd: Server resumed on <ip>:<port>`
Existing FTP control/data connections are **not** preserved (TCP is dead).
Clients reconnect; file resume remains FTP `REST` + `STOR`/`RETR`.
## Server (HTTP / ZHTTP)
`http_server.c` mirrors the same model on the event-loop listener:
1. `EVENT_ERROR` / `EVENT_CLOSE` or lost-errno on `accept()` schedules recreate
2. A detached thread retries with the same backoff
3. A wake pipe hands the new FD back to the event-loop thread for
`event_loop_add` (handler table is not thread-safe)
4. Notification: `zftpd: HTTP resumed on port <n>`
## Daemon identity
Process-lifetime values from `ftp_instance.c`:
| Field | Meaning |
|-------|---------|
| `daemon_instance_id` | Random `uint64`, stable until process exit |
| `daemon_start_monotonic_ns` | `CLOCK_MONOTONIC` at first generation |
Exposed to clients via:
- **HTTP** `GET /api/status` → JSON `instance_id`, `start_monotonic_ns`, `version`, `pid`
- **FTP** `FEAT` line `XZFTPD INSTANCE <hex>`
- **FTP** `STAT` reply includes the instance hex
| `instance_id` vs previous | Meaning | Client action |
|---------------------------|---------|---------------|
| Same | Payload survived | Keep UI state, refresh listings |
| Different | Re-injected / new process | Treat remote handles as invalid |
| Missing / zero (legacy) | Old build | Skip rotation check |
## ZHTTP client reconnect
`web/js/api.js` (connection monitor) and `web/js/app.js` (status UI):
- Health poll of `/api/status` every 5s
- Two consecutive transport failures → offline banner in the UI
- Exponential backoff probe (0.5s → 10s) until `/api/status` succeeds
- `api.ready()` gates POST/upload while offline or reconnecting
- Toast on reconnect; a changed `instance_id` is reported as a daemon restart
## What is preserved vs invalidated
**Preserved (same process):** daemon heap, session pool slots free for new clients,
`instance_id`, filesystem, config.
**Invalidated always:** live TCP sessions, PASV ports, in-flight transfers.
**Client-side preserved across ZHTTP reconnect:** current path, UI view, theme.
Listings are refreshed on reconnect.
## Tests
```sh
make test
# includes tests/test_instance.c — identity stability + backoff helpers
```
## Hardware validation (recommended)
1. Start zftpd on PS5, connect FTP + open ZHTTP
2. Enter Rest Mode → wait → wake
3. Confirm notification “Server resumed” / “HTTP resumed”
4. FTP client reconnects; ZHTTP shows “Connection restored”
5. `GET /api/status` returns the **same** `instance_id` if the payload survived
6. Kill/re-inject payload → `instance_id` changes; ZHTTP reports daemon restart
+22 -38
View File
@@ -1,38 +1,22 @@
/*
MIT License
Copyright (c) 2026 Seregon
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
/**
* @file http_api.h
* @brief REST API handlers
*/
#ifndef HTTP_API_H
#define HTTP_API_H
#include "http_parser.h"
#include "http_response.h"
http_response_t* http_api_handle(const http_request_t *request);
#endif /* HTTP_API_H */
Copyright 2013-2014 RAD Game Tools and Valve Software
Copyright 2010-2014 Rich Geldreich and Tenacious Software LLC
All Rights Reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+646
View File
@@ -0,0 +1,646 @@
/**************************************************************************
*
* Copyright 2013-2014 RAD Game Tools and Valve Software
* Copyright 2010-2014 Rich Geldreich and Tenacious Software LLC
* All Rights Reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
**************************************************************************/
#include "miniz.h"
typedef unsigned char mz_validate_uint16[sizeof(mz_uint16) == 2 ? 1 : -1];
typedef unsigned char mz_validate_uint32[sizeof(mz_uint32) == 4 ? 1 : -1];
typedef unsigned char mz_validate_uint64[sizeof(mz_uint64) == 8 ? 1 : -1];
#ifdef __cplusplus
extern "C"
{
#endif
/* ------------------- zlib-style API's */
mz_ulong mz_adler32(mz_ulong adler, const unsigned char *ptr, size_t buf_len)
{
mz_uint32 i, s1 = (mz_uint32)(adler & 0xffff), s2 = (mz_uint32)(adler >> 16);
size_t block_len = buf_len % 5552;
if (!ptr)
return MZ_ADLER32_INIT;
while (buf_len)
{
for (i = 0; i + 7 < block_len; i += 8, ptr += 8)
{
s1 += ptr[0], s2 += s1;
s1 += ptr[1], s2 += s1;
s1 += ptr[2], s2 += s1;
s1 += ptr[3], s2 += s1;
s1 += ptr[4], s2 += s1;
s1 += ptr[5], s2 += s1;
s1 += ptr[6], s2 += s1;
s1 += ptr[7], s2 += s1;
}
for (; i < block_len; ++i)
s1 += *ptr++, s2 += s1;
s1 %= 65521U, s2 %= 65521U;
buf_len -= block_len;
block_len = 5552;
}
return (s2 << 16) + s1;
}
/* Karl Malbrain's compact CRC-32. See "A compact CCITT crc16 and crc32 C implementation that balances processor cache usage against speed": http://www.geocities.com/malbrain/ */
#if 0
mz_ulong mz_crc32(mz_ulong crc, const mz_uint8 *ptr, size_t buf_len)
{
static const mz_uint32 s_crc32[16] = { 0, 0x1db71064, 0x3b6e20c8, 0x26d930ac, 0x76dc4190, 0x6b6b51f4, 0x4db26158, 0x5005713c,
0xedb88320, 0xf00f9344, 0xd6d6a3e8, 0xcb61b38c, 0x9b64c2b0, 0x86d3d2d4, 0xa00ae278, 0xbdbdf21c };
mz_uint32 crcu32 = (mz_uint32)crc;
if (!ptr)
return MZ_CRC32_INIT;
crcu32 = ~crcu32;
while (buf_len--)
{
mz_uint8 b = *ptr++;
crcu32 = (crcu32 >> 4) ^ s_crc32[(crcu32 & 0xF) ^ (b & 0xF)];
crcu32 = (crcu32 >> 4) ^ s_crc32[(crcu32 & 0xF) ^ (b >> 4)];
}
return ~crcu32;
}
#elif defined(USE_EXTERNAL_MZCRC)
/* If USE_EXTERNAL_CRC is defined, an external module will export the
* mz_crc32() symbol for us to use, e.g. an SSE-accelerated version.
* Depending on the impl, it may be necessary to ~ the input/output crc values.
*/
mz_ulong mz_crc32(mz_ulong crc, const mz_uint8 *ptr, size_t buf_len);
#else
/* Faster, but larger CPU cache footprint.
*/
mz_ulong mz_crc32(mz_ulong crc, const mz_uint8 *ptr, size_t buf_len)
{
static const mz_uint32 s_crc_table[256] = {
0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, 0x706AF48F, 0xE963A535,
0x9E6495A3, 0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988, 0x09B64C2B, 0x7EB17CBD,
0xE7B82D07, 0x90BF1D91, 0x1DB71064, 0x6AB020F2, 0xF3B97148, 0x84BE41DE, 0x1ADAD47D,
0x6DDDE4EB, 0xF4D4B551, 0x83D385C7, 0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC,
0x14015C4F, 0x63066CD9, 0xFA0F3D63, 0x8D080DF5, 0x3B6E20C8, 0x4C69105E, 0xD56041E4,
0xA2677172, 0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B, 0x35B5A8FA, 0x42B2986C,
0xDBBBC9D6, 0xACBCF940, 0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59, 0x26D930AC,
0x51DE003A, 0xC8D75180, 0xBFD06116, 0x21B4F4B5, 0x56B3C423, 0xCFBA9599, 0xB8BDA50F,
0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924, 0x2F6F7C87, 0x58684C11, 0xC1611DAB,
0xB6662D3D, 0x76DC4190, 0x01DB7106, 0x98D220BC, 0xEFD5102A, 0x71B18589, 0x06B6B51F,
0x9FBFE4A5, 0xE8B8D433, 0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818, 0x7F6A0DBB,
0x086D3D2D, 0x91646C97, 0xE6635C01, 0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E,
0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457, 0x65B0D9C6, 0x12B7E950, 0x8BBEB8EA,
0xFCB9887C, 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65, 0x4DB26158, 0x3AB551CE,
0xA3BC0074, 0xD4BB30E2, 0x4ADFA541, 0x3DD895D7, 0xA4D1C46D, 0xD3D6F4FB, 0x4369E96A,
0x346ED9FC, 0xAD678846, 0xDA60B8D0, 0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9,
0x5005713C, 0x270241AA, 0xBE0B1010, 0xC90C2086, 0x5768B525, 0x206F85B3, 0xB966D409,
0xCE61E49F, 0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, 0x59B33D17, 0x2EB40D81,
0xB7BD5C3B, 0xC0BA6CAD, 0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A, 0xEAD54739,
0x9DD277AF, 0x04DB2615, 0x73DC1683, 0xE3630B12, 0x94643B84, 0x0D6D6A3E, 0x7A6A5AA8,
0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1, 0xF00F9344, 0x8708A3D2, 0x1E01F268,
0x6906C2FE, 0xF762575D, 0x806567CB, 0x196C3671, 0x6E6B06E7, 0xFED41B76, 0x89D32BE0,
0x10DA7A5A, 0x67DD4ACC, 0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, 0xD6D6A3E8,
0xA1D1937E, 0x38D8C2C4, 0x4FDFF252, 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B,
0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60, 0xDF60EFC3, 0xA867DF55, 0x316E8EEF,
0x4669BE79, 0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236, 0xCC0C7795, 0xBB0B4703,
0x220216B9, 0x5505262F, 0xC5BA3BBE, 0xB2BD0B28, 0x2BB45A92, 0x5CB36A04, 0xC2D7FFA7,
0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D, 0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A,
0x9C0906A9, 0xEB0E363F, 0x72076785, 0x05005713, 0x95BF4A82, 0xE2B87A14, 0x7BB12BAE,
0x0CB61B38, 0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21, 0x86D3D2D4, 0xF1D4E242,
0x68DDB3F8, 0x1FDA836E, 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777, 0x88085AE6,
0xFF0F6A70, 0x66063BCA, 0x11010B5C, 0x8F659EFF, 0xF862AE69, 0x616BFFD3, 0x166CCF45,
0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2, 0xA7672661, 0xD06016F7, 0x4969474D,
0x3E6E77DB, 0xAED16A4A, 0xD9D65ADC, 0x40DF0B66, 0x37D83BF0, 0xA9BCAE53, 0xDEBB9EC5,
0x47B2CF7F, 0x30B5FFE9, 0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, 0xBAD03605,
0xCDD70693, 0x54DE5729, 0x23D967BF, 0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94,
0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D
};
mz_uint32 crc32 = (mz_uint32)crc ^ 0xFFFFFFFF;
const mz_uint8 *pByte_buf = (const mz_uint8 *)ptr;
while (buf_len >= 4)
{
crc32 = (crc32 >> 8) ^ s_crc_table[(crc32 ^ pByte_buf[0]) & 0xFF];
crc32 = (crc32 >> 8) ^ s_crc_table[(crc32 ^ pByte_buf[1]) & 0xFF];
crc32 = (crc32 >> 8) ^ s_crc_table[(crc32 ^ pByte_buf[2]) & 0xFF];
crc32 = (crc32 >> 8) ^ s_crc_table[(crc32 ^ pByte_buf[3]) & 0xFF];
pByte_buf += 4;
buf_len -= 4;
}
while (buf_len)
{
crc32 = (crc32 >> 8) ^ s_crc_table[(crc32 ^ pByte_buf[0]) & 0xFF];
++pByte_buf;
--buf_len;
}
return ~crc32;
}
#endif
void mz_free(void *p)
{
MZ_FREE(p);
}
MINIZ_EXPORT void *miniz_def_alloc_func(void *opaque, size_t items, size_t size)
{
(void)opaque, (void)items, (void)size;
return MZ_MALLOC(items * size);
}
MINIZ_EXPORT void miniz_def_free_func(void *opaque, void *address)
{
(void)opaque, (void)address;
MZ_FREE(address);
}
MINIZ_EXPORT void *miniz_def_realloc_func(void *opaque, void *address, size_t items, size_t size)
{
(void)opaque, (void)address, (void)items, (void)size;
return MZ_REALLOC(address, items * size);
}
const char *mz_version(void)
{
return MZ_VERSION;
}
#ifndef MINIZ_NO_ZLIB_APIS
#ifndef MINIZ_NO_DEFLATE_APIS
int mz_deflateInit(mz_streamp pStream, int level)
{
return mz_deflateInit2(pStream, level, MZ_DEFLATED, MZ_DEFAULT_WINDOW_BITS, 9, MZ_DEFAULT_STRATEGY);
}
int mz_deflateInit2(mz_streamp pStream, int level, int method, int window_bits, int mem_level, int strategy)
{
tdefl_compressor *pComp;
mz_uint comp_flags = TDEFL_COMPUTE_ADLER32 | tdefl_create_comp_flags_from_zip_params(level, window_bits, strategy);
if (!pStream)
return MZ_STREAM_ERROR;
if ((method != MZ_DEFLATED) || ((mem_level < 1) || (mem_level > 9)) || ((window_bits != MZ_DEFAULT_WINDOW_BITS) && (-window_bits != MZ_DEFAULT_WINDOW_BITS)))
return MZ_PARAM_ERROR;
pStream->data_type = 0;
pStream->adler = MZ_ADLER32_INIT;
pStream->msg = NULL;
pStream->reserved = 0;
pStream->total_in = 0;
pStream->total_out = 0;
if (!pStream->zalloc)
pStream->zalloc = miniz_def_alloc_func;
if (!pStream->zfree)
pStream->zfree = miniz_def_free_func;
pComp = (tdefl_compressor *)pStream->zalloc(pStream->opaque, 1, sizeof(tdefl_compressor));
if (!pComp)
return MZ_MEM_ERROR;
pStream->state = (struct mz_internal_state *)pComp;
if (tdefl_init(pComp, NULL, NULL, comp_flags) != TDEFL_STATUS_OKAY)
{
mz_deflateEnd(pStream);
return MZ_PARAM_ERROR;
}
return MZ_OK;
}
int mz_deflateReset(mz_streamp pStream)
{
if ((!pStream) || (!pStream->state) || (!pStream->zalloc) || (!pStream->zfree))
return MZ_STREAM_ERROR;
pStream->total_in = pStream->total_out = 0;
tdefl_init((tdefl_compressor *)pStream->state, NULL, NULL, ((tdefl_compressor *)pStream->state)->m_flags);
return MZ_OK;
}
int mz_deflate(mz_streamp pStream, int flush)
{
size_t in_bytes, out_bytes;
mz_ulong orig_total_in, orig_total_out;
int mz_status = MZ_OK;
if ((!pStream) || (!pStream->state) || (flush < 0) || (flush > MZ_FINISH) || (!pStream->next_out))
return MZ_STREAM_ERROR;
if (!pStream->avail_out)
return MZ_BUF_ERROR;
if (flush == MZ_PARTIAL_FLUSH)
flush = MZ_SYNC_FLUSH;
if (((tdefl_compressor *)pStream->state)->m_prev_return_status == TDEFL_STATUS_DONE)
return (flush == MZ_FINISH) ? MZ_STREAM_END : MZ_BUF_ERROR;
orig_total_in = pStream->total_in;
orig_total_out = pStream->total_out;
for (;;)
{
tdefl_status defl_status;
in_bytes = pStream->avail_in;
out_bytes = pStream->avail_out;
defl_status = tdefl_compress((tdefl_compressor *)pStream->state, pStream->next_in, &in_bytes, pStream->next_out, &out_bytes, (tdefl_flush)flush);
pStream->next_in += (mz_uint)in_bytes;
pStream->avail_in -= (mz_uint)in_bytes;
pStream->total_in += (mz_uint)in_bytes;
pStream->adler = tdefl_get_adler32((tdefl_compressor *)pStream->state);
pStream->next_out += (mz_uint)out_bytes;
pStream->avail_out -= (mz_uint)out_bytes;
pStream->total_out += (mz_uint)out_bytes;
if (defl_status < 0)
{
mz_status = MZ_STREAM_ERROR;
break;
}
else if (defl_status == TDEFL_STATUS_DONE)
{
mz_status = MZ_STREAM_END;
break;
}
else if (!pStream->avail_out)
break;
else if ((!pStream->avail_in) && (flush != MZ_FINISH))
{
if ((flush) || (pStream->total_in != orig_total_in) || (pStream->total_out != orig_total_out))
break;
return MZ_BUF_ERROR; /* Can't make forward progress without some input.
*/
}
}
return mz_status;
}
int mz_deflateEnd(mz_streamp pStream)
{
if (!pStream)
return MZ_STREAM_ERROR;
if (pStream->state)
{
pStream->zfree(pStream->opaque, pStream->state);
pStream->state = NULL;
}
return MZ_OK;
}
mz_ulong mz_deflateBound(mz_streamp pStream, mz_ulong source_len)
{
(void)pStream;
/* This is really over conservative. (And lame, but it's actually pretty tricky to compute a true upper bound given the way tdefl's blocking works.) */
return MZ_MAX(128 + (source_len * 110) / 100, 128 + source_len + ((source_len / (31 * 1024)) + 1) * 5);
}
int mz_compress2(unsigned char *pDest, mz_ulong *pDest_len, const unsigned char *pSource, mz_ulong source_len, int level)
{
int status;
mz_stream stream;
memset(&stream, 0, sizeof(stream));
/* In case mz_ulong is 64-bits (argh I hate longs). */
if ((mz_uint64)(source_len | *pDest_len) > 0xFFFFFFFFU)
return MZ_PARAM_ERROR;
stream.next_in = pSource;
stream.avail_in = (mz_uint32)source_len;
stream.next_out = pDest;
stream.avail_out = (mz_uint32)*pDest_len;
status = mz_deflateInit(&stream, level);
if (status != MZ_OK)
return status;
status = mz_deflate(&stream, MZ_FINISH);
if (status != MZ_STREAM_END)
{
mz_deflateEnd(&stream);
return (status == MZ_OK) ? MZ_BUF_ERROR : status;
}
*pDest_len = stream.total_out;
return mz_deflateEnd(&stream);
}
int mz_compress(unsigned char *pDest, mz_ulong *pDest_len, const unsigned char *pSource, mz_ulong source_len)
{
return mz_compress2(pDest, pDest_len, pSource, source_len, MZ_DEFAULT_COMPRESSION);
}
mz_ulong mz_compressBound(mz_ulong source_len)
{
return mz_deflateBound(NULL, source_len);
}
#endif /*#ifndef MINIZ_NO_DEFLATE_APIS*/
#ifndef MINIZ_NO_INFLATE_APIS
typedef struct
{
tinfl_decompressor m_decomp;
mz_uint m_dict_ofs, m_dict_avail, m_first_call, m_has_flushed;
int m_window_bits;
mz_uint8 m_dict[TINFL_LZ_DICT_SIZE];
tinfl_status m_last_status;
} inflate_state;
int mz_inflateInit2(mz_streamp pStream, int window_bits)
{
inflate_state *pDecomp;
if (!pStream)
return MZ_STREAM_ERROR;
if ((window_bits != MZ_DEFAULT_WINDOW_BITS) && (-window_bits != MZ_DEFAULT_WINDOW_BITS))
return MZ_PARAM_ERROR;
pStream->data_type = 0;
pStream->adler = 0;
pStream->msg = NULL;
pStream->total_in = 0;
pStream->total_out = 0;
pStream->reserved = 0;
if (!pStream->zalloc)
pStream->zalloc = miniz_def_alloc_func;
if (!pStream->zfree)
pStream->zfree = miniz_def_free_func;
pDecomp = (inflate_state *)pStream->zalloc(pStream->opaque, 1, sizeof(inflate_state));
if (!pDecomp)
return MZ_MEM_ERROR;
pStream->state = (struct mz_internal_state *)pDecomp;
tinfl_init(&pDecomp->m_decomp);
pDecomp->m_dict_ofs = 0;
pDecomp->m_dict_avail = 0;
pDecomp->m_last_status = TINFL_STATUS_NEEDS_MORE_INPUT;
pDecomp->m_first_call = 1;
pDecomp->m_has_flushed = 0;
pDecomp->m_window_bits = window_bits;
return MZ_OK;
}
int mz_inflateInit(mz_streamp pStream)
{
return mz_inflateInit2(pStream, MZ_DEFAULT_WINDOW_BITS);
}
int mz_inflateReset(mz_streamp pStream)
{
inflate_state *pDecomp;
if (!pStream)
return MZ_STREAM_ERROR;
pStream->data_type = 0;
pStream->adler = 0;
pStream->msg = NULL;
pStream->total_in = 0;
pStream->total_out = 0;
pStream->reserved = 0;
pDecomp = (inflate_state *)pStream->state;
tinfl_init(&pDecomp->m_decomp);
pDecomp->m_dict_ofs = 0;
pDecomp->m_dict_avail = 0;
pDecomp->m_last_status = TINFL_STATUS_NEEDS_MORE_INPUT;
pDecomp->m_first_call = 1;
pDecomp->m_has_flushed = 0;
/* pDecomp->m_window_bits = window_bits */;
return MZ_OK;
}
int mz_inflate(mz_streamp pStream, int flush)
{
inflate_state *pState;
mz_uint n, first_call, decomp_flags = TINFL_FLAG_COMPUTE_ADLER32;
size_t in_bytes, out_bytes, orig_avail_in;
tinfl_status status;
if ((!pStream) || (!pStream->state))
return MZ_STREAM_ERROR;
if (flush == MZ_PARTIAL_FLUSH)
flush = MZ_SYNC_FLUSH;
if ((flush) && (flush != MZ_SYNC_FLUSH) && (flush != MZ_FINISH))
return MZ_STREAM_ERROR;
pState = (inflate_state *)pStream->state;
if (pState->m_window_bits > 0)
decomp_flags |= TINFL_FLAG_PARSE_ZLIB_HEADER;
orig_avail_in = pStream->avail_in;
first_call = pState->m_first_call;
pState->m_first_call = 0;
if (pState->m_last_status < 0)
return MZ_DATA_ERROR;
if (pState->m_has_flushed && (flush != MZ_FINISH))
return MZ_STREAM_ERROR;
pState->m_has_flushed |= (flush == MZ_FINISH);
if ((flush == MZ_FINISH) && (first_call))
{
/* MZ_FINISH on the first call implies that the input and output buffers are large enough to hold the entire compressed/decompressed file. */
decomp_flags |= TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF;
in_bytes = pStream->avail_in;
out_bytes = pStream->avail_out;
status = tinfl_decompress(&pState->m_decomp, pStream->next_in, &in_bytes, pStream->next_out, pStream->next_out, &out_bytes, decomp_flags);
pState->m_last_status = status;
pStream->next_in += (mz_uint)in_bytes;
pStream->avail_in -= (mz_uint)in_bytes;
pStream->total_in += (mz_uint)in_bytes;
pStream->adler = tinfl_get_adler32(&pState->m_decomp);
pStream->next_out += (mz_uint)out_bytes;
pStream->avail_out -= (mz_uint)out_bytes;
pStream->total_out += (mz_uint)out_bytes;
if (status < 0)
return MZ_DATA_ERROR;
else if (status != TINFL_STATUS_DONE)
{
pState->m_last_status = TINFL_STATUS_FAILED;
return MZ_BUF_ERROR;
}
return MZ_STREAM_END;
}
/* flush != MZ_FINISH then we must assume there's more input. */
if (flush != MZ_FINISH)
decomp_flags |= TINFL_FLAG_HAS_MORE_INPUT;
if (pState->m_dict_avail)
{
n = MZ_MIN(pState->m_dict_avail, pStream->avail_out);
memcpy(pStream->next_out, pState->m_dict + pState->m_dict_ofs, n);
pStream->next_out += n;
pStream->avail_out -= n;
pStream->total_out += n;
pState->m_dict_avail -= n;
pState->m_dict_ofs = (pState->m_dict_ofs + n) & (TINFL_LZ_DICT_SIZE - 1);
return ((pState->m_last_status == TINFL_STATUS_DONE) && (!pState->m_dict_avail)) ? MZ_STREAM_END : MZ_OK;
}
for (;;)
{
in_bytes = pStream->avail_in;
out_bytes = TINFL_LZ_DICT_SIZE - pState->m_dict_ofs;
status = tinfl_decompress(&pState->m_decomp, pStream->next_in, &in_bytes, pState->m_dict, pState->m_dict + pState->m_dict_ofs, &out_bytes, decomp_flags);
pState->m_last_status = status;
pStream->next_in += (mz_uint)in_bytes;
pStream->avail_in -= (mz_uint)in_bytes;
pStream->total_in += (mz_uint)in_bytes;
pStream->adler = tinfl_get_adler32(&pState->m_decomp);
pState->m_dict_avail = (mz_uint)out_bytes;
n = MZ_MIN(pState->m_dict_avail, pStream->avail_out);
memcpy(pStream->next_out, pState->m_dict + pState->m_dict_ofs, n);
pStream->next_out += n;
pStream->avail_out -= n;
pStream->total_out += n;
pState->m_dict_avail -= n;
pState->m_dict_ofs = (pState->m_dict_ofs + n) & (TINFL_LZ_DICT_SIZE - 1);
if (status < 0)
return MZ_DATA_ERROR; /* Stream is corrupted (there could be some uncompressed data left in the output dictionary - oh well). */
else if ((status == TINFL_STATUS_NEEDS_MORE_INPUT) && (!orig_avail_in))
return MZ_BUF_ERROR; /* Signal caller that we can't make forward progress without supplying more input or by setting flush to MZ_FINISH. */
else if (flush == MZ_FINISH)
{
/* The output buffer MUST be large to hold the remaining uncompressed data when flush==MZ_FINISH. */
if (status == TINFL_STATUS_DONE)
return pState->m_dict_avail ? MZ_BUF_ERROR : MZ_STREAM_END;
/* status here must be TINFL_STATUS_HAS_MORE_OUTPUT, which means there's at least 1 more byte on the way. If there's no more room left in the output buffer then something is wrong. */
else if (!pStream->avail_out)
return MZ_BUF_ERROR;
}
else if ((status == TINFL_STATUS_DONE) || (!pStream->avail_in) || (!pStream->avail_out) || (pState->m_dict_avail))
break;
}
return ((status == TINFL_STATUS_DONE) && (!pState->m_dict_avail)) ? MZ_STREAM_END : MZ_OK;
}
int mz_inflateEnd(mz_streamp pStream)
{
if (!pStream)
return MZ_STREAM_ERROR;
if (pStream->state)
{
pStream->zfree(pStream->opaque, pStream->state);
pStream->state = NULL;
}
return MZ_OK;
}
int mz_uncompress2(unsigned char *pDest, mz_ulong *pDest_len, const unsigned char *pSource, mz_ulong *pSource_len)
{
mz_stream stream;
int status;
memset(&stream, 0, sizeof(stream));
/* In case mz_ulong is 64-bits (argh I hate longs). */
if ((mz_uint64)(*pSource_len | *pDest_len) > 0xFFFFFFFFU)
return MZ_PARAM_ERROR;
stream.next_in = pSource;
stream.avail_in = (mz_uint32)*pSource_len;
stream.next_out = pDest;
stream.avail_out = (mz_uint32)*pDest_len;
status = mz_inflateInit(&stream);
if (status != MZ_OK)
return status;
status = mz_inflate(&stream, MZ_FINISH);
*pSource_len = *pSource_len - stream.avail_in;
if (status != MZ_STREAM_END)
{
mz_inflateEnd(&stream);
return ((status == MZ_BUF_ERROR) && (!stream.avail_in)) ? MZ_DATA_ERROR : status;
}
*pDest_len = stream.total_out;
return mz_inflateEnd(&stream);
}
int mz_uncompress(unsigned char *pDest, mz_ulong *pDest_len, const unsigned char *pSource, mz_ulong source_len)
{
return mz_uncompress2(pDest, pDest_len, pSource, &source_len);
}
#endif /*#ifndef MINIZ_NO_INFLATE_APIS*/
const char *mz_error(int err)
{
static struct
{
int m_err;
const char *m_pDesc;
} s_error_descs[] = {
{ MZ_OK, "" }, { MZ_STREAM_END, "stream end" }, { MZ_NEED_DICT, "need dictionary" }, { MZ_ERRNO, "file error" }, { MZ_STREAM_ERROR, "stream error" }, { MZ_DATA_ERROR, "data error" }, { MZ_MEM_ERROR, "out of memory" }, { MZ_BUF_ERROR, "buf error" }, { MZ_VERSION_ERROR, "version error" }, { MZ_PARAM_ERROR, "parameter error" }
};
mz_uint i;
for (i = 0; i < sizeof(s_error_descs) / sizeof(s_error_descs[0]); ++i)
if (s_error_descs[i].m_err == err)
return s_error_descs[i].m_pDesc;
return NULL;
}
#endif /*MINIZ_NO_ZLIB_APIS */
#ifdef __cplusplus
}
#endif
/*
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/>
*/
+615
View File
@@ -0,0 +1,615 @@
/* miniz.c 3.1.0 - public domain deflate/inflate, zlib-subset, ZIP reading/writing/appending, PNG writing
See "unlicense" statement at the end of this file.
Rich Geldreich <richgel99@gmail.com>, last updated Oct. 13, 2013
Implements RFC 1950: http://www.ietf.org/rfc/rfc1950.txt and RFC 1951: http://www.ietf.org/rfc/rfc1951.txt
Most API's defined in miniz.c are optional. For example, to disable the archive related functions just define
MINIZ_NO_ARCHIVE_APIS, or to get rid of all stdio usage define MINIZ_NO_STDIO (see the list below for more macros).
* Low-level Deflate/Inflate implementation notes:
Compression: Use the "tdefl" API's. The compressor supports raw, static, and dynamic blocks, lazy or
greedy parsing, match length filtering, RLE-only, and Huffman-only streams. It performs and compresses
approximately as well as zlib.
Decompression: Use the "tinfl" API's. The entire decompressor is implemented as a single function
coroutine: see tinfl_decompress(). It supports decompression into a 32KB (or larger power of 2) wrapping buffer, or into a memory
block large enough to hold the entire file.
The low-level tdefl/tinfl API's do not make any use of dynamic memory allocation.
* zlib-style API notes:
miniz.c implements a fairly large subset of zlib. There's enough functionality present for it to be a drop-in
zlib replacement in many apps:
The z_stream struct, optional memory allocation callbacks
deflateInit/deflateInit2/deflate/deflateReset/deflateEnd/deflateBound
inflateInit/inflateInit2/inflate/inflateReset/inflateEnd
compress, compress2, compressBound, uncompress
CRC-32, Adler-32 - Using modern, minimal code size, CPU cache friendly routines.
Supports raw deflate streams or standard zlib streams with adler-32 checking.
Limitations:
The callback API's are not implemented yet. No support for gzip headers or zlib static dictionaries.
I've tried to closely emulate zlib's various flavors of stream flushing and return status codes, but
there are no guarantees that miniz.c pulls this off perfectly.
* PNG writing: See the tdefl_write_image_to_png_file_in_memory() function, originally written by
Alex Evans. Supports 1-4 bytes/pixel images.
* ZIP archive API notes:
The ZIP archive API's where designed with simplicity and efficiency in mind, with just enough abstraction to
get the job done with minimal fuss. There are simple API's to retrieve file information, read files from
existing archives, create new archives, append new files to existing archives, or clone archive data from
one archive to another. It supports archives located in memory or the heap, on disk (using stdio.h),
or you can specify custom file read/write callbacks.
- Archive reading: Just call this function to read a single file from a disk archive:
void *mz_zip_extract_archive_file_to_heap(const char *pZip_filename, const char *pArchive_name,
size_t *pSize, mz_uint zip_flags);
For more complex cases, use the "mz_zip_reader" functions. Upon opening an archive, the entire central
directory is located and read as-is into memory, and subsequent file access only occurs when reading individual files.
- Archives file scanning: The simple way is to use this function to scan a loaded archive for a specific file:
int mz_zip_reader_locate_file(mz_zip_archive *pZip, const char *pName, const char *pComment, mz_uint flags);
The locate operation can optionally check file comments too, which (as one example) can be used to identify
multiple versions of the same file in an archive. This function uses a simple linear search through the central
directory, so it's not very fast.
Alternately, you can iterate through all the files in an archive (using mz_zip_reader_get_num_files()) and
retrieve detailed info on each file by calling mz_zip_reader_file_stat().
- Archive creation: Use the "mz_zip_writer" functions. The ZIP writer immediately writes compressed file data
to disk and builds an exact image of the central directory in memory. The central directory image is written
all at once at the end of the archive file when the archive is finalized.
The archive writer can optionally align each file's local header and file data to any power of 2 alignment,
which can be useful when the archive will be read from optical media. Also, the writer supports placing
arbitrary data blobs at the very beginning of ZIP archives. Archives written using either feature are still
readable by any ZIP tool.
- Archive appending: The simple way to add a single file to an archive is to call this function:
mz_bool mz_zip_add_mem_to_archive_file_in_place(const char *pZip_filename, const char *pArchive_name,
const void *pBuf, size_t buf_size, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags);
The archive will be created if it doesn't already exist, otherwise it'll be appended to.
Note the appending is done in-place and is not an atomic operation, so if something goes wrong
during the operation it's possible the archive could be left without a central directory (although the local
file headers and file data will be fine, so the archive will be recoverable).
For more complex archive modification scenarios:
1. The safest way is to use a mz_zip_reader to read the existing archive, cloning only those bits you want to
preserve into a new archive using using the mz_zip_writer_add_from_zip_reader() function (which compiles the
compressed file data as-is). When you're done, delete the old archive and rename the newly written archive, and
you're done. This is safe but requires a bunch of temporary disk space or heap memory.
2. Or, you can convert an mz_zip_reader in-place to an mz_zip_writer using mz_zip_writer_init_from_reader(),
append new files as needed, then finalize the archive which will write an updated central directory to the
original archive. (This is basically what mz_zip_add_mem_to_archive_file_in_place() does.) There's a
possibility that the archive's central directory could be lost with this method if anything goes wrong, though.
- ZIP archive support limitations:
No spanning support. Extraction functions can only handle unencrypted, stored or deflated files.
Requires streams capable of seeking.
* This is a header file library, like stb_image.c. To get only a header file, either cut and paste the
below header, or create miniz.h, #define MINIZ_HEADER_FILE_ONLY, and then include miniz.c from it.
* Important: For best perf. be sure to customize the below macros for your target platform:
#define MINIZ_USE_UNALIGNED_LOADS_AND_STORES 1
#define MINIZ_LITTLE_ENDIAN 1
#define MINIZ_HAS_64BIT_REGISTERS 1
* On platforms using glibc, Be sure to "#define _LARGEFILE64_SOURCE 1" before including miniz.c to ensure miniz
uses the 64-bit variants: fopen64(), stat64(), etc. Otherwise you won't be able to process large files
(i.e. 32-bit stat() fails for me on files > 0x7FFFFFFF bytes).
*/
#pragma once
#include "miniz_export.h"
#if defined(__STRICT_ANSI__)
#define MZ_FORCEINLINE
#elif defined(_MSC_VER)
#define MZ_FORCEINLINE __forceinline
#elif defined(__GNUC__)
#define MZ_FORCEINLINE __inline__ __attribute__((__always_inline__))
#else
#define MZ_FORCEINLINE inline
#endif
/* Defines to completely disable specific portions of miniz.c:
If all macros here are defined the only functionality remaining will be CRC-32 and adler-32. */
/* Define MINIZ_NO_STDIO to disable all usage and any functions which rely on stdio for file I/O. */
/*#define MINIZ_NO_STDIO */
/* If MINIZ_NO_TIME is specified then the ZIP archive functions will not be able to get the current time, or */
/* get/set file times, and the C run-time funcs that get/set times won't be called. */
/* The current downside is the times written to your archives will be from 1979. */
/*#define MINIZ_NO_TIME */
/* Define MINIZ_NO_DEFLATE_APIS to disable all compression API's. */
/*#define MINIZ_NO_DEFLATE_APIS */
/* Define MINIZ_NO_INFLATE_APIS to disable all decompression API's. */
/*#define MINIZ_NO_INFLATE_APIS */
/* Define MINIZ_NO_ARCHIVE_APIS to disable all ZIP archive API's. */
/*#define MINIZ_NO_ARCHIVE_APIS */
/* Define MINIZ_NO_ARCHIVE_WRITING_APIS to disable all writing related ZIP archive API's. */
/*#define MINIZ_NO_ARCHIVE_WRITING_APIS */
/* Define MINIZ_NO_ZLIB_APIS to remove all ZLIB-style compression/decompression API's. */
/*#define MINIZ_NO_ZLIB_APIS */
/* Define MINIZ_NO_ZLIB_COMPATIBLE_NAME to disable zlib names, to prevent conflicts against stock zlib. */
/*#define MINIZ_NO_ZLIB_COMPATIBLE_NAMES */
/* Define MINIZ_NO_MALLOC to disable all calls to malloc, free, and realloc.
Note if MINIZ_NO_MALLOC is defined then the user must always provide custom user alloc/free/realloc
callbacks to the zlib and archive API's, and a few stand-alone helper API's which don't provide custom user
functions (such as tdefl_compress_mem_to_heap() and tinfl_decompress_mem_to_heap()) won't work. */
/*#define MINIZ_NO_MALLOC */
#ifdef MINIZ_NO_INFLATE_APIS
#define MINIZ_NO_ARCHIVE_APIS
#endif
#ifdef MINIZ_NO_DEFLATE_APIS
#define MINIZ_NO_ARCHIVE_WRITING_APIS
#endif
#if defined(__TINYC__) && (defined(__linux) || defined(__linux__))
/* TODO: Work around "error: include file 'sys\utime.h' when compiling with tcc on Linux */
#define MINIZ_NO_TIME
#endif
#include <stddef.h>
#if !defined(MINIZ_NO_TIME) && !defined(MINIZ_NO_ARCHIVE_APIS)
#include <time.h>
#endif
#if defined(_M_IX86) || defined(_M_X64) || defined(__i386__) || defined(__i386) || defined(__i486__) || defined(__i486) || defined(i386) || defined(__ia64__) || defined(__x86_64__)
/* MINIZ_X86_OR_X64_CPU is only used to help set the below macros. */
#define MINIZ_X86_OR_X64_CPU 1
#else
#define MINIZ_X86_OR_X64_CPU 0
#endif
/* Set MINIZ_LITTLE_ENDIAN only if not set */
#if !defined(MINIZ_LITTLE_ENDIAN)
#if defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__)
#if (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)
/* Set MINIZ_LITTLE_ENDIAN to 1 if the processor is little endian. */
#define MINIZ_LITTLE_ENDIAN 1
#else
#define MINIZ_LITTLE_ENDIAN 0
#endif
#else
#if MINIZ_X86_OR_X64_CPU
#define MINIZ_LITTLE_ENDIAN 1
#else
#define MINIZ_LITTLE_ENDIAN 0
#endif
#endif
#endif
/* Using unaligned loads and stores causes errors when using UBSan */
#if defined(__has_feature)
#if __has_feature(undefined_behavior_sanitizer)
#define MINIZ_USE_UNALIGNED_LOADS_AND_STORES 0
#endif
#endif
/* Set MINIZ_USE_UNALIGNED_LOADS_AND_STORES only if not set */
#if !defined(MINIZ_USE_UNALIGNED_LOADS_AND_STORES)
#if MINIZ_X86_OR_X64_CPU
/* Set MINIZ_USE_UNALIGNED_LOADS_AND_STORES to 1 on CPU's that permit efficient integer loads and stores from unaligned addresses. */
#define MINIZ_USE_UNALIGNED_LOADS_AND_STORES 0
#define MINIZ_UNALIGNED_USE_MEMCPY
#else
#define MINIZ_USE_UNALIGNED_LOADS_AND_STORES 0
#endif
#endif
#if defined(_M_X64) || defined(_WIN64) || defined(__MINGW64__) || defined(_LP64) || defined(__LP64__) || defined(__ia64__) || defined(__x86_64__)
/* Set MINIZ_HAS_64BIT_REGISTERS to 1 if operations on 64-bit integers are reasonably fast (and don't involve compiler generated calls to helper functions). */
#define MINIZ_HAS_64BIT_REGISTERS 1
#else
#define MINIZ_HAS_64BIT_REGISTERS 0
#endif
#ifdef __cplusplus
extern "C"
{
#endif
/* ------------------- zlib-style API Definitions. */
/* For more compatibility with zlib, miniz.c uses unsigned long for some parameters/struct members. Beware: mz_ulong can be either 32 or 64-bits! */
typedef unsigned long mz_ulong;
/* mz_free() internally uses the MZ_FREE() macro (which by default calls free() unless you've modified the MZ_MALLOC macro) to release a block allocated from the heap. */
MINIZ_EXPORT void mz_free(void *p);
#define MZ_ADLER32_INIT (1)
/* mz_adler32() returns the initial adler-32 value to use when called with ptr==NULL. */
MINIZ_EXPORT mz_ulong mz_adler32(mz_ulong adler, const unsigned char *ptr, size_t buf_len);
#define MZ_CRC32_INIT (0)
/* mz_crc32() returns the initial CRC-32 value to use when called with ptr==NULL. */
MINIZ_EXPORT mz_ulong mz_crc32(mz_ulong crc, const unsigned char *ptr, size_t buf_len);
/* Compression strategies. */
enum
{
MZ_DEFAULT_STRATEGY = 0,
MZ_FILTERED = 1,
MZ_HUFFMAN_ONLY = 2,
MZ_RLE = 3,
MZ_FIXED = 4
};
/* Method */
#define MZ_DEFLATED 8
/* Heap allocation callbacks.
Note that mz_alloc_func parameter types purposely differ from zlib's: items/size is size_t, not unsigned long. */
typedef void *(*mz_alloc_func)(void *opaque, size_t items, size_t size);
typedef void (*mz_free_func)(void *opaque, void *address);
typedef void *(*mz_realloc_func)(void *opaque, void *address, size_t items, size_t size);
/* Compression levels: 0-9 are the standard zlib-style levels, 10 is best possible compression (not zlib compatible, and may be very slow), MZ_DEFAULT_COMPRESSION=MZ_DEFAULT_LEVEL. */
enum
{
MZ_NO_COMPRESSION = 0,
MZ_BEST_SPEED = 1,
MZ_BEST_COMPRESSION = 9,
MZ_UBER_COMPRESSION = 10,
MZ_DEFAULT_LEVEL = 6,
MZ_DEFAULT_COMPRESSION = -1
};
#define MZ_VERSION "11.3.1"
#define MZ_VERNUM 0xB301
#define MZ_VER_MAJOR 11
#define MZ_VER_MINOR 3
#define MZ_VER_REVISION 1
#define MZ_VER_SUBREVISION 0
#ifndef MINIZ_NO_ZLIB_APIS
/* Flush values. For typical usage you only need MZ_NO_FLUSH and MZ_FINISH. The other values are for advanced use (refer to the zlib docs). */
enum
{
MZ_NO_FLUSH = 0,
MZ_PARTIAL_FLUSH = 1,
MZ_SYNC_FLUSH = 2,
MZ_FULL_FLUSH = 3,
MZ_FINISH = 4,
MZ_BLOCK = 5
};
/* Return status codes. MZ_PARAM_ERROR is non-standard. */
enum
{
MZ_OK = 0,
MZ_STREAM_END = 1,
MZ_NEED_DICT = 2,
MZ_ERRNO = -1,
MZ_STREAM_ERROR = -2,
MZ_DATA_ERROR = -3,
MZ_MEM_ERROR = -4,
MZ_BUF_ERROR = -5,
MZ_VERSION_ERROR = -6,
MZ_PARAM_ERROR = -10000
};
/* Window bits */
#define MZ_DEFAULT_WINDOW_BITS 15
struct mz_internal_state;
/* Compression/decompression stream struct. */
typedef struct mz_stream_s
{
const unsigned char *next_in; /* pointer to next byte to read */
unsigned int avail_in; /* number of bytes available at next_in */
mz_ulong total_in; /* total number of bytes consumed so far */
unsigned char *next_out; /* pointer to next byte to write */
unsigned int avail_out; /* number of bytes that can be written to next_out */
mz_ulong total_out; /* total number of bytes produced so far */
char *msg; /* error msg (unused) */
struct mz_internal_state *state; /* internal state, allocated by zalloc/zfree */
mz_alloc_func zalloc; /* optional heap allocation function (defaults to malloc) */
mz_free_func zfree; /* optional heap free function (defaults to free) */
void *opaque; /* heap alloc function user pointer */
int data_type; /* data_type (unused) */
mz_ulong adler; /* adler32 of the source or uncompressed data */
mz_ulong reserved; /* not used */
} mz_stream;
typedef mz_stream *mz_streamp;
/* Returns the version string of miniz.c. */
MINIZ_EXPORT const char *mz_version(void);
#ifndef MINIZ_NO_DEFLATE_APIS
/* mz_deflateInit() initializes a compressor with default options: */
/* Parameters: */
/* pStream must point to an initialized mz_stream struct. */
/* level must be between [MZ_NO_COMPRESSION, MZ_BEST_COMPRESSION]. */
/* level 1 enables a specially optimized compression function that's been optimized purely for performance, not ratio. */
/* (This special func. is currently only enabled when MINIZ_USE_UNALIGNED_LOADS_AND_STORES and MINIZ_LITTLE_ENDIAN are defined.) */
/* Return values: */
/* MZ_OK on success. */
/* MZ_STREAM_ERROR if the stream is bogus. */
/* MZ_PARAM_ERROR if the input parameters are bogus. */
/* MZ_MEM_ERROR on out of memory. */
MINIZ_EXPORT int mz_deflateInit(mz_streamp pStream, int level);
/* mz_deflateInit2() is like mz_deflate(), except with more control: */
/* Additional parameters: */
/* method must be MZ_DEFLATED */
/* window_bits must be MZ_DEFAULT_WINDOW_BITS (to wrap the deflate stream with zlib header/adler-32 footer) or -MZ_DEFAULT_WINDOW_BITS (raw deflate/no header or footer) */
/* mem_level must be between [1, 9] (it's checked but ignored by miniz.c) */
MINIZ_EXPORT int mz_deflateInit2(mz_streamp pStream, int level, int method, int window_bits, int mem_level, int strategy);
/* Quickly resets a compressor without having to reallocate anything. Same as calling mz_deflateEnd() followed by mz_deflateInit()/mz_deflateInit2(). */
MINIZ_EXPORT int mz_deflateReset(mz_streamp pStream);
/* mz_deflate() compresses the input to output, consuming as much of the input and producing as much output as possible. */
/* Parameters: */
/* pStream is the stream to read from and write to. You must initialize/update the next_in, avail_in, next_out, and avail_out members. */
/* flush may be MZ_NO_FLUSH, MZ_PARTIAL_FLUSH/MZ_SYNC_FLUSH, MZ_FULL_FLUSH, or MZ_FINISH. */
/* Return values: */
/* MZ_OK on success (when flushing, or if more input is needed but not available, and/or there's more output to be written but the output buffer is full). */
/* MZ_STREAM_END if all input has been consumed and all output bytes have been written. Don't call mz_deflate() on the stream anymore. */
/* MZ_STREAM_ERROR if the stream is bogus. */
/* MZ_PARAM_ERROR if one of the parameters is invalid. */
/* MZ_BUF_ERROR if no forward progress is possible because the input and/or output buffers are empty. (Fill up the input buffer or free up some output space and try again.) */
MINIZ_EXPORT int mz_deflate(mz_streamp pStream, int flush);
/* mz_deflateEnd() deinitializes a compressor: */
/* Return values: */
/* MZ_OK on success. */
/* MZ_STREAM_ERROR if the stream is bogus. */
MINIZ_EXPORT int mz_deflateEnd(mz_streamp pStream);
/* mz_deflateBound() returns a (very) conservative upper bound on the amount of data that could be generated by deflate(), assuming flush is set to only MZ_NO_FLUSH or MZ_FINISH. */
MINIZ_EXPORT mz_ulong mz_deflateBound(mz_streamp pStream, mz_ulong source_len);
/* Single-call compression functions mz_compress() and mz_compress2(): */
/* Returns MZ_OK on success, or one of the error codes from mz_deflate() on failure. */
MINIZ_EXPORT int mz_compress(unsigned char *pDest, mz_ulong *pDest_len, const unsigned char *pSource, mz_ulong source_len);
MINIZ_EXPORT int mz_compress2(unsigned char *pDest, mz_ulong *pDest_len, const unsigned char *pSource, mz_ulong source_len, int level);
/* mz_compressBound() returns a (very) conservative upper bound on the amount of data that could be generated by calling mz_compress(). */
MINIZ_EXPORT mz_ulong mz_compressBound(mz_ulong source_len);
#endif /*#ifndef MINIZ_NO_DEFLATE_APIS*/
#ifndef MINIZ_NO_INFLATE_APIS
/* Initializes a decompressor. */
MINIZ_EXPORT int mz_inflateInit(mz_streamp pStream);
/* mz_inflateInit2() is like mz_inflateInit() with an additional option that controls the window size and whether or not the stream has been wrapped with a zlib header/footer: */
/* window_bits must be MZ_DEFAULT_WINDOW_BITS (to parse zlib header/footer) or -MZ_DEFAULT_WINDOW_BITS (raw deflate). */
MINIZ_EXPORT int mz_inflateInit2(mz_streamp pStream, int window_bits);
/* Quickly resets a compressor without having to reallocate anything. Same as calling mz_inflateEnd() followed by mz_inflateInit()/mz_inflateInit2(). */
MINIZ_EXPORT int mz_inflateReset(mz_streamp pStream);
/* Decompresses the input stream to the output, consuming only as much of the input as needed, and writing as much to the output as possible. */
/* Parameters: */
/* pStream is the stream to read from and write to. You must initialize/update the next_in, avail_in, next_out, and avail_out members. */
/* flush may be MZ_NO_FLUSH, MZ_SYNC_FLUSH, or MZ_FINISH. */
/* On the first call, if flush is MZ_FINISH it's assumed the input and output buffers are both sized large enough to decompress the entire stream in a single call (this is slightly faster). */
/* MZ_FINISH implies that there are no more source bytes available beside what's already in the input buffer, and that the output buffer is large enough to hold the rest of the decompressed data. */
/* Return values: */
/* MZ_OK on success. Either more input is needed but not available, and/or there's more output to be written but the output buffer is full. */
/* MZ_STREAM_END if all needed input has been consumed and all output bytes have been written. For zlib streams, the adler-32 of the decompressed data has also been verified. */
/* MZ_STREAM_ERROR if the stream is bogus. */
/* MZ_DATA_ERROR if the deflate stream is invalid. */
/* MZ_PARAM_ERROR if one of the parameters is invalid. */
/* MZ_BUF_ERROR if no forward progress is possible because the input buffer is empty but the inflater needs more input to continue, or if the output buffer is not large enough. Call mz_inflate() again */
/* with more input data, or with more room in the output buffer (except when using single call decompression, described above). */
MINIZ_EXPORT int mz_inflate(mz_streamp pStream, int flush);
/* Deinitializes a decompressor. */
MINIZ_EXPORT int mz_inflateEnd(mz_streamp pStream);
/* Single-call decompression. */
/* Returns MZ_OK on success, or one of the error codes from mz_inflate() on failure. */
MINIZ_EXPORT int mz_uncompress(unsigned char *pDest, mz_ulong *pDest_len, const unsigned char *pSource, mz_ulong source_len);
MINIZ_EXPORT int mz_uncompress2(unsigned char *pDest, mz_ulong *pDest_len, const unsigned char *pSource, mz_ulong *pSource_len);
#endif /*#ifndef MINIZ_NO_INFLATE_APIS*/
/* Returns a string description of the specified error code, or NULL if the error code is invalid. */
MINIZ_EXPORT const char *mz_error(int err);
/* Redefine zlib-compatible names to miniz equivalents, so miniz.c can be used as a drop-in replacement for the subset of zlib that miniz.c supports. */
/* Define MINIZ_NO_ZLIB_COMPATIBLE_NAMES to disable zlib-compatibility if you use zlib in the same project. */
#ifndef MINIZ_NO_ZLIB_COMPATIBLE_NAMES
typedef unsigned char Byte;
typedef unsigned int uInt;
typedef mz_ulong uLong;
typedef Byte Bytef;
typedef uInt uIntf;
typedef char charf;
typedef int intf;
typedef void *voidpf;
typedef uLong uLongf;
typedef void *voidp;
typedef void *const voidpc;
#define Z_NULL 0
#define Z_NO_FLUSH MZ_NO_FLUSH
#define Z_PARTIAL_FLUSH MZ_PARTIAL_FLUSH
#define Z_SYNC_FLUSH MZ_SYNC_FLUSH
#define Z_FULL_FLUSH MZ_FULL_FLUSH
#define Z_FINISH MZ_FINISH
#define Z_BLOCK MZ_BLOCK
#define Z_OK MZ_OK
#define Z_STREAM_END MZ_STREAM_END
#define Z_NEED_DICT MZ_NEED_DICT
#define Z_ERRNO MZ_ERRNO
#define Z_STREAM_ERROR MZ_STREAM_ERROR
#define Z_DATA_ERROR MZ_DATA_ERROR
#define Z_MEM_ERROR MZ_MEM_ERROR
#define Z_BUF_ERROR MZ_BUF_ERROR
#define Z_VERSION_ERROR MZ_VERSION_ERROR
#define Z_PARAM_ERROR MZ_PARAM_ERROR
#define Z_NO_COMPRESSION MZ_NO_COMPRESSION
#define Z_BEST_SPEED MZ_BEST_SPEED
#define Z_BEST_COMPRESSION MZ_BEST_COMPRESSION
#define Z_DEFAULT_COMPRESSION MZ_DEFAULT_COMPRESSION
#define Z_DEFAULT_STRATEGY MZ_DEFAULT_STRATEGY
#define Z_FILTERED MZ_FILTERED
#define Z_HUFFMAN_ONLY MZ_HUFFMAN_ONLY
#define Z_RLE MZ_RLE
#define Z_FIXED MZ_FIXED
#define Z_DEFLATED MZ_DEFLATED
#define Z_DEFAULT_WINDOW_BITS MZ_DEFAULT_WINDOW_BITS
/* See mz_alloc_func */
typedef void *(*alloc_func)(void *opaque, size_t items, size_t size);
/* See mz_free_func */
typedef void (*free_func)(void *opaque, void *address);
#define internal_state mz_internal_state
#define z_stream mz_stream
#ifndef MINIZ_NO_DEFLATE_APIS
/* Compatiblity with zlib API. See called functions for documentation */
static MZ_FORCEINLINE int deflateInit(mz_streamp pStream, int level)
{
return mz_deflateInit(pStream, level);
}
static MZ_FORCEINLINE int deflateInit2(mz_streamp pStream, int level, int method, int window_bits, int mem_level, int strategy)
{
return mz_deflateInit2(pStream, level, method, window_bits, mem_level, strategy);
}
static MZ_FORCEINLINE int deflateReset(mz_streamp pStream)
{
return mz_deflateReset(pStream);
}
static MZ_FORCEINLINE int deflate(mz_streamp pStream, int flush)
{
return mz_deflate(pStream, flush);
}
static MZ_FORCEINLINE int deflateEnd(mz_streamp pStream)
{
return mz_deflateEnd(pStream);
}
static MZ_FORCEINLINE mz_ulong deflateBound(mz_streamp pStream, mz_ulong source_len)
{
return mz_deflateBound(pStream, source_len);
}
static MZ_FORCEINLINE int compress(unsigned char *pDest, mz_ulong *pDest_len, const unsigned char *pSource, mz_ulong source_len)
{
return mz_compress(pDest, pDest_len, pSource, source_len);
}
static MZ_FORCEINLINE int compress2(unsigned char *pDest, mz_ulong *pDest_len, const unsigned char *pSource, mz_ulong source_len, int level)
{
return mz_compress2(pDest, pDest_len, pSource, source_len, level);
}
static MZ_FORCEINLINE mz_ulong compressBound(mz_ulong source_len)
{
return mz_compressBound(source_len);
}
#endif /*#ifndef MINIZ_NO_DEFLATE_APIS*/
#ifndef MINIZ_NO_INFLATE_APIS
/* Compatiblity with zlib API. See called functions for documentation */
static MZ_FORCEINLINE int inflateInit(mz_streamp pStream)
{
return mz_inflateInit(pStream);
}
static MZ_FORCEINLINE int inflateInit2(mz_streamp pStream, int window_bits)
{
return mz_inflateInit2(pStream, window_bits);
}
static MZ_FORCEINLINE int inflateReset(mz_streamp pStream)
{
return mz_inflateReset(pStream);
}
static MZ_FORCEINLINE int inflate(mz_streamp pStream, int flush)
{
return mz_inflate(pStream, flush);
}
static MZ_FORCEINLINE int inflateEnd(mz_streamp pStream)
{
return mz_inflateEnd(pStream);
}
static MZ_FORCEINLINE int uncompress(unsigned char* pDest, mz_ulong* pDest_len, const unsigned char* pSource, mz_ulong source_len)
{
return mz_uncompress(pDest, pDest_len, pSource, source_len);
}
static MZ_FORCEINLINE int uncompress2(unsigned char* pDest, mz_ulong* pDest_len, const unsigned char* pSource, mz_ulong* pSource_len)
{
return mz_uncompress2(pDest, pDest_len, pSource, pSource_len);
}
#endif /*#ifndef MINIZ_NO_INFLATE_APIS*/
static MZ_FORCEINLINE mz_ulong crc32(mz_ulong crc, const unsigned char *ptr, size_t buf_len)
{
return mz_crc32(crc, ptr, buf_len);
}
static MZ_FORCEINLINE mz_ulong adler32(mz_ulong adler, const unsigned char *ptr, size_t buf_len)
{
return mz_adler32(adler, ptr, buf_len);
}
#define MAX_WBITS 15
#define MAX_MEM_LEVEL 9
static MZ_FORCEINLINE const char* zError(int err)
{
return mz_error(err);
}
#define ZLIB_VERSION MZ_VERSION
#define ZLIB_VERNUM MZ_VERNUM
#define ZLIB_VER_MAJOR MZ_VER_MAJOR
#define ZLIB_VER_MINOR MZ_VER_MINOR
#define ZLIB_VER_REVISION MZ_VER_REVISION
#define ZLIB_VER_SUBREVISION MZ_VER_SUBREVISION
#define zlibVersion mz_version
#define zlib_version mz_version()
#endif /* #ifndef MINIZ_NO_ZLIB_COMPATIBLE_NAMES */
#endif /* MINIZ_NO_ZLIB_APIS */
#ifdef __cplusplus
}
#endif
#include "miniz_common.h"
#include "miniz_tdef.h"
#include "miniz_tinfl.h"
#include "miniz_zip.h"
+89
View File
@@ -0,0 +1,89 @@
#pragma once
#include <assert.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include "miniz_export.h"
/* ------------------- Types and macros */
typedef unsigned char mz_uint8;
typedef int16_t mz_int16;
typedef uint16_t mz_uint16;
typedef uint32_t mz_uint32;
typedef uint32_t mz_uint;
typedef int64_t mz_int64;
typedef uint64_t mz_uint64;
typedef int mz_bool;
#define MZ_FALSE (0)
#define MZ_TRUE (1)
/* Works around MSVC's spammy "warning C4127: conditional expression is constant" message. */
#ifdef _MSC_VER
#define MZ_MACRO_END while (0, 0)
#else
#define MZ_MACRO_END while (0)
#endif
#ifdef MINIZ_NO_STDIO
#define MZ_FILE void *
#else
#include <stdio.h>
#define MZ_FILE FILE
#endif /* #ifdef MINIZ_NO_STDIO */
#ifdef MINIZ_NO_TIME
typedef struct mz_dummy_time_t_tag
{
mz_uint32 m_dummy1;
mz_uint32 m_dummy2;
} mz_dummy_time_t;
#define MZ_TIME_T mz_dummy_time_t
#else
#define MZ_TIME_T time_t
#endif
#define MZ_ASSERT(x) assert(x)
#ifdef MINIZ_NO_MALLOC
#define MZ_MALLOC(x) NULL
#define MZ_FREE(x) (void)x, ((void)0)
#define MZ_REALLOC(p, x) NULL
#else
#define MZ_MALLOC(x) malloc(x)
#define MZ_FREE(x) free(x)
#define MZ_REALLOC(p, x) realloc(p, x)
#endif
#define MZ_MAX(a, b) (((a) > (b)) ? (a) : (b))
#define MZ_MIN(a, b) (((a) < (b)) ? (a) : (b))
#define MZ_CLEAR_OBJ(obj) memset(&(obj), 0, sizeof(obj))
#define MZ_CLEAR_ARR(obj) memset((obj), 0, sizeof(obj))
#define MZ_CLEAR_PTR(obj) memset((obj), 0, sizeof(*obj))
#if MINIZ_USE_UNALIGNED_LOADS_AND_STORES && MINIZ_LITTLE_ENDIAN
#define MZ_READ_LE16(p) *((const mz_uint16 *)(p))
#define MZ_READ_LE32(p) *((const mz_uint32 *)(p))
#else
#define MZ_READ_LE16(p) ((mz_uint32)(((const mz_uint8 *)(p))[0]) | ((mz_uint32)(((const mz_uint8 *)(p))[1]) << 8U))
#define MZ_READ_LE32(p) ((mz_uint32)(((const mz_uint8 *)(p))[0]) | ((mz_uint32)(((const mz_uint8 *)(p))[1]) << 8U) | ((mz_uint32)(((const mz_uint8 *)(p))[2]) << 16U) | ((mz_uint32)(((const mz_uint8 *)(p))[3]) << 24U))
#endif
#define MZ_READ_LE64(p) (((mz_uint64)MZ_READ_LE32(p)) | (((mz_uint64)MZ_READ_LE32((const mz_uint8 *)(p) + sizeof(mz_uint32))) << 32U))
#ifdef __cplusplus
extern "C"
{
#endif
extern MINIZ_EXPORT void *miniz_def_alloc_func(void *opaque, size_t items, size_t size);
extern MINIZ_EXPORT void miniz_def_free_func(void *opaque, void *address);
extern MINIZ_EXPORT void *miniz_def_realloc_func(void *opaque, void *address, size_t items, size_t size);
#define MZ_UINT16_MAX (0xFFFFU)
#define MZ_UINT32_MAX (0xFFFFFFFFU)
#ifdef __cplusplus
}
#endif
+8
View File
@@ -0,0 +1,8 @@
#ifndef MINIZ_EXPORT_H
#define MINIZ_EXPORT_H
#ifndef MINIZ_EXPORT
#define MINIZ_EXPORT
#endif
#endif /* MINIZ_EXPORT_H */
+199
View File
@@ -0,0 +1,199 @@
#pragma once
#include "miniz_common.h"
#ifndef MINIZ_NO_DEFLATE_APIS
#ifdef __cplusplus
extern "C"
{
#endif
/* ------------------- Low-level Compression API Definitions */
/* Set TDEFL_LESS_MEMORY to 1 to use less memory (compression will be slightly slower, and raw/dynamic blocks will be output more frequently). */
#ifndef TDEFL_LESS_MEMORY
#define TDEFL_LESS_MEMORY 0
#endif
/* tdefl_init() compression flags logically OR'd together (low 12 bits contain the max. number of probes per dictionary search): */
/* TDEFL_DEFAULT_MAX_PROBES: The compressor defaults to 128 dictionary probes per dictionary search. 0=Huffman only, 1=Huffman+LZ (fastest/crap compression), 4095=Huffman+LZ (slowest/best compression). */
enum
{
TDEFL_HUFFMAN_ONLY = 0,
TDEFL_DEFAULT_MAX_PROBES = 128,
TDEFL_MAX_PROBES_MASK = 0xFFF
};
/* TDEFL_WRITE_ZLIB_HEADER: If set, the compressor outputs a zlib header before the deflate data, and the Adler-32 of the source data at the end. Otherwise, you'll get raw deflate data. */
/* TDEFL_COMPUTE_ADLER32: Always compute the adler-32 of the input data (even when not writing zlib headers). */
/* TDEFL_GREEDY_PARSING_FLAG: Set to use faster greedy parsing, instead of more efficient lazy parsing. */
/* TDEFL_NONDETERMINISTIC_PARSING_FLAG: Enable to decrease the compressor's initialization time to the minimum, but the output may vary from run to run given the same input (depending on the contents of memory). */
/* TDEFL_RLE_MATCHES: Only look for RLE matches (matches with a distance of 1) */
/* TDEFL_FILTER_MATCHES: Discards matches <= 5 chars if enabled. */
/* TDEFL_FORCE_ALL_STATIC_BLOCKS: Disable usage of optimized Huffman tables. */
/* TDEFL_FORCE_ALL_RAW_BLOCKS: Only use raw (uncompressed) deflate blocks. */
/* The low 12 bits are reserved to control the max # of hash probes per dictionary lookup (see TDEFL_MAX_PROBES_MASK). */
enum
{
TDEFL_WRITE_ZLIB_HEADER = 0x01000,
TDEFL_COMPUTE_ADLER32 = 0x02000,
TDEFL_GREEDY_PARSING_FLAG = 0x04000,
TDEFL_NONDETERMINISTIC_PARSING_FLAG = 0x08000,
TDEFL_RLE_MATCHES = 0x10000,
TDEFL_FILTER_MATCHES = 0x20000,
TDEFL_FORCE_ALL_STATIC_BLOCKS = 0x40000,
TDEFL_FORCE_ALL_RAW_BLOCKS = 0x80000
};
/* High level compression functions: */
/* tdefl_compress_mem_to_heap() compresses a block in memory to a heap block allocated via malloc(). */
/* On entry: */
/* pSrc_buf, src_buf_len: Pointer and size of source block to compress. */
/* flags: The max match finder probes (default is 128) logically OR'd against the above flags. Higher probes are slower but improve compression. */
/* On return: */
/* Function returns a pointer to the compressed data, or NULL on failure. */
/* *pOut_len will be set to the compressed data's size, which could be larger than src_buf_len on uncompressible data. */
/* The caller must free() the returned block when it's no longer needed. */
MINIZ_EXPORT void *tdefl_compress_mem_to_heap(const void *pSrc_buf, size_t src_buf_len, size_t *pOut_len, int flags);
/* tdefl_compress_mem_to_mem() compresses a block in memory to another block in memory. */
/* Returns 0 on failure. */
MINIZ_EXPORT size_t tdefl_compress_mem_to_mem(void *pOut_buf, size_t out_buf_len, const void *pSrc_buf, size_t src_buf_len, int flags);
/* Compresses an image to a compressed PNG file in memory. */
/* On entry: */
/* pImage, w, h, and num_chans describe the image to compress. num_chans may be 1, 2, 3, or 4. */
/* The image pitch in bytes per scanline will be w*num_chans. The leftmost pixel on the top scanline is stored first in memory. */
/* level may range from [0,10], use MZ_NO_COMPRESSION, MZ_BEST_SPEED, MZ_BEST_COMPRESSION, etc. or a decent default is MZ_DEFAULT_LEVEL */
/* If flip is true, the image will be flipped on the Y axis (useful for OpenGL apps). */
/* On return: */
/* Function returns a pointer to the compressed data, or NULL on failure. */
/* *pLen_out will be set to the size of the PNG image file. */
/* The caller must mz_free() the returned heap block (which will typically be larger than *pLen_out) when it's no longer needed. */
MINIZ_EXPORT void *tdefl_write_image_to_png_file_in_memory_ex(const void *pImage, int w, int h, int num_chans, size_t *pLen_out, mz_uint level, mz_bool flip);
MINIZ_EXPORT void *tdefl_write_image_to_png_file_in_memory(const void *pImage, int w, int h, int num_chans, size_t *pLen_out);
/* Output stream interface. The compressor uses this interface to write compressed data. It'll typically be called TDEFL_OUT_BUF_SIZE at a time. */
typedef mz_bool (*tdefl_put_buf_func_ptr)(const void *pBuf, int len, void *pUser);
/* tdefl_compress_mem_to_output() compresses a block to an output stream. The above helpers use this function internally. */
MINIZ_EXPORT mz_bool tdefl_compress_mem_to_output(const void *pBuf, size_t buf_len, tdefl_put_buf_func_ptr pPut_buf_func, void *pPut_buf_user, int flags);
enum
{
TDEFL_MAX_HUFF_TABLES = 3,
TDEFL_MAX_HUFF_SYMBOLS_0 = 288,
TDEFL_MAX_HUFF_SYMBOLS_1 = 32,
TDEFL_MAX_HUFF_SYMBOLS_2 = 19,
TDEFL_LZ_DICT_SIZE = 32768,
TDEFL_LZ_DICT_SIZE_MASK = TDEFL_LZ_DICT_SIZE - 1,
TDEFL_MIN_MATCH_LEN = 3,
TDEFL_MAX_MATCH_LEN = 258
};
/* TDEFL_OUT_BUF_SIZE MUST be large enough to hold a single entire compressed output block (using static/fixed Huffman codes). */
#if TDEFL_LESS_MEMORY
enum
{
TDEFL_LZ_CODE_BUF_SIZE = 24 * 1024,
TDEFL_OUT_BUF_SIZE = (TDEFL_LZ_CODE_BUF_SIZE * 13) / 10,
TDEFL_MAX_HUFF_SYMBOLS = 288,
TDEFL_LZ_HASH_BITS = 12,
TDEFL_LEVEL1_HASH_SIZE_MASK = 4095,
TDEFL_LZ_HASH_SHIFT = (TDEFL_LZ_HASH_BITS + 2) / 3,
TDEFL_LZ_HASH_SIZE = 1 << TDEFL_LZ_HASH_BITS
};
#else
enum
{
TDEFL_LZ_CODE_BUF_SIZE = 64 * 1024,
TDEFL_OUT_BUF_SIZE = (mz_uint)((TDEFL_LZ_CODE_BUF_SIZE * 13) / 10),
TDEFL_MAX_HUFF_SYMBOLS = 288,
TDEFL_LZ_HASH_BITS = 15,
TDEFL_LEVEL1_HASH_SIZE_MASK = 4095,
TDEFL_LZ_HASH_SHIFT = (TDEFL_LZ_HASH_BITS + 2) / 3,
TDEFL_LZ_HASH_SIZE = 1 << TDEFL_LZ_HASH_BITS
};
#endif
/* The low-level tdefl functions below may be used directly if the above helper functions aren't flexible enough. The low-level functions don't make any heap allocations, unlike the above helper functions. */
typedef enum
{
TDEFL_STATUS_BAD_PARAM = -2,
TDEFL_STATUS_PUT_BUF_FAILED = -1,
TDEFL_STATUS_OKAY = 0,
TDEFL_STATUS_DONE = 1
} tdefl_status;
/* Must map to MZ_NO_FLUSH, MZ_SYNC_FLUSH, etc. enums */
typedef enum
{
TDEFL_NO_FLUSH = 0,
TDEFL_SYNC_FLUSH = 2,
TDEFL_FULL_FLUSH = 3,
TDEFL_FINISH = 4
} tdefl_flush;
/* tdefl's compression state structure. */
typedef struct
{
tdefl_put_buf_func_ptr m_pPut_buf_func;
void *m_pPut_buf_user;
mz_uint m_flags, m_max_probes[2];
int m_greedy_parsing;
mz_uint m_adler32, m_lookahead_pos, m_lookahead_size, m_dict_size;
mz_uint8 *m_pLZ_code_buf, *m_pLZ_flags, *m_pOutput_buf, *m_pOutput_buf_end;
mz_uint m_num_flags_left, m_total_lz_bytes, m_lz_code_buf_dict_pos, m_bits_in, m_bit_buffer;
mz_uint m_saved_match_dist, m_saved_match_len, m_saved_lit, m_output_flush_ofs, m_output_flush_remaining, m_finished, m_block_index, m_wants_to_finish;
tdefl_status m_prev_return_status;
const void *m_pIn_buf;
void *m_pOut_buf;
size_t *m_pIn_buf_size, *m_pOut_buf_size;
tdefl_flush m_flush;
const mz_uint8 *m_pSrc;
size_t m_src_buf_left, m_out_buf_ofs;
mz_uint8 m_dict[TDEFL_LZ_DICT_SIZE + TDEFL_MAX_MATCH_LEN - 1];
mz_uint16 m_huff_count[TDEFL_MAX_HUFF_TABLES][TDEFL_MAX_HUFF_SYMBOLS];
mz_uint16 m_huff_codes[TDEFL_MAX_HUFF_TABLES][TDEFL_MAX_HUFF_SYMBOLS];
mz_uint8 m_huff_code_sizes[TDEFL_MAX_HUFF_TABLES][TDEFL_MAX_HUFF_SYMBOLS];
mz_uint8 m_lz_code_buf[TDEFL_LZ_CODE_BUF_SIZE];
mz_uint16 m_next[TDEFL_LZ_DICT_SIZE];
mz_uint16 m_hash[TDEFL_LZ_HASH_SIZE];
mz_uint8 m_output_buf[TDEFL_OUT_BUF_SIZE];
} tdefl_compressor;
/* Initializes the compressor. */
/* There is no corresponding deinit() function because the tdefl API's do not dynamically allocate memory. */
/* pBut_buf_func: If NULL, output data will be supplied to the specified callback. In this case, the user should call the tdefl_compress_buffer() API for compression. */
/* If pBut_buf_func is NULL the user should always call the tdefl_compress() API. */
/* flags: See the above enums (TDEFL_HUFFMAN_ONLY, TDEFL_WRITE_ZLIB_HEADER, etc.) */
MINIZ_EXPORT tdefl_status tdefl_init(tdefl_compressor *d, tdefl_put_buf_func_ptr pPut_buf_func, void *pPut_buf_user, int flags);
/* Compresses a block of data, consuming as much of the specified input buffer as possible, and writing as much compressed data to the specified output buffer as possible. */
MINIZ_EXPORT tdefl_status tdefl_compress(tdefl_compressor *d, const void *pIn_buf, size_t *pIn_buf_size, void *pOut_buf, size_t *pOut_buf_size, tdefl_flush flush);
/* tdefl_compress_buffer() is only usable when the tdefl_init() is called with a non-NULL tdefl_put_buf_func_ptr. */
/* tdefl_compress_buffer() always consumes the entire input buffer. */
MINIZ_EXPORT tdefl_status tdefl_compress_buffer(tdefl_compressor *d, const void *pIn_buf, size_t in_buf_size, tdefl_flush flush);
MINIZ_EXPORT tdefl_status tdefl_get_prev_return_status(tdefl_compressor *d);
MINIZ_EXPORT mz_uint32 tdefl_get_adler32(tdefl_compressor *d);
/* Create tdefl_compress() flags given zlib-style compression parameters. */
/* level may range from [0,10] (where 10 is absolute max compression, but may be much slower on some files) */
/* window_bits may be -15 (raw deflate) or 15 (zlib) */
/* strategy may be either MZ_DEFAULT_STRATEGY, MZ_FILTERED, MZ_HUFFMAN_ONLY, MZ_RLE, or MZ_FIXED */
MINIZ_EXPORT mz_uint tdefl_create_comp_flags_from_zip_params(int level, int window_bits, int strategy);
#ifndef MINIZ_NO_MALLOC
/* Allocate the tdefl_compressor structure in C so that */
/* non-C language bindings to tdefl_ API don't need to worry about */
/* structure size and allocation mechanism. */
MINIZ_EXPORT tdefl_compressor *tdefl_compressor_alloc(void);
MINIZ_EXPORT void tdefl_compressor_free(tdefl_compressor *pComp);
#endif
#ifdef __cplusplus
}
#endif
#endif /*#ifndef MINIZ_NO_DEFLATE_APIS*/
+778
View File
@@ -0,0 +1,778 @@
/**************************************************************************
*
* Copyright 2013-2014 RAD Game Tools and Valve Software
* Copyright 2010-2014 Rich Geldreich and Tenacious Software LLC
* All Rights Reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
**************************************************************************/
#include "miniz.h"
#ifndef MINIZ_NO_INFLATE_APIS
#ifdef __cplusplus
extern "C"
{
#endif
/* ------------------- Low-level Decompression (completely independent from all compression API's) */
#define TINFL_MEMCPY(d, s, l) memcpy(d, s, l)
#define TINFL_MEMSET(p, c, l) memset(p, c, l)
#define TINFL_CR_BEGIN \
switch (r->m_state) \
{ \
case 0:
#define TINFL_CR_RETURN(state_index, result) \
do \
{ \
status = result; \
r->m_state = state_index; \
goto common_exit; \
case state_index:; \
} \
MZ_MACRO_END
#define TINFL_CR_RETURN_FOREVER(state_index, result) \
do \
{ \
for (;;) \
{ \
TINFL_CR_RETURN(state_index, result); \
} \
} \
MZ_MACRO_END
#define TINFL_CR_FINISH }
#define TINFL_GET_BYTE(state_index, c) \
do \
{ \
while (pIn_buf_cur >= pIn_buf_end) \
{ \
TINFL_CR_RETURN(state_index, (decomp_flags & TINFL_FLAG_HAS_MORE_INPUT) ? TINFL_STATUS_NEEDS_MORE_INPUT : TINFL_STATUS_FAILED_CANNOT_MAKE_PROGRESS); \
} \
c = *pIn_buf_cur++; \
} \
MZ_MACRO_END
#define TINFL_NEED_BITS(state_index, n) \
do \
{ \
mz_uint c; \
TINFL_GET_BYTE(state_index, c); \
bit_buf |= (((tinfl_bit_buf_t)c) << num_bits); \
num_bits += 8; \
} while (num_bits < (mz_uint)(n))
#define TINFL_SKIP_BITS(state_index, n) \
do \
{ \
if (num_bits < (mz_uint)(n)) \
{ \
TINFL_NEED_BITS(state_index, n); \
} \
bit_buf >>= (n); \
num_bits -= (n); \
} \
MZ_MACRO_END
#define TINFL_GET_BITS(state_index, b, n) \
do \
{ \
if (num_bits < (mz_uint)(n)) \
{ \
TINFL_NEED_BITS(state_index, n); \
} \
b = bit_buf & ((1 << (n)) - 1); \
bit_buf >>= (n); \
num_bits -= (n); \
} \
MZ_MACRO_END
/* TINFL_HUFF_BITBUF_FILL() is only used rarely, when the number of bytes remaining in the input buffer falls below 2. */
/* It reads just enough bytes from the input stream that are needed to decode the next Huffman code (and absolutely no more). It works by trying to fully decode a */
/* Huffman code by using whatever bits are currently present in the bit buffer. If this fails, it reads another byte, and tries again until it succeeds or until the */
/* bit buffer contains >=15 bits (deflate's max. Huffman code size). */
#define TINFL_HUFF_BITBUF_FILL(state_index, pLookUp, pTree) \
do \
{ \
temp = pLookUp[bit_buf & (TINFL_FAST_LOOKUP_SIZE - 1)]; \
if (temp >= 0) \
{ \
code_len = temp >> 9; \
if ((code_len) && (num_bits >= code_len)) \
break; \
} \
else if (num_bits > TINFL_FAST_LOOKUP_BITS) \
{ \
code_len = TINFL_FAST_LOOKUP_BITS; \
do \
{ \
temp = pTree[~temp + ((bit_buf >> code_len++) & 1)]; \
} while ((temp < 0) && (num_bits >= (code_len + 1))); \
if (temp >= 0) \
break; \
} \
TINFL_GET_BYTE(state_index, c); \
bit_buf |= (((tinfl_bit_buf_t)c) << num_bits); \
num_bits += 8; \
} while (num_bits < 15);
/* TINFL_HUFF_DECODE() decodes the next Huffman coded symbol. It's more complex than you would initially expect because the zlib API expects the decompressor to never read */
/* beyond the final byte of the deflate stream. (In other words, when this macro wants to read another byte from the input, it REALLY needs another byte in order to fully */
/* decode the next Huffman code.) Handling this properly is particularly important on raw deflate (non-zlib) streams, which aren't followed by a byte aligned adler-32. */
/* The slow path is only executed at the very end of the input buffer. */
/* v1.16: The original macro handled the case at the very end of the passed-in input buffer, but we also need to handle the case where the user passes in 1+zillion bytes */
/* following the deflate data and our non-conservative read-ahead path won't kick in here on this code. This is much trickier. */
#define TINFL_HUFF_DECODE(state_index, sym, pLookUp, pTree) \
do \
{ \
int temp; \
mz_uint code_len, c; \
if (num_bits < 15) \
{ \
if ((pIn_buf_end - pIn_buf_cur) < 2) \
{ \
TINFL_HUFF_BITBUF_FILL(state_index, pLookUp, pTree); \
} \
else \
{ \
bit_buf |= (((tinfl_bit_buf_t)pIn_buf_cur[0]) << num_bits) | (((tinfl_bit_buf_t)pIn_buf_cur[1]) << (num_bits + 8)); \
pIn_buf_cur += 2; \
num_bits += 16; \
} \
} \
if ((temp = pLookUp[bit_buf & (TINFL_FAST_LOOKUP_SIZE - 1)]) >= 0) \
code_len = temp >> 9, temp &= 511; \
else \
{ \
code_len = TINFL_FAST_LOOKUP_BITS; \
do \
{ \
temp = pTree[~temp + ((bit_buf >> code_len++) & 1)]; \
} while (temp < 0); \
} \
sym = temp; \
bit_buf >>= code_len; \
num_bits -= code_len; \
} \
MZ_MACRO_END
static void tinfl_clear_tree(tinfl_decompressor *r)
{
if (r->m_type == 0)
MZ_CLEAR_ARR(r->m_tree_0);
else if (r->m_type == 1)
MZ_CLEAR_ARR(r->m_tree_1);
else
MZ_CLEAR_ARR(r->m_tree_2);
}
tinfl_status tinfl_decompress(tinfl_decompressor *r, const mz_uint8 *pIn_buf_next, size_t *pIn_buf_size, mz_uint8 *pOut_buf_start, mz_uint8 *pOut_buf_next, size_t *pOut_buf_size, const mz_uint32 decomp_flags)
{
static const mz_uint16 s_length_base[31] = { 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258, 0, 0 };
static const mz_uint8 s_length_extra[31] = { 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0, 0, 0 };
static const mz_uint16 s_dist_base[32] = { 1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577, 0, 0 };
static const mz_uint8 s_dist_extra[32] = { 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13 };
static const mz_uint8 s_length_dezigzag[19] = { 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 };
static const mz_uint16 s_min_table_sizes[3] = { 257, 1, 4 };
mz_int16 *pTrees[3];
mz_uint8 *pCode_sizes[3];
tinfl_status status = TINFL_STATUS_FAILED;
mz_uint32 num_bits, dist, counter, num_extra;
tinfl_bit_buf_t bit_buf;
const mz_uint8 *pIn_buf_cur = pIn_buf_next, *const pIn_buf_end = pIn_buf_next + *pIn_buf_size;
mz_uint8 *pOut_buf_cur = pOut_buf_next, *const pOut_buf_end = pOut_buf_next ? pOut_buf_next + *pOut_buf_size : NULL;
size_t out_buf_size_mask = (decomp_flags & TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF) ? (size_t)-1 : ((pOut_buf_next - pOut_buf_start) + *pOut_buf_size) - 1, dist_from_out_buf_start;
/* Ensure the output buffer's size is a power of 2, unless the output buffer is large enough to hold the entire output file (in which case it doesn't matter). */
if (((out_buf_size_mask + 1) & out_buf_size_mask) || (pOut_buf_next < pOut_buf_start))
{
*pIn_buf_size = *pOut_buf_size = 0;
return TINFL_STATUS_BAD_PARAM;
}
pTrees[0] = r->m_tree_0;
pTrees[1] = r->m_tree_1;
pTrees[2] = r->m_tree_2;
pCode_sizes[0] = r->m_code_size_0;
pCode_sizes[1] = r->m_code_size_1;
pCode_sizes[2] = r->m_code_size_2;
num_bits = r->m_num_bits;
bit_buf = r->m_bit_buf;
dist = r->m_dist;
counter = r->m_counter;
num_extra = r->m_num_extra;
dist_from_out_buf_start = r->m_dist_from_out_buf_start;
TINFL_CR_BEGIN
bit_buf = num_bits = dist = counter = num_extra = r->m_zhdr0 = r->m_zhdr1 = 0;
r->m_z_adler32 = r->m_check_adler32 = 1;
if (decomp_flags & TINFL_FLAG_PARSE_ZLIB_HEADER)
{
TINFL_GET_BYTE(1, r->m_zhdr0);
TINFL_GET_BYTE(2, r->m_zhdr1);
counter = (((r->m_zhdr0 * 256 + r->m_zhdr1) % 31 != 0) || (r->m_zhdr1 & 32) || ((r->m_zhdr0 & 15) != 8));
if (!(decomp_flags & TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF))
counter |= (((1U << (8U + (r->m_zhdr0 >> 4))) > 32768U) || ((out_buf_size_mask + 1) < (size_t)((size_t)1 << (8U + (r->m_zhdr0 >> 4)))));
if (counter)
{
TINFL_CR_RETURN_FOREVER(36, TINFL_STATUS_FAILED);
}
}
do
{
TINFL_GET_BITS(3, r->m_final, 3);
r->m_type = r->m_final >> 1;
if (r->m_type == 0)
{
TINFL_SKIP_BITS(5, num_bits & 7);
for (counter = 0; counter < 4; ++counter)
{
if (num_bits)
TINFL_GET_BITS(6, r->m_raw_header[counter], 8);
else
TINFL_GET_BYTE(7, r->m_raw_header[counter]);
}
if ((counter = (r->m_raw_header[0] | (r->m_raw_header[1] << 8))) != (mz_uint)(0xFFFF ^ (r->m_raw_header[2] | (r->m_raw_header[3] << 8))))
{
TINFL_CR_RETURN_FOREVER(39, TINFL_STATUS_FAILED);
}
while ((counter) && (num_bits))
{
TINFL_GET_BITS(51, dist, 8);
while (pOut_buf_cur >= pOut_buf_end)
{
TINFL_CR_RETURN(52, TINFL_STATUS_HAS_MORE_OUTPUT);
}
*pOut_buf_cur++ = (mz_uint8)dist;
counter--;
}
while (counter)
{
size_t n;
while (pOut_buf_cur >= pOut_buf_end)
{
TINFL_CR_RETURN(9, TINFL_STATUS_HAS_MORE_OUTPUT);
}
while (pIn_buf_cur >= pIn_buf_end)
{
TINFL_CR_RETURN(38, (decomp_flags & TINFL_FLAG_HAS_MORE_INPUT) ? TINFL_STATUS_NEEDS_MORE_INPUT : TINFL_STATUS_FAILED_CANNOT_MAKE_PROGRESS);
}
n = MZ_MIN(MZ_MIN((size_t)(pOut_buf_end - pOut_buf_cur), (size_t)(pIn_buf_end - pIn_buf_cur)), counter);
TINFL_MEMCPY(pOut_buf_cur, pIn_buf_cur, n);
pIn_buf_cur += n;
pOut_buf_cur += n;
counter -= (mz_uint)n;
}
}
else if (r->m_type == 3)
{
TINFL_CR_RETURN_FOREVER(10, TINFL_STATUS_FAILED);
}
else
{
if (r->m_type == 1)
{
mz_uint8 *p = r->m_code_size_0;
mz_uint i;
r->m_table_sizes[0] = 288;
r->m_table_sizes[1] = 32;
TINFL_MEMSET(r->m_code_size_1, 5, 32);
for (i = 0; i <= 143; ++i)
*p++ = 8;
for (; i <= 255; ++i)
*p++ = 9;
for (; i <= 279; ++i)
*p++ = 7;
for (; i <= 287; ++i)
*p++ = 8;
}
else
{
for (counter = 0; counter < 3; counter++)
{
TINFL_GET_BITS(11, r->m_table_sizes[counter], "\05\05\04"[counter]);
r->m_table_sizes[counter] += s_min_table_sizes[counter];
}
MZ_CLEAR_ARR(r->m_code_size_2);
for (counter = 0; counter < r->m_table_sizes[2]; counter++)
{
mz_uint s;
TINFL_GET_BITS(14, s, 3);
r->m_code_size_2[s_length_dezigzag[counter]] = (mz_uint8)s;
}
r->m_table_sizes[2] = 19;
}
for (; (int)r->m_type >= 0; r->m_type--)
{
int tree_next, tree_cur;
mz_int16 *pLookUp;
mz_int16 *pTree;
mz_uint8 *pCode_size;
mz_uint i, j, used_syms, total, sym_index, next_code[17], total_syms[16];
pLookUp = r->m_look_up[r->m_type];
pTree = pTrees[r->m_type];
pCode_size = pCode_sizes[r->m_type];
MZ_CLEAR_ARR(total_syms);
TINFL_MEMSET(pLookUp, 0, sizeof(r->m_look_up[0]));
tinfl_clear_tree(r);
for (i = 0; i < r->m_table_sizes[r->m_type]; ++i)
total_syms[pCode_size[i]]++;
used_syms = 0, total = 0;
next_code[0] = next_code[1] = 0;
for (i = 1; i <= 15; ++i)
{
used_syms += total_syms[i];
next_code[i + 1] = (total = ((total + total_syms[i]) << 1));
}
if ((65536 != total) && (used_syms > 1))
{
TINFL_CR_RETURN_FOREVER(35, TINFL_STATUS_FAILED);
}
for (tree_next = -1, sym_index = 0; sym_index < r->m_table_sizes[r->m_type]; ++sym_index)
{
mz_uint rev_code = 0, l, cur_code, code_size = pCode_size[sym_index];
if (!code_size)
continue;
cur_code = next_code[code_size]++;
for (l = code_size; l > 0; l--, cur_code >>= 1)
rev_code = (rev_code << 1) | (cur_code & 1);
if (code_size <= TINFL_FAST_LOOKUP_BITS)
{
mz_int16 k = (mz_int16)((code_size << 9) | sym_index);
while (rev_code < TINFL_FAST_LOOKUP_SIZE)
{
pLookUp[rev_code] = k;
rev_code += (1 << code_size);
}
continue;
}
if (0 == (tree_cur = pLookUp[rev_code & (TINFL_FAST_LOOKUP_SIZE - 1)]))
{
pLookUp[rev_code & (TINFL_FAST_LOOKUP_SIZE - 1)] = (mz_int16)tree_next;
tree_cur = tree_next;
tree_next -= 2;
}
rev_code >>= (TINFL_FAST_LOOKUP_BITS - 1);
for (j = code_size; j > (TINFL_FAST_LOOKUP_BITS + 1); j--)
{
tree_cur -= ((rev_code >>= 1) & 1);
if (!pTree[-tree_cur - 1])
{
pTree[-tree_cur - 1] = (mz_int16)tree_next;
tree_cur = tree_next;
tree_next -= 2;
}
else
tree_cur = pTree[-tree_cur - 1];
}
tree_cur -= ((rev_code >>= 1) & 1);
pTree[-tree_cur - 1] = (mz_int16)sym_index;
}
if (r->m_type == 2)
{
for (counter = 0; counter < (r->m_table_sizes[0] + r->m_table_sizes[1]);)
{
mz_uint s;
TINFL_HUFF_DECODE(16, dist, r->m_look_up[2], r->m_tree_2);
if (dist < 16)
{
r->m_len_codes[counter++] = (mz_uint8)dist;
continue;
}
if ((dist == 16) && (!counter))
{
TINFL_CR_RETURN_FOREVER(17, TINFL_STATUS_FAILED);
}
num_extra = "\02\03\07"[dist - 16];
TINFL_GET_BITS(18, s, num_extra);
s += "\03\03\013"[dist - 16];
TINFL_MEMSET(r->m_len_codes + counter, (dist == 16) ? r->m_len_codes[counter - 1] : 0, s);
counter += s;
}
if ((r->m_table_sizes[0] + r->m_table_sizes[1]) != counter)
{
TINFL_CR_RETURN_FOREVER(21, TINFL_STATUS_FAILED);
}
TINFL_MEMCPY(r->m_code_size_0, r->m_len_codes, r->m_table_sizes[0]);
TINFL_MEMCPY(r->m_code_size_1, r->m_len_codes + r->m_table_sizes[0], r->m_table_sizes[1]);
}
}
for (;;)
{
mz_uint8 *pSrc;
for (;;)
{
if (((pIn_buf_end - pIn_buf_cur) < 4) || ((pOut_buf_end - pOut_buf_cur) < 2))
{
TINFL_HUFF_DECODE(23, counter, r->m_look_up[0], r->m_tree_0);
if (counter >= 256)
break;
while (pOut_buf_cur >= pOut_buf_end)
{
TINFL_CR_RETURN(24, TINFL_STATUS_HAS_MORE_OUTPUT);
}
*pOut_buf_cur++ = (mz_uint8)counter;
}
else
{
int sym2;
mz_uint code_len;
#if TINFL_USE_64BIT_BITBUF
if (num_bits < 30)
{
bit_buf |= (((tinfl_bit_buf_t)MZ_READ_LE32(pIn_buf_cur)) << num_bits);
pIn_buf_cur += 4;
num_bits += 32;
}
#else
if (num_bits < 15)
{
bit_buf |= (((tinfl_bit_buf_t)MZ_READ_LE16(pIn_buf_cur)) << num_bits);
pIn_buf_cur += 2;
num_bits += 16;
}
#endif
if ((sym2 = r->m_look_up[0][bit_buf & (TINFL_FAST_LOOKUP_SIZE - 1)]) >= 0)
code_len = sym2 >> 9;
else
{
code_len = TINFL_FAST_LOOKUP_BITS;
do
{
sym2 = r->m_tree_0[~sym2 + ((bit_buf >> code_len++) & 1)];
} while (sym2 < 0);
}
counter = sym2;
bit_buf >>= code_len;
num_bits -= code_len;
if (code_len == 0)
{
TINFL_CR_RETURN_FOREVER(40, TINFL_STATUS_FAILED);
}
if (counter & 256)
break;
#if !TINFL_USE_64BIT_BITBUF
if (num_bits < 15)
{
bit_buf |= (((tinfl_bit_buf_t)MZ_READ_LE16(pIn_buf_cur)) << num_bits);
pIn_buf_cur += 2;
num_bits += 16;
}
#endif
if ((sym2 = r->m_look_up[0][bit_buf & (TINFL_FAST_LOOKUP_SIZE - 1)]) >= 0)
code_len = sym2 >> 9;
else
{
code_len = TINFL_FAST_LOOKUP_BITS;
do
{
sym2 = r->m_tree_0[~sym2 + ((bit_buf >> code_len++) & 1)];
} while (sym2 < 0);
}
bit_buf >>= code_len;
num_bits -= code_len;
if (code_len == 0)
{
TINFL_CR_RETURN_FOREVER(54, TINFL_STATUS_FAILED);
}
pOut_buf_cur[0] = (mz_uint8)counter;
if (sym2 & 256)
{
pOut_buf_cur++;
counter = sym2;
break;
}
pOut_buf_cur[1] = (mz_uint8)sym2;
pOut_buf_cur += 2;
}
}
if ((counter &= 511) == 256)
break;
num_extra = s_length_extra[counter - 257];
counter = s_length_base[counter - 257];
if (num_extra)
{
mz_uint extra_bits;
TINFL_GET_BITS(25, extra_bits, num_extra);
counter += extra_bits;
}
TINFL_HUFF_DECODE(26, dist, r->m_look_up[1], r->m_tree_1);
num_extra = s_dist_extra[dist];
dist = s_dist_base[dist];
if (num_extra)
{
mz_uint extra_bits;
TINFL_GET_BITS(27, extra_bits, num_extra);
dist += extra_bits;
}
dist_from_out_buf_start = pOut_buf_cur - pOut_buf_start;
if ((dist == 0 || dist > dist_from_out_buf_start || dist_from_out_buf_start == 0) && (decomp_flags & TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF))
{
TINFL_CR_RETURN_FOREVER(37, TINFL_STATUS_FAILED);
}
pSrc = pOut_buf_start + ((dist_from_out_buf_start - dist) & out_buf_size_mask);
if ((MZ_MAX(pOut_buf_cur, pSrc) + counter) > pOut_buf_end)
{
while (counter--)
{
while (pOut_buf_cur >= pOut_buf_end)
{
TINFL_CR_RETURN(53, TINFL_STATUS_HAS_MORE_OUTPUT);
}
*pOut_buf_cur++ = pOut_buf_start[(dist_from_out_buf_start++ - dist) & out_buf_size_mask];
}
continue;
}
#if MINIZ_USE_UNALIGNED_LOADS_AND_STORES
else if ((counter >= 9) && (counter <= dist))
{
const mz_uint8 *pSrc_end = pSrc + (counter & ~7);
do
{
#ifdef MINIZ_UNALIGNED_USE_MEMCPY
memcpy(pOut_buf_cur, pSrc, sizeof(mz_uint32) * 2);
#else
((mz_uint32 *)pOut_buf_cur)[0] = ((const mz_uint32 *)pSrc)[0];
((mz_uint32 *)pOut_buf_cur)[1] = ((const mz_uint32 *)pSrc)[1];
#endif
pOut_buf_cur += 8;
} while ((pSrc += 8) < pSrc_end);
if ((counter &= 7) < 3)
{
if (counter)
{
pOut_buf_cur[0] = pSrc[0];
if (counter > 1)
pOut_buf_cur[1] = pSrc[1];
pOut_buf_cur += counter;
}
continue;
}
}
#endif
while (counter > 2)
{
pOut_buf_cur[0] = pSrc[0];
pOut_buf_cur[1] = pSrc[1];
pOut_buf_cur[2] = pSrc[2];
pOut_buf_cur += 3;
pSrc += 3;
counter -= 3;
}
if (counter > 0)
{
pOut_buf_cur[0] = pSrc[0];
if (counter > 1)
pOut_buf_cur[1] = pSrc[1];
pOut_buf_cur += counter;
}
}
}
} while (!(r->m_final & 1));
/* Ensure byte alignment and put back any bytes from the bitbuf if we've looked ahead too far on gzip, or other Deflate streams followed by arbitrary data. */
/* I'm being super conservative here. A number of simplifications can be made to the byte alignment part, and the Adler32 check shouldn't ever need to worry about reading from the bitbuf now. */
TINFL_SKIP_BITS(32, num_bits & 7);
while ((pIn_buf_cur > pIn_buf_next) && (num_bits >= 8))
{
--pIn_buf_cur;
num_bits -= 8;
}
bit_buf &= ~(~(tinfl_bit_buf_t)0 << num_bits);
MZ_ASSERT(!num_bits); /* if this assert fires then we've read beyond the end of non-deflate/zlib streams with following data (such as gzip streams). */
if (decomp_flags & TINFL_FLAG_PARSE_ZLIB_HEADER)
{
for (counter = 0; counter < 4; ++counter)
{
mz_uint s;
if (num_bits)
TINFL_GET_BITS(41, s, 8);
else
TINFL_GET_BYTE(42, s);
r->m_z_adler32 = (r->m_z_adler32 << 8) | s;
}
}
TINFL_CR_RETURN_FOREVER(34, TINFL_STATUS_DONE);
TINFL_CR_FINISH
common_exit:
/* As long as we aren't telling the caller that we NEED more input to make forward progress: */
/* Put back any bytes from the bitbuf in case we've looked ahead too far on gzip, or other Deflate streams followed by arbitrary data. */
/* We need to be very careful here to NOT push back any bytes we definitely know we need to make forward progress, though, or we'll lock the caller up into an inf loop. */
if ((status != TINFL_STATUS_NEEDS_MORE_INPUT) && (status != TINFL_STATUS_FAILED_CANNOT_MAKE_PROGRESS))
{
while ((pIn_buf_cur > pIn_buf_next) && (num_bits >= 8))
{
--pIn_buf_cur;
num_bits -= 8;
}
}
r->m_num_bits = num_bits;
r->m_bit_buf = bit_buf & ~(~(tinfl_bit_buf_t)0 << num_bits);
r->m_dist = dist;
r->m_counter = counter;
r->m_num_extra = num_extra;
r->m_dist_from_out_buf_start = dist_from_out_buf_start;
*pIn_buf_size = pIn_buf_cur - pIn_buf_next;
*pOut_buf_size = pOut_buf_cur - pOut_buf_next;
if ((decomp_flags & (TINFL_FLAG_PARSE_ZLIB_HEADER | TINFL_FLAG_COMPUTE_ADLER32)) && (status >= 0))
{
const mz_uint8 *ptr = pOut_buf_next;
size_t buf_len = *pOut_buf_size;
mz_uint32 i, s1 = r->m_check_adler32 & 0xffff, s2 = r->m_check_adler32 >> 16;
size_t block_len = buf_len % 5552;
while (buf_len)
{
for (i = 0; i + 7 < block_len; i += 8, ptr += 8)
{
s1 += ptr[0], s2 += s1;
s1 += ptr[1], s2 += s1;
s1 += ptr[2], s2 += s1;
s1 += ptr[3], s2 += s1;
s1 += ptr[4], s2 += s1;
s1 += ptr[5], s2 += s1;
s1 += ptr[6], s2 += s1;
s1 += ptr[7], s2 += s1;
}
for (; i < block_len; ++i)
s1 += *ptr++, s2 += s1;
s1 %= 65521U, s2 %= 65521U;
buf_len -= block_len;
block_len = 5552;
}
r->m_check_adler32 = (s2 << 16) + s1;
if ((status == TINFL_STATUS_DONE) && (decomp_flags & TINFL_FLAG_PARSE_ZLIB_HEADER) && (r->m_check_adler32 != r->m_z_adler32))
status = TINFL_STATUS_ADLER32_MISMATCH;
}
return status;
}
/* Higher level helper functions. */
void *tinfl_decompress_mem_to_heap(const void *pSrc_buf, size_t src_buf_len, size_t *pOut_len, int flags)
{
tinfl_decompressor decomp;
void *pBuf = NULL, *pNew_buf;
size_t src_buf_ofs = 0, out_buf_capacity = 0;
*pOut_len = 0;
tinfl_init(&decomp);
for (;;)
{
size_t src_buf_size = src_buf_len - src_buf_ofs, dst_buf_size = out_buf_capacity - *pOut_len, new_out_buf_capacity;
tinfl_status status = tinfl_decompress(&decomp, (const mz_uint8 *)pSrc_buf + src_buf_ofs, &src_buf_size, (mz_uint8 *)pBuf, pBuf ? (mz_uint8 *)pBuf + *pOut_len : NULL, &dst_buf_size,
(flags & ~TINFL_FLAG_HAS_MORE_INPUT) | TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF);
if ((status < 0) || (status == TINFL_STATUS_NEEDS_MORE_INPUT))
{
MZ_FREE(pBuf);
*pOut_len = 0;
return NULL;
}
src_buf_ofs += src_buf_size;
*pOut_len += dst_buf_size;
if (status == TINFL_STATUS_DONE)
break;
new_out_buf_capacity = out_buf_capacity * 2;
if (new_out_buf_capacity < 128)
new_out_buf_capacity = 128;
pNew_buf = MZ_REALLOC(pBuf, new_out_buf_capacity);
if (!pNew_buf)
{
MZ_FREE(pBuf);
*pOut_len = 0;
return NULL;
}
pBuf = pNew_buf;
out_buf_capacity = new_out_buf_capacity;
}
return pBuf;
}
size_t tinfl_decompress_mem_to_mem(void *pOut_buf, size_t out_buf_len, const void *pSrc_buf, size_t src_buf_len, int flags)
{
tinfl_decompressor decomp;
tinfl_status status;
tinfl_init(&decomp);
status = tinfl_decompress(&decomp, (const mz_uint8 *)pSrc_buf, &src_buf_len, (mz_uint8 *)pOut_buf, (mz_uint8 *)pOut_buf, &out_buf_len, (flags & ~TINFL_FLAG_HAS_MORE_INPUT) | TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF);
return (status != TINFL_STATUS_DONE) ? TINFL_DECOMPRESS_MEM_TO_MEM_FAILED : out_buf_len;
}
int tinfl_decompress_mem_to_callback(const void *pIn_buf, size_t *pIn_buf_size, tinfl_put_buf_func_ptr pPut_buf_func, void *pPut_buf_user, int flags)
{
int result = 0;
tinfl_decompressor decomp;
mz_uint8 *pDict = (mz_uint8 *)MZ_MALLOC(TINFL_LZ_DICT_SIZE);
size_t in_buf_ofs = 0, dict_ofs = 0;
if (!pDict)
return TINFL_STATUS_FAILED;
memset(pDict, 0, TINFL_LZ_DICT_SIZE);
tinfl_init(&decomp);
for (;;)
{
size_t in_buf_size = *pIn_buf_size - in_buf_ofs, dst_buf_size = TINFL_LZ_DICT_SIZE - dict_ofs;
tinfl_status status = tinfl_decompress(&decomp, (const mz_uint8 *)pIn_buf + in_buf_ofs, &in_buf_size, pDict, pDict + dict_ofs, &dst_buf_size,
(flags & ~(TINFL_FLAG_HAS_MORE_INPUT | TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF)));
in_buf_ofs += in_buf_size;
if ((dst_buf_size) && (!(*pPut_buf_func)(pDict + dict_ofs, (int)dst_buf_size, pPut_buf_user)))
break;
if (status != TINFL_STATUS_HAS_MORE_OUTPUT)
{
result = (status == TINFL_STATUS_DONE);
break;
}
dict_ofs = (dict_ofs + dst_buf_size) & (TINFL_LZ_DICT_SIZE - 1);
}
MZ_FREE(pDict);
*pIn_buf_size = in_buf_ofs;
return result;
}
#ifndef MINIZ_NO_MALLOC
tinfl_decompressor *tinfl_decompressor_alloc(void)
{
tinfl_decompressor *pDecomp = (tinfl_decompressor *)MZ_MALLOC(sizeof(tinfl_decompressor));
if (pDecomp)
tinfl_init(pDecomp);
return pDecomp;
}
void tinfl_decompressor_free(tinfl_decompressor *pDecomp)
{
MZ_FREE(pDecomp);
}
#endif
#ifdef __cplusplus
}
#endif
#endif /*#ifndef MINIZ_NO_INFLATE_APIS*/
+150
View File
@@ -0,0 +1,150 @@
#pragma once
#include "miniz_common.h"
/* ------------------- Low-level Decompression API Definitions */
#ifndef MINIZ_NO_INFLATE_APIS
#ifdef __cplusplus
extern "C"
{
#endif
/* Decompression flags used by tinfl_decompress(). */
/* TINFL_FLAG_PARSE_ZLIB_HEADER: If set, the input has a valid zlib header and ends with an adler32 checksum (it's a valid zlib stream). Otherwise, the input is a raw deflate stream. */
/* TINFL_FLAG_HAS_MORE_INPUT: If set, there are more input bytes available beyond the end of the supplied input buffer. If clear, the input buffer contains all remaining input. */
/* TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF: If set, the output buffer is large enough to hold the entire decompressed stream. If clear, the output buffer is at least the size of the dictionary (typically 32KB). */
/* TINFL_FLAG_COMPUTE_ADLER32: Force adler-32 checksum computation of the decompressed bytes. */
enum
{
TINFL_FLAG_PARSE_ZLIB_HEADER = 1,
TINFL_FLAG_HAS_MORE_INPUT = 2,
TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF = 4,
TINFL_FLAG_COMPUTE_ADLER32 = 8
};
/* High level decompression functions: */
/* tinfl_decompress_mem_to_heap() decompresses a block in memory to a heap block allocated via malloc(). */
/* On entry: */
/* pSrc_buf, src_buf_len: Pointer and size of the Deflate or zlib source data to decompress. */
/* On return: */
/* Function returns a pointer to the decompressed data, or NULL on failure. */
/* *pOut_len will be set to the decompressed data's size, which could be larger than src_buf_len on uncompressible data. */
/* The caller must call mz_free() on the returned block when it's no longer needed. */
MINIZ_EXPORT void *tinfl_decompress_mem_to_heap(const void *pSrc_buf, size_t src_buf_len, size_t *pOut_len, int flags);
/* tinfl_decompress_mem_to_mem() decompresses a block in memory to another block in memory. */
/* Returns TINFL_DECOMPRESS_MEM_TO_MEM_FAILED on failure, or the number of bytes written on success. */
#define TINFL_DECOMPRESS_MEM_TO_MEM_FAILED ((size_t)(-1))
MINIZ_EXPORT size_t tinfl_decompress_mem_to_mem(void *pOut_buf, size_t out_buf_len, const void *pSrc_buf, size_t src_buf_len, int flags);
/* tinfl_decompress_mem_to_callback() decompresses a block in memory to an internal 32KB buffer, and a user provided callback function will be called to flush the buffer. */
/* Returns 1 on success or 0 on failure. */
typedef int (*tinfl_put_buf_func_ptr)(const void *pBuf, int len, void *pUser);
MINIZ_EXPORT int tinfl_decompress_mem_to_callback(const void *pIn_buf, size_t *pIn_buf_size, tinfl_put_buf_func_ptr pPut_buf_func, void *pPut_buf_user, int flags);
struct tinfl_decompressor_tag;
typedef struct tinfl_decompressor_tag tinfl_decompressor;
#ifndef MINIZ_NO_MALLOC
/* Allocate the tinfl_decompressor structure in C so that */
/* non-C language bindings to tinfl_ API don't need to worry about */
/* structure size and allocation mechanism. */
MINIZ_EXPORT tinfl_decompressor *tinfl_decompressor_alloc(void);
MINIZ_EXPORT void tinfl_decompressor_free(tinfl_decompressor *pDecomp);
#endif
/* Max size of LZ dictionary. */
#define TINFL_LZ_DICT_SIZE 32768
/* Return status. */
typedef enum
{
/* This flags indicates the inflator needs 1 or more input bytes to make forward progress, but the caller is indicating that no more are available. The compressed data */
/* is probably corrupted. If you call the inflator again with more bytes it'll try to continue processing the input but this is a BAD sign (either the data is corrupted or you called it incorrectly). */
/* If you call it again with no input you'll just get TINFL_STATUS_FAILED_CANNOT_MAKE_PROGRESS again. */
TINFL_STATUS_FAILED_CANNOT_MAKE_PROGRESS = -4,
/* This flag indicates that one or more of the input parameters was obviously bogus. (You can try calling it again, but if you get this error the calling code is wrong.) */
TINFL_STATUS_BAD_PARAM = -3,
/* This flags indicate the inflator is finished but the adler32 check of the uncompressed data didn't match. If you call it again it'll return TINFL_STATUS_DONE. */
TINFL_STATUS_ADLER32_MISMATCH = -2,
/* This flags indicate the inflator has somehow failed (bad code, corrupted input, etc.). If you call it again without resetting via tinfl_init() it it'll just keep on returning the same status failure code. */
TINFL_STATUS_FAILED = -1,
/* Any status code less than TINFL_STATUS_DONE must indicate a failure. */
/* This flag indicates the inflator has returned every byte of uncompressed data that it can, has consumed every byte that it needed, has successfully reached the end of the deflate stream, and */
/* if zlib headers and adler32 checking enabled that it has successfully checked the uncompressed data's adler32. If you call it again you'll just get TINFL_STATUS_DONE over and over again. */
TINFL_STATUS_DONE = 0,
/* This flag indicates the inflator MUST have more input data (even 1 byte) before it can make any more forward progress, or you need to clear the TINFL_FLAG_HAS_MORE_INPUT */
/* flag on the next call if you don't have any more source data. If the source data was somehow corrupted it's also possible (but unlikely) for the inflator to keep on demanding input to */
/* proceed, so be sure to properly set the TINFL_FLAG_HAS_MORE_INPUT flag. */
TINFL_STATUS_NEEDS_MORE_INPUT = 1,
/* This flag indicates the inflator definitely has 1 or more bytes of uncompressed data available, but it cannot write this data into the output buffer. */
/* Note if the source compressed data was corrupted it's possible for the inflator to return a lot of uncompressed data to the caller. I've been assuming you know how much uncompressed data to expect */
/* (either exact or worst case) and will stop calling the inflator and fail after receiving too much. In pure streaming scenarios where you have no idea how many bytes to expect this may not be possible */
/* so I may need to add some code to address this. */
TINFL_STATUS_HAS_MORE_OUTPUT = 2
} tinfl_status;
/* Initializes the decompressor to its initial state. */
#define tinfl_init(r) \
do \
{ \
(r)->m_state = 0; \
} \
MZ_MACRO_END
#define tinfl_get_adler32(r) (r)->m_check_adler32
/* Main low-level decompressor coroutine function. This is the only function actually needed for decompression. All the other functions are just high-level helpers for improved usability. */
/* This is a universal API, i.e. it can be used as a building block to build any desired higher level decompression API. In the limit case, it can be called once per every byte input or output. */
MINIZ_EXPORT tinfl_status tinfl_decompress(tinfl_decompressor *r, const mz_uint8 *pIn_buf_next, size_t *pIn_buf_size, mz_uint8 *pOut_buf_start, mz_uint8 *pOut_buf_next, size_t *pOut_buf_size, const mz_uint32 decomp_flags);
/* Internal/private bits follow. */
enum
{
TINFL_MAX_HUFF_TABLES = 3,
TINFL_MAX_HUFF_SYMBOLS_0 = 288,
TINFL_MAX_HUFF_SYMBOLS_1 = 32,
TINFL_MAX_HUFF_SYMBOLS_2 = 19,
TINFL_FAST_LOOKUP_BITS = 10,
TINFL_FAST_LOOKUP_SIZE = 1 << TINFL_FAST_LOOKUP_BITS
};
#if MINIZ_HAS_64BIT_REGISTERS
#define TINFL_USE_64BIT_BITBUF 1
#else
#define TINFL_USE_64BIT_BITBUF 0
#endif
#if TINFL_USE_64BIT_BITBUF
typedef mz_uint64 tinfl_bit_buf_t;
#define TINFL_BITBUF_SIZE (64)
#else
typedef mz_uint32 tinfl_bit_buf_t;
#define TINFL_BITBUF_SIZE (32)
#endif
struct tinfl_decompressor_tag
{
mz_uint32 m_state, m_num_bits, m_zhdr0, m_zhdr1, m_z_adler32, m_final, m_type, m_check_adler32, m_dist, m_counter, m_num_extra, m_table_sizes[TINFL_MAX_HUFF_TABLES];
tinfl_bit_buf_t m_bit_buf;
size_t m_dist_from_out_buf_start;
mz_int16 m_look_up[TINFL_MAX_HUFF_TABLES][TINFL_FAST_LOOKUP_SIZE];
mz_int16 m_tree_0[TINFL_MAX_HUFF_SYMBOLS_0 * 2];
mz_int16 m_tree_1[TINFL_MAX_HUFF_SYMBOLS_1 * 2];
mz_int16 m_tree_2[TINFL_MAX_HUFF_SYMBOLS_2 * 2];
mz_uint8 m_code_size_0[TINFL_MAX_HUFF_SYMBOLS_0];
mz_uint8 m_code_size_1[TINFL_MAX_HUFF_SYMBOLS_1];
mz_uint8 m_code_size_2[TINFL_MAX_HUFF_SYMBOLS_2];
mz_uint8 m_raw_header[4], m_len_codes[TINFL_MAX_HUFF_SYMBOLS_0 + TINFL_MAX_HUFF_SYMBOLS_1 + 137];
};
#ifdef __cplusplus
}
#endif
#endif /*#ifndef MINIZ_NO_INFLATE_APIS*/
+4895
View File
File diff suppressed because it is too large. Load diff
+454
View File
@@ -0,0 +1,454 @@
#pragma once
#include "miniz_common.h"
/* ------------------- ZIP archive reading/writing */
#ifndef MINIZ_NO_ARCHIVE_APIS
#ifdef __cplusplus
extern "C"
{
#endif
enum
{
/* Note: These enums can be reduced as needed to save memory or stack space - they are pretty conservative. */
MZ_ZIP_MAX_IO_BUF_SIZE = 64 * 1024,
MZ_ZIP_MAX_ARCHIVE_FILENAME_SIZE = 512,
MZ_ZIP_MAX_ARCHIVE_FILE_COMMENT_SIZE = 512
};
typedef struct
{
/* Central directory file index. */
mz_uint32 m_file_index;
/* Byte offset of this entry in the archive's central directory. Note we currently only support up to UINT_MAX or less bytes in the central dir. */
mz_uint64 m_central_dir_ofs;
/* These fields are copied directly from the zip's central dir. */
mz_uint16 m_version_made_by;
mz_uint16 m_version_needed;
mz_uint16 m_bit_flag;
mz_uint16 m_method;
/* CRC-32 of uncompressed data. */
mz_uint32 m_crc32;
/* File's compressed size. */
mz_uint64 m_comp_size;
/* File's uncompressed size. Note, I've seen some old archives where directory entries had 512 bytes for their uncompressed sizes, but when you try to unpack them you actually get 0 bytes. */
mz_uint64 m_uncomp_size;
/* Zip internal and external file attributes. */
mz_uint16 m_internal_attr;
mz_uint32 m_external_attr;
/* Entry's local header file offset in bytes. */
mz_uint64 m_local_header_ofs;
/* Size of comment in bytes. */
mz_uint32 m_comment_size;
/* MZ_TRUE if the entry appears to be a directory. */
mz_bool m_is_directory;
/* MZ_TRUE if the entry uses encryption/strong encryption (which miniz_zip doesn't support) */
mz_bool m_is_encrypted;
/* MZ_TRUE if the file is not encrypted, a patch file, and if it uses a compression method we support. */
mz_bool m_is_supported;
/* Filename. If string ends in '/' it's a subdirectory entry. */
/* Guaranteed to be zero terminated, may be truncated to fit. */
char m_filename[MZ_ZIP_MAX_ARCHIVE_FILENAME_SIZE];
/* Comment field. */
/* Guaranteed to be zero terminated, may be truncated to fit. */
char m_comment[MZ_ZIP_MAX_ARCHIVE_FILE_COMMENT_SIZE];
#ifdef MINIZ_NO_TIME
MZ_TIME_T m_padding;
#else
MZ_TIME_T m_time;
#endif
} mz_zip_archive_file_stat;
typedef size_t (*mz_file_read_func)(void *pOpaque, mz_uint64 file_ofs, void *pBuf, size_t n);
typedef size_t (*mz_file_write_func)(void *pOpaque, mz_uint64 file_ofs, const void *pBuf, size_t n);
typedef mz_bool (*mz_file_needs_keepalive)(void *pOpaque);
struct mz_zip_internal_state_tag;
typedef struct mz_zip_internal_state_tag mz_zip_internal_state;
typedef enum
{
MZ_ZIP_MODE_INVALID = 0,
MZ_ZIP_MODE_READING = 1,
MZ_ZIP_MODE_WRITING = 2,
MZ_ZIP_MODE_WRITING_HAS_BEEN_FINALIZED = 3
} mz_zip_mode;
typedef enum
{
MZ_ZIP_FLAG_CASE_SENSITIVE = 0x0100,
MZ_ZIP_FLAG_IGNORE_PATH = 0x0200,
MZ_ZIP_FLAG_COMPRESSED_DATA = 0x0400,
MZ_ZIP_FLAG_DO_NOT_SORT_CENTRAL_DIRECTORY = 0x0800,
MZ_ZIP_FLAG_VALIDATE_LOCATE_FILE_FLAG = 0x1000, /* if enabled, mz_zip_reader_locate_file() will be called on each file as its validated to ensure the func finds the file in the central dir (intended for testing) */
MZ_ZIP_FLAG_VALIDATE_HEADERS_ONLY = 0x2000, /* validate the local headers, but don't decompress the entire file and check the crc32 */
MZ_ZIP_FLAG_WRITE_ZIP64 = 0x4000, /* always use the zip64 file format, instead of the original zip file format with automatic switch to zip64. Use as flags parameter with mz_zip_writer_init*_v2 */
MZ_ZIP_FLAG_WRITE_ALLOW_READING = 0x8000,
MZ_ZIP_FLAG_ASCII_FILENAME = 0x10000,
/*After adding a compressed file, seek back
to local file header and set the correct sizes*/
MZ_ZIP_FLAG_WRITE_HEADER_SET_SIZE = 0x20000,
MZ_ZIP_FLAG_READ_ALLOW_WRITING = 0x40000
} mz_zip_flags;
typedef enum
{
MZ_ZIP_TYPE_INVALID = 0,
MZ_ZIP_TYPE_USER,
MZ_ZIP_TYPE_MEMORY,
MZ_ZIP_TYPE_HEAP,
MZ_ZIP_TYPE_FILE,
MZ_ZIP_TYPE_CFILE,
MZ_ZIP_TOTAL_TYPES
} mz_zip_type;
/* miniz error codes. Be sure to update mz_zip_get_error_string() if you add or modify this enum. */
typedef enum
{
MZ_ZIP_NO_ERROR = 0,
MZ_ZIP_UNDEFINED_ERROR,
MZ_ZIP_TOO_MANY_FILES,
MZ_ZIP_FILE_TOO_LARGE,
MZ_ZIP_UNSUPPORTED_METHOD,
MZ_ZIP_UNSUPPORTED_ENCRYPTION,
MZ_ZIP_UNSUPPORTED_FEATURE,
MZ_ZIP_FAILED_FINDING_CENTRAL_DIR,
MZ_ZIP_NOT_AN_ARCHIVE,
MZ_ZIP_INVALID_HEADER_OR_CORRUPTED,
MZ_ZIP_UNSUPPORTED_MULTIDISK,
MZ_ZIP_DECOMPRESSION_FAILED,
MZ_ZIP_COMPRESSION_FAILED,
MZ_ZIP_UNEXPECTED_DECOMPRESSED_SIZE,
MZ_ZIP_CRC_CHECK_FAILED,
MZ_ZIP_UNSUPPORTED_CDIR_SIZE,
MZ_ZIP_ALLOC_FAILED,
MZ_ZIP_FILE_OPEN_FAILED,
MZ_ZIP_FILE_CREATE_FAILED,
MZ_ZIP_FILE_WRITE_FAILED,
MZ_ZIP_FILE_READ_FAILED,
MZ_ZIP_FILE_CLOSE_FAILED,
MZ_ZIP_FILE_SEEK_FAILED,
MZ_ZIP_FILE_STAT_FAILED,
MZ_ZIP_INVALID_PARAMETER,
MZ_ZIP_INVALID_FILENAME,
MZ_ZIP_BUF_TOO_SMALL,
MZ_ZIP_INTERNAL_ERROR,
MZ_ZIP_FILE_NOT_FOUND,
MZ_ZIP_ARCHIVE_TOO_LARGE,
MZ_ZIP_VALIDATION_FAILED,
MZ_ZIP_WRITE_CALLBACK_FAILED,
MZ_ZIP_TOTAL_ERRORS
} mz_zip_error;
typedef struct
{
mz_uint64 m_archive_size;
mz_uint64 m_central_directory_file_ofs;
/* We only support up to UINT32_MAX files in zip64 mode. */
mz_uint32 m_total_files;
mz_zip_mode m_zip_mode;
mz_zip_type m_zip_type;
mz_zip_error m_last_error;
mz_uint64 m_file_offset_alignment;
mz_alloc_func m_pAlloc;
mz_free_func m_pFree;
mz_realloc_func m_pRealloc;
void *m_pAlloc_opaque;
mz_file_read_func m_pRead;
mz_file_write_func m_pWrite;
mz_file_needs_keepalive m_pNeeds_keepalive;
void *m_pIO_opaque;
mz_zip_internal_state *m_pState;
} mz_zip_archive;
typedef struct
{
mz_zip_archive *pZip;
mz_uint flags;
int status;
mz_uint64 read_buf_size, read_buf_ofs, read_buf_avail, comp_remaining, out_buf_ofs, cur_file_ofs;
mz_zip_archive_file_stat file_stat;
void *pRead_buf;
void *pWrite_buf;
size_t out_blk_remain;
tinfl_decompressor inflator;
#ifdef MINIZ_DISABLE_ZIP_READER_CRC32_CHECKS
mz_uint padding;
#else
mz_uint file_crc32;
#endif
} mz_zip_reader_extract_iter_state;
/* -------- ZIP reading */
/* Inits a ZIP archive reader. */
/* These functions read and validate the archive's central directory. */
MINIZ_EXPORT mz_bool mz_zip_reader_init(mz_zip_archive *pZip, mz_uint64 size, mz_uint flags);
MINIZ_EXPORT mz_bool mz_zip_reader_init_mem(mz_zip_archive *pZip, const void *pMem, size_t size, mz_uint flags);
#ifndef MINIZ_NO_STDIO
/* Read a archive from a disk file. */
/* file_start_ofs is the file offset where the archive actually begins, or 0. */
/* actual_archive_size is the true total size of the archive, which may be smaller than the file's actual size on disk. If zero the entire file is treated as the archive. */
MINIZ_EXPORT mz_bool mz_zip_reader_init_file(mz_zip_archive *pZip, const char *pFilename, mz_uint32 flags);
MINIZ_EXPORT mz_bool mz_zip_reader_init_file_v2(mz_zip_archive *pZip, const char *pFilename, mz_uint flags, mz_uint64 file_start_ofs, mz_uint64 archive_size);
/* Read an archive from an already opened FILE, beginning at the current file position. */
/* The archive is assumed to be archive_size bytes long. If archive_size is 0, then the entire rest of the file is assumed to contain the archive. */
/* The FILE will NOT be closed when mz_zip_reader_end() is called. */
MINIZ_EXPORT mz_bool mz_zip_reader_init_cfile(mz_zip_archive *pZip, MZ_FILE *pFile, mz_uint64 archive_size, mz_uint flags);
#endif
/* Ends archive reading, freeing all allocations, and closing the input archive file if mz_zip_reader_init_file() was used. */
MINIZ_EXPORT mz_bool mz_zip_reader_end(mz_zip_archive *pZip);
/* -------- ZIP reading or writing */
/* Clears a mz_zip_archive struct to all zeros. */
/* Important: This must be done before passing the struct to any mz_zip functions. */
MINIZ_EXPORT void mz_zip_zero_struct(mz_zip_archive *pZip);
MINIZ_EXPORT mz_zip_mode mz_zip_get_mode(mz_zip_archive *pZip);
MINIZ_EXPORT mz_zip_type mz_zip_get_type(mz_zip_archive *pZip);
/* Returns the total number of files in the archive. */
MINIZ_EXPORT mz_uint mz_zip_reader_get_num_files(mz_zip_archive *pZip);
MINIZ_EXPORT mz_uint64 mz_zip_get_archive_size(mz_zip_archive *pZip);
MINIZ_EXPORT mz_uint64 mz_zip_get_archive_file_start_offset(mz_zip_archive *pZip);
MINIZ_EXPORT MZ_FILE *mz_zip_get_cfile(mz_zip_archive *pZip);
/* Reads n bytes of raw archive data, starting at file offset file_ofs, to pBuf. */
MINIZ_EXPORT size_t mz_zip_read_archive_data(mz_zip_archive *pZip, mz_uint64 file_ofs, void *pBuf, size_t n);
/* All mz_zip funcs set the m_last_error field in the mz_zip_archive struct. These functions retrieve/manipulate this field. */
/* Note that the m_last_error functionality is not thread safe. */
MINIZ_EXPORT mz_zip_error mz_zip_set_last_error(mz_zip_archive *pZip, mz_zip_error err_num);
MINIZ_EXPORT mz_zip_error mz_zip_peek_last_error(mz_zip_archive *pZip);
MINIZ_EXPORT mz_zip_error mz_zip_clear_last_error(mz_zip_archive *pZip);
MINIZ_EXPORT mz_zip_error mz_zip_get_last_error(mz_zip_archive *pZip);
MINIZ_EXPORT const char *mz_zip_get_error_string(mz_zip_error mz_err);
/* MZ_TRUE if the archive file entry is a directory entry. */
MINIZ_EXPORT mz_bool mz_zip_reader_is_file_a_directory(mz_zip_archive *pZip, mz_uint file_index);
/* MZ_TRUE if the file is encrypted/strong encrypted. */
MINIZ_EXPORT mz_bool mz_zip_reader_is_file_encrypted(mz_zip_archive *pZip, mz_uint file_index);
/* MZ_TRUE if the compression method is supported, and the file is not encrypted, and the file is not a compressed patch file. */
MINIZ_EXPORT mz_bool mz_zip_reader_is_file_supported(mz_zip_archive *pZip, mz_uint file_index);
/* Retrieves the filename of an archive file entry. */
/* Returns the number of bytes written to pFilename, or if filename_buf_size is 0 this function returns the number of bytes needed to fully store the filename. */
MINIZ_EXPORT mz_uint mz_zip_reader_get_filename(mz_zip_archive *pZip, mz_uint file_index, char *pFilename, mz_uint filename_buf_size);
/* Attempts to locates a file in the archive's central directory. */
/* Valid flags: MZ_ZIP_FLAG_CASE_SENSITIVE, MZ_ZIP_FLAG_IGNORE_PATH */
/* Returns -1 if the file cannot be found. */
MINIZ_EXPORT int mz_zip_reader_locate_file(mz_zip_archive *pZip, const char *pName, const char *pComment, mz_uint flags);
MINIZ_EXPORT mz_bool mz_zip_reader_locate_file_v2(mz_zip_archive *pZip, const char *pName, const char *pComment, mz_uint flags, mz_uint32 *file_index);
/* Returns detailed information about an archive file entry. */
MINIZ_EXPORT mz_bool mz_zip_reader_file_stat(mz_zip_archive *pZip, mz_uint file_index, mz_zip_archive_file_stat *pStat);
/* MZ_TRUE if the file is in zip64 format. */
/* A file is considered zip64 if it contained a zip64 end of central directory marker, or if it contained any zip64 extended file information fields in the central directory. */
MINIZ_EXPORT mz_bool mz_zip_is_zip64(mz_zip_archive *pZip);
/* Returns the total central directory size in bytes. */
/* The current max supported size is <= MZ_UINT32_MAX. */
MINIZ_EXPORT size_t mz_zip_get_central_dir_size(mz_zip_archive *pZip);
/* Extracts a archive file to a memory buffer using no memory allocation. */
/* There must be at least enough room on the stack to store the inflator's state (~34KB or so). */
MINIZ_EXPORT mz_bool mz_zip_reader_extract_to_mem_no_alloc(mz_zip_archive *pZip, mz_uint file_index, void *pBuf, size_t buf_size, mz_uint flags, void *pUser_read_buf, size_t user_read_buf_size);
MINIZ_EXPORT mz_bool mz_zip_reader_extract_file_to_mem_no_alloc(mz_zip_archive *pZip, const char *pFilename, void *pBuf, size_t buf_size, mz_uint flags, void *pUser_read_buf, size_t user_read_buf_size);
/* Extracts a archive file to a memory buffer. */
MINIZ_EXPORT mz_bool mz_zip_reader_extract_to_mem(mz_zip_archive *pZip, mz_uint file_index, void *pBuf, size_t buf_size, mz_uint flags);
MINIZ_EXPORT mz_bool mz_zip_reader_extract_file_to_mem(mz_zip_archive *pZip, const char *pFilename, void *pBuf, size_t buf_size, mz_uint flags);
/* Extracts a archive file to a dynamically allocated heap buffer. */
/* The memory will be allocated via the mz_zip_archive's alloc/realloc functions. */
/* Returns NULL and sets the last error on failure. */
MINIZ_EXPORT void *mz_zip_reader_extract_to_heap(mz_zip_archive *pZip, mz_uint file_index, size_t *pSize, mz_uint flags);
MINIZ_EXPORT void *mz_zip_reader_extract_file_to_heap(mz_zip_archive *pZip, const char *pFilename, size_t *pSize, mz_uint flags);
/* Extracts a archive file using a callback function to output the file's data. */
MINIZ_EXPORT mz_bool mz_zip_reader_extract_to_callback(mz_zip_archive *pZip, mz_uint file_index, mz_file_write_func pCallback, void *pOpaque, mz_uint flags);
MINIZ_EXPORT mz_bool mz_zip_reader_extract_file_to_callback(mz_zip_archive *pZip, const char *pFilename, mz_file_write_func pCallback, void *pOpaque, mz_uint flags);
/* Extract a file iteratively */
MINIZ_EXPORT mz_zip_reader_extract_iter_state *mz_zip_reader_extract_iter_new(mz_zip_archive *pZip, mz_uint file_index, mz_uint flags);
MINIZ_EXPORT mz_zip_reader_extract_iter_state *mz_zip_reader_extract_file_iter_new(mz_zip_archive *pZip, const char *pFilename, mz_uint flags);
MINIZ_EXPORT size_t mz_zip_reader_extract_iter_read(mz_zip_reader_extract_iter_state *pState, void *pvBuf, size_t buf_size);
MINIZ_EXPORT mz_bool mz_zip_reader_extract_iter_free(mz_zip_reader_extract_iter_state *pState);
#ifndef MINIZ_NO_STDIO
/* Extracts a archive file to a disk file and sets its last accessed and modified times. */
/* This function only extracts files, not archive directory records. */
MINIZ_EXPORT mz_bool mz_zip_reader_extract_to_file(mz_zip_archive *pZip, mz_uint file_index, const char *pDst_filename, mz_uint flags);
MINIZ_EXPORT mz_bool mz_zip_reader_extract_file_to_file(mz_zip_archive *pZip, const char *pArchive_filename, const char *pDst_filename, mz_uint flags);
/* Extracts a archive file starting at the current position in the destination FILE stream. */
MINIZ_EXPORT mz_bool mz_zip_reader_extract_to_cfile(mz_zip_archive *pZip, mz_uint file_index, MZ_FILE *File, mz_uint flags);
MINIZ_EXPORT mz_bool mz_zip_reader_extract_file_to_cfile(mz_zip_archive *pZip, const char *pArchive_filename, MZ_FILE *pFile, mz_uint flags);
#endif
#if 0
/* TODO */
typedef void *mz_zip_streaming_extract_state_ptr;
mz_zip_streaming_extract_state_ptr mz_zip_streaming_extract_begin(mz_zip_archive *pZip, mz_uint file_index, mz_uint flags);
mz_uint64 mz_zip_streaming_extract_get_size(mz_zip_archive *pZip, mz_zip_streaming_extract_state_ptr pState);
mz_uint64 mz_zip_streaming_extract_get_cur_ofs(mz_zip_archive *pZip, mz_zip_streaming_extract_state_ptr pState);
mz_bool mz_zip_streaming_extract_seek(mz_zip_archive *pZip, mz_zip_streaming_extract_state_ptr pState, mz_uint64 new_ofs);
size_t mz_zip_streaming_extract_read(mz_zip_archive *pZip, mz_zip_streaming_extract_state_ptr pState, void *pBuf, size_t buf_size);
mz_bool mz_zip_streaming_extract_end(mz_zip_archive *pZip, mz_zip_streaming_extract_state_ptr pState);
#endif
/* This function compares the archive's local headers, the optional local zip64 extended information block, and the optional descriptor following the compressed data vs. the data in the central directory. */
/* It also validates that each file can be successfully uncompressed unless the MZ_ZIP_FLAG_VALIDATE_HEADERS_ONLY is specified. */
MINIZ_EXPORT mz_bool mz_zip_validate_file(mz_zip_archive *pZip, mz_uint file_index, mz_uint flags);
/* Validates an entire archive by calling mz_zip_validate_file() on each file. */
MINIZ_EXPORT mz_bool mz_zip_validate_archive(mz_zip_archive *pZip, mz_uint flags);
/* Misc utils/helpers, valid for ZIP reading or writing */
MINIZ_EXPORT mz_bool mz_zip_validate_mem_archive(const void *pMem, size_t size, mz_uint flags, mz_zip_error *pErr);
#ifndef MINIZ_NO_STDIO
MINIZ_EXPORT mz_bool mz_zip_validate_file_archive(const char *pFilename, mz_uint flags, mz_zip_error *pErr);
#endif
/* Universal end function - calls either mz_zip_reader_end() or mz_zip_writer_end(). */
MINIZ_EXPORT mz_bool mz_zip_end(mz_zip_archive *pZip);
/* -------- ZIP writing */
#ifndef MINIZ_NO_ARCHIVE_WRITING_APIS
/* Inits a ZIP archive writer. */
/*Set pZip->m_pWrite (and pZip->m_pIO_opaque) before calling mz_zip_writer_init or mz_zip_writer_init_v2*/
/*The output is streamable, i.e. file_ofs in mz_file_write_func always increases only by n*/
MINIZ_EXPORT mz_bool mz_zip_writer_init(mz_zip_archive *pZip, mz_uint64 existing_size);
MINIZ_EXPORT mz_bool mz_zip_writer_init_v2(mz_zip_archive *pZip, mz_uint64 existing_size, mz_uint flags);
MINIZ_EXPORT mz_bool mz_zip_writer_init_heap(mz_zip_archive *pZip, size_t size_to_reserve_at_beginning, size_t initial_allocation_size);
MINIZ_EXPORT mz_bool mz_zip_writer_init_heap_v2(mz_zip_archive *pZip, size_t size_to_reserve_at_beginning, size_t initial_allocation_size, mz_uint flags);
#ifndef MINIZ_NO_STDIO
MINIZ_EXPORT mz_bool mz_zip_writer_init_file(mz_zip_archive *pZip, const char *pFilename, mz_uint64 size_to_reserve_at_beginning);
MINIZ_EXPORT mz_bool mz_zip_writer_init_file_v2(mz_zip_archive *pZip, const char *pFilename, mz_uint64 size_to_reserve_at_beginning, mz_uint flags);
MINIZ_EXPORT mz_bool mz_zip_writer_init_cfile(mz_zip_archive *pZip, MZ_FILE *pFile, mz_uint flags);
#endif
/* Converts a ZIP archive reader object into a writer object, to allow efficient in-place file appends to occur on an existing archive. */
/* For archives opened using mz_zip_reader_init_file, pFilename must be the archive's filename so it can be reopened for writing. If the file can't be reopened, mz_zip_reader_end() will be called. */
/* For archives opened using mz_zip_reader_init_mem, the memory block must be growable using the realloc callback (which defaults to realloc unless you've overridden it). */
/* Finally, for archives opened using mz_zip_reader_init, the mz_zip_archive's user provided m_pWrite function cannot be NULL. */
/* Note: In-place archive modification is not recommended unless you know what you're doing, because if execution stops or something goes wrong before */
/* the archive is finalized the file's central directory will be hosed. */
MINIZ_EXPORT mz_bool mz_zip_writer_init_from_reader(mz_zip_archive *pZip, const char *pFilename);
MINIZ_EXPORT mz_bool mz_zip_writer_init_from_reader_v2(mz_zip_archive *pZip, const char *pFilename, mz_uint flags);
/* Adds the contents of a memory buffer to an archive. These functions record the current local time into the archive. */
/* To add a directory entry, call this method with an archive name ending in a forwardslash with an empty buffer. */
/* level_and_flags - compression level (0-10, see MZ_BEST_SPEED, MZ_BEST_COMPRESSION, etc.) logically OR'd with zero or more mz_zip_flags, or just set to MZ_DEFAULT_COMPRESSION. */
MINIZ_EXPORT mz_bool mz_zip_writer_add_mem(mz_zip_archive *pZip, const char *pArchive_name, const void *pBuf, size_t buf_size, mz_uint level_and_flags);
/* Like mz_zip_writer_add_mem(), except you can specify a file comment field, and optionally supply the function with already compressed data. */
/* uncomp_size/uncomp_crc32 are only used if the MZ_ZIP_FLAG_COMPRESSED_DATA flag is specified. */
MINIZ_EXPORT mz_bool mz_zip_writer_add_mem_ex(mz_zip_archive *pZip, const char *pArchive_name, const void *pBuf, size_t buf_size, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags,
mz_uint64 uncomp_size, mz_uint32 uncomp_crc32);
MINIZ_EXPORT mz_bool mz_zip_writer_add_mem_ex_v2(mz_zip_archive *pZip, const char *pArchive_name, const void *pBuf, size_t buf_size, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags,
mz_uint64 uncomp_size, mz_uint32 uncomp_crc32, MZ_TIME_T *last_modified, const char *user_extra_data_local, mz_uint user_extra_data_local_len,
const char *user_extra_data_central, mz_uint user_extra_data_central_len);
/* Adds the contents of a file to an archive. This function also records the disk file's modified time into the archive. */
/* File data is supplied via a read callback function. User mz_zip_writer_add_(c)file to add a file directly.*/
MINIZ_EXPORT mz_bool mz_zip_writer_add_read_buf_callback(mz_zip_archive *pZip, const char *pArchive_name, mz_file_read_func read_callback, void *callback_opaque, mz_uint64 max_size,
const MZ_TIME_T *pFile_time, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags, const char *user_extra_data_local, mz_uint user_extra_data_local_len,
const char *user_extra_data_central, mz_uint user_extra_data_central_len);
#ifndef MINIZ_NO_STDIO
/* Adds the contents of a disk file to an archive. This function also records the disk file's modified time into the archive. */
/* level_and_flags - compression level (0-10, see MZ_BEST_SPEED, MZ_BEST_COMPRESSION, etc.) logically OR'd with zero or more mz_zip_flags, or just set to MZ_DEFAULT_COMPRESSION. */
MINIZ_EXPORT mz_bool mz_zip_writer_add_file(mz_zip_archive *pZip, const char *pArchive_name, const char *pSrc_filename, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags);
/* Like mz_zip_writer_add_file(), except the file data is read from the specified FILE stream. */
MINIZ_EXPORT mz_bool mz_zip_writer_add_cfile(mz_zip_archive *pZip, const char *pArchive_name, MZ_FILE *pSrc_file, mz_uint64 max_size,
const MZ_TIME_T *pFile_time, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags, const char *user_extra_data_local, mz_uint user_extra_data_local_len,
const char *user_extra_data_central, mz_uint user_extra_data_central_len);
#endif
/* Adds a file to an archive by fully cloning the data from another archive. */
/* This function fully clones the source file's compressed data (no recompression), along with its full filename, extra data (it may add or modify the zip64 local header extra data field), and the optional descriptor following the compressed data. */
MINIZ_EXPORT mz_bool mz_zip_writer_add_from_zip_reader(mz_zip_archive *pZip, mz_zip_archive *pSource_zip, mz_uint src_file_index);
/* Finalizes the archive by writing the central directory records followed by the end of central directory record. */
/* After an archive is finalized, the only valid call on the mz_zip_archive struct is mz_zip_writer_end(). */
/* An archive must be manually finalized by calling this function for it to be valid. */
MINIZ_EXPORT mz_bool mz_zip_writer_finalize_archive(mz_zip_archive *pZip);
/* Finalizes a heap archive, returning a pointer to the heap block and its size. */
/* The heap block will be allocated using the mz_zip_archive's alloc/realloc callbacks. */
MINIZ_EXPORT mz_bool mz_zip_writer_finalize_heap_archive(mz_zip_archive *pZip, void **ppBuf, size_t *pSize);
/* Ends archive writing, freeing all allocations, and closing the output file if mz_zip_writer_init_file() was used. */
/* Note for the archive to be valid, it *must* have been finalized before ending (this function will not do it for you). */
MINIZ_EXPORT mz_bool mz_zip_writer_end(mz_zip_archive *pZip);
/* -------- Misc. high-level helper functions: */
/* mz_zip_add_mem_to_archive_file_in_place() efficiently (but not atomically) appends a memory blob to a ZIP archive. */
/* Note this is NOT a fully safe operation. If it crashes or dies in some way your archive can be left in a screwed up state (without a central directory). */
/* level_and_flags - compression level (0-10, see MZ_BEST_SPEED, MZ_BEST_COMPRESSION, etc.) logically OR'd with zero or more mz_zip_flags, or just set to MZ_DEFAULT_COMPRESSION. */
/* TODO: Perhaps add an option to leave the existing central dir in place in case the add dies? We could then truncate the file (so the old central dir would be at the end) if something goes wrong. */
MINIZ_EXPORT mz_bool mz_zip_add_mem_to_archive_file_in_place(const char *pZip_filename, const char *pArchive_name, const void *pBuf, size_t buf_size, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags);
MINIZ_EXPORT mz_bool mz_zip_add_mem_to_archive_file_in_place_v2(const char *pZip_filename, const char *pArchive_name, const void *pBuf, size_t buf_size, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags, mz_zip_error *pErr);
#ifndef MINIZ_NO_STDIO
/* Reads a single file from an archive into a heap block. */
/* If pComment is not NULL, only the file with the specified comment will be extracted. */
/* Returns NULL on failure. */
MINIZ_EXPORT void *mz_zip_extract_archive_file_to_heap(const char *pZip_filename, const char *pArchive_name, size_t *pSize, mz_uint flags);
MINIZ_EXPORT void *mz_zip_extract_archive_file_to_heap_v2(const char *pZip_filename, const char *pArchive_name, const char *pComment, size_t *pSize, mz_uint flags, mz_zip_error *pErr);
#endif
#endif /* #ifndef MINIZ_NO_ARCHIVE_WRITING_APIS */
#ifdef __cplusplus
}
#endif
#endif /* MINIZ_NO_ARCHIVE_APIS */
+2 -1
View File
@@ -1 +1,2 @@
put here ps4-payload-sdk, ps5-payload-sdk
put here ps4-payload-sdk, ps5-payload-sdk
(pacbrew PS4 portlibs are installed by tools/fetch_pacbrew_ps4.sh into pacbrew/)
+11 -6
View File
@@ -24,11 +24,12 @@ SOFTWARE.
#include "pal_ffi.h"
#include "../../include/event_loop.h"
#include "../../include/ftp_server.h"
#include "../../include/http_server.h"
#include "../../include/pal_alloc.h"
#include "event_loop.h"
#include "ftp_server.h"
#include "http_server.h"
#include "pal_alloc.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -140,9 +141,13 @@ void pal_ffi_ftp_server_destroy(void *server) {
#if defined(ENABLE_ZHTTPD) && (ENABLE_ZHTTPD == 1)
void *pal_ffi_http_server_create(void *loop, uint16_t port) {
if (loop == NULL)
if (loop == NULL || port == 0U)
return NULL;
return (void *)http_server_create((event_loop_t *)loop, port);
char bind_addr[64];
int n = snprintf(bind_addr, sizeof(bind_addr), "[::]:%u", (unsigned)port);
if (n <= 0 || (size_t)n >= sizeof(bind_addr))
return NULL;
return (void *)http_server_create((event_loop_t *)loop, bind_addr, "/");
}
void pal_ffi_http_server_destroy(void *server) {
+10
View File
@@ -0,0 +1,10 @@
#ifndef ZFTPD_ARCHIVE_PATH_H
#define ZFTPD_ARCHIVE_PATH_H
#include <stddef.h>
int archive_path_prepare(const char *root, const char *entry, size_t entry_len,
int is_directory, char *out, size_t out_size);
int archive_path_is_safe_relative(const char *entry, size_t entry_len);
#endif
+39
View File
@@ -0,0 +1,39 @@
/* ══ BUILTIN UNZIP ═══════════════════════════════════════════════════════════
* Self-contained ZIP extractor — no external libraries required.
* Supports Store (0) and Deflate (8) compression methods.
* Works on all platforms including PS4/PS5 without libarchive.
*
* ZIP format reference: PKWARE APPNOTE.TXT v6.3.4
* Deflate reference: RFC 1951
* ═════════════════════════════════════════════════════════════════════════ */
#ifndef BUILTIN_UNZIP_H
#define BUILTIN_UNZIP_H
#include <stddef.h>
#include <stdint.h>
#include <stdatomic.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Extract a ZIP archive to a destination directory.
*
* @param zip_path Path to the ZIP file.
* @param dest_dir Destination directory (must exist).
* @param cancelled Atomic cancellation flag; set to 1 to abort.
* @param error_msg Buffer for error message on failure.
* @param error_msg_size Size of error_msg buffer.
* @return 0 on success, -1 on failure (error_msg is populated).
*/
int builtin_unzip(const char *zip_path, const char *dest_dir,
_Atomic int *cancelled,
char *error_msg, size_t error_msg_size);
#ifdef __cplusplus
}
#endif
#endif /* BUILTIN_UNZIP_H */
+366
View File
@@ -0,0 +1,366 @@
#ifndef EXFAT_UNPACKER_H
#define EXFAT_UNPACKER_H
/*
* exfat_unpacker — exFAT filesystem image parser
* Copyright (C) 2025 seregonwar
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* exfat_unpacker.h — exFAT filesystem image parser
*
* Provides read-only access to exFAT images: boot sector, FAT chain,
* directory traversal and in-memory file extraction.
*
* THREAD SAFETY: NOT thread-safe. Callers that share an exfat_context_t
* across threads must provide external serialisation. In the NetMount
* server each call to ensure_meta() holds the per-export meta_mutex, so
* only one thread ever uses a context at a time — no additional locking
* is needed.
*
* BUG HISTORY (original library, fixed here):
* 1. exfat_boot_sector_t: volume_flags was uint32_t (should be uint16_t),
* shifting bytes_per_sector_power to 0x6E instead of the correct 0x6C.
* 2. exfat_file_entry_t: phantom first_cluster/data_length fields made
* the struct 44 bytes instead of 32; tenth-of-second fields were
* uint16_t instead of uint8_t.
* 3. exfat_filename_entry_t: name_length + name_hash overflowed beyond
* the 32-byte entry boundary, causing out-of-bounds reads.
* 4. exfat_read_directory: read only one cluster, silently truncating
* large directories that span multiple clusters.
* 5. exfat_extract_*: per-cluster malloc in hot path; NoFatChain flag
* (stream extension GeneralSecondaryFlags bit 1) ignored.
*/
#include <stdint.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <sys/types.h> /* ssize_t */
#if defined(_MSC_VER)
# include <BaseTsd.h>
# ifndef ssize_t
typedef SSIZE_T ssize_t;
# endif
#endif
#if defined(_MSC_VER)
# pragma pack(push, 1)
# define EXFAT_PACKED
#else
# define EXFAT_PACKED __attribute__((packed))
#endif
#ifdef __cplusplus
extern "C" {
#endif
/* ── Constants ──────────────────────────────────────────────────────────── */
#define EXFAT_SECTOR_SIZE 512
#define EXFAT_BOOT_SIGNATURE 0xAA55U
#define EXFAT_FS_NAME "EXFAT " /* 8 bytes per spec */
#define EXFAT_FS_NAME_LEN 8
#define EXFAT_ENTRY_SIZE 32
#define EXFAT_MAX_FILENAME_LEN 255 /* spec max: 255 chars */
#define EXFAT_MAX_NAME_ENTRIES 17 /* ceil(255/15) */
/* Directory entry types */
#define EXFAT_ENTRY_TYPE_FILE_DIR 0x85U
#define EXFAT_ENTRY_TYPE_STREAM_EXT 0xC0U
#define EXFAT_ENTRY_TYPE_FILENAME 0xC1U
#define EXFAT_ENTRY_TYPE_BITMAP 0x81U
#define EXFAT_ENTRY_TYPE_UPCASE 0x82U
#define EXFAT_ENTRY_TYPE_VOLID 0x83U
#define EXFAT_ENTRY_TYPE_EOD 0x00U /* end-of-directory */
/* GeneralSecondaryFlags (stream extension offset 0x01) */
#define EXFAT_FLAG_ALLOC_POSSIBLE 0x01U
#define EXFAT_FLAG_NO_FAT_CHAIN 0x02U /* contiguous clusters */
/* File attributes */
#define EXFAT_ATTR_READ_ONLY 0x0001U
#define EXFAT_ATTR_HIDDEN 0x0002U
#define EXFAT_ATTR_SYSTEM 0x0004U
#define EXFAT_ATTR_VOLUME_LABEL 0x0008U
#define EXFAT_ATTR_DIRECTORY 0x0010U
#define EXFAT_ATTR_ARCHIVE 0x0020U
/* FAT special values */
#define EXFAT_FAT_FREE_CLUSTER 0x00000000U
#define EXFAT_FAT_MEDIA_DESCRIPTOR 0xFFFFFFF8U
#define EXFAT_FAT_END_OF_CHAIN 0xFFFFFFFFU
#define EXFAT_FAT_BAD_CLUSTER 0xFFFFFFF7U
/* I/O chunk size for file extraction (avoids per-cluster malloc) */
#define EXFAT_IO_CHUNK_SIZE (256U * 1024U) /* 256 KiB */
/* ── Boot Sector (512 bytes, Microsoft exFAT spec §3.1) ─────────────────── */
/*
* Accurate field layout with correct offsets. Previously volume_flags was
* uint32_t (4 bytes) which shifted bytes_per_sector_power from 0x6C to 0x6E.
*
* Offset Field Size
* ────── ─────────────────────────── ────
* 0x00 JumpBoot 3
* 0x03 FileSystemName ("EXFAT ") 8
* 0x0B MustBeZero 53
* 0x40 PartitionOffset (uint64) 8
* 0x48 VolumeLength (uint64) 8
* 0x50 FatOffset (uint32) 4
* 0x54 FatLength (uint32) 4
* 0x58 ClusterHeapOffset (uint32) 4
* 0x5C ClusterCount (uint32) 4
* 0x60 FirstClusterOfRootDir (u32) 4
* 0x64 VolumeSerialNumber (uint32) 4
* 0x68 FileSystemRevision (uint16) 2
* 0x6A VolumeFlags (uint16) 2 ← was uint32_t (BUG)
* 0x6C BytesPerSectorShift (uint8) 1 ← was at 0x6E (BUG)
* 0x6D SectorsPerClusterShift (u8) 1 ← was at 0x6F (BUG)
* 0x6E NumberOfFats (uint8) 1
* 0x6F DriveSelect (uint8) 1
* 0x70 PercentInUse (uint8) 1
* 0x71 Reserved 7
* 0x78 BootCode 390
* 0x1FE BootSignature (uint16) 2
* ──────────────────────────────────────
* Total 512
*/
typedef struct EXFAT_PACKED {
uint8_t jump_boot[3]; /* 0x00 */
uint8_t fs_name[8]; /* 0x03 "EXFAT " */
uint8_t must_be_zero[53]; /* 0x0B */
uint64_t partition_offset; /* 0x40 sectors from media start */
uint64_t volume_length; /* 0x48 sectors in volume */
uint32_t fat_offset; /* 0x50 sectors from VBR start */
uint32_t fat_length; /* 0x54 sectors */
uint32_t cluster_heap_offset; /* 0x58 sectors from VBR start */
uint32_t total_clusters; /* 0x5C */
uint32_t root_dir_first_cluster; /* 0x60 */
uint32_t volume_serial; /* 0x64 */
uint16_t fs_revision; /* 0x68 e.g. 0x0100 = 1.00 */
uint16_t volume_flags; /* 0x6A FIXED: was uint32_t */
uint8_t bytes_per_sector_shift; /* 0x6C 2^n bytes/sector [9,12] */
uint8_t sectors_per_cluster_shift; /* 0x6D 2^n sectors/cluster */
uint8_t num_fats; /* 0x6E usually 1 */
uint8_t drive_select; /* 0x6F */
uint8_t percent_in_use; /* 0x70 0xFF = unknown */
uint8_t reserved[7]; /* 0x71 */
uint8_t boot_code[390]; /* 0x78 */
uint16_t boot_signature; /* 0x1FE must be 0xAA55 */
} exfat_boot_sector_t;
/* Compile-time size assertion — catches any future struct changes. */
typedef char exfat_boot_sector_size_check[
(sizeof(exfat_boot_sector_t) == 512) ? 1 : -1];
/* ── File Directory Entry (0x85, 32 bytes) ──────────────────────────────── */
/*
* FIXED: removed phantom first_cluster / data_length fields that do not
* exist in the 0x85 entry (they belong to the stream extension 0xC0).
* Also fixed: tenth-of-second fields changed from uint16_t to uint8_t.
*
* 0x00 EntryType 1
* 0x01 SecondaryCount 1
* 0x02 SetChecksum 2
* 0x04 FileAttributes 2
* 0x06 Reserved1 2
* 0x08 CreateTimestamp 4
* 0x0C LastModTimestamp 4
* 0x10 LastAccTimestamp 4
* 0x14 Create10ms 1
* 0x15 LastMod10ms 1
* 0x16 CreateUtcOffset 1
* 0x17 LastModUtcOffset 1
* 0x18 LastAccUtcOffset 1
* 0x19 Reserved2 7
* ─────────────────────── ──
* 32
*/
typedef struct EXFAT_PACKED {
uint8_t entry_type; /* 0x00 0x85 */
uint8_t secondary_count; /* 0x01 number of secondary entries */
uint16_t set_checksum; /* 0x02 */
uint16_t file_attributes; /* 0x04 EXFAT_ATTR_* */
uint16_t reserved1; /* 0x06 */
uint32_t create_time; /* 0x08 DOS timestamp */
uint32_t last_modified_time; /* 0x0C DOS timestamp */
uint32_t last_accessed_time; /* 0x10 DOS timestamp */
uint8_t create_10ms; /* 0x14 FIXED: was uint16_t */
uint8_t last_mod_10ms; /* 0x15 FIXED: was uint16_t */
uint8_t create_utc_offset; /* 0x16 */
uint8_t last_mod_utc_offset;/* 0x17 */
uint8_t last_acc_utc_offset;/* 0x18 */
uint8_t reserved2[7]; /* 0x19 */
} exfat_file_entry_t;
typedef char exfat_file_entry_size_check[
(sizeof(exfat_file_entry_t) == 32) ? 1 : -1];
/* ── Stream Extension Entry (0xC0, 32 bytes) ────────────────────────────── */
/*
* 0x00 EntryType 1
* 0x01 GeneralSecondaryFlags 1 bit0=AllocPossible, bit1=NoFatChain
* 0x02 Reserved1 1
* 0x03 NameLength 1 filename length in chars (max 255)
* 0x04 NameHash 2
* 0x06 Reserved2 2
* 0x08 ValidDataLength 8
* 0x10 Reserved3 4
* 0x14 FirstCluster 4
* 0x18 DataLength 8
* ─────────────────────────────────
* 32
*/
typedef struct EXFAT_PACKED {
uint8_t entry_type; /* 0x00 0xC0 */
uint8_t flags; /* 0x01 GeneralSecondaryFlags */
uint8_t reserved1; /* 0x02 */
uint8_t name_length; /* 0x03 total filename chars */
uint16_t name_hash; /* 0x04 */
uint16_t reserved2; /* 0x06 */
uint64_t valid_data_length; /* 0x08 */
uint32_t reserved3; /* 0x10 */
uint32_t first_cluster; /* 0x14 */
uint64_t data_length; /* 0x18 */
} exfat_stream_ext_t;
typedef char exfat_stream_ext_size_check[
(sizeof(exfat_stream_ext_t) == 32) ? 1 : -1];
/* ── Filename Entry (0xC1, 32 bytes) ────────────────────────────────────── */
/*
* FIXED: removed name_length + name_hash fields that were placed beyond the
* 32-byte boundary, causing out-of-bounds reads in the original code.
* NameLength lives in the stream extension; each filename entry carries
* exactly 15 UTF-16LE code units.
*
* 0x00 EntryType 1
* 0x01 GeneralSecondaryFlags 1
* 0x02 FileName[15] (UTF-16LE) 30
* ──────────────────────────────────
* 32
*/
typedef struct EXFAT_PACKED {
uint8_t entry_type; /* 0x00 0xC1 */
uint8_t flags; /* 0x01 GeneralSecondaryFlags */
uint16_t file_name[15]; /* 0x02 up to 15 UTF-16LE chars */
} exfat_filename_entry_t;
typedef char exfat_filename_entry_size_check[
(sizeof(exfat_filename_entry_t) == 32) ? 1 : -1];
/* ── Main context ───────────────────────────────────────────────────────── */
typedef struct {
FILE *image_file; /* open for reading; owned by context */
exfat_boot_sector_t boot_sector;
uint32_t bytes_per_sector;
uint32_t sectors_per_cluster;
uint32_t bytes_per_cluster;
uint64_t cluster_heap_offset_bytes; /* absolute byte offset in image */
uint32_t *fat_table;
size_t fat_entries;
} exfat_context_t;
/* ── Parsed file/directory descriptor ──────────────────────────────────── */
typedef struct {
char filename[EXFAT_MAX_FILENAME_LEN + 1];
uint16_t attributes;
uint32_t first_cluster;
uint64_t data_length;
uint32_t create_time;
uint32_t last_modified_time;
uint32_t last_accessed_time;
int is_directory;
int no_fat_chain; /* 1 = contiguous clusters, FAT not needed */
} exfat_file_info_t;
/* ── API ────────────────────────────────────────────────────────────────── */
/* Context lifecycle */
int exfat_init(exfat_context_t *ctx, const char *image_path);
void exfat_cleanup(exfat_context_t *ctx);
/* Boot sector */
int exfat_read_boot_sector(exfat_context_t *ctx);
int exfat_validate_boot_sector(const exfat_boot_sector_t *boot);
void exfat_print_boot_sector(const exfat_boot_sector_t *boot);
/* FAT */
int exfat_read_fat(exfat_context_t *ctx);
uint32_t exfat_get_next_cluster(const exfat_context_t *ctx, uint32_t cluster);
void exfat_free_fat(exfat_context_t *ctx);
/* Cluster I/O */
uint64_t exfat_get_cluster_offset(const exfat_context_t *ctx, uint32_t cluster);
ssize_t exfat_read_cluster(exfat_context_t *ctx, uint32_t cluster,
uint8_t *buffer, size_t size);
/* Directory */
int exfat_read_directory(exfat_context_t *ctx, uint32_t first_cluster,
exfat_file_info_t *entries, int max_entries);
int exfat_parse_file_entry(const uint8_t *data, size_t offset, size_t data_len,
exfat_file_info_t *info);
int exfat_utf16_to_utf8(const uint16_t *utf16, int nchars,
char *utf8, size_t utf8_size);
/* File extraction */
int exfat_extract_file(exfat_context_t *ctx, const exfat_file_info_t *info,
const char *output_path);
int exfat_extract_file_fd(exfat_context_t *ctx, const exfat_file_info_t *info,
int output_fd);
ssize_t exfat_extract_to_buffer(exfat_context_t *ctx, const exfat_file_info_t *info,
uint8_t *buf, size_t buf_size);
/* Utilities */
void exfat_print_file_info(const exfat_file_info_t *info);
time_t exfat_dos_time_to_unix(uint32_t dos_time);
void exfat_format_size(uint64_t size, char *buffer, size_t buf_size);
/* ── Inline helpers ─────────────────────────────────────────────────────── */
static inline int exfat_is_valid_cluster(uint32_t c) {
return c >= 2U && c < EXFAT_FAT_BAD_CLUSTER;
}
static inline int exfat_is_end_of_chain(uint32_t c) {
return c >= EXFAT_FAT_BAD_CLUSTER;
}
static inline int exfat_is_directory(uint16_t attr) {
return (attr & EXFAT_ATTR_DIRECTORY) != 0;
}
static inline uint16_t exfat_read_le16(const uint8_t *p) {
return (uint16_t)((unsigned)p[0] | ((unsigned)p[1] << 8));
}
static inline uint32_t exfat_read_le32(const uint8_t *p) {
return (uint32_t)p[0] | ((uint32_t)p[1] << 8) |
((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
}
static inline uint64_t exfat_read_le64(const uint8_t *p) {
return (uint64_t)exfat_read_le32(p) |
((uint64_t)exfat_read_le32(p + 4) << 32);
}
#ifdef __cplusplus
}
#endif
#if defined(_MSC_VER)
# pragma pack(pop)
#endif
#undef EXFAT_PACKED
#endif /* EXFAT_UNPACKER_H */
+334
View File
@@ -0,0 +1,334 @@
#ifndef PKG_UNPACKER_H
#define PKG_UNPACKER_H
/*
* GNU GPLv3 License — Copyright (c) 2026 SeregonWar
* See LICENSE for full text.
*/
/*
* pkg_unpacker — PS4 PKG archive metadata parser
*
* Provides read-only access to *unencrypted* metadata entries embedded in the
* PS4 PKG entry table (e.g., param.sfo, icon0.png, pic1.png). It does NOT
* decrypt NPDRM-protected entries or the PFS filesystem image.
*
* Modelled after the exfat_unpacker.h API for consistency.
*
* ── Thread-safety ──────────────────────────────────────────────────────────
* NONE. All functions require external synchronisation when a context is
* shared across threads.
*
* ── Re-entrancy ────────────────────────────────────────────────────────────
* Functions are NOT re-entrant.
*
* ── Platform ───────────────────────────────────────────────────────────────
* POSIX (_FILE_OFFSET_BITS=64) and Windows (_fseeki64 / _ftelli64).
*
* ── Format reference ───────────────────────────────────────────────────────
* PS4 PKG specification: https://www.psdevwiki.com/ps4/PKG_files
* shadPS4 implementation (pkg.h / pkg.cpp) cross-referenced for field offsets.
*/
#include <stdint.h>
#include <stddef.h>
#include <stdio.h>
#include <sys/types.h> /* ssize_t */
#if defined(_MSC_VER)
# ifndef ssize_t
typedef SSIZE_T ssize_t;
# endif
#endif
#ifdef __cplusplus
extern "C" {
#endif
/* ═══════════════════════════════════════════════════════════════════════════
* Constants
* ═════════════════════════════════════════════════════════════════════════*/
/** ASCII ".CNT" (big-endian) — used in PS4 PKGs. */
#define PKG_MAGIC_CNT 0x7F434E54U
/** ASCII ".PKG" (big-endian) — used in PS3/some mock PKGs. */
#define PKG_MAGIC_PKG 0x7F504B47U
/** Bytes read from the file start to cover all used header fields. */
#define PKG_HEADER_SIZE 0x1000U
/** On-disk byte length of the content_id field (no NUL in the file). */
#define PKG_CONTENT_ID_LEN 36U
/**
* Hard cap on entry_count. A PKG with more than this many entries is
* treated as malformed. Keeps allocation bounded and avoids amplification
* attacks on untrusted files.
*/
#define PKG_MAX_ENTRY_COUNT 10000U
/**
* Hard cap on the size of a single entry that may be extracted into memory.
* Individual metadata entries (SFO, icons) are never this large in practice;
* this guards against corrupt or crafted size fields.
*/
#define PKG_MAX_ENTRY_SIZE (64U * 1024U * 1024U) /* 64 MiB */
/**
* The entry table must not overlap the header.
* Any table_offset below this value is treated as corrupt.
*/
#define PKG_MIN_TABLE_OFFSET PKG_HEADER_SIZE
/** Size in bytes of one serialised entry record in the file. */
#define PKG_ENTRY_RECORD_SIZE 32U
/* ── Common unencrypted entry IDs ────────────────────────────────────────── */
#define PKG_ENTRY_ID_PARAM_SFO 0x1000U
#define PKG_ENTRY_ID_ICON0_PNG 0x1200U
#define PKG_ENTRY_ID_ICON1_PNG 0x1210U
#define PKG_ENTRY_ID_PIC0_PNG 0x1220U
#define PKG_ENTRY_ID_PIC1_PNG 0x1230U
/**
* Bit 31 of flags1.
* 0 = entry data is plaintext; safe to extract.
* 1 = entry data is encrypted (NPDRM / system key); extraction yields
* raw ciphertext, which is almost certainly not what the caller wants.
*
* Always test with pkg_entry_is_encrypted() before extracting.
*/
#define PKG_ENTRY_FLAG_ENCRYPTED 0x80000000U
/* ═══════════════════════════════════════════════════════════════════════════
* Error codes
* ═════════════════════════════════════════════════════════════════════════*/
/**
* Return type for all pkg_* functions that can fail.
* Values are negative so that a single `if (rc < 0)` test catches any error.
*/
typedef enum {
PKG_OK = 0, /**< Success. */
PKG_ERR_PARAM = -1, /**< NULL or invalid argument. */
PKG_ERR_IO = -2, /**< File I/O failure (check errno for detail). */
PKG_ERR_FORMAT = -3, /**< File does not conform to the PKG format. */
PKG_ERR_RANGE = -4, /**< Value would overflow or is out of range. */
PKG_ERR_NOMEM = -5, /**< malloc() returned NULL. */
PKG_ERR_ENCRYPTED = -6, /**< Entry is encrypted; cannot yield plaintext.*/
PKG_ERR_BUFFER_SMALL = -7 /**< Caller buffer is smaller than entry->size. */
} pkg_error_t;
/* ═══════════════════════════════════════════════════════════════════════════
* Data structures
* ═════════════════════════════════════════════════════════════════════════*/
/**
* Decoded fields from the on-disk PKG header.
*
* @note content_id is always NUL-terminated even though the on-disk field
* has no NUL (PKG_CONTENT_ID_LEN bytes, no terminator).
*/
typedef struct {
uint32_t magic;
uint32_t type;
uint32_t file_count;
uint32_t entry_count;
uint32_t table_offset;
char content_id[PKG_CONTENT_ID_LEN + 1U]; /* +1 for '\0' */
} pkg_header_t;
/**
* One entry decoded from the PKG entry table.
*
* DESIGN RATIONALE — why expose flags1 / flags2:
* Bit 31 of flags1 signals encryption (PKG_ENTRY_FLAG_ENCRYPTED).
* Callers must check this before extracting to avoid silently processing
* ciphertext. Hiding these fields would force callers to call
* pkg_extract_* and check for PKG_ERR_ENCRYPTED after the seek — wasteful
* and harder to reason about.
*
* @note Use pkg_entry_is_encrypted() rather than testing flags1 directly.
*/
typedef struct {
uint32_t id; /**< Entry type identifier (e.g. PKG_ENTRY_ID_*). */
uint32_t filename_offset; /**< Offset into the "entry_names" name table. */
uint32_t flags1; /**< Bit 31 set → PKG_ENTRY_FLAG_ENCRYPTED. */
uint32_t flags2; /**< Reserved / key index for encrypted entries. */
uint32_t offset; /**< Absolute byte offset of data in the PKG file.*/
uint32_t size; /**< Byte length of the entry data. */
} pkg_entry_t;
/**
* Parser context.
*
* Treat as opaque: do not modify fields directly. All internal invariants
* are established by pkg_init() and held until pkg_cleanup().
*/
typedef struct {
FILE *file; /**< Open file handle (owned by this context). */
uint64_t file_size; /**< Total PKG file size in bytes. */
pkg_header_t header; /**< Decoded header fields. */
pkg_entry_t *entries; /**< Heap-allocated array [0, num_entries). */
size_t num_entries; /**< Element count of entries[]. */
} pkg_context_t;
/* ═══════════════════════════════════════════════════════════════════════════
* Inline helpers
* ═════════════════════════════════════════════════════════════════════════*/
/**
* @brief Test whether an entry is flagged as encrypted.
*
* Always call this before pkg_extract_to_buffer() or pkg_extract_file_fd()
* to avoid obtaining ciphertext instead of plaintext.
*
* @param[in] entry Non-NULL pointer to an entry; behaviour undefined if NULL.
* @return Non-zero if the entry is encrypted, zero if it is plaintext.
*
* @pre entry != NULL
* @note WCET: O(1), no I/O.
*/
static inline int pkg_entry_is_encrypted(const pkg_entry_t *entry)
{
return (entry->flags1 & PKG_ENTRY_FLAG_ENCRYPTED) != 0U;
}
/* ═══════════════════════════════════════════════════════════════════════════
* API
* ═════════════════════════════════════════════════════════════════════════*/
/**
* @brief Open a PKG file and parse its header and entry table into ctx.
*
* On failure the context is left in a state that is safe to pass to
* pkg_cleanup() — the caller MUST still call pkg_cleanup() to release any
* partially-acquired resources (e.g., the file handle that was opened before
* the header was validated).
*
* Validation performed:
* - Magic bytes == PKG_MAGIC.
* - entry_count in [1, PKG_MAX_ENTRY_COUNT].
* - table_offset >= PKG_MIN_TABLE_OFFSET.
* - table_offset + entry_count * PKG_ENTRY_RECORD_SIZE <= file_size (no OOB).
* - Each entry's [offset, offset+size) is contained within the file.
*
* @param[out] ctx Caller-allocated context; must not be NULL.
* @param[in] pkg_path NUL-terminated path to the PKG file; must not be NULL
* or empty.
*
* @return PKG_OK on success.
* @retval PKG_ERR_PARAM ctx or pkg_path is NULL / pkg_path is empty.
* @retval PKG_ERR_IO fopen, fread, fseeko, or ftello failed.
* @retval PKG_ERR_FORMAT File is too small, magic mismatch, entry_count
* out of range, or an entry extends beyond EOF.
* @retval PKG_ERR_RANGE Internal arithmetic overflow (defensive).
* @retval PKG_ERR_NOMEM malloc() returned NULL.
*
* @pre ctx != NULL
* @pre pkg_path != NULL && pkg_path[0] != '\0'
* @post On PKG_OK: ctx->file != NULL, ctx->entries != NULL,
* ctx->num_entries == ctx->header.entry_count,
* ctx->header.content_id is NUL-terminated.
*
* @note Thread-safety: NOT thread-safe.
* @note WCET: Unbounded (disk I/O; up to PKG_MAX_ENTRY_COUNT entry reads).
* @warning Do not call from interrupt or hard-real-time context.
*/
int pkg_init(pkg_context_t *ctx, const char *pkg_path);
/**
* @brief Release all resources owned by ctx.
*
* Closes the file handle and frees the entries array. Safe to call on a
* partially-initialised context (e.g., after a failed pkg_init).
* Idempotent: safe to call more than once on the same ctx.
*
* @param[in,out] ctx Context to clean up. Ignored if NULL.
*
* @note Thread-safety: NOT thread-safe.
* @note WCET: O(1), no I/O.
*/
void pkg_cleanup(pkg_context_t *ctx);
/**
* @brief Find an entry in the table by its numeric ID.
*
* Performs a linear scan over the parsed entry array. For the typical
* entry counts found in real PKG files (<200) this is faster than a hash
* map due to cache locality.
*
* @param[in] ctx Initialised context.
* @param[in] id Entry ID to search for (e.g., PKG_ENTRY_ID_PARAM_SFO).
*
* @return Pointer into ctx->entries[] for the first matching entry, or NULL
* if not found or if ctx / ctx->entries is NULL.
*
* @warning The returned pointer is invalidated by pkg_cleanup().
* Do not free or retain it beyond the lifetime of ctx.
*
* @note Thread-safety: NOT thread-safe.
* @note WCET: O(num_entries).
*/
const pkg_entry_t *pkg_find_entry_by_id(const pkg_context_t *ctx, uint32_t id);
/**
* @brief Extract a plaintext entry's raw bytes into a caller-supplied buffer.
*
* Seeks to entry->offset and reads entry->size bytes into buf.
* The call is rejected if the entry is encrypted (flags1 bit 31 set).
*
* @param[in,out] ctx Initialised context (file position is modified).
* @param[in] entry Entry to extract; must not be NULL.
* @param[out] buf Destination buffer; must not be NULL.
* @param[in] buf_size Usable bytes in buf.
*
* @return Bytes written (>= 0) on success, or a negative pkg_error_t value.
* @retval PKG_ERR_PARAM A required argument is NULL or buf_size == 0.
* @retval PKG_ERR_ENCRYPTED Entry is encrypted; call refused.
* @retval PKG_ERR_RANGE entry->size > PKG_MAX_ENTRY_SIZE or > SSIZE_MAX.
* @retval PKG_ERR_BUFFER_SMALL buf_size < entry->size.
* @retval PKG_ERR_IO fseeko or fread failed, or short read occurred.
*
* @pre (entry->flags1 & PKG_ENTRY_FLAG_ENCRYPTED) == 0
* @pre buf_size >= entry->size
* @post On success, buf[0..entry->size) contains the raw entry data.
*
* @note Thread-safety: NOT thread-safe.
* @note WCET: Proportional to entry->size; dominated by disk I/O.
*/
ssize_t pkg_extract_to_buffer(pkg_context_t *ctx, const pkg_entry_t *entry,
uint8_t *buf, size_t buf_size);
/**
* @brief Stream a plaintext entry's raw bytes to an open file descriptor.
*
* Uses a fixed-size stack buffer (COPY_BUF_SIZE bytes) internally; never
* allocates from the heap. EINTR-interrupted write(2) calls are retried
* automatically.
*
* @param[in,out] ctx Initialised context (file position is modified).
* @param[in] entry Entry to extract; must not be NULL.
* @param[in] output_fd Open, writable, blocking file descriptor (>= 0).
*
* @return PKG_OK on success.
* @retval PKG_ERR_PARAM A required argument is NULL or output_fd < 0.
* @retval PKG_ERR_ENCRYPTED Entry is encrypted; call refused.
* @retval PKG_ERR_RANGE entry->size > PKG_MAX_ENTRY_SIZE.
* @retval PKG_ERR_IO fseeko, fread, or write(2) failed, or the read
* returned fewer bytes than entry->size (truncated).
*
* @pre (entry->flags1 & PKG_ENTRY_FLAG_ENCRYPTED) == 0
* @pre output_fd is open for writing in blocking mode.
*
* @note Thread-safety: NOT thread-safe.
* @note WCET: Proportional to entry->size; dominated by disk I/O.
*/
int pkg_extract_file_fd(pkg_context_t *ctx, const pkg_entry_t *entry,
int output_fd);
#ifdef __cplusplus
}
#endif
#endif /* PKG_UNPACKER_H */
+86
View File
@@ -0,0 +1,86 @@
/*
* MIT License — Copyright (c) 2026 SeregonWar
* See LICENSE for full text.
*/
/**
* @file zip_writer.h
* @brief Streaming ZIP writer for bulk downloads
*
* The web UI lets the user pick many files (and whole folders) and download
* them as one archive. Building the archive on disk first would need as much
* free space as the selection and would double the I/O, so the entries are
* produced while the HTTP response streams:
*
* local header → file bytes → … → central directory → EOCD
*
* Entries are STORED (method 0): console payloads and media files are already
* compressed, and skipping deflate keeps the transfer CPU-free. The writer
* emits ZIP64 records when an entry, the archive offset or the entry count
* crosses the 32-bit limits, so multi-gigabyte selections stay valid.
*/
#ifndef ZIP_WRITER_H
#define ZIP_WRITER_H
#include <stddef.h>
#include <stdint.h>
#include <sys/types.h>
/** Upper bound on collected entries; further files are skipped. */
#define ZIP_WRITER_MAX_ENTRIES 4096
typedef struct zip_writer zip_writer_t;
/**
* @brief Collect @p paths (files or directories, recursed) into a writer.
*
* @param[in] paths Absolute filesystem paths.
* @param[in] count Number of paths.
* @return Writer on success, NULL on allocation failure.
*
* @note The archive names are relative to each requested path's parent, so
* zipping /data/games yields "games/…" entries.
*/
zip_writer_t *zip_writer_create(const char **paths, size_t count);
/** Number of files/directories collected (the EOCD count). */
size_t zip_writer_entry_count(const zip_writer_t *zip);
/**
* @brief Stream SELF containers decrypted (MAP_SELF) instead of raw.
*
* Must be set before the entries are collected: a decrypted container has a
* different size, and the archive states every size before streaming.
* Ignored on platforms without the PS4/PS5 pager.
*/
void zip_writer_set_decrypt(zip_writer_t *zip, int decrypt);
/** Same as zip_writer_create(), with the decryption flag set before the walk. */
zip_writer_t *zip_writer_create_decrypt(const char **paths, size_t count,
int decrypt);
/**
* @brief Exact size of the archive that zip_writer_read() will produce.
*
* Lets the HTTP layer answer with Content-Length instead of chunked encoding:
* console browsers and download managers show progress and complete
* reliably only when the length is known. Computed with the same header
* builders that emit the archive, so the two always agree.
*/
uint64_t zip_writer_total_size(zip_writer_t *zip);
/** 1 when the selection exceeded ZIP_WRITER_MAX_ENTRIES and was cut short. */
int zip_writer_truncated(const zip_writer_t *zip);
/**
* @brief Producer callback for the HTTP streaming response.
*
* @return Bytes written to @p buf, 0 at end of archive, -1 on error.
*/
ssize_t zip_writer_read(zip_writer_t *zip, void *buf, size_t len);
/** Close the current file and free the writer. Safe on a NULL pointer. */
void zip_writer_destroy(zip_writer_t *zip);
#endif /* ZIP_WRITER_H */
File renamed without changes.
@@ -271,6 +271,36 @@ ftp_error_t cmd_RNTO(ftp_session_t *session, const char *args);
* DATA CONNECTION
*===========================================================================*/
/**
* @brief CPFR command - Copy from
*
* @param session Client session
* @param args Source path
*
* @return FTP_OK on success, negative error code on failure
*/
ftp_error_t cmd_CPFR(ftp_session_t *session, const char *args);
/**
* @brief CPTO command - Copy to
*
* @param session Client session
* @param args Destination path
*
* @return FTP_OK on success, negative error code on failure
*/
ftp_error_t cmd_CPTO(ftp_session_t *session, const char *args);
/**
* @brief COPY command - Copy file
*
* @param session Client session
* @param args Source and Destination path
*
* @return FTP_OK on success, negative error code on failure
*/
ftp_error_t cmd_COPY(ftp_session_t *session, const char *args);
/**
* @brief PORT command - Active mode data connection
*
@@ -319,7 +349,7 @@ ftp_error_t cmd_OPTS(ftp_session_t *session, const char *args);
* @param session Client session
* @param args Site command (e.g. "CHMOD 755 file")
*
* @return FTP_OK (accepts CHMOD as no-op)
* @return FTP_OK after sending a reply (SITE CHMOD applies chmod(2))
*/
ftp_error_t cmd_SITE(ftp_session_t *session, const char *args);
@@ -43,7 +43,7 @@ SOFTWARE.
*===========================================================================*/
#ifndef RELEASE_VERSION
#define RELEASE_VERSION "1.3.0"
#define RELEASE_VERSION "1.6.0"
#endif
/*===========================================================================*
@@ -54,20 +54,20 @@ SOFTWARE.
* Default FTP server port
* @note Well-known FTP port is 21, but requires root on POSIX systems
* @note Using 2121 as default for unprivileged operation on POSIX systems
* @note Using 2122 as default on PS4/PS5 because 2121 may be occupied
* @note Using 2120 as default on PS4/PS5 because 2121 may be occupied
*/
#ifndef FTP_DEFAULT_PORT
#if defined(PS4) || defined(PS5)
#define FTP_DEFAULT_PORT 2122U
#if defined(PS4) || defined(PS5) || defined(PLATFORM_PS4) || defined(PLATFORM_PS5)
#define FTP_DEFAULT_PORT 2120U
#else
#define FTP_DEFAULT_PORT 2121U
#endif
#endif
#ifndef FTP_STREAM_BUFFER_SIZE
#if defined(PS5)
#if defined(PS5) || defined(PLATFORM_PS5)
#define FTP_STREAM_BUFFER_SIZE 1048576U /* 1 MB — PS5 has plenty of RAM */
#elif defined(PS4)
#elif defined(PS4) || defined(PLATFORM_PS4)
#define FTP_STREAM_BUFFER_SIZE 262144U /* 256 KB */
#else
#define FTP_STREAM_BUFFER_SIZE 524288U /* 512 KB — saturates GbE links */
@@ -92,7 +92,7 @@ SOFTWARE.
* @note Client disconnected after this period of inactivity
*/
#ifndef FTP_SESSION_TIMEOUT
#if defined(PS5) || defined(PS4)
#if defined(PS5) || defined(PS4) || defined(PLATFORM_PS5) || defined(PLATFORM_PS4)
#define FTP_SESSION_TIMEOUT 7200U
#else
#define FTP_SESSION_TIMEOUT 300U
@@ -147,9 +147,9 @@ SOFTWARE.
* @note Larger buffers provide diminishing returns
*/
#ifndef FTP_BUFFER_SIZE
#if defined(PS5)
#if defined(PS5) || defined(PLATFORM_PS5)
#define FTP_BUFFER_SIZE 1048576U /* 1 MB */
#elif defined(PS4)
#elif defined(PS4) || defined(PLATFORM_PS4)
#define FTP_BUFFER_SIZE 262144U /* 256 KB */
#else
#define FTP_BUFFER_SIZE 524288U /* 512 KB */
@@ -180,34 +180,10 @@ SOFTWARE.
#define FTP_LIST_LINE_SIZE 512U
#endif
/*===========================================================================*
* PATH LIMITS (Platform-dependent)
*===========================================================================*/
#if defined(PS4) || defined(PS5)
/**
* PlayStation maximum path length
* @note PS4/PS5 use custom BSD with 1024-byte limit
*/
/* Filesystem path capacity is platform-owned; keep the FTP name as an ABI alias. */
#include "pal_limits.h"
#ifndef FTP_PATH_MAX
#define FTP_PATH_MAX 1024U
#endif
#elif defined(PS3)
/**
* PlayStation 3 maximum path length
* @note PS3 has more limited path support
*/
#ifndef FTP_PATH_MAX
#define FTP_PATH_MAX 512U
#endif
#else
/**
* POSIX maximum path length
* @note Linux typically supports 4096 bytes
*/
#ifndef FTP_PATH_MAX
#define FTP_PATH_MAX 4096U
#endif
#define FTP_PATH_MAX PAL_PATH_MAX
#endif
/**
@@ -330,7 +306,7 @@ SOFTWARE.
#endif
/**
* TCP send buffer size in bytes
* TCP send buffer size for control socket (bytes)
*
* Sized to fill the bandwidth-delay product (BDP):
* GbE 1 ms RTT -> BDP = 125 KB -> 1 MB is generous
@@ -340,11 +316,62 @@ SOFTWARE.
#define FTP_TCP_SNDBUF 1048576U
#endif
/**
* TCP send buffer size for DATA socket (bytes)
*
* On PS5/PS4, OrbisOS clamps SO_SNDBUF auto-tuning to a system
* maximum that is lower than what GbE LAN transfers need.
* Setting this explicitly forces 4 MB, covering the BDP for
* a 1 GbE link at LAN RTTs (0.1–1 ms) with headroom.
*
* Set to 0 on other platforms to leave kernel auto-tuning active.
*/
#ifndef FTP_TCP_DATA_SNDBUF
#if defined(PS5) || defined(PLATFORM_PS5) || defined(PS4) || defined(PLATFORM_PS4)
#define FTP_TCP_DATA_SNDBUF (4U * 1024U * 1024U) /* 4 MB — supera il clamping OrbisOS */
#else
#define FTP_TCP_DATA_SNDBUF 0U /* 0 = lascia auto-tuning attivo */
#endif
#endif
/**
* Maximum retries when sendfile() returns 0 bytes (TCP back-pressure)
*
* Each retry sleeps FTP_SENDFILE_EAGAIN_SLEEP_US microseconds.
* Total maximum wait = retries × sleep = 256 × 1000 µs = 256 ms.
* This covers internet RTTs up to ~256 ms before declaring a driver stall.
*/
#ifndef FTP_SENDFILE_EAGAIN_RETRIES
#define FTP_SENDFILE_EAGAIN_RETRIES 256
#endif
/** Sleep between sendfile EAGAIN retries (microseconds) */
#ifndef FTP_SENDFILE_EAGAIN_SLEEP_US
#define FTP_SENDFILE_EAGAIN_SLEEP_US 500U /* 0.5 ms — PS5 LAN RTT ~0.1-0.3ms, 1ms era eccessivo */
#endif
/** FTP RETR sendfile chunk size. */
#ifndef FTP_RETR_SENDFILE_CHUNK
#define FTP_RETR_SENDFILE_CHUNK (2U * 1024U * 1024U)
#endif
/**
* TCP receive buffer size in bytes
*
* PS4/PS5: PFS-encrypted writes stall the recv thread while the
* double-buffer writer drains each chunk through PFS crypto.
* At typical LAN speeds, 1 MB of recv buffer fills in ~10ms
* → TCP zero window → sender stalls → FileZilla 20s timeout.
* 4 MB gives enough runway to cover the write latency.
*/
#ifndef FTP_TCP_RCVBUF
#define FTP_TCP_RCVBUF 1048576U
#if defined(PS5) || defined(PLATFORM_PS5)
#define FTP_TCP_RCVBUF 4194304U /* 4 MB — absorb PFS write latency */
#elif defined(PS4) || defined(PLATFORM_PS4)
#define FTP_TCP_RCVBUF 4194304U /* 4 MB — same reason: PFS crypto stalls recv */
#else
#define FTP_TCP_RCVBUF 1048576U /* 1 MB — generic default */
#endif
#endif
/**
File renamed without changes.
+89
View File
@@ -0,0 +1,89 @@
/*
MIT License
Copyright (c) 2026 Seregon
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
/**
* @file ftp_instance.h
* @brief Process-lifetime daemon identity for rest-mode resilience
*
* Clients compare daemon_instance_id across reconnect cycles to distinguish
* "payload survived rest mode" from "payload was re-injected".
*/
#ifndef FTP_INSTANCE_H
#define FTP_INSTANCE_H
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Ensure instance identity is generated (idempotent, thread-safe).
*/
void ftp_instance_init(void);
/**
* @brief Random uint64 stable for the lifetime of this process.
* @return Non-zero instance id.
*/
uint64_t ftp_daemon_instance_id(void);
/**
* @brief CLOCK_MONOTONIC nanoseconds captured at first identity generation.
*/
uint64_t ftp_daemon_start_monotonic_ns(void);
/**
* @brief Format the control identity line ("<pid> <token hex>\n").
* @return 0 on success, -1 when the buffer cannot hold the line.
*/
int ftp_instance_identity_format(char *out, size_t capacity, int pid,
uint64_t token);
/**
* @brief Parse a control identity line written by the formatter.
* @return 0 when the line is complete and the pid is usable, -1 otherwise.
*/
int ftp_instance_identity_parse(const char *text, int *pid, uint64_t *token);
/**
* @brief Format the cooperative shutdown request ("STOP <token hex>\n").
* @return 0 on success, -1 when the buffer cannot hold the command.
*/
int ftp_instance_stop_command_format(char *out, size_t capacity,
uint64_t token);
/**
* @brief Parse a cooperative shutdown request.
* @return 0 when the line is exactly the command, -1 otherwise.
*/
int ftp_instance_stop_command_parse(const char *line, uint64_t *token);
#ifdef __cplusplus
}
#endif
#endif /* FTP_INSTANCE_H */
File renamed without changes.
@@ -214,4 +214,21 @@ ftp_error_t ftp_path_join(const char *base,
char *output,
size_t size);
/*===========================================================================*
* LIST FLAG DISAMBIGUATION
*===========================================================================*/
/*
* Returns 1 if args start with a LIST flag (-a/-l/-la/-al) that
* should be stripped, 0 otherwise. Single-token args (e.g. "LIST -a")
* are stat()'d relative to CWD to disambiguate flag from real path.
*/
int ftp_path_has_list_flag(const char *args, const char *cwd,
char *scratch, size_t scratch_size);
/*
* Strips the leading flag token; returns remainder or CWD.
*/
const char *ftp_path_skip_list_flag(const char *args, const char *cwd);
#endif /* FTP_PATH_H */
File renamed without changes.
@@ -164,4 +164,36 @@ void ftp_server_get_stats(const ftp_server_context_t *ctx,
uint64_t *bytes_sent,
uint64_t *bytes_received);
/*===========================================================================*
* SESSION POOL MANAGEMENT (called by session threads)
*===========================================================================*/
/**
* @brief Release a session slot back to the server pool.
*
* MUST be called by the session thread as the very last action before it
* returns, AFTER ftp_session_cleanup() has closed all file descriptors.
*
* WHY THIS EXISTS:
* ftp_server_stop() blocks on (active_sessions > 0). Without this call
* active_sessions is never decremented, causing an infinite loop that
* prevents orderly PS5 shutdown and leads to SIGKILL → forced M2 unmount
* → kernel panic.
*
* @param ctx Owning server context (session->server_ctx)
* @param session Session to release
*
* @pre ctx != NULL
* @pre session != NULL
* @pre Called only from the session's own thread
* @pre ftp_session_cleanup() already called (all FDs closed)
*
* @post session->state == FTP_STATE_INIT (slot is reusable)
* @post ctx->active_sessions decremented by 1
*
* @note Thread-safety: Protected internally by ctx->session_lock
*/
void ftp_server_release_session(ftp_server_context_t *ctx,
ftp_session_t *session);
#endif /* FTP_SERVER_H */
File renamed without changes.
@@ -77,6 +77,8 @@ typedef enum {
FTP_ERR_AUTH_FAILED = -21, /**< Authentication failed */
FTP_ERR_PROTOCOL = -22, /**< Protocol violation */
FTP_ERR_DIR_EXISTS = -23, /**< Directory already exists */
FTP_ERR_CROSS_DEVICE = -24, /**< Cross-device link (EXDEV) */
FTP_ERR_CANCELLED = -25, /**< Operation cancelled by caller */
FTP_ERR_UNKNOWN = -99, /**< Unknown error */
} ftp_error_t;
@@ -204,6 +206,21 @@ typedef struct {
atomic_uint_fast32_t errors; /**< Error count */
} ftp_session_stats_t;
/*===========================================================================*
* FORWARD DECLARATIONS
*===========================================================================*/
/**
* Forward declaration of server context.
*
* WHY: ftp_session_t needs a back-pointer to its owning ftp_server_context_t
* so the session thread can call ftp_server_release_session() at exit.
* ftp_server_context_t is defined later in this file (it embeds
* ftp_session_t[]), so a forward declaration is required to break the
* circular dependency.
*/
struct ftp_server_context;
/*===========================================================================*
* SESSION STRUCTURE
*===========================================================================*/
@@ -242,6 +259,17 @@ typedef struct ftp_session {
char cwd[FTP_PATH_MAX]; /**< Current working directory */
char rename_from[FTP_PATH_MAX]; /**< RNFR source path */
/* Async Copy State */
char copy_from[FTP_PATH_MAX]; /**< CPFR source path */
pthread_t copy_thread; /**< Background copy thread */
pthread_mutex_t copy_mutex; /**< Mutex for copy thread state */
atomic_int copy_in_progress; /**< Flag indicating active background copy */
uint8_t
copy_thread_valid; /**< Flag indicating if thread handle is joinable */
uint8_t copy_is_move; /**< Flag indicating if copy should delete source (RNTO
fallback) */
uint8_t _padding_copy[2]; /**< Alignment padding */
/* Authentication */
uint8_t auth_attempts; /**< Failed auth attempts */
uint8_t authenticated; /**< Login status (0/1) */
@@ -275,6 +303,21 @@ typedef struct ftp_session {
/* Statistics */
ftp_session_stats_t stats;
/*
* Back-pointer to the owning server context.
*
* WHY: The session thread must decrement active_sessions and reset the
* slot state when it exits (Bug #1 fix). Without this pointer the thread
* cannot reach the server context to call ftp_server_release_session().
*
* Set by server_accept_thread immediately after ftp_session_init().
* Never NULL for a session that is running in its own thread.
*
* @note Thread-safety: written once before thread creation, read-only
* thereafter — no synchronisation required.
*/
struct ftp_server_context *server_ctx; /**< Owning server context */
} ftp_session_t;
/*===========================================================================*
@@ -323,7 +366,7 @@ typedef struct {
* @note Single instance, initialized at startup
* @note Thread-safe: atomic operations for shared state
*/
typedef struct {
typedef struct ftp_server_context {
/* Server socket */
int listen_fd; /**< Listening socket */
struct sockaddr_in listen_addr; /**< Server bind address */
+76
View File
@@ -0,0 +1,76 @@
/*
MIT License
Copyright (c) 2026 Seregon
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
/**
* @file http_api.h
* @brief REST API handlers
*/
#ifndef HTTP_API_H
#define HTTP_API_H
#include "ftp_types.h"
#include "http_parser.h"
#include "http_response.h"
http_response_t *http_api_handle(const http_request_t *request);
/**
* @brief Attach the FTP server context to the HTTP API layer.
*
* Required for the POST /api/network/reset endpoint (Fix #4).
* Must be called after ftp_server_init() and before http_server_create().
*
* @param ctx Initialized FTP server context, or NULL to detach.
*/
void http_api_set_server_ctx(ftp_server_context_t *ctx);
/**
* @brief Set the root directory for HTTP API path confinement
*
* All HTTP file operations (list, download, upload, stats) will be
* restricted to paths within this root. Must be called before
* http_server_create().
*
* FTP path confinement: ftp_path_resolve() -> ftp_path_is_within_root()
* HTTP path confinement: validate_path() -> http_is_within_root() [NEW]
*/
void http_api_set_root(const char *root);
/**
* @brief Get the configured HTTP root directory
* @return Pointer to static root buffer (empty string if unset)
*/
const char *http_api_get_root(void);
/**
* @brief Recursively compute the total size of all regular files under a directory.
*
* @param path Absolute path to the directory.
* @param depth Initial recursion depth (pass 0).
* @return Total bytes of all regular files, or 0 on error / empty dir.
*/
uint64_t http_dir_size_recursive(const char *path, int depth);
#endif /* HTTP_API_H */
@@ -45,21 +45,36 @@ SOFTWARE.
#define HTTP_DEBUG_LOG_HEADERS 0
#endif
/*---------------------------------------------------------------------------*
* Web root directory — no longer used.
*
* All static files are now embedded directly in the binary via
* http_resources.c (generated by tools/generate_resources.py).
* The filesystem path HTTP_WEB_ROOT is kept as a no-op for backward
* compatibility with any external code that may reference it.
*---------------------------------------------------------------------------*/
/* Server configuration */
#define HTTP_DEFAULT_PORT 8888
#define HTTP_MAX_CONNECTIONS 100
#define HTTP_REQUEST_TIMEOUT 30
#define HTTP_KEEPALIVE_TIMEOUT 60
/* Buffer sizes */
#define HTTP_REQUEST_BUFFER_SIZE 8192
/* Buffer sizes.
*
* The request buffer also holds JSON bodies (bulk ZIP selections carry dozens
* of paths), so it is larger than the URI limit below. */
#define HTTP_REQUEST_BUFFER_SIZE 16384
#define HTTP_RESPONSE_BUFFER_SIZE 8192
#define HTTP_URI_MAX_LENGTH 2048
#define HTTP_HEADER_MAX_COUNT 32
#define HTTP_HEADER_LINE_MAX 1024
/* File transfer */
#define HTTP_SENDFILE_CHUNK_SIZE 65536
/* Keep HTTP sendfile chunks aligned with the FTP zero-copy path. */
#define HTTP_SENDFILE_CHUNK_SIZE (2U * 1024U * 1024U)
#ifndef HTTP_SENDFILE_EAGAIN_RETRIES
#define HTTP_SENDFILE_EAGAIN_RETRIES 16U
#endif
#ifndef HTTP_SENDFILE_EAGAIN_SLEEP_US
#define HTTP_SENDFILE_EAGAIN_SLEEP_US 1000U
#endif
/* Thread stack size (bytes) */
#ifndef HTTP_THREAD_STACK_SIZE
@@ -69,11 +84,27 @@ SOFTWARE.
/* CSRF token length in hex characters (32 hex = 16 random bytes) */
#define HTTP_CSRF_TOKEN_LENGTH 32
/*---------------------------------------------------------------------------*
* Upload feature toggle (disabled by default for security)
*---------------------------------------------------------------------------*/
/* Gates mutating web filesystem operations and CSRF protection. */
#ifndef ENABLE_WEB_UPLOAD
#define ENABLE_WEB_UPLOAD 0
#define ENABLE_WEB_UPLOAD 1
#endif
/* Larger upload reads reduce event-loop wakeups; SO_RCVBUF is only a hint. */
#ifndef HTTP_UPLOAD_CHUNK_SIZE
#define HTTP_UPLOAD_CHUNK_SIZE (512U * 1024U) /* 512 KB per active upload */
#endif
#ifndef HTTP_UPLOAD_RCVBUF_SIZE
#define HTTP_UPLOAD_RCVBUF_SIZE (2U * 1024U * 1024U) /* 2 MB SO_RCVBUF hint */
#endif
/* OrbisOS needs an explicit send buffer to keep GbE downloads saturated. */
#ifndef HTTP_SNDBUF_SIZE
#if defined(PS5) || defined(PLATFORM_PS5) || defined(PS4) || defined(PLATFORM_PS4)
#define HTTP_SNDBUF_SIZE (4U * 1024U * 1024U) /* 4 MB — bypass OrbisOS clamp */
#else
#define HTTP_SNDBUF_SIZE 0U /* 0 = leave kernel auto-tuning active */
#endif
#endif
#endif /* HTTP_CONFIG_H */
@@ -35,8 +35,10 @@ SOFTWARE.
/**
* @brief Initialize CSRF protection (generate random token)
*
* @return 0 on success, -1 if entropy unavailable (uploads disabled)
*/
void http_csrf_init(void);
int http_csrf_init(void);
/**
* @brief Get the current CSRF token
+16
View File
@@ -0,0 +1,16 @@
#ifndef ZFTPD_HTTP_JSON_H
#define ZFTPD_HTTP_JSON_H
#include <stddef.h>
#include <stdint.h>
int http_buf_append_bytes(char *buf, size_t cap, size_t *pos,
const char *data, size_t len);
int http_buf_append_cstr(char *buf, size_t cap, size_t *pos, const char *str);
int http_buf_append_u64(char *buf, size_t cap, size_t *pos, uint64_t value);
int http_buf_append_u32(char *buf, size_t cap, size_t *pos, uint32_t value);
int http_buf_append_i32(char *buf, size_t cap, size_t *pos, int32_t value);
int http_json_escape_append(char *buf, size_t cap, size_t *pos,
const char *str);
#endif
@@ -56,7 +56,15 @@ typedef struct {
size_t body_length;
} http_request_t;
typedef struct {
http_method_t method;
char uri[HTTP_URI_MAX_LENGTH];
size_t header_length;
size_t content_length;
} http_request_head_t;
int http_parse_request(char *buffer, size_t length, http_request_t *request);
int http_peek_request_head(const char *buffer, size_t length, http_request_head_t *head);
const char* http_get_header(const http_request_t *request, const char *name);
#endif /* HTTP_PARSER_H */
+44
View File
@@ -0,0 +1,44 @@
/*
MIT License
Copyright (c) 2026 Seregon
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#ifndef HTTP_RESOURCES_H
#define HTTP_RESOURCES_H
#include <stddef.h>
/**
* Pre-compressed HTTP resource descriptor.
* @field is_gzip 1 => send Content-Encoding: gzip before Content-Type
*/
typedef struct {
const char *path;
const unsigned char *data;
size_t size;
size_t original_size;
const char *content_type;
int is_gzip;
} http_resource_t;
int http_resource_get(const char *path, const http_resource_t **out);
#endif /* HTTP_RESOURCES_H */
@@ -26,9 +26,9 @@ SOFTWARE.
* @brief HTTP response builder
*
* STATUS CODES:
* 2xx Success (200, 201, 204)
* 2xx Success (200, 201, 204, 206)
* 3xx Redirection (301, 304)
* 4xx Client error (400, 403, 404, 405)
* 4xx Client error (400, 403, 404, 405, 409, 415, 416)
* 5xx Server error (500)
*/
@@ -36,6 +36,7 @@ SOFTWARE.
#define HTTP_RESPONSE_H
#include "http_config.h"
#include "pal_limits.h"
#include <stddef.h>
#include <sys/types.h>
@@ -46,6 +47,7 @@ SOFTWARE.
typedef enum {
/* 2xx ── Success */
HTTP_STATUS_200_OK = 200,
HTTP_STATUS_206_PARTIAL_CONTENT = 206,
HTTP_STATUS_201_CREATED = 201,
HTTP_STATUS_204_NO_CONTENT = 204,
@@ -58,6 +60,9 @@ typedef enum {
HTTP_STATUS_403_FORBIDDEN = 403,
HTTP_STATUS_404_NOT_FOUND = 404,
HTTP_STATUS_405_METHOD_NOT_ALLOWED = 405,
HTTP_STATUS_409_CONFLICT = 409,
HTTP_STATUS_415_UNSUPPORTED_MEDIA_TYPE = 415,
HTTP_STATUS_416_RANGE_NOT_SATISFIABLE = 416,
/* 5xx ── Server Error */
HTTP_STATUS_500_INTERNAL_ERROR = 500,
@@ -75,6 +80,7 @@ typedef struct {
const void *mem_body; /**< Pointer to body bytes (NULL = not used) */
size_t mem_length; /**< Total bytes to send */
size_t mem_sent; /**< Bytes already sent */
int mem_body_owned; /**< 1 = malloc'd, destroy() must free it */
/* In-memory segmented body streaming (for injected HTML) */
const void *mem_segs[3]; /**< Segment pointers */
@@ -89,9 +95,29 @@ typedef struct {
off_t sendfile_offset; /**< Current offset in file */
size_t sendfile_count; /**< Remaining bytes to send */
/* Chunked directory streaming (for /api/list) */
void *stream_dir; /**< DIR* — NULL = not streaming */
char stream_path[1024]; /**< Base path for stat() calls */
char stream_path[PAL_PATH_MAX]; /**< Canonical directory path */
/*
* Producer-driven chunked body, used when the bytes are not simply the file
* content (SELF decryption through the VFS). stream_read returns 0 at EOF,
* a negative value on error; stream_close releases the context and is called
* by http_response_destroy() even when the transfer was cut short.
*/
void *stream_ctx;
ssize_t (*stream_read)(void *ctx, void *buf, size_t len);
/* Called once with 0 after the entire body was sent, or -1 if the
* connection/producer failed or the response was discarded. */
void (*stream_result)(void *ctx, int result);
void (*stream_close)(void *ctx);
/**
* 1 = frame the produced bytes with chunked encoding (no known length),
* 0 = send them raw, which requires a Content-Length header. Console
* browsers download reliably only in the second case.
*/
int stream_chunked;
} http_response_t;
/*===========================================================================*
@@ -103,6 +129,8 @@ int http_response_add_header(http_response_t *resp, const char *name,
const char *value);
int http_response_set_body(http_response_t *resp, const void *body,
size_t length);
int http_response_set_body_owned(http_response_t *resp, void *body,
size_t length);
int http_response_set_body_ref(http_response_t *resp, const void *body,
size_t length);
int http_response_set_body_splice(http_response_t *resp, const void *prefix,
@@ -26,7 +26,7 @@ SOFTWARE.
* @brief HTTP server public API
*
* Usage:
* http_server_t *http = http_server_create(loop, 8888);
* http_server_t *http = http_server_create(loop, "[::]:8888", "/");
* // ... event loop runs ...
* http_server_destroy(http);
*/
@@ -41,10 +41,16 @@ SOFTWARE.
typedef struct http_server http_server_t;
/**
* @brief Create and start HTTP server on given port
* @brief Create HTTP server
*
* @param loop Event loop handle
* @param bind_addr Bind address with port (e.g., "[::1]:8888" or "0.0.0.0:8888")
* @param root_path Filesystem root for confinement
*
* @return Server handle, or NULL on failure
*/
http_server_t *http_server_create(event_loop_t *loop, uint16_t port);
http_server_t *http_server_create(event_loop_t *loop, const char *bind_addr,
const char *root_path);
/**
* @brief Stop and destroy HTTP server
+17
View File
@@ -0,0 +1,17 @@
#ifndef ZFTPD_INSTANCE_CONTROL_H
#define ZFTPD_INSTANCE_CONTROL_H
/* Replace the previous console payload: ask it to shut down cooperatively,
* verify that it released the console, and only then let this instance start.
* Returns -1 when a verified previous instance is still alive; the caller must
* not bind fallback service ports in that case. */
int instance_control_replace_previous(void);
/* Control listener and identity file for this payload. */
int instance_control_start(void);
void instance_control_stop(void);
/* Ask the payload's main thread to run its normal shutdown sequence. */
void zftpd_shutdown_request(void);
#endif
File renamed without changes.
File renamed without changes.
+49
View File
@@ -0,0 +1,49 @@
/*
* MIT License — Copyright (c) 2026 SeregonWar
* See LICENSE for full text.
*/
/**
* @file pal_disc.h
* @brief Platform Abstraction Layer — optical drive control
*
* Ported from the author's BD-EJ payload: the Blu-ray drive is a standard
* FreeBSD cd(4) device, so unloading the tray is open("/dev/cd0") plus the
* CDIOCALLOW / CDIOCEJECT ioctls handled by SceBdSvc. Exposing it here lets
* the console be controlled from the web UI like the other system actions.
*
* PLATFORMS: PS4/PS5. Every other target reports "not available" so callers
* never need platform conditionals.
*/
#ifndef PAL_DISC_H
#define PAL_DISC_H
/**
* @brief Eject the Blu-ray disc.
*
* @return 0 on success, a negative errno otherwise (-ENOSYS when the platform
* has no optical drive support).
*
* @note CDIOCALLOW is attempted first and its failure ignored: some drives
* keep the software ejection interlock engaged, some ignore the call.
*/
int pal_disc_eject(void);
/**
* @brief Whether the console exposes an optical drive.
*
* @return 1 when /dev/cd0 (or the fallback node) can be opened, 0 otherwise.
* Digital Edition consoles always answer 0.
*/
int pal_disc_present(void);
/**
* @brief Human readable description of a pal_disc_eject() result.
*
* @param[in] err 0 or the negative errno returned by pal_disc_eject().
* @return Static string, never NULL.
*/
const char *pal_disc_reason(int err);
#endif /* PAL_DISC_H */
+70
View File
@@ -0,0 +1,70 @@
/**
* @file pal_dns_filter.h
* @brief Userland DNS filter for the Sony CDN domains.
*
* The kernel-level net filter needs an executable kernel page, which some
* firmwares refuse to hand out. This listener achieves the same goal in
* userland: the console is pointed at this resolver, the Sony CDN names answer
* with 0.0.0.0 (a black hole) and everything else is forwarded upstream.
*
* The filter is inert until the console's DNS server is set to this host, and
* it is harmless when port 53 is already taken (the listener reports that and
* step aside).
*/
#ifndef PAL_DNS_FILTER_H
#define PAL_DNS_FILTER_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
/**
* @brief Shell-style wildcard match, case-insensitive.
*
* Supports `*` (any run, including empty) and `?` (exactly one character),
* which covers the mask shapes used by the Sony CDN lists.
*/
bool pal_dns_mask_match(const char *mask, const char *name);
/**
* @brief Resolve a queried name to the override address.
*
* @param[out] ip Receives the 4 bytes to answer with (0.0.0.0).
* @return true when the name is blackholed.
*/
bool pal_dns_override_for(const char *name, uint8_t ip[4]);
/**
* @brief Start the UDP/TCP DNS listeners on port 53.
*
* @return 0 when the listeners are running, -1 otherwise (logged).
*/
int pal_dns_filter_start(void);
/**
* @brief Stop the listeners and release the port.
*/
void pal_dns_filter_stop(void);
/** @return true while the listeners are bound. */
bool pal_dns_filter_running(void);
/** @return true when the user disabled the filter from the settings. */
bool pal_dns_filter_enabled(void);
/** @return true when another resolver is configured on this unit. */
bool pal_dns_filter_other_resolver_present(void);
/** @return Number of blackholed masks / forwarded exceptions. */
size_t pal_dns_filter_mask_count(void);
size_t pal_dns_filter_exception_count(void);
/**
* @brief Enable or disable the filter and remember the choice.
*
* @return 0 on success, -1 when the listeners could not be started.
*/
int pal_dns_filter_set_enabled(bool enabled);
#endif /* PAL_DNS_FILTER_H */
@@ -24,34 +24,26 @@ SOFTWARE.
/**
* @file pal_fileio.h
* @brief Platform-agnostic file I/O with zero-copy support
*
* @author SeregonWar
* @version 1.0.0
* @date 2026-02-13
*
* OPTIMIZATION: Zero-copy file transfer via sendfile() where supported
* FALLBACK: Buffered read/write for platforms without sendfile()
*
* @brief Platform-agnostic file I/O and zero-copy transfer primitives.
*/
#ifndef PAL_FILEIO_H
#define PAL_FILEIO_H
#include "ftp_types.h"
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/types.h>
/*===========================================================================*
* FILE PERMISSIONS
*===========================================================================*/
#ifndef FILE_PERM
#define FILE_PERM 0666 /**< Default file permissions (rw-rw-rw-) */
#define FILE_PERM 0666 /**< Default file permissions (rw-rw-rw-) */
#endif
#ifndef DIR_PERM
#define DIR_PERM 0777 /**< Default directory permissions (rwxrwxrwx) */
#define DIR_PERM 0777 /**< Default directory permissions (rwxrwxrwx) */
#endif
/*===========================================================================*
@@ -59,54 +51,28 @@ SOFTWARE.
*===========================================================================*/
#if defined(__linux__)
#define HAS_SENDFILE 1
#include <sys/sendfile.h>
#define HAS_SENDFILE 1
#include <sys/sendfile.h>
#elif defined(__APPLE__) && defined(__MACH__)
#define HAS_SENDFILE 1
#include <sys/socket.h>
#elif defined(__FreeBSD__) || defined(PLATFORM_PS4) || defined(PLATFORM_PS5)
#define HAS_SENDFILE 1
#include <sys/uio.h>
#define HAS_SENDFILE 1
#include <sys/uio.h>
#else
#define HAS_SENDFILE 0
#define HAS_SENDFILE 0
#endif
/*===========================================================================*
* ZERO-COPY FILE TRANSFER
*===========================================================================*/
/**
* @brief Send file data via socket (zero-copy where supported)
*
* OPTIMIZATION: Uses sendfile() syscall when available for zero-copy
* kernel-to-socket transfer without userspace buffering.
*
* PERFORMANCE:
* - Zero-copy: ~950 MB/s on PS4, ~118 MB/s on PS5 (network-limited)
* - Buffered: ~300-400 MB/s (userspace copy overhead)
*
* @param sock_fd Socket file descriptor (destination)
* @param file_fd File descriptor (source)
* @param offset Starting offset in file (updated on return)
* @param count Number of bytes to send
*
* @return Number of bytes sent on success, negative error code on failure
* @retval >0 Number of bytes successfully sent
* @retval 0 End of file reached
* @retval -1 I/O error (check errno)
*
* @pre sock_fd >= 0 (valid socket)
* @pre file_fd >= 0 (valid file)
* @pre offset != NULL
* @pre *offset >= 0
* @pre count > 0
*
* @post *offset updated by number of bytes sent
*
* @note Thread-safety: Safe if file descriptors not shared
* @note WCET: Unbounded (depends on network/disk I/O)
*
* @warning Non-blocking sockets may return partial writes (EAGAIN)
* @warning Caller must handle partial transfers in loop
* Send up to count bytes starting at *offset and advance the offset by bytes sent.
* Callers must handle partial transfers and EAGAIN.
* On PS4/PS5, filesystems with incompatible vnode pagers (or a failed mount
* lookup) use positional reads and socket sends before attempting sendfile.
*/
ssize_t pal_sendfile(int sock_fd, int file_fd, off_t *offset, size_t count);
ssize_t pal_sendfile_retry(int sock_fd, int file_fd, off_t *offset, size_t count,
unsigned retries, unsigned sleep_us);
int pal_file_error_is_fatal(int error);
/*===========================================================================*
* FILE OPERATIONS
@@ -114,18 +80,18 @@ ssize_t pal_sendfile(int sock_fd, int file_fd, off_t *offset, size_t count);
/**
* @brief Safe file open with validation
*
*
* @param path File path (null-terminated)
* @param flags Open flags (O_RDONLY, O_WRONLY, O_RDWR, etc.)
* @param mode Permission mode (for O_CREAT)
*
*
* @return File descriptor on success, negative error code on failure
* @retval >=0 Valid file descriptor
* @retval <0 Error code
*
*
* @pre path != NULL
* @pre strlen(path) < FTP_PATH_MAX
*
*
* @note Thread-safety: Safe (kernel-level synchronization)
* @note Caller must close file descriptor when done
*/
@@ -133,44 +99,54 @@ int pal_file_open(const char *path, int flags, mode_t mode);
/**
* @brief Safe file close
*
*
* @param fd File descriptor
*
*
* @return FTP_OK on success, negative error code on failure
*
*
* @pre fd >= 0
*
*
* @post File descriptor is invalid after successful close
*
*
* @note Thread-safety: Safe if fd not shared
*/
ftp_error_t pal_file_close(int fd);
/**
* @brief Get file status
*
*
* @param path File path
* @param st Output stat structure
*
*
* @return FTP_OK on success, negative error code on failure
*
*
* @pre path != NULL
* @pre st != NULL
*
*
* @post On success, st contains file metadata
*
*
* @note Thread-safety: Safe (kernel-level synchronization)
*/
ftp_error_t pal_file_stat(const char *path, struct stat *st);
/**
* @brief Change file or directory permission bits
*
* @param path Absolute filesystem path
* @param mode Permission bits (e.g. 0777), typically masked to 07777
*
* @return FTP_OK on success, negative error code on failure
*/
ftp_error_t pal_file_chmod(const char *path, mode_t mode);
/**
* @brief Get file status from descriptor
*
*
* @param fd File descriptor
* @param st Output stat structure
*
*
* @return FTP_OK on success, negative error code on failure
*
*
* @pre fd >= 0
* @pre st != NULL
*/
@@ -178,37 +154,37 @@ ftp_error_t pal_file_fstat(int fd, struct stat *st);
/**
* @brief Read file data
*
*
* @param fd File descriptor
* @param buffer Output buffer
* @param count Number of bytes to read
*
*
* @return Number of bytes read on success, negative on error
* @retval >0 Bytes read
* @retval 0 End of file
* @retval <0 Error code
*
*
* @pre fd >= 0
* @pre buffer != NULL
* @pre count > 0
*
*
* @note May return less than count bytes (not an error)
*/
ssize_t pal_file_read(int fd, void *buffer, size_t count);
/**
* @brief Write file data
*
*
* @param fd File descriptor
* @param buffer Input buffer
* @param count Number of bytes to write
*
*
* @return Number of bytes written on success, negative on error
*
*
* @pre fd >= 0
* @pre buffer != NULL
* @pre count > 0
*
*
* @note May return less than count bytes (disk full, etc.)
*/
ssize_t pal_file_write(int fd, const void *buffer, size_t count);
@@ -217,25 +193,25 @@ ssize_t pal_file_write_all(int fd, const void *buffer, size_t count);
/**
* @brief Seek to file position
*
*
* @param fd File descriptor
* @param offset Offset in bytes
* @param whence SEEK_SET, SEEK_CUR, or SEEK_END
*
*
* @return New file position on success, negative on error
*
*
* @pre fd >= 0
*/
off_t pal_file_seek(int fd, off_t offset, int whence);
/**
* @brief Truncate file to specified length
*
*
* @param fd File descriptor
* @param len New file length
*
*
* @return FTP_OK on success, negative error code on failure
*
*
* @pre fd >= 0
* @pre len >= 0
*/
@@ -243,86 +219,113 @@ ftp_error_t pal_file_truncate(int fd, off_t len);
/**
* @brief Delete file
*
*
* @param path File path
*
*
* @return FTP_OK on success, negative error code on failure
*
*
* @pre path != NULL
*/
ftp_error_t pal_file_delete(const char *path);
/**
* @brief Rename/move file
*
*
* @param old_path Source path
* @param new_path Destination path
*
*
* @return FTP_OK on success, negative error code on failure
*
*
* @pre old_path != NULL
* @pre new_path != NULL
*/
ftp_error_t pal_file_rename(const char *old_path, const char *new_path);
/**
* @brief Recursively copy file or directory
*
* @param src Source path
* @param dst Destination path
* @param keep_src If 0, delete source after successful copy (move)
*
* @return FTP_OK on success, negative error code on failure
*/
ftp_error_t pal_file_copy_recursive(const char *src, const char *dst,
int keep_src);
/* Progress callbacks receive cumulative bytes; a negative return cancels the copy. */
typedef int (*pal_copy_progress_cb_t)(uint64_t bytes_copied, void *user_data);
/**
* @brief Recursively copy with progress reporting
*
* Same as pal_file_copy_recursive() but invokes cb after each I/O chunk.
*/
ftp_error_t pal_file_copy_recursive_ex(const char *src, const char *dst,
int keep_src, pal_copy_progress_cb_t cb,
void *user_data, int *out_errno);
/*===========================================================================*
* DIRECTORY OPERATIONS
*===========================================================================*/
/**
* @brief Create directory
*
*
* @param path Directory path
* @param mode Permission mode
*
*
* @return FTP_OK on success, negative error code on failure
*
*
* @pre path != NULL
*/
ftp_error_t pal_dir_create(const char *path, mode_t mode);
ftp_error_t pal_dir_create_recursive(const char *path, mode_t mode);
/**
* @brief Remove directory
*
*
* @param path Directory path
*
*
* @return FTP_OK on success, negative error code on failure
*
*
* @pre path != NULL
*
*
* @note Directory must be empty
*/
ftp_error_t pal_dir_remove(const char *path);
ftp_error_t pal_dir_remove_recursive_pub(const char *path);
/**
* @brief Check if path exists
*
*
* @param path File or directory path
*
*
* @return 1 if exists, 0 if not, negative on error
*
*
* @pre path != NULL
*/
int pal_path_exists(const char *path);
/**
* @brief Check if path is a directory
*
*
* @param path Path to check
*
*
* @return 1 if directory, 0 if not, negative on error
*
*
* @pre path != NULL
*/
int pal_path_is_directory(const char *path);
/**
* @brief Check if path is a regular file
*
*
* @param path Path to check
*
*
* @return 1 if regular file, 0 if not, negative on error
*
*
* @pre path != NULL
*/
int pal_path_is_file(const char *path);
@@ -58,6 +58,9 @@ typedef struct vfs_node {
uint64_t phoff;
uint64_t file_size;
uint32_t magic;
void *mapped_segment; /**< Current decrypted ELF segment */
uint64_t mapped_offset; /**< ELF offset of mapped segment */
size_t mapped_length;
} psx;
#endif
} vfs_node_t;
@@ -68,6 +71,32 @@ void vfs_close(vfs_node_t *node);
void vfs_set_offset(vfs_node_t *node, uint64_t offset);
ssize_t vfs_read(vfs_node_t *node, void *buffer, size_t length);
#if defined(PLATFORM_PS4) || defined(PLATFORM_PS5)
/**
* @brief Open a SELF container through the kernel pager.
*
* The node then streams the DECRYPTED ELF payload (MAP_SELF) instead of the
* raw container bytes; vfs_read()/vfs_close() handle it like any other node.
*
* @return 1 when the file is a SELF and the node was set up, 0 when it is not
* a SELF, -1 on error.
*
* @note Only used when the caller explicitly asks for decrypted content: the
* default transfer path must move the on-disk bytes untouched.
*/
int psx_vfs_try_open_self(vfs_node_t *node, const char *path);
ssize_t psx_vfs_read(vfs_node_t *node, void *buffer, size_t length);
/**
* @brief Expose the resolved pager table for diagnostics.
*
* Zeroes when the platform has no MAP_SELF support; a zero table on a console
* means the decrypting read path cannot work.
*/
void psx_pager_debug(uintptr_t *table, uintptr_t *ops_vnode,
uintptr_t *ops_self);
#endif
static inline vfs_capability_t vfs_get_caps(const vfs_node_t *node)
{
return (node != NULL) ? node->caps : 0;
+21
View File
@@ -0,0 +1,21 @@
#ifndef ZFTPD_PAL_LIMITS_H
#define ZFTPD_PAL_LIMITS_H
#include <limits.h>
#ifndef PAL_PATH_MAX
#if defined(__APPLE__) && defined(__MACH__)
#define PAL_PATH_MAX 1024U
#elif defined(PS4) || defined(PS5) || defined(PLATFORM_PS4) || \
defined(PLATFORM_PS5)
#define PAL_PATH_MAX 1024U
#elif defined(PS3)
#define PAL_PATH_MAX 512U
#elif defined(PATH_MAX)
#define PAL_PATH_MAX PATH_MAX
#else
#define PAL_PATH_MAX 4096U
#endif
#endif
#endif
@@ -228,6 +228,13 @@ typedef int socket_t;
*/
ftp_error_t pal_network_init(void);
/**
* @brief Best-effort network stack re-init after Rest Mode.
*
* Calls fini + init. Safe no-op on platforms where networking is always up.
*/
ftp_error_t pal_network_reinit(void);
/**
* @brief Cleanup platform network subsystem
*
@@ -316,6 +323,21 @@ ftp_error_t pal_socket_set_blocking(socket_t fd);
*/
ftp_error_t pal_socket_set_reuseaddr(socket_t fd);
/**
* @brief Suppress SIGPIPE when a write hits a closed peer
*
* FreeBSD (PS4/PS5) and Darwin raise SIGPIPE when a socket write finds the
* connection gone, and the default disposition kills the process: a download
* aborted from the browser (or an FTP client that drops the data connection)
* would take the whole daemon down. Platforms without the socket option rely
* on the process-wide SIGPIPE ignore instead.
*
* @param fd Socket file descriptor
*
* @note Best-effort: a failure leaves the process-wide disposition in charge.
*/
void pal_socket_set_nosigpipe(socket_t fd);
/**
* @brief Cork data socket (hold TCP segments for coalescing)
*
@@ -396,4 +418,46 @@ uint16_t pal_sockaddr_get_port(const struct sockaddr_in *addr);
ftp_error_t pal_make_sockaddr(const char *ip, uint16_t port,
struct sockaddr_in *addr);
/**
* @brief Create sockaddr from address:port string with IPv6 bracket support
*
* Parses both IPv4 and IPv6 addresses:
* - "192.168.1.1:8888" → IPv4
* - "[::1]:8888" → IPv6 loopback
* - "[fe80::2e4:21ff:fef5:240d]:8888" → IPv6 link-local
*
* @param addr_str Address string with embedded port
* @param out_addr Output sockaddr_storage (union of IPv4/IPv6)
* @param out_len Output length (sizeof(sockaddr_in) or sizeof(sockaddr_in6))
*
* @return FTP_OK on success, FTP_ERR_INVALID_PARAM on parse error
*
* @note Thread-safety: Safe (no shared state)
*/
ftp_error_t pal_make_sockaddr_ex(const char *addr_str,
struct sockaddr_storage *out_addr,
socklen_t *out_len);
/*===========================================================================*
* NETWORK STACK RESET (Issues #3, #4, #7)
*===========================================================================*/
/**
* @brief Reset TCP buffer accounting for idle FTP sessions.
*
* Flushes kernel SO_SNDBUF / SO_RCVBUF accounting and closes orphaned data
* sockets. Addresses progressive throughput degradation after large transfers
* to the internal SSD or M.2 on PS4/PS5.
*
* @param sessions Pointer to the server session pool (ftp_server_context_t.sessions)
* @param count Pool size (FTP_MAX_SESSIONS)
*
* @return 0 on success, -1 if sessions/count invalid
*
* @note Thread-safety: Do NOT call while a session is mid-accept.
* Safe to call from the HTTP API handler thread.
* @note Does NOT interrupt active (TRANSFERRING) sessions.
*/
int pal_network_reset_ftp_stack(ftp_session_t *sessions, size_t count);
#endif /* PAL_NETWORK_H */
@@ -32,6 +32,21 @@ SOFTWARE.
int pal_notification_init(void);
void pal_notification_shutdown(void);
/**
* @brief Send a system notification with the default icon.
*
* On PS4/PS5 this shows a console toast. On other platforms it logs via syslog.
*/
void pal_notification_send(const char *message);
/**
* @brief Send a system notification with an optional icon name.
*
* @param message Notification text (required). Truncated to 1023 bytes.
* @param icon_name PS notification texture suffix (e.g. "icon_system").
* If NULL or empty, uses "icon_system". Ignored on non-PS.
*/
void pal_notification_send_ex(const char *message, const char *icon_name);
#endif /* PAL_NOTIFICATION_H */
+90
View File
@@ -0,0 +1,90 @@
/*
MIT License
Copyright (c) 2026 Seregon
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
/**
* @file pal_resilient_server.h
* @brief Rest-mode resilient listen/accept helpers
*
* After console Rest Mode the listen FD may become EBADF/ENOTSOCK, or look
* valid while the network stack is down (stale FD). These helpers recreate
* the listener with indefinite exponential backoff.
*/
#ifndef PAL_RESILIENT_SERVER_H
#define PAL_RESILIENT_SERVER_H
#include "pal_network.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief True if errno indicates the listen socket was invalidated.
*
* Typical after Rest Mode / kernel reclaim: EBADF, ENOTSOCK.
*/
int pal_errno_is_listener_lost(int err);
/**
* @brief Probe whether a listen FD is still a live accepting stream socket.
*
* @return 1 if alive, 0 if invalid / not accepting.
*/
int pal_listen_fd_alive(int fd);
/**
* @brief Backoff delay for recreate attempt N (0-based), capped at 10s.
*/
unsigned pal_restmode_backoff_ms(unsigned attempt);
/**
* @brief Sleep up to @p ms while @p running_flag stays non-zero.
*
* @return 0 if full sleep completed, -1 if interrupted by shutdown.
*/
int pal_restmode_sleep_ms(unsigned ms, atomic_int *running_flag);
/**
* @brief Accept connections resiliently, recreating the listening socket.
*
* @param listen_fd Pointer to the listening socket descriptor.
* @param listen_addr Socket address the server is bound/should be bound to.
* @param client_addr Output client socket address structure.
* @param addr_len Input/Output length of client_addr.
* @param running_flag Pointer to the atomic running flag of the server.
*
* @return Accepted client socket descriptor, or -1 on shutdown.
*/
int pal_resilient_accept(int *listen_fd,
const struct sockaddr_in *listen_addr,
struct sockaddr *client_addr,
socklen_t *addr_len,
atomic_int *running_flag);
#ifdef __cplusplus
}
#endif
#endif /* PAL_RESILIENT_SERVER_H */
File renamed without changes.
+36
View File
@@ -0,0 +1,36 @@
/*
* MIT License — Copyright (c) 2026 SeregonWar
* See LICENSE for full text.
*/
/**
* @file pal_volume.h
* @brief Filesystem volume labels (exFAT/FAT) for removable volumes
*
* The console mounts every USB stick on /mnt/usbN, so the mountpoint says
* nothing about the device. The label the user set on the stick (e.g.
* "SanDisk") lives inside the filesystem, and reading it is enough to label
* the volume in the UI.
*
* Plugins read the raw device exported by statfs() (e.g. /dev/da0s1); the
* parser only needs a readable file, which is what makes it testable on the
* host with synthetic images.
*/
#ifndef PAL_VOLUME_H
#define PAL_VOLUME_H
#include <stddef.h>
/**
* @brief Read the volume label of an exFAT or FAT filesystem.
*
* @param[in] device Device or image to inspect.
* @param[out] out Label buffer, always NUL terminated on success.
* @param[in] out_size Size of @p out (12 bytes are enough for exFAT/FAT).
*
* @return 0 when a non-empty label was found, -1 otherwise.
*/
int pal_volume_label(const char *device, char *out, size_t out_size);
#endif /* PAL_VOLUME_H */
+97
View File
@@ -0,0 +1,97 @@
/*
* MIT License — Copyright (c) 2026 SeregonWar
* See LICENSE for full text.
*/
/**
* @file ps5_fw_offsets.h
* @brief Single source of truth for per-firmware PS5 kernel offsets
*
* WHY THIS MODULE EXISTS
* ----------------------
* Two consumers need firmware-keyed kernel addresses:
*
* - SELF pager swap (pal_filesystem_psx.c) → pager_table_off
* - sysent hook (ps5_net_filter.c) → sysent_off
*
* Each used to carry its own firmware switch, in two different styles, keyed
* on two different encodings. Adding a firmware meant editing both, and the
* two tables were free to drift — they did: the sysent table stopped at
* 12.70 while the pager switch already covered 13.40. Both read from the
* table in ps5_fw_offsets.c now.
*
* KEY ENCODING
* ------------
* `fw` is decimal major * 100 + minor: 1360 means FW 13.60, which the SDK
* reports as 0x13600000. ps5_fw_normalize() converts the raw value returned
* by kernel_get_fw_version() into this key so callers do no bit arithmetic.
*
* ADDING A FIRMWARE
* -----------------
* 1. Verify the offsets for the release (kernel dump, kstuff, SDK).
* 2. Add ONE row to g_ps5_fw_offsets[] in ps5_fw_offsets.c.
* 3. Leave an offset at 0 when it is not verified: the consumer then
* reports "unsupported on this firmware" instead of reading a guessed
* address into the kernel.
*
* @note This module is platform-neutral (no PS5 headers): it is compiled into
* PS5 builds only, but it can be unit-tested on the host.
*/
#ifndef PS5_FW_OFFSETS_H
#define PS5_FW_OFFSETS_H
#include <stdint.h>
/**
* td->td_proc offset within struct thread.
*
* Firmware-invariant on every supported release, matches the ps5-payload-sdk
* constant of the same meaning.
*/
#define PS5_OFF_THREAD_TD_PROC 0x008U
/**
* proc->p_pid offset within struct proc (4-byte field).
*
* Firmware-invariant, matches KERNEL_OFFSET_PROC_P_PID from the ps5-payload-sdk.
* This is a PS5 layout: the PS4 value (0x060) points at unrelated bytes here.
*/
#define PS5_OFF_PROC_P_PID 0x0BCU
/** One row per supported firmware. */
typedef struct {
/** Decimal-encoded version: 1360 == FW 13.60 (see ps5_fw_normalize()). */
uint32_t fw;
/** pagertab[] offset from KERNEL_ADDRESS_DATA_BASE, 0 if not verified. */
uint32_t pager_table_off;
/** sysent[] offset from KERNEL_ADDRESS_DATA_BASE, 0 if not verified. */
uint32_t sysent_off;
} ps5_fw_offsets_t;
/**
* @brief Convert a raw kernel_get_fw_version() value to the table key.
*
* The SDK reports the system software version as BCD nibbles, e.g.
* 0x13600000 for FW 13.60 and 0x04030000 for FW 4.03.
*
* @param[in] fw_version Raw value from kernel_get_fw_version().
* @return Decimal-encoded major * 100 + minor (e.g. 1360 for 13.60),
* or 0 if fw_version carries no version information.
*/
uint32_t ps5_fw_normalize(uint32_t fw_version);
/**
* @brief Look up the offsets row for a firmware.
*
* @param[in] fw_version Raw value from kernel_get_fw_version().
* @return Pointer to the matching row, or NULL when the firmware is unknown.
*
* @note O(n) linear scan over a fixed table; no allocation, no failure modes
* beyond "not found".
*/
const ps5_fw_offsets_t *ps5_fw_offsets_lookup(uint32_t fw_version);
#endif /* PS5_FW_OFFSETS_H */
+372
View File
@@ -0,0 +1,372 @@
/*
* MIT License — Copyright (c) 2026 SeregonWar
* See LICENSE for full text.
*/
/**
* @file ps5_net_filter.h
* @brief PS5 kernel-level outbound connection filter
*
* PROBLEM AD DRESSED
* -----------------
* Sony system daemons (ScePatchChecker, PFAuthClient, SceConsoleFeatureFlagChecker,
* SceHidConfigService, SceWorkaroundCtl, RNPS Curl) enter aggressive retry loops
* when DNS blocks their servers. Each failed attempt still generates outgoing
* packets, saturating the bandwidth available to FTP transfers by up to 40%.
*
* ROOT CAUSE
* ----------
* DNS-level blocking returns NXDOMAIN or timeout, which the daemons treat as a
* *transient* network failure and retry with exponential back-off that saturates
* the wire. The kernel itself never learns that the connection should be rejected.
*
* SOLUTION
* --------
* Intercept connect(2) at the kernel sysent table level. When a non-zftpd process
* attempts to reach a destination outside the local subnet, return ENETUNREACH
* immediately. The daemon interprets this as a *local* error and either backs off
* aggressively or stops retrying altogether — no packet is ever generated.
*
*
* ARCHITECTURE OVERVIEW
* ---------------------
*
* ┌──────────────────────────────────────────────────────────────┐
* │ Userland (zftpd) │
* │ │
* │ ps5_net_filter_install() │
* │ │ │
* │ ├─ [1] Locate sysent[SYS_CONNECT] via firmware table │
* │ ├─ [2] Save original sy_call pointer │
* │ ├─ [3] Copy hook_sys_connect() bytes → kernel RWX page │
* │ └─ [4] kernel_copyin(&hook_addr, &sysent[98].sy_call, 8) │
* │ │
* └──────────────────────────────────────────────────────────────┘
*
* ┌──────────────────────────────────────────────────────────────┐
* │ Kernel space (PS5 FreeBSD 11) │
* │ │
* │ ScePatchChecker calls connect(sock, &sony_addr, len) │
* │ │ │
* │ └─► sysent[98].sy_call → hook_sys_connect() │
* │ │ │
* │ ├─ td->td_proc->p_pid == zftpd_pid? │
* │ │ YES → original_connect() (FTP traffic) │
* │ │ │
* │ └─ destination in SONY_IP_RANGES? │
* │ YES → return ENETUNREACH (no packet!) │
* │ NO → original_connect() │
* │ │
* └──────────────────────────────────────────────────────────────┘
*
*
* USAGE
* -----
* // After ps5_jailbreak():
* ps5_net_filter_config_t cfg = PS5_NET_FILTER_CONFIG_DEFAULT;
* int rc = ps5_net_filter_install(&cfg);
* if (rc != 0) { // handle: filter not installed, FTP still works }
*
* // At shutdown:
* ps5_net_filter_uninstall();
*
*
* SAFETY PROPERTIES
* -----------------
* - The hook ALWAYS passes through connections from zftpd's own PID.
* - The hook ALWAYS passes through connections to RFC-1918 addresses
* (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) and loopback.
* - The blocklist is a fixed compile-time array; no dynamic allocation
* occurs in kernel context.
* - If the sysent offset table does not contain an entry for the running
* firmware version, install() returns an error and the kernel is NOT
* modified.
* - uninstall() is idempotent.
*
*
* FIRMWARE SUPPORT
* ----------------
* The supported firmware matrix (and every per-firmware kernel offset) lives
* in ONE place: src/platform/ps5/ps5_fw_offsets.c, shared with the SELF
* pager. A firmware whose row carries no sysent offset is treated as
* unsupported here.
*
* Unsupported firmware: install() returns PS5_NET_FILTER_ERR_FW_UNSUPPORTED.
*
*
* THREAD SAFETY
* -------------
* - install() and uninstall() must be called from the main thread only.
* - Must be called after ps5_jailbreak() and before ftp_server_start().
* - The installed kernel hook itself is fully re-entrant (no mutable globals
* in the kernel-context code path).
*
*
* @note This module is compiled ONLY when PLATFORM_PS5 is defined.
* On all other platforms the header provides no-op stubs.
*
* @warning Modifies kernel sysent table. Always call uninstall() before
* the payload exits, otherwise the kernel will jump to freed memory.
*/
#ifndef PS5_NET_FILTER_H
#define PS5_NET_FILTER_H
/*===========================================================================*
* COMPILE GUARD — PS5 only
*===========================================================================*/
#ifdef PLATFORM_PS5
#include <stdint.h>
#include <stdatomic.h>
#include <netinet/in.h>
#ifdef __cplusplus
extern "C" {
#endif
/*===========================================================================*
* CONSTANTS
*===========================================================================*/
/**
* Maximum number of IP subnet rules in the block list.
*
* RATIONALE: Fixed compile-time limit prevents any dynamic allocation
* in the kernel-context hot path. 32 entries covers all known Sony
* CDN and PSN IP ranges with room to spare.
*/
#define PS5_NET_FILTER_MAX_RULES 32U
/** Maximum length of a daemon name string (including NUL). */
#define PS5_NET_FILTER_MAX_NAME 24U
/*===========================================================================*
* ERROR CODES
*===========================================================================*/
typedef enum {
PS5_NET_FILTER_OK = 0, /**< Success */
PS5_NET_FILTER_ERR_INVALID_PARAM = -1, /**< NULL pointer or bad argument */
PS5_NET_FILTER_ERR_FW_UNSUPPORTED = -2, /**< Firmware version not in table */
PS5_NET_FILTER_ERR_ALREADY_ACTIVE = -3, /**< install() called twice */
PS5_NET_FILTER_ERR_NOT_INSTALLED = -4, /**< uninstall() without install() */
PS5_NET_FILTER_ERR_KMAP_FAILED = -5, /**< Could not map kernel executable page */
PS5_NET_FILTER_ERR_KWRITE_FAILED = -6, /**< kernel_copyin() failed */
PS5_NET_FILTER_ERR_FW_DETECT = -7, /**< Could not read firmware version */
PS5_NET_FILTER_ERR_SYSENT_INVALID = -8, /**< Sysent sanity check failed */
PS5_NET_FILTER_ERR_EXTERNAL_HOOK = -9, /**< Another payload already hooked sysent */
} ps5_net_filter_err_t;
/*===========================================================================*
* IP SUBNET RULE
*===========================================================================*/
/**
* An IPv4 subnet to block (network byte order).
*
* A connection is blocked when:
* (dest_addr & mask) == (network & mask)
*/
typedef struct {
uint32_t network; /**< Network address (network byte order) */
uint32_t mask; /**< Subnet mask (network byte order) */
} ps5_net_filter_rule_t;
/*===========================================================================*
* STATISTICS
*===========================================================================*/
/**
* Runtime statistics collected by the kernel hook.
*
* @note All fields updated with atomic increments; safe to read at any time.
*/
typedef struct {
uint64_t blocked_total; /**< Total connections blocked */
uint64_t allowed_self; /**< Connections allowed: zftpd's own PID */
uint64_t allowed_local; /**< Connections allowed: RFC-1918 / loopback */
uint64_t allowed_other; /**< Connections allowed: not in block list */
uint64_t hook_calls_total; /**< Total hook invocations */
} ps5_net_filter_stats_t;
/*===========================================================================*
* CONFIGURATION
*===========================================================================*/
/**
* Filter configuration passed to ps5_net_filter_install().
*
* DESIGN: The caller builds the rule table in userland before install().
* The table is copied into the kernel-accessible hook page alongside the
* hook code itself, avoiding any userland pointer dereferences in kernel
* context.
*/
typedef struct {
/**
* Array of IP subnets to block.
*
* If rule_count == 0, the default Sony IP table is used automatically.
* To disable all blocking (test/debug), set rule_count = 0 and
* use_default_rules = false — install() will still succeed but the
* hook will be a passthrough.
*/
ps5_net_filter_rule_t rules[PS5_NET_FILTER_MAX_RULES];
/** Number of valid entries in rules[]. Range [0, PS5_NET_FILTER_MAX_RULES]. */
uint32_t rule_count;
/**
* If true and rule_count == 0, fill rules[] with the built-in Sony
* CDN/PSN table before installing. Default: true.
*/
uint8_t use_default_rules;
/**
* Also hook sendto(2) in addition to connect(2).
*
* Sony's Curl-based daemons (RNPS) use sendto() on UDP sockets for
* some telemetry paths. Enabling this intercepts those as well.
*
* Default: true.
*/
uint8_t hook_sendto;
/** Reserved for future use. Must be zero-initialised. */
uint8_t _reserved[2];
} ps5_net_filter_config_t;
/**
* Default configuration initialiser.
*
* Usage:
* ps5_net_filter_config_t cfg = PS5_NET_FILTER_CONFIG_DEFAULT;
* ps5_net_filter_install(&cfg);
*/
#define PS5_NET_FILTER_CONFIG_DEFAULT \
{ .rules = {{0, 0}}, .rule_count = 0U, .use_default_rules = 1, .hook_sendto = 1, ._reserved = {0, 0} }
/*===========================================================================*
* PUBLIC API
*===========================================================================*/
/**
* @brief Install the outbound connection filter into the PS5 kernel.
*
* Steps performed:
* 1. Detect running firmware version.
* 2. Look up sysent[SYS_CONNECT] offset for that firmware.
* 3. Populate cfg->rules[] with default Sony IP table if requested.
* 4. Allocate a kernel-executable page and copy the hook code + rule
* table into it.
* 5. Atomically replace sysent[98].sy_call (and optionally sysent[133])
* with the address of the installed hook.
* 6. Save our own PID so the hook can fast-path our FTP traffic.
*
* @param[in] cfg Filter configuration. May be NULL to use all defaults.
*
* @return PS5_NET_FILTER_OK on success, negative error code on failure.
*
* @retval PS5_NET_FILTER_OK Hook installed successfully.
* @retval PS5_NET_FILTER_ERR_FW_UNSUPPORTED Firmware not in support table.
* @retval PS5_NET_FILTER_ERR_ALREADY_ACTIVE Called twice without uninstall.
* @retval PS5_NET_FILTER_ERR_KMAP_FAILED Kernel page allocation failed.
* @retval PS5_NET_FILTER_ERR_KWRITE_FAILED kernel_copyin() failed.
*
* @pre ps5_jailbreak() has been called successfully (kernel r/w access).
* @pre Must be called from the main thread.
* @post On success, sysent[98].sy_call points to the installed hook.
*
* @note Thread-safety: NOT thread-safe. Call from main thread only.
* @note The install is reversible: always call ps5_net_filter_uninstall()
* before the payload exits.
* @note Failure is non-fatal: FTP server can start and operate normally;
* bandwidth saturation will continue to occur.
*/
int ps5_net_filter_install(const ps5_net_filter_config_t *cfg);
/**
* @brief Uninstall the connection filter and restore the original kernel state.
*
* Restores the original sysent[98].sy_call (and sysent[133] if hooked).
* Frees the kernel-executable page.
*
* @return PS5_NET_FILTER_OK on success, negative error code on failure.
*
* @retval PS5_NET_FILTER_OK Hook removed, kernel restored.
* @retval PS5_NET_FILTER_ERR_NOT_INSTALLED install() was never called.
* @retval PS5_NET_FILTER_ERR_KWRITE_FAILED kernel_copyin() failed.
*
* @note Idempotent: safe to call even if install() failed.
* @note Thread-safety: NOT thread-safe. Call from main thread only.
* @note WCET: Two kernel_copyin() calls + one munmap(). Bounded.
*/
int ps5_net_filter_uninstall(void);
/**
* @brief Check whether the filter is currently installed.
*
* @return 1 if installed and active, 0 otherwise.
*
* @note Thread-safety: Safe (atomic load).
*/
int ps5_net_filter_is_active(void);
/**
* @brief Read runtime statistics from the filter.
*
* @param[out] out Destination buffer for statistics snapshot.
*
* @return PS5_NET_FILTER_OK on success, PS5_NET_FILTER_ERR_INVALID_PARAM
* if out is NULL.
*
* @pre out != NULL
* @note Thread-safety: Safe (atomic reads, snapshot may be slightly stale).
*/
int ps5_net_filter_get_stats(ps5_net_filter_stats_t *out);
/**
* @brief Retrieve a human-readable description of an error code.
*
* @param[in] err Error code returned by any ps5_net_filter_* function.
*
* @return Static string describing the error. Never returns NULL.
*
* @note Thread-safety: Safe (read-only static data).
*/
const char *ps5_net_filter_strerror(int err);
#ifdef __cplusplus
}
#endif
/*===========================================================================*
* STUB IMPLEMENTATIONS — non-PS5 platforms
*===========================================================================*/
#else /* !PLATFORM_PS5 */
/*
* Provide no-op stubs so callers can compile on POSIX without ifdefs.
* The compiler will inline and eliminate these entirely at -O2.
*/
typedef struct { int _dummy; } ps5_net_filter_config_t;
typedef struct { int _dummy; } ps5_net_filter_stats_t;
#define PS5_NET_FILTER_CONFIG_DEFAULT { 0 }
#define PS5_NET_FILTER_OK 0
static inline int ps5_net_filter_install(const ps5_net_filter_config_t *cfg)
{ (void)cfg; return 0; }
static inline int ps5_net_filter_uninstall(void) { return 0; }
static inline int ps5_net_filter_is_active(void) { return 0; }
static inline int ps5_net_filter_get_stats(ps5_net_filter_stats_t *s)
{ (void)s; return 0; }
static inline const char *ps5_net_filter_strerror(int e)
{ (void)e; return "not supported on this platform"; }
#endif /* PLATFORM_PS5 */
#endif /* PS5_NET_FILTER_H */
File renamed without changes.
+26
View File
@@ -0,0 +1,26 @@
#ifndef ZFTPD_BACKEND_TORRENT_H
#define ZFTPD_BACKEND_TORRENT_H
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Called from the BitTorrent worker. Return -1 to cancel, 1 to pause, or 0
* to continue. Progress is verified payload, not network protocol overhead. */
typedef int (*transfer_torrent_progress_cb)(void *ctx, uint64_t done,
uint64_t total, uint64_t speed);
int transfer_torrent_validate(const char *uri);
int transfer_torrent_run(const char *uri, const char *save_dir,
const char *resume_file,
transfer_torrent_progress_cb progress, void *ctx,
char *error, size_t error_size);
#ifdef __cplusplus
}
#endif
#endif
+92
View File
@@ -0,0 +1,92 @@
/*
* MIT License — Copyright (c) 2026 SeregonWar
* See LICENSE for full text.
*/
/**
* @file dump_job.h
* @brief Decrypted game dump (browser download or console path)
*
* Source is the running title's sandbox mount (/mnt/sandbox/<TITLE>_000/app0),
* where the kernel exposes the application decrypted; SELF/SPRX inside the tree
* are decrypted through the self pager while they are read.
*
* The same walk feeds two destinations:
* - streamed to a client (ZIP bytes through a callback, no temp space), or
* - written on the console: one ZIP archive, or a plain directory tree.
*/
#ifndef DUMP_JOB_H
#define DUMP_JOB_H
#include <stddef.h>
#include <stdint.h>
#include <sys/types.h>
#define DUMP_PATH_MAX 1024
typedef enum {
DUMP_FORMAT_ZIP = 0, /**< one archive, streamed or written */
DUMP_FORMAT_FILES = 1 /**< directory tree, local targets only */
} dump_format_t;
typedef struct dump_job dump_job_t;
/** Producer callback: returns bytes produced, 0 at end, -1 on error. */
typedef ssize_t (*dump_read_fn)(void *ctx, void *buf, size_t len);
typedef void (*dump_progress_fn)(uint64_t bytes, void *ctx);
/**
* @brief Resolve the source path for a title.
*
* @return 1 when the running sandbox mount was found, -1 when it is not
* mounted. There is no fallback to the encrypted /user/app image.
*/
int dump_resolve_source(const char *title_id, char *out, size_t out_size);
/** 1 when the sandbox for @p title_id is mounted right now. */
int dump_source_ready(const char *title_id);
dump_job_t *dump_job_create(const char *title_id, dump_format_t format,
int decrypt);
/**
* @brief Create a job from an explicit source path.
*
* dump_job_create() resolves the sandbox mount from the title id first; this
* variant skips the lookup, so the archive/copy pipeline can be exercised
* without a console.
*/
dump_job_t *dump_job_create_at(const char *title_id, const char *source,
dump_format_t format, int decrypt);
/**
* @brief Ask a running job to stop.
*
* Safe to call from another thread: the producer checks the flag at every
* chunk and gives up with errno ECANCELED.
*/
void dump_job_cancel(dump_job_t *job);
const char *dump_job_title(const dump_job_t *job);
const char *dump_job_source(const dump_job_t *job);
uint64_t dump_job_size(const dump_job_t *job);
size_t dump_job_entries(const dump_job_t *job);
int dump_job_truncated(const dump_job_t *job);
/**
* @brief Produce the dump as ZIP bytes (DUMP_FORMAT_ZIP only).
*
* Feeds the HTTP streaming response directly: returns the bytes written into
* @p buf, 0 at the end of the archive, -1 on error.
*/
ssize_t dump_job_read(dump_job_t *job, void *buf, size_t len);
/** Write the dump under @p dest_dir (ZIP archive or file tree). */
int dump_job_write_local(dump_job_t *job, const char *dest_dir, char *error,
size_t error_size, dump_progress_fn progress,
void *progress_ctx);
void dump_job_destroy(dump_job_t *job);
#endif /* DUMP_JOB_H */
+81
View File
@@ -0,0 +1,81 @@
#ifndef ZFTPD_TRANSFER_MANAGER_H
#define ZFTPD_TRANSFER_MANAGER_H
#include <stddef.h>
#include <stdint.h>
/** Jobs tracked at once: queued + running + finished-but-not-dismissed. */
#define TRANSFER_MAX_ACTIVE 16
/** Downloads running at the same time; the rest wait in the queue. */
#ifndef TRANSFER_MAX_CONCURRENT
#define TRANSFER_MAX_CONCURRENT 2
#endif
#define TRANSFER_URL_MAX 2048
#define TRANSFER_PATH_MAX 1024
#define TRANSFER_NAME_MAX 256
#define TRANSFER_ERROR_MAX 256
#define TRANSFER_ORPHAN_MAX 64
#define TRANSFER_PART_PATH_MAX (TRANSFER_PATH_MAX + TRANSFER_NAME_MAX + 16)
typedef struct {
char path[TRANSFER_PART_PATH_MAX];
uint64_t size;
} transfer_orphan_t;
typedef struct {
int id;
int active;
int done;
int paused;
int error;
int queued; /**< 1 while waiting for a free download slot */
int queue_position; /**< 1-based queue position, 0 when not queued */
int cancelled; /**< 1 when the job was cancelled by the user */
char url[TRANSFER_URL_MAX];
char filename[TRANSFER_NAME_MAX];
char dst_path[TRANSFER_PATH_MAX];
char error_msg[TRANSFER_ERROR_MAX];
uint64_t total_size;
uint64_t downloaded;
double speed;
} transfer_snapshot_t;
int transfer_url_supported(const char *url, char *reason, size_t reason_size);
/**
* Queue a download.
*
* The job starts immediately when a download slot is free; otherwise it waits
* in the queue (out_queued is set to 1) and starts on its own once an older
* job finishes.
*/
int transfer_start(const char *url, const char *dst_dir, int *out_id,
int *out_queued, char *out_name, size_t out_name_size,
char *error, size_t error_size);
size_t transfer_snapshot_all(transfer_snapshot_t *out, size_t capacity);
int transfer_toggle_pause(int id, int *out_paused);
int transfer_cancel(int id);
int transfer_retry(int id);
/** Delete a tracked job and its unfinished .part file; keep completed files. */
int transfer_delete(int id);
/** Cached, periodically refreshed partial files without a download record. */
size_t transfer_orphans_snapshot(transfer_orphan_t *out, size_t capacity,
int *scanning);
/** Delete a discovered orphan only if it is still an untracked regular file. */
int transfer_orphan_delete(const char *path);
/** Maximum concurrent downloads; 0 or less restores the default. */
void transfer_set_max_concurrent(int max);
/**
* Re-enqueue the unfinished downloads recorded in the state file.
*
* Called once at startup: after a payload re-injection the in-memory queue is
* gone, but the .zftpd.part files are still on disk, so the restored jobs
* resume from the byte offset already downloaded. Idempotent.
*/
int transfer_manager_restore(void);
#endif
+397
View File
@@ -0,0 +1,397 @@
#include "instance_control.h"
#if defined(PLATFORM_PS5)
#include "ftp_instance.h"
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <poll.h>
#include <pthread.h>
#include <signal.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/sysctl.h>
#include <sys/time.h>
#include <sys/user.h>
#include <time.h>
#include <unistd.h>
#ifndef INSTANCE_DIR
#define INSTANCE_DIR "/data/zftpd"
#endif
#ifndef INSTANCE_FILE
#define INSTANCE_FILE INSTANCE_DIR "/instance.pid"
#endif
#define INSTANCE_PORT 28888
#define INSTANCE_PAYLOAD_THREAD "zftpd.elf"
#define INSTANCE_HANDSHAKE_WAIT_MS 5000U
#define INSTANCE_LEGACY_TERM_WAIT_MS 3000U
/* Console kinfo_proc layout, the one this payload already reads for its
* process list: record size, ki_pid and the thread name. */
#define KINFO_PID_OFFSET 72U
#define KINFO_THREAD_OFFSET 447U
#define KINFO_THREAD_SIZE 20U
static int g_control_fd = -1;
static uint64_t g_token;
static volatile sig_atomic_t g_control_running;
static void pause_ms(unsigned ms) {
struct timespec ts = {(time_t)(ms / 1000U),
(long)(ms % 1000U) * 1000000L};
(void)nanosleep(&ts, NULL);
}
static int send_exact(int fd, const char *data, size_t size) {
while (size > 0U) {
ssize_t sent = send(fd, data, size, 0);
if (sent < 0 && errno == EINTR) continue;
if (sent <= 0) return -1;
data += sent;
size -= (size_t)sent;
}
return 0;
}
static int read_line(int fd, char *out, size_t capacity) {
if (capacity < 2U) return -1;
size_t used = 0U;
while (used + 1U < capacity) {
char c;
ssize_t got = recv(fd, &c, 1U, 0);
if (got < 0 && errno == EINTR) continue;
if (got != 1) return -1;
out[used++] = c;
if (c == '\n') {
out[used] = '\0';
return (int)used;
}
}
return -1;
}
/* A payload the loader has not reaped yet stays visible to kill(pid, 0) while
* its sockets and kernel resources are already gone, so zombies must not count
* as alive here. */
static int process_alive(pid_t pid) {
if (pid <= 1) return 0;
if (kill(pid, 0) != 0 && errno == ESRCH) return 0;
int mib[4] = {1, 14, 8, 0};
size_t size = 0U;
if (sysctl(mib, 4, NULL, &size, NULL, 0) != 0 || size == 0U) return 1;
unsigned char *buffer = malloc(size);
if (buffer == NULL) return 1;
if (sysctl(mib, 4, buffer, &size, NULL, 0) != 0) {
free(buffer);
return 1;
}
int alive = 1;
for (size_t offset = 0U;
offset + offsetof(struct kinfo_proc, ki_stat) + 1U <= size;) {
const struct kinfo_proc *info =
(const struct kinfo_proc *)(const void *)(buffer + offset);
int record_size = info->ki_structsize;
if (record_size <= 0 || offset + (size_t)record_size > size ||
(size_t)record_size <= offsetof(struct kinfo_proc, ki_stat)) break;
if (info->ki_pid == pid) {
alive = info->ki_stat != 5; /* FreeBSD SZOMB */
break;
}
offset += (size_t)record_size;
}
free(buffer);
return alive;
}
/* Every process whose thread carries this payload's name, excluding this
* process and its process group: the loader shares the group with the payload
* it started, and signalling it would tear down the console session. */
static size_t verified_payload_pids(pid_t *out, size_t capacity) {
if (out == NULL || capacity == 0U) return 0U;
int mib[4] = {1, 14, 8, 0};
size_t size = 0U;
if (sysctl(mib, 4, NULL, &size, NULL, 0) != 0 || size == 0U) return 0U;
unsigned char *buffer = malloc(size);
if (buffer == NULL) return 0U;
if (sysctl(mib, 4, buffer, &size, NULL, 0) != 0) {
free(buffer);
return 0U;
}
pid_t self = getpid();
pid_t group = getpgrp();
size_t count = 0U;
for (size_t offset = 0U;
offset + KINFO_THREAD_OFFSET + KINFO_THREAD_SIZE <= size;) {
int record_size = 0;
pid_t pid = -1;
memcpy(&record_size, buffer + offset, sizeof(record_size));
if (record_size <= 0 || offset + (size_t)record_size > size) break;
memcpy(&pid, buffer + offset + KINFO_PID_OFFSET, sizeof(pid));
const char *thread = (const char *)(const void *)(buffer + offset +
KINFO_THREAD_OFFSET);
if (pid > 1 && pid != self && pid != group &&
(size_t)record_size > KINFO_THREAD_OFFSET + KINFO_THREAD_SIZE &&
memcmp(thread, INSTANCE_PAYLOAD_THREAD "\0",
sizeof(INSTANCE_PAYLOAD_THREAD)) == 0 &&
count < capacity) {
out[count++] = pid;
}
offset += (size_t)record_size;
}
free(buffer);
return count;
}
static int read_identity(pid_t *pid, uint64_t *token) {
char text[128];
FILE *fp = fopen(INSTANCE_FILE, "r");
if (fp == NULL) return -1;
size_t used = fread(text, 1U, sizeof(text) - 1U, fp);
(void)fclose(fp);
text[used] = '\0';
int parsed_pid = 0;
uint64_t parsed_token = 0U;
if (ftp_instance_identity_parse(text, &parsed_pid, &parsed_token) != 0) {
return -1;
}
if (pid != NULL) *pid = (pid_t)parsed_pid;
if (token != NULL) *token = parsed_token;
return 0;
}
static int write_identity(void) {
if (mkdir(INSTANCE_DIR, 0755) != 0 && errno != EEXIST) return -1;
char line[80];
if (ftp_instance_identity_format(line, sizeof(line), (int)getpid(),
g_token) != 0) {
return -1;
}
char temp[128];
int n = snprintf(temp, sizeof(temp), "%s.%d", INSTANCE_FILE, (int)getpid());
if (n <= 0 || (size_t)n >= sizeof(temp)) return -1;
int fd = open(temp, O_WRONLY | O_CREAT | O_TRUNC, 0600);
if (fd < 0) return -1;
size_t length = strlen(line);
int ok = write(fd, line, length) == (ssize_t)length;
if (close(fd) != 0) ok = 0;
if (ok && rename(temp, INSTANCE_FILE) != 0) ok = 0;
if (!ok) (void)unlink(temp);
return ok ? 0 : -1;
}
static void remove_identity(void) {
(void)unlink(INSTANCE_FILE);
}
/* A matching token authenticates the live control listener; a pid in a file
* alone does not, since the console can reuse it after a reboot. */
static int request_previous_stop(uint64_t token) {
char command[64];
if (ftp_instance_stop_command_format(command, sizeof(command), token) != 0) {
return -1;
}
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) return -1;
struct timeval timeout = {0, 500000};
(void)setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
(void)setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout));
struct sockaddr_in address;
memset(&address, 0, sizeof(address));
address.sin_family = AF_INET;
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
address.sin_port = htons(INSTANCE_PORT);
if (connect(fd, (struct sockaddr *)&address, sizeof(address)) != 0) {
close(fd);
return -1;
}
int result = -1;
if (send_exact(fd, command, strlen(command)) == 0) {
char answer[8] = {0};
if (read_line(fd, answer, sizeof(answer)) == 3 &&
memcmp(answer, "OK\n", 3U) == 0) {
result = 0;
}
}
close(fd);
return result;
}
static int wait_for_exit(pid_t pid, unsigned timeout_ms) {
for (unsigned waited = 0U; waited < timeout_ms; waited += 100U) {
if (!process_alive(pid)) return 1;
pause_ms(100U);
}
return process_alive(pid) ? 0 : 1;
}
/* Older payloads have no control port. Signal only verified zftpd processes,
* then refuse to start if any remain; a forced kill can panic the console. */
static int stop_verified_payloads(void) {
pid_t pids[16];
size_t count = verified_payload_pids(pids, sizeof(pids) / sizeof(pids[0]));
if (count == 0U) return 1;
int alive = 0;
for (size_t i = 0U; i < count; i++) {
if (!process_alive(pids[i])) continue;
printf("[zftpd] Requesting SIGTERM for verified old pid %d\n", (int)pids[i]);
(void)kill(pids[i], SIGTERM);
}
for (unsigned waited = 0U; waited < INSTANCE_LEGACY_TERM_WAIT_MS;
waited += 100U) {
alive = 0;
for (size_t i = 0U; i < count; i++) alive |= process_alive(pids[i]);
if (!alive) return 1;
pause_ms(100U);
}
return 0;
}
int instance_control_replace_previous(void) {
pid_t previous = -1;
uint64_t token = 0U;
int have_identity = read_identity(&previous, &token) == 0;
int authenticated = 0;
if (have_identity && previous == getpid()) {
/* The file names this process: either a stale file whose pid the loader
* reused, or a second entry point inside the same loader process. Neither
* is another payload, so it is not evidence worth acting on. */
printf("[zftpd] Instance file names this pid; treating it as stale\n");
previous = -1;
}
if (previous > 1 && process_alive(previous)) {
if (request_previous_stop(token) == 0) {
authenticated = 1;
printf("[zftpd] Previous instance accepted the shutdown request\n");
(void)wait_for_exit(previous, INSTANCE_HANDSHAKE_WAIT_MS);
}
}
if (!stop_verified_payloads() ||
(authenticated && process_alive(previous))) {
printf("[zftpd] A zftpd payload is still alive; refusing a second instance\n");
return -1;
}
/* Dead, malformed or reused identities cannot block startup. Keep the
* transfer state: it records resumable downloads, not process ownership. */
remove_identity();
return 0;
}
static void *control_loop(void *unused) {
(void)unused;
while (g_control_running != 0) {
struct pollfd ready = {g_control_fd, POLLIN, 0};
int polled = poll(&ready, 1, 500);
if (polled <= 0) continue;
if ((ready.revents & POLLIN) == 0) continue;
int fd = accept(g_control_fd, NULL, NULL);
if (fd < 0) {
if (errno == EINTR) continue;
if (g_control_running == 0) break;
pause_ms(50U);
continue;
}
struct timeval timeout = {0, 500000};
(void)setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
char command[64] = {0};
uint64_t received_token = 0U;
int received = read_line(fd, command, sizeof(command));
if (received > 0 &&
ftp_instance_stop_command_parse(command, &received_token) == 0 &&
received_token == g_token) {
(void)send_exact(fd, "OK\n", 3U);
close(fd);
printf("[zftpd] Shutdown requested by a new payload\n");
zftpd_shutdown_request();
continue;
}
close(fd);
}
return NULL;
}
int instance_control_start(void) {
g_token = ftp_daemon_instance_id();
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) return -1;
int reuse = 1;
(void)setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
struct sockaddr_in address;
memset(&address, 0, sizeof(address));
address.sin_family = AF_INET;
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
address.sin_port = htons(INSTANCE_PORT);
if (bind(fd, (struct sockaddr *)&address, sizeof(address)) != 0 ||
listen(fd, 4) != 0) {
close(fd);
return -1;
}
g_control_fd = fd;
g_control_running = 1;
/* Detached: the shutdown path must never wait for this thread, because this
* thread may itself be the one running the shutdown. */
pthread_attr_t attr;
int detached = pthread_attr_init(&attr) == 0 &&
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED) == 0;
pthread_t thread;
int created = pthread_create(&thread, detached ? &attr : NULL,
control_loop, NULL);
if (detached) {
(void)pthread_attr_destroy(&attr);
}
if (created != 0) {
close(fd);
g_control_fd = -1;
g_control_running = 0;
return -1;
}
if (write_identity() != 0) {
instance_control_stop();
return -1;
}
return 0;
}
void instance_control_stop(void) {
/* Keep the identity until process exit. If cleanup blocks, a replacement
* can still identify this live instance and refuse duplicate listeners. */
g_control_running = 0;
if (g_control_fd >= 0) {
(void)shutdown(g_control_fd, SHUT_RDWR);
close(g_control_fd);
g_control_fd = -1;
}
/* The listener is closed and the thread is detached: it either observes the
* flag or is torn down with the process image. */
}
#else
int instance_control_replace_previous(void) { return 0; }
int instance_control_start(void) { return 0; }
void instance_control_stop(void) {}
#endif
+444 -85
View File
@@ -36,11 +36,13 @@ SOFTWARE.
#include "ftp_config.h"
#include "ftp_server.h"
#include "instance_control.h"
#include "pal_fileio.h"
#include "pal_network.h"
#include "pal_notification.h"
#include <errno.h>
#include <getopt.h>
#include <inttypes.h>
#include <pthread.h>
#include <signal.h>
#include <stdio.h>
@@ -48,21 +50,80 @@ SOFTWARE.
#include <string.h>
#include <unistd.h>
/*---------------------------------------------------------------------------*
* ZHTTPD (Web File Explorer) — conditional compilation
*---------------------------------------------------------------------------*/
/* ZHTTPD (Web File Explorer) is a compile-time switch: default it before any
* use in this file, so the non-web builds evaluate it instead of tripping
* -Wundef. */
#ifndef ENABLE_ZHTTPD
#define ENABLE_ZHTTPD 0
#endif
#if defined(PLATFORM_PS5) && ENABLE_ZHTTPD
#include <netinet/in.h>
#include <sys/socket.h>
#endif
#ifndef ENABLE_PS5_NET_FILTER
#define ENABLE_PS5_NET_FILTER 0
#endif
/* How long a shutdown request is given before the watchdog performs the
* teardown itself instead of leaving a half-stopped payload behind. */
#if defined(PLATFORM_PS4) || defined(PLATFORM_PS5)
#include "pal_dns_filter.h"
#endif
#if defined(PLATFORM_PS5) && ENABLE_PS5_NET_FILTER
#include <pthread.h>
#include "ps5_net_filter.h"
/*
* Retries the net filter install until the kernel primitives answer, then
* reports the outcome once (the install itself raises the on-screen
* "zftpd: netfilter installed" notification).
*/
static void *net_filter_install_deferred(void *unused) {
(void)unused;
ps5_net_filter_config_t cfg = PS5_NET_FILTER_CONFIG_DEFAULT;
for (int attempt = 0; attempt < 40; attempt++) {
usleep(500000);
int rc = ps5_net_filter_install(&cfg);
if (rc == PS5_NET_FILTER_OK) {
printf("[zftpd - ps5] Net filter: installed (Sony retry suppression active)\n");
return NULL;
}
if (rc != PS5_NET_FILTER_ERR_KMAP_FAILED) {
printf("[zftpd - ps5] Net filter: not installed (%s) — FTP will still work\n",
ps5_net_filter_strerror(rc));
return NULL;
}
}
printf("[zftpd - ps5] Net filter: not installed (kernel primitives never came up)\n");
return NULL;
}
#endif
/*---------------------------------------------------------------------------*
* ZHTTPD (Web File Explorer) — conditional compilation
*---------------------------------------------------------------------------*/
#if ENABLE_ZHTTPD
#include "event_loop.h"
#include "ftp_log.h"
#include "http_api.h"
#include "http_config.h"
#include "http_csrf.h"
#include "http_server.h"
#include "transfer/transfer_manager.h"
static event_loop_t *g_event_loop = NULL;
static http_server_t *g_http_server = NULL;
#if defined(PLATFORM_PS5)
static pthread_t g_http_thread;
static int g_http_thread_started;
#endif
/**
* @brief Background thread running the HTTP event loop
@@ -97,11 +158,22 @@ static int start_http_thread(pthread_t *thread, event_loop_t *loop) {
}
#endif /* ENABLE_ZHTTPD */
/*---------------------------------------------------------------------------*
* MCP (Model Context Protocol) — Native Kernel Analysis Module
*---------------------------------------------------------------------------*/
#ifndef ENABLE_MCP
#define ENABLE_MCP 0
#endif
#if ENABLE_MCP
#include "mcp_server.h"
#endif /* ENABLE_MCP */
#if defined(PLATFORM_PS4) || defined(PLATFORM_PS5)
#include "pal_scratch.h"
#include <stdint.h>
#include <sys/syscall.h>
#include <sys/sysctl.h>
#include "pal_scratch.h"
#endif
/*===========================================================================*
@@ -111,7 +183,83 @@ static int start_http_thread(pthread_t *thread, event_loop_t *loop) {
static ftp_server_context_t g_server_ctx;
static volatile sig_atomic_t g_shutdown_requested = 0;
#if defined(PLATFORM_PS4) || defined(PLATFORM_PS5)
#if defined(PLATFORM_PS5)
/* Set by the shutdown signals and by the control port a replacing payload
* talks to; the PS5 main loop watches the flag with or without the web
* interface, so both helpers exist in every PS5 build. */
static void ps5_signal_shutdown(int sig) {
(void)sig;
g_shutdown_requested = 1;
}
void zftpd_shutdown_request(void) {
g_shutdown_requested = 1;
}
#endif
#if defined(PLATFORM_PS5) && ENABLE_ZHTTPD
/* Check the actual HTTP request path, not merely whether port 8888 is open:
* a blocked event-loop callback still accepts TCP connections in the kernel. */
static int ps5_http_healthy(void) {
int fd = socket(AF_INET6, SOCK_STREAM, 0);
if (fd < 0) return 0;
struct timeval timeout = {2, 0};
(void)setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
(void)setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout));
struct sockaddr_in6 addr;
memset(&addr, 0, sizeof(addr));
addr.sin6_family = AF_INET6;
addr.sin6_addr = in6addr_loopback;
addr.sin6_port = htons(HTTP_DEFAULT_PORT);
int ok = connect(fd, (struct sockaddr *)&addr, sizeof(addr)) == 0;
if (ok) {
static const char probe[] =
"GET /api/status HTTP/1.0\r\nHost: localhost\r\nConnection: close\r\n\r\n";
ok = send(fd, probe, sizeof(probe) - 1U, 0) == (ssize_t)(sizeof(probe) - 1U);
}
if (ok) {
char head[8];
ssize_t got = recv(fd, head, sizeof(head), MSG_WAITALL);
ok = got == (ssize_t)sizeof(head) && memcmp(head, "HTTP/1.", 7U) == 0;
}
close(fd);
return ok;
}
static void *ps5_health_watchdog(void *unused) {
(void)unused;
unsigned failures = 0U;
unsigned since_probe_ms = 0U;
while (!g_shutdown_requested) {
usleep(100000U);
if (g_shutdown_requested) break;
since_probe_ms += 100U;
if (since_probe_ms < 10000U) continue;
since_probe_ms = 0U;
failures = ps5_http_healthy() ? 0U : failures + 1U;
if (failures < 3U) continue;
printf("[zftpd] HTTP failed three health probes; shutting down\n");
zftpd_shutdown_request();
}
return NULL;
}
static void ps5_start_health_watchdog(void) {
pthread_t thread;
pthread_attr_t attr;
if (pthread_attr_init(&attr) != 0) return;
(void)pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
(void)pthread_create(&thread, &attr, ps5_health_watchdog, NULL);
(void)pthread_attr_destroy(&attr);
}
static void ps5_shutdown_sequence(void);
#endif
#ifdef PLATFORM_PS4
static pid_t find_pid_by_name(const char *name) {
int mib[4] = {1, 14, 8, 0};
pid_t mypid = getpid();
@@ -166,7 +314,9 @@ static void terminate_existing_instance(const char *name) {
sleep(1);
}
}
#endif
#ifdef PLATFORM_PS4
static ftp_error_t server_init_with_fallback(const char *bind_ip,
uint16_t base_port,
const char *root_path,
@@ -223,12 +373,17 @@ static void install_signal_handlers(void) {
#ifdef PLATFORM_PS4
int main(void) {
printf("[PS4 FTP] Version " RELEASE_VERSION "\n");
printf("[PS4 FTP] Initializing...\n");
printf("[zftpd - ps4] Version " RELEASE_VERSION "\n");
printf("[zftpd - ps4] Initializing...\n");
(void)pal_notification_init();
(void)syscall(SYS_thr_set_name, -1, "zftpd.elf");
/* A client that vanishes mid-transfer (e.g. a download aborted from the
* browser) must only break its own connection, never the daemon: without
* this the offending write raises SIGPIPE and the default disposition kills
* the payload. */
signal(SIGPIPE, SIG_IGN);
const char *bind_ip = "0.0.0.0";
@@ -262,10 +417,10 @@ int main(void) {
root_path, &selected_port);
if (err != FTP_OK) {
printf("[PS4 FTP] Initialization failed: %d\n", (int)err);
printf("[zftpd - ps4] Initialization failed: %d\n", (int)err);
if (err == FTP_ERR_SOCKET_BIND) {
printf("[PS4 FTP] Bind failed: %s\n", strerror(errno));
printf("[PS4 FTP] Hint: port busy or invalid bind_ip.\n");
printf("[zftpd - ps4] Bind failed: %s\n", strerror(errno));
printf("[zftpd - ps4] Hint: port busy or invalid bind_ip.\n");
}
{
char msg[160];
@@ -284,23 +439,29 @@ int main(void) {
pal_notification_send(msg);
}
printf("[PS4 FTP] Listening on %s:%u\n", display_ip, selected_port);
printf("[PS4 FTP] Root: %s\n", root_path);
printf("[zftpd - ps4] Listening on %s:%u\n", display_ip, selected_port);
printf("[zftpd - ps4] Root: %s\n", root_path);
/* Start server */
err = ftp_server_start(&g_server_ctx);
if (err != FTP_OK) {
printf("[PS4 FTP] Failed to start: %d\n", (int)err);
printf("[zftpd - ps4] Failed to start: %d\n", (int)err);
ftp_server_cleanup(&g_server_ctx);
return -1;
}
printf("[PS4 FTP] Server started successfully\n");
printf("[zftpd - ps4] Server started successfully\n");
#if ENABLE_ZHTTPD
/* Pick up the downloads that were still unfinished when the previous
* instance stopped (payload re-injection). */
(void)transfer_manager_restore();
#endif
{
char notify_msg[128];
(void)snprintf(notify_msg, sizeof(notify_msg), "zftpd v%s (PS4) started",
(void)snprintf(notify_msg, sizeof(notify_msg), "zftpd by SeregonWar v%s (PS4) started",
RELEASE_VERSION);
pal_notification_send(notify_msg);
@@ -313,10 +474,21 @@ int main(void) {
* ZHTTPD — Start Web File Explorer
*=========================================================================*/
#if ENABLE_ZHTTPD
http_csrf_init();
#if ENABLE_MCP
static mcp_server_t *g_mcp_server = NULL;
#endif
if (http_csrf_init() != 0) {
ftp_log_line(FTP_LOG_WARN, "CSRF init failed: web upload disabled");
}
/* Attach FTP context so /api/network/reset can reach the session pool */
http_api_set_server_ctx(&g_server_ctx);
g_event_loop = event_loop_create();
if (g_event_loop != NULL) {
g_http_server = http_server_create(g_event_loop, HTTP_DEFAULT_PORT);
char http_bind[64];
(void)snprintf(http_bind, sizeof(http_bind), "[::]:%u",
(unsigned)HTTP_DEFAULT_PORT);
g_http_server =
http_server_create(g_event_loop, http_bind, root_path);
if (g_http_server != NULL) {
pthread_t http_thread;
int rc = start_http_thread(&http_thread, g_event_loop);
@@ -335,6 +507,36 @@ int main(void) {
printf("[PS4 HTTP] Failed to start HTTP thread (rc=%d)\n", rc);
}
}
/*=========================================================================*
* MCP — Start Kernel Analysis Server
*=========================================================================*/
#if ENABLE_MCP
printf("[PS4 MCP] Initializing MCP server...\n");
pal_notification_send("MCP: Initializing server...");
if (g_event_loop == NULL) {
printf("[PS4 MCP] ERROR: Event loop not initialized!\n");
pal_notification_send("MCP ERROR: No event loop");
} else {
char mcp_bind[64];
(void)snprintf(mcp_bind, sizeof(mcp_bind), "0.0.0.0:%u", (unsigned)MCP_PORT);
printf("[PS4 MCP] Binding to %s\n", mcp_bind);
g_mcp_server = mcp_server_create(g_event_loop, mcp_bind);
if (g_mcp_server != NULL) {
printf("[PS4 MCP] SUCCESS: Server listening on tcp://%s:%u\n",
display_ip, (unsigned)MCP_PORT);
char msg[128];
(void)snprintf(msg, sizeof(msg), "MCP Ready: %s:%u", display_ip,
(unsigned)MCP_PORT);
pal_notification_send(msg);
} else {
printf("[PS4 MCP] ERROR: Failed to create MCP server (check logs)\n");
pal_notification_send("MCP ERROR: Server failed");
}
}
#endif /* ENABLE_MCP */
}
#endif
@@ -344,16 +546,26 @@ int main(void) {
}
/* Shutdown */
printf("[PS4 FTP] Shutting down...\n");
printf("[zftpd - ps4] Shutting down...\n");
#if ENABLE_ZHTTPD
if (g_event_loop != NULL) {
event_loop_stop(g_event_loop);
}
/* Detach FTP context before tearing down HTTP — prevents use-after-free
* if a /api/network/reset request races with server shutdown */
http_api_set_server_ctx(NULL);
if (g_http_server != NULL) {
http_server_destroy(g_http_server);
g_http_server = NULL;
}
#if ENABLE_MCP
if (g_mcp_server != NULL) {
mcp_server_destroy(g_mcp_server);
g_mcp_server = NULL;
printf("[PS4 MCP] Stopped\n");
}
#endif
if (g_event_loop != NULL) {
event_loop_destroy(g_event_loop);
g_event_loop = NULL;
@@ -365,7 +577,7 @@ int main(void) {
ftp_server_cleanup(&g_server_ctx);
pal_notification_shutdown();
printf("[PS4 FTP] Stopped\n");
printf("[zftpd - ps4] Stopped\n");
return 0;
}
@@ -373,6 +585,7 @@ int main(void) {
#elif defined(PLATFORM_PS5)
#include <ps5/kernel.h>
#include "ps5_net_filter.h"
/**
* @brief Escalate privileges and spoof AuthID to bypass PPR
@@ -426,51 +639,155 @@ static void ps5_jailbreak(void) {
}
}
/* Orderly teardown from the main thread. Worker threads must leave their
* event loops before the structures they access are destroyed. */
static void ps5_shutdown_sequence(void) {
instance_control_stop();
/* Uninstall net filter before tearing down the server.
* CRITICAL: must happen before exit — the hook page is freed here.
* If the payload exits with the sysent still patched, the kernel
* will jump to freed memory on the next connect() syscall. */
#if ENABLE_PS5_NET_FILTER
if (ps5_net_filter_is_active()) {
int urc = ps5_net_filter_uninstall();
if (urc != PS5_NET_FILTER_OK) {
printf("[zftpd - ps5] WARNING: net filter uninstall failed: %s\n",
ps5_net_filter_strerror(urc));
/* Log but continue shutdown — we've done our best. */
} else {
printf("[zftpd - ps5] Net filter: uninstalled\n");
}
}
#endif
#if ENABLE_ZHTTPD
if (g_event_loop != NULL) {
event_loop_stop(g_event_loop);
}
if (g_http_thread_started) {
(void)pthread_join(g_http_thread, NULL);
g_http_thread_started = 0;
}
/* Detach FTP context before tearing down HTTP */
http_api_set_server_ctx(NULL);
if (g_http_server != NULL) {
http_server_destroy(g_http_server);
g_http_server = NULL;
}
#if ENABLE_MCP
if (g_mcp_server != NULL) {
mcp_server_destroy(g_mcp_server);
g_mcp_server = NULL;
printf("[PS5 MCP] Stopped\n");
}
#endif
if (g_event_loop != NULL) {
event_loop_destroy(g_event_loop);
g_event_loop = NULL;
}
printf("[PS5 HTTP] Stopped\n");
#endif
pal_dns_filter_stop();
ftp_server_stop(&g_server_ctx);
ftp_server_cleanup(&g_server_ctx);
pal_notification_shutdown();
printf("[zftpd - ps5] Goodbye!\n");
}
/**
* @brief PlayStation 5 entry point
*/
int main(void) {
printf("[PS5 FTP] Version " RELEASE_VERSION "\n");
printf("[PS5 FTP] Initializing...\n");
printf("[zftpd - ps5] Version " RELEASE_VERSION "\n");
printf("[zftpd - ps5] Initializing...\n");
(void)syscall(SYS_thr_set_name, -1, "zftpd.elf");
signal(SIGPIPE, SIG_IGN);
signal(SIGTERM, ps5_signal_shutdown);
signal(SIGINT, ps5_signal_shutdown);
(void)pal_notification_init();
pid_t existing = find_pid_by_name("zftpd.elf");
if (existing > 0) {
{
char msg[160];
(void)snprintf(msg, sizeof(msg), "zftpd v%s: port %u in use by zftpd",
RELEASE_VERSION, (unsigned)FTP_DEFAULT_PORT);
pal_notification_send(msg);
}
terminate_existing_instance("zftpd.elf");
{
char msg[160];
(void)snprintf(msg, sizeof(msg), "zftpd v%s: process terminated",
RELEASE_VERSION);
pal_notification_send(msg);
}
}
/* Apply kernel patches/jailbreak immediately */
ps5_jailbreak();
/* A previous payload is asked to shut down on its control port first, and it
* answers that request by running its own teardown even when its threads are
* blocked. Anything still alive afterwards is verified by thread name and
* stopped; if a payload keeps holding the console, this instance exits
* instead of binding ports another instance still owns. */
if (instance_control_replace_previous() != 0) {
pal_notification_send("zftpd: previous instance is stuck; restart console");
return EXIT_FAILURE;
}
/*
* Install outbound connection filter.
*
* PURPOSE: Sony system daemons (ScePatchChecker, PFAuthClient, etc.)
* enter aggressive retry loops when their servers are unreachable via
* DNS blocking. Each retry generates real packets that saturate the
* available bandwidth, reducing FTP transfer throughput by up to 40%.
*
* This filter intercepts connect(2) and sendto(2) at the kernel sysent
* level and returns ENETUNREACH immediately for connections to known
* Sony CDN/PSN IP ranges. The daemons interpret this as a local hardware
* error and cease retrying — no packets are generated.
*
* FAILURE IS NON-FATAL: if install fails (unsupported firmware, kernel
* write error, etc.) we log a warning and continue. The FTP server
* operates normally; bandwidth saturation will continue to occur.
*/
{
#if ENABLE_PS5_NET_FILTER
/*
* The kernel primitives the filter needs are not usable while the payload
* is still starting: kernel_get_proc() answers 0 there, which aborts the
* exec page allocation, yet the same call works once the daemons are
* running. Hand the install to a helper thread that keeps retrying, so a
* slow-starting kernel costs nothing but a few hundred milliseconds.
*/
pthread_t nf_thread;
if (pthread_create(&nf_thread, NULL, net_filter_install_deferred, NULL) == 0) {
pthread_detach(nf_thread);
} else {
printf("[zftpd - ps5] Net filter: not installed (helper thread failed)\n");
}
#else
printf("[zftpd - ps5] Kernel net filter disabled\n");
#endif
}
#if defined(PLATFORM_PS4) || defined(PLATFORM_PS5)
/*
* Userland counterpart of the kernel net filter: point the console's DNS at
* this host and the Sony CDN names answer 0.0.0.0. Nothing changes until the
* console is configured to use it, and a busy port 53 is not an error.
*/
(void)pal_dns_filter_start();
#endif
install_signal_handlers();
/* Get PS5 IP address (simplified) */
/*
* Get PS5 primary IP for display/notifications.
* Bind on 0.0.0.0 so loopback (127.0.0.1 localhost) is also reachable.
*/
const char *bind_ip = "0.0.0.0";
char ip_address[INET_ADDRSTRLEN];
if (pal_network_get_primary_ip(ip_address, sizeof(ip_address)) != FTP_OK) {
(void)snprintf(ip_address, sizeof(ip_address), "%s", "0.0.0.0");
}
uint16_t selected_port = FTP_DEFAULT_PORT;
ftp_error_t err = server_init_with_fallback(ip_address, FTP_DEFAULT_PORT, "/",
&selected_port);
ftp_error_t err = ftp_server_init(&g_server_ctx, bind_ip,
FTP_DEFAULT_PORT, "/");
if (err != FTP_OK) {
fprintf(stderr, "[PS5 FTP] Init failed: %d\n", (int)err);
fprintf(stderr, "[zftpd - ps5] Init failed: %d\n", (int)err);
{
char msg[160];
(void)snprintf(msg, sizeof(msg), "zftpd v%s: init failed (%d)",
@@ -480,29 +797,28 @@ int main(void) {
return EXIT_FAILURE;
}
if (selected_port != FTP_DEFAULT_PORT) {
char msg[160];
(void)snprintf(msg, sizeof(msg), "zftpd v%s: port %u in use, fallback %u",
RELEASE_VERSION, (unsigned)FTP_DEFAULT_PORT,
(unsigned)selected_port);
pal_notification_send(msg);
}
printf("[PS5 FTP] Listening on %s:%u\n", ip_address, selected_port);
printf("[zftpd - ps5] Listening on %s:%u\n", ip_address, selected_port);
err = ftp_server_start(&g_server_ctx);
if (err != FTP_OK) {
fprintf(stderr, "[PS5 FTP] Start failed: %d\n", (int)err);
fprintf(stderr, "[zftpd - ps5] Start failed: %d\n", (int)err);
ftp_server_cleanup(&g_server_ctx);
return EXIT_FAILURE;
}
printf("[PS5 FTP] Server running. Press Ctrl+C to stop.\n");
printf("[zftpd - ps5] Server running. Press Ctrl+C to stop.\n");
if (instance_control_start() != 0) {
printf("[zftpd - ps5] Instance control unavailable\n");
ftp_server_stop(&g_server_ctx);
ftp_server_cleanup(&g_server_ctx);
return EXIT_FAILURE;
}
{
char notify_msg[128];
(void)snprintf(notify_msg, sizeof(notify_msg), "zftpd v%s (PS5) started",
(void)snprintf(notify_msg, sizeof(notify_msg), "zftpd by SeregonWar v%s (PS5) started",
RELEASE_VERSION);
pal_notification_send(notify_msg);
@@ -515,14 +831,26 @@ int main(void) {
* ZHTTPD — Start Web File Explorer
*=========================================================================*/
#if ENABLE_ZHTTPD
#if ENABLE_MCP
static mcp_server_t *g_mcp_server = NULL;
#endif
/* Restore persisted downloads before exposing the PS5 Transfers UI. */
(void)transfer_manager_restore();
if (http_csrf_init() != 0) {
ftp_log_line(FTP_LOG_WARN, "CSRF init failed: web upload disabled");
}
/* Attach FTP context so /api/network/reset can reach the session pool */
http_api_set_server_ctx(&g_server_ctx);
g_event_loop = event_loop_create();
if (g_event_loop != NULL) {
g_http_server = http_server_create(g_event_loop, HTTP_DEFAULT_PORT);
char http_bind[64];
(void)snprintf(http_bind, sizeof(http_bind), "[::]:%u",
(unsigned)HTTP_DEFAULT_PORT);
g_http_server = http_server_create(g_event_loop, http_bind, "/");
if (g_http_server != NULL) {
pthread_t http_thread;
int rc = start_http_thread(&http_thread, g_event_loop);
int rc = start_http_thread(&g_http_thread, g_event_loop);
if (rc == 0) {
pthread_detach(http_thread);
g_http_thread_started = 1;
printf("[PS5 HTTP] Web Explorer: http://%s:%u\n", ip_address,
(unsigned)HTTP_DEFAULT_PORT);
{
@@ -531,10 +859,32 @@ int main(void) {
(unsigned)HTTP_DEFAULT_PORT);
pal_notification_send(msg);
}
ps5_start_health_watchdog();
} else {
printf("[PS5 HTTP] Failed to start HTTP thread (rc=%d)\n", rc);
}
}
/*=========================================================================*
* MCP — Start Kernel Analysis Server
*=========================================================================*/
#if ENABLE_MCP
char mcp_bind[64];
(void)snprintf(mcp_bind, sizeof(mcp_bind), "0.0.0.0:%u", (unsigned)MCP_PORT);
g_mcp_server = mcp_server_create(g_event_loop, mcp_bind);
if (g_mcp_server != NULL) {
printf("[PS5 MCP] Kernel analysis: tcp://%s:%u\n", ip_address,
(unsigned)MCP_PORT);
{
char msg[128];
(void)snprintf(msg, sizeof(msg), "MCP: %s:%u", ip_address,
(unsigned)MCP_PORT);
pal_notification_send(msg);
}
} else {
printf("[PS5 MCP] Failed to create MCP server\n");
}
#endif /* ENABLE_MCP */
}
#endif
@@ -547,35 +897,33 @@ int main(void) {
if ((counter++ % 60) == 0) { /* Every 60 seconds */
uint32_t active = ftp_server_get_active_sessions(&g_server_ctx);
if (active > 0U) {
printf("[PS5 FTP] Active sessions: %u\n", active);
printf("[zftpd - ps5] Active sessions: %u\n", active);
}
/* Log net filter statistics every 60 seconds if active */
#if ENABLE_PS5_NET_FILTER
if (ps5_net_filter_is_active()) {
ps5_net_filter_stats_t fstats;
if (ps5_net_filter_get_stats(&fstats) == PS5_NET_FILTER_OK) {
printf("[net_filter] calls=%" PRIu64 " blocked=%" PRIu64
" self=%" PRIu64 " local=%" PRIu64 " other=%" PRIu64 "\n",
fstats.hook_calls_total,
fstats.blocked_total,
fstats.allowed_self,
fstats.allowed_local,
fstats.allowed_other);
}
}
#endif
}
}
printf("\n[PS5 FTP] Shutting down...\n");
#if ENABLE_ZHTTPD
if (g_event_loop != NULL) {
event_loop_stop(g_event_loop);
}
if (g_http_server != NULL) {
http_server_destroy(g_http_server);
g_http_server = NULL;
}
if (g_event_loop != NULL) {
event_loop_destroy(g_event_loop);
g_event_loop = NULL;
}
printf("[PS5 HTTP] Stopped\n");
#endif
ftp_server_stop(&g_server_ctx);
ftp_server_cleanup(&g_server_ctx);
pal_notification_shutdown();
printf("[PS5 FTP] Goodbye!\n");
return EXIT_SUCCESS;
printf("\n[zftpd - ps5] Shutting down...\n");
ps5_shutdown_sequence();
/* The loader leaves a SceSpZeroConfMain thread in this process. Returning
* from main runs libc exit handlers while that foreign thread is still
* active; terminate the whole process after our own services are closed. */
_exit(EXIT_SUCCESS);
}
#else /* POSIX / Linux */
@@ -705,14 +1053,25 @@ int main(int argc, char **argv) {
printf("\n");
printf("FTP server started on 0.0.0.0:%u\n", port);
#if ENABLE_ZHTTPD
/* Pick up the downloads that were still unfinished when the previous
* instance stopped (payload re-injection). */
(void)transfer_manager_restore();
#endif
/*=========================================================================*
* ZHTTPD — Start Web File Explorer
*=========================================================================*/
#if ENABLE_ZHTTPD
http_csrf_init();
if (http_csrf_init() != 0) {
ftp_log_line(FTP_LOG_WARN, "CSRF init failed: web upload disabled");
}
g_event_loop = event_loop_create();
if (g_event_loop != NULL) {
g_http_server = http_server_create(g_event_loop, http_port);
char http_bind[64];
(void)snprintf(http_bind, sizeof(http_bind), "[::]:%u",
(unsigned)http_port);
g_http_server = http_server_create(g_event_loop, http_bind, root_path);
if (g_http_server != NULL) {
pthread_t http_thread;
int rc = start_http_thread(&http_thread, g_event_loop);
+112
View File
@@ -0,0 +1,112 @@
#include "archive_path.h"
#include "pal_limits.h"
#include <errno.h>
#include <string.h>
#include <sys/stat.h>
static int is_separator(char c) { return c == '/' || c == '\\'; }
int archive_path_is_safe_relative(const char *entry, size_t entry_len) {
if (entry == NULL || entry_len == 0U || is_separator(entry[0]) ||
memchr(entry, '\0', entry_len) != NULL)
return 0;
if (entry_len >= 2U && entry[1] == ':') return 0;
while (entry_len > 0U && is_separator(entry[entry_len - 1U])) entry_len--;
if (entry_len == 0U) return 0;
size_t component_start = 0U;
for (size_t i = 0U; i <= entry_len; i++) {
if (i < entry_len && !is_separator(entry[i])) {
if ((unsigned char)entry[i] < 0x20U) return 0;
continue;
}
size_t component_len = i - component_start;
if (component_len == 0U) return 0;
if ((component_len == 1U && entry[component_start] == '.') ||
(component_len == 2U && entry[component_start] == '.' &&
entry[component_start + 1U] == '.'))
return 0;
component_start = i + 1U;
}
return 1;
}
static int ensure_directory(const char *path) {
struct stat st;
if (lstat(path, &st) == 0)
return S_ISDIR(st.st_mode) && !S_ISLNK(st.st_mode) ? 0 : -1;
if (errno != ENOENT || mkdir(path, 0777) != 0) return -1;
return lstat(path, &st) == 0 && S_ISDIR(st.st_mode) && !S_ISLNK(st.st_mode)
? 0
: -1;
}
static int append_component(char *path, size_t path_size,
const char *component, size_t component_len) {
size_t used = strlen(path);
int need_slash = used == 0U || path[used - 1U] != '/';
size_t needed = used + (size_t)need_slash + component_len + 1U;
if (needed > path_size) return -1;
if (need_slash) path[used++] = '/';
memcpy(path + used, component, component_len);
path[used + component_len] = '\0';
return 0;
}
static int prepare_root(const char *root, char *out, size_t out_size) {
if (root == NULL || root[0] != '/') return -1;
size_t len = strlen(root);
if (len == 0U || len >= out_size) return -1;
memcpy(out, root, len + 1U);
while (len > 1U && out[len - 1U] == '/') out[--len] = '\0';
char walk[PAL_PATH_MAX] = "/";
const char *p = out + 1;
while (*p != '\0') {
const char *slash = strchr(p, '/');
size_t component_len = slash != NULL ? (size_t)(slash - p) : strlen(p);
if (component_len == 0U ||
append_component(walk, sizeof(walk), p, component_len) != 0 ||
ensure_directory(walk) != 0)
return -1;
if (slash == NULL) break;
p = slash + 1;
}
return 0;
}
int archive_path_prepare(const char *root, const char *entry, size_t entry_len,
int is_directory, char *out, size_t out_size) {
if (out == NULL || out_size == 0U ||
!archive_path_is_safe_relative(entry, entry_len))
return -1;
while (entry_len > 0U && is_separator(entry[entry_len - 1U])) entry_len--;
if (prepare_root(root, out, out_size) != 0) return -1;
size_t component_start = 0U;
for (size_t i = 0U; i <= entry_len; i++) {
if (i < entry_len && !is_separator(entry[i])) continue;
size_t component_len = i - component_start;
if (append_component(out, out_size, entry + component_start,
component_len) != 0)
return -1;
int is_last = i == entry_len;
if (!is_last || is_directory) {
if (ensure_directory(out) != 0) return -1;
} else {
struct stat st;
if (lstat(out, &st) == 0) {
if (S_ISLNK(st.st_mode) || !S_ISREG(st.st_mode) || st.st_nlink != 1)
return -1;
} else if (errno != ENOENT) {
return -1;
}
}
component_start = i + 1U;
}
return 0;
}
+491
View File
@@ -0,0 +1,491 @@
/* ══ BUILTIN UNZIP ═══════════════════════════════════════════════════════════
* Self-contained ZIP extractor backed by the bundled miniz reader.
*
* No platform library dependencies — compiles and runs on PS4/PS5 without
* libarchive or libz.
*
* Supported by miniz: Store (method 0), Deflate (method 8), Zip64, data
* descriptors, and CRC validation.
*
* ZIP reference: PKWARE APPNOTE.TXT v6.3.4
* Deflate ref: RFC 1951
* ═════════════════════════════════════════════════════════════════════════ */
#include "builtin_unzip.h"
#include "archive_path.h"
#include "pal_limits.h"
#include "ftp_types.h"
#include <errno.h>
#include <fcntl.h>
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#define MINIZ_NO_ARCHIVE_WRITING_APIS
#define MINIZ_NO_DEFLATE_APIS
#define MINIZ_NO_STDIO
#define MINIZ_NO_TIME
#define MINIZ_NO_ZLIB_APIS
#define MINIZ_NO_ZLIB_COMPATIBLE_NAMES
#if defined(__clang__)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wcast-align"
#pragma clang diagnostic ignored "-Wcast-qual"
#pragma clang diagnostic ignored "-Wconversion"
#pragma clang diagnostic ignored "-Wmissing-prototypes"
#pragma clang diagnostic ignored "-Wredundant-decls"
#pragma clang diagnostic ignored "-Wshadow"
#pragma clang diagnostic ignored "-Wsign-conversion"
#pragma clang diagnostic ignored "-Wstrict-prototypes"
#pragma clang diagnostic ignored "-Wundef"
#pragma clang diagnostic ignored "-Wunused-function"
#pragma clang diagnostic ignored "-Wwrite-strings"
#elif defined(__GNUC__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wcast-align"
#pragma GCC diagnostic ignored "-Wcast-qual"
#pragma GCC diagnostic ignored "-Wconversion"
#pragma GCC diagnostic ignored "-Wmissing-prototypes"
#pragma GCC diagnostic ignored "-Wredundant-decls"
#pragma GCC diagnostic ignored "-Wshadow"
#pragma GCC diagnostic ignored "-Wsign-conversion"
#pragma GCC diagnostic ignored "-Wstrict-prototypes"
#pragma GCC diagnostic ignored "-Wundef"
#pragma GCC diagnostic ignored "-Wunused-function"
#pragma GCC diagnostic ignored "-Wwrite-strings"
#endif
#include "../external/miniz/miniz.h"
#include "../external/miniz/miniz.c"
#include "../external/miniz/miniz_tinfl.c"
#include "../external/miniz/miniz_zip.c"
#if defined(__clang__)
#pragma clang diagnostic pop
#elif defined(__GNUC__)
#pragma GCC diagnostic pop
#endif
#define ZIP_WRITE_BUF_SIZE (256U * 1024U)
/* Write callback: append to output fd */
typedef struct {
int fd;
uint8_t buf[ZIP_WRITE_BUF_SIZE];
size_t buf_pos;
} write_ctx_t;
static int write_flush(write_ctx_t *wc) {
if (wc->buf_pos == 0) return 0;
size_t written = 0;
while (written < wc->buf_pos) {
ssize_t n = write(wc->fd, wc->buf + written, wc->buf_pos - written);
if (n < 0) {
if (errno == EINTR) continue;
return -1;
}
written += (size_t)n;
}
wc->buf_pos = 0;
return 0;
}
static int write_byte(void *userdata, const uint8_t *data, size_t len) {
write_ctx_t *wc = (write_ctx_t *)userdata;
size_t off = 0U;
while (off < len) {
size_t space = ZIP_WRITE_BUF_SIZE - wc->buf_pos;
size_t chunk = len - off;
if (chunk > space) {
chunk = space;
}
memcpy(wc->buf + wc->buf_pos, data + off, chunk);
wc->buf_pos += chunk;
off += chunk;
if (wc->buf_pos >= ZIP_WRITE_BUF_SIZE) {
if (write_flush(wc) != 0) return -1;
}
}
return 0;
}
typedef struct {
int fd;
} unzip_read_ctx_t;
static size_t unzip_miniz_read(void *opaque, mz_uint64 file_ofs,
void *buf, size_t len) {
unzip_read_ctx_t *ctx = (unzip_read_ctx_t *)opaque;
uint8_t *out = (uint8_t *)buf;
size_t done = 0U;
if (ctx == NULL || ctx->fd < 0 || buf == NULL) {
return 0U;
}
off_t off = (off_t)file_ofs;
if ((mz_uint64)off != file_ofs) {
return 0U;
}
while (done < len) {
ssize_t n = pread(ctx->fd, out + done, len - done, off + (off_t)done);
if (n < 0) {
if (errno == EINTR) {
continue;
}
break;
}
if (n == 0) {
break;
}
done += (size_t)n;
}
return done;
}
typedef struct {
write_ctx_t wc;
_Atomic int *cancelled;
} unzip_write_ctx_t;
static size_t unzip_miniz_write(void *opaque, mz_uint64 file_ofs,
const void *buf, size_t len) {
unzip_write_ctx_t *ctx = (unzip_write_ctx_t *)opaque;
(void)file_ofs;
if (ctx == NULL || buf == NULL) {
return 0U;
}
if (ctx->cancelled != NULL &&
atomic_load_explicit(ctx->cancelled, memory_order_relaxed) != 0) {
return 0U;
}
return (write_byte(&ctx->wc, (const uint8_t *)buf, len) == 0) ? len : 0U;
}
/* Materialize a safe in-archive symlink as a regular file. This also works on
* console USB filesystems that cannot store symlinks. Links escaping the
* extraction tree, linking to another symlink or targeting a hardlink fail. */
static int materialize_zip_link(mz_zip_archive *zip, mz_uint index,
const char *dest_dir, const char *entry_name,
const mz_zip_archive_file_stat *file_stat,
_Atomic int *cancelled, char *error_msg,
size_t error_msg_size) {
if (file_stat->m_uncomp_size == 0U ||
file_stat->m_uncomp_size >= PAL_PATH_MAX) goto unsafe_link;
size_t target_len = 0U;
char *target = mz_zip_reader_extract_to_heap(zip, index, &target_len, 0);
if (target == NULL) goto unsafe_link;
int safe = target_len == (size_t)file_stat->m_uncomp_size &&
archive_path_is_safe_relative(target, target_len);
if (!safe) { mz_free(target); goto unsafe_link; }
char source_entry[PAL_PATH_MAX];
const char *slash = strrchr(entry_name, '/');
size_t prefix = slash != NULL ? (size_t)(slash - entry_name + 1) : 0U;
if (prefix + target_len >= sizeof(source_entry)) {
mz_free(target); goto unsafe_link;
}
memcpy(source_entry, entry_name, prefix);
memcpy(source_entry + prefix, target, target_len);
source_entry[prefix + target_len] = '\0';
mz_free(target);
int target_is_archived = 0;
mz_uint num_files = mz_zip_reader_get_num_files(zip);
for (mz_uint j = 0U; j < num_files; j++) {
char candidate[PAL_PATH_MAX];
mz_uint needed = mz_zip_reader_get_filename(zip, j, NULL, 0);
if (needed == 0U || needed > (mz_uint)sizeof(candidate)) continue;
(void)mz_zip_reader_get_filename(zip, j, candidate,
(mz_uint)sizeof(candidate));
if (strcmp(candidate, source_entry) != 0) continue;
mz_zip_archive_file_stat candidate_stat;
if (mz_zip_reader_file_stat(zip, j, &candidate_stat) &&
!candidate_stat.m_is_directory && candidate_stat.m_is_supported &&
(((mode_t)(candidate_stat.m_external_attr >> 16)) & S_IFMT) != S_IFLNK)
target_is_archived = 1;
break;
}
if (!target_is_archived) goto unsafe_link;
char source[PAL_PATH_MAX], output[PAL_PATH_MAX];
if (archive_path_prepare(dest_dir, source_entry, strlen(source_entry), 0,
source, sizeof(source)) != 0 ||
archive_path_prepare(dest_dir, entry_name, strlen(entry_name), 0,
output, sizeof(output)) != 0 ||
strcmp(source, output) == 0)
goto unsafe_link;
int in_flags = O_RDONLY;
#ifdef O_NOFOLLOW
in_flags |= O_NOFOLLOW;
#endif
int input = open(source, in_flags);
struct stat input_st;
if (input < 0 || fstat(input, &input_st) != 0 ||
!S_ISREG(input_st.st_mode) || input_st.st_nlink != 1) {
if (input >= 0) (void)close(input);
goto unsafe_link;
}
if (unlink(output) != 0 && errno != ENOENT) {
(void)close(input);
goto unsafe_link;
}
int out_flags = O_WRONLY | O_CREAT | O_EXCL;
#ifdef O_NOFOLLOW
out_flags |= O_NOFOLLOW;
#endif
int output_fd = open(output, out_flags, 0666);
if (output_fd < 0) { (void)close(input); goto unsafe_link; }
int result = 0;
unsigned char buf[64U * 1024U];
for (;;) {
if (cancelled != NULL &&
atomic_load_explicit(cancelled, memory_order_relaxed) != 0) {
result = -1;
break;
}
ssize_t n = read(input, buf, sizeof(buf));
if (n < 0 && errno == EINTR) continue;
if (n < 0) result = -1;
if (n <= 0) break;
size_t off = 0U;
while (off < (size_t)n) {
ssize_t written = write(output_fd, buf + off, (size_t)n - off);
if (written < 0 && errno == EINTR) continue;
if (written <= 0) { result = -1; break; }
off += (size_t)written;
}
if (result != 0) break;
}
if (close(input) != 0) result = -1;
if (close(output_fd) != 0) result = -1;
if (result != 0) {
(void)unlink(output);
if (error_msg != NULL)
snprintf(error_msg, error_msg_size, "Cannot copy ZIP link: %s", entry_name);
}
return result;
unsafe_link:
if (error_msg != NULL)
snprintf(error_msg, error_msg_size, "Unsafe or missing ZIP link target: %s",
entry_name);
return -1;
}
int builtin_unzip(const char *zip_path, const char *dest_dir,
_Atomic int *cancelled,
char *error_msg, size_t error_msg_size) {
if (error_msg && error_msg_size > 0) error_msg[0] = '\0';
int zip_fd = open(zip_path, O_RDONLY);
if (zip_fd < 0) {
if (error_msg) snprintf(error_msg, error_msg_size,
"Cannot open ZIP: %s", strerror(errno));
return -1;
}
struct stat st;
if (fstat(zip_fd, &st) != 0 || st.st_size < 0) {
if (error_msg) snprintf(error_msg, error_msg_size, "Cannot stat ZIP: %s",
strerror(errno));
close(zip_fd);
return -1;
}
mz_zip_archive zip;
mz_zip_zero_struct(&zip);
unzip_read_ctx_t read_ctx;
read_ctx.fd = zip_fd;
zip.m_pRead = unzip_miniz_read;
zip.m_pIO_opaque = &read_ctx;
if (!mz_zip_reader_init(&zip, (mz_uint64)st.st_size, 0)) {
mz_zip_error mz_code = mz_zip_get_last_error(&zip);
const char *mz_err = mz_zip_get_error_string(mz_zip_get_last_error(&zip));
if (error_msg) {
if (mz_code == MZ_ZIP_FAILED_FINDING_CENTRAL_DIR)
snprintf(error_msg, error_msg_size,
"Not a valid or complete ZIP archive (directory missing)");
else
snprintf(error_msg, error_msg_size, "Invalid ZIP: %s",
mz_err ? mz_err : "unknown error");
}
close(zip_fd);
return -1;
}
int result = 0;
mz_uint num_files = mz_zip_reader_get_num_files(&zip);
for (mz_uint i = 0; i < num_files; i++) {
if (cancelled != NULL &&
atomic_load_explicit(cancelled, memory_order_relaxed) != 0) {
if (error_msg) snprintf(error_msg, error_msg_size, "Cancelled");
result = -1;
break;
}
char entry_name[PAL_PATH_MAX];
mz_uint name_need = mz_zip_reader_get_filename(&zip, i, NULL, 0);
if (name_need == 0U || name_need > (mz_uint)sizeof(entry_name)) {
if (error_msg) snprintf(error_msg, error_msg_size,
"ZIP entry path too long");
result = -1;
break;
}
(void)mz_zip_reader_get_filename(&zip, i, entry_name,
(mz_uint)sizeof(entry_name));
mz_zip_archive_file_stat file_stat;
if (!mz_zip_reader_file_stat(&zip, i, &file_stat)) {
const char *mz_err = mz_zip_get_error_string(mz_zip_get_last_error(&zip));
if (error_msg) snprintf(error_msg, error_msg_size,
"ZIP metadata error: %s",
mz_err ? mz_err : "unknown error");
result = -1;
break;
}
size_t name_len = strlen(entry_name);
if (name_len == 0U) {
continue;
}
mode_t archived_mode = (mode_t)(file_stat.m_external_attr >> 16);
if ((archived_mode & S_IFMT) == S_IFLNK) {
/* Resolve links after ordinary entries, regardless of ZIP ordering. */
continue;
}
if (file_stat.m_is_directory ||
entry_name[name_len - 1U] == '/' ||
entry_name[name_len - 1U] == '\\') {
while (name_len > 0U &&
(entry_name[name_len - 1U] == '/' ||
entry_name[name_len - 1U] == '\\')) {
name_len--;
}
if (name_len == 0U) {
continue;
}
char dest_full[PAL_PATH_MAX];
if (archive_path_prepare(dest_dir, entry_name, name_len, 1,
dest_full, sizeof(dest_full)) != 0) {
if (error_msg)
snprintf(error_msg, error_msg_size, "Unsafe ZIP directory: %s",
entry_name);
result = -1;
break;
}
continue;
}
if (!file_stat.m_is_supported) {
if (error_msg && file_stat.m_is_encrypted) {
snprintf(error_msg, error_msg_size,
"Encrypted ZIP entry is not supported: %s", entry_name);
} else if (error_msg) {
snprintf(error_msg, error_msg_size,
"Unsupported ZIP compression method %u in: %s",
(unsigned)file_stat.m_method, entry_name);
}
result = -1;
break;
}
char dest_full[PAL_PATH_MAX];
if (archive_path_prepare(dest_dir, entry_name, name_len, 0,
dest_full, sizeof(dest_full)) != 0) {
if (error_msg)
snprintf(error_msg, error_msg_size, "Unsafe ZIP entry: %s", entry_name);
result = -1;
break;
}
if (unlink(dest_full) != 0 && errno != ENOENT) {
if (error_msg)
snprintf(error_msg, error_msg_size, "Cannot replace %s: %s",
dest_full, strerror(errno));
result = -1;
break;
}
int open_flags = O_WRONLY | O_CREAT | O_EXCL;
#ifdef O_NOFOLLOW
open_flags |= O_NOFOLLOW;
#endif
int out_fd = open(dest_full, open_flags, 0666);
struct stat out_st;
if (out_fd < 0 || fstat(out_fd, &out_st) != 0 ||
!S_ISREG(out_st.st_mode) || out_st.st_nlink != 1) {
int saved_errno = errno;
if (out_fd >= 0) close(out_fd);
(void)unlink(dest_full);
if (error_msg)
snprintf(error_msg, error_msg_size, "Cannot create %s: %s",
dest_full, strerror(saved_errno));
result = -1;
break;
}
unzip_write_ctx_t write_ctx;
write_ctx.wc.fd = out_fd;
write_ctx.wc.buf_pos = 0U;
write_ctx.cancelled = cancelled;
if (!mz_zip_reader_extract_to_callback(&zip, i, unzip_miniz_write,
&write_ctx, 0) ||
write_flush(&write_ctx.wc) != 0) {
const char *mz_err = mz_zip_get_error_string(mz_zip_get_last_error(&zip));
if (error_msg && cancelled != NULL &&
atomic_load_explicit(cancelled, memory_order_relaxed) != 0) {
snprintf(error_msg, error_msg_size, "Cancelled");
} else if (error_msg) {
snprintf(error_msg, error_msg_size, "Failed to extract %s: %s",
entry_name, mz_err ? mz_err : strerror(errno));
}
close(out_fd);
(void)unlink(dest_full);
result = -1;
break;
}
close(out_fd);
}
/* Resolve only links to regular files extracted inside this archive. */
for (mz_uint i = 0U; result == 0 && i < num_files; i++) {
mz_zip_archive_file_stat file_stat;
if (!mz_zip_reader_file_stat(&zip, i, &file_stat)) { result = -1; break; }
mode_t archived_mode = (mode_t)(file_stat.m_external_attr >> 16);
if ((archived_mode & S_IFMT) != S_IFLNK) continue;
char entry_name[PAL_PATH_MAX];
mz_uint name_need = mz_zip_reader_get_filename(&zip, i, NULL, 0);
if (name_need == 0U || name_need > (mz_uint)sizeof(entry_name)) {
result = -1;
break;
}
(void)mz_zip_reader_get_filename(&zip, i, entry_name,
(mz_uint)sizeof(entry_name));
result = materialize_zip_link(&zip, i, dest_dir, entry_name, &file_stat,
cancelled, error_msg, error_msg_size);
}
(void)mz_zip_reader_end(&zip);
close(zip_fd);
return result;
}
+643
View File
@@ -0,0 +1,643 @@
/*
* Feature-test macros must appear before any system include.
* _FILE_OFFSET_BITS=64: enables fseeko/ftello on 32-bit hosts.
* _POSIX_C_SOURCE=200809L: exposes fseeko, fdopen, strcasecmp etc. in strict C99 mode.
*/
#ifndef _FILE_OFFSET_BITS
# define _FILE_OFFSET_BITS 64
#endif
#ifndef _POSIX_C_SOURCE
# define _POSIX_C_SOURCE 200809L
#endif
/*
* exfat_unpacker — exFAT filesystem image parser
* Copyright (C) 2025 seregonwar
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* exfat_unpacker.c — read-only exFAT filesystem image parser
*
* Fixes applied over the original version:
* 1. Boot sector: volume_flags uint32→uint16; bytes_per_sector_shift now
* correctly at 0x6C instead of 0x6E.
* 2. File entry struct trimmed to exactly 32 bytes; phantom fields removed.
* 3. Filename entry: name_length/name_hash moved to stream extension where
* they actually live; out-of-bounds reads eliminated.
* 4. exfat_read_directory: follows the cluster chain (was reading only the
* first cluster, silently truncating large directories).
* 5. exfat_parse_file_entry: gets NameLength from stream extension;
* accumulates chars across multiple filename entries; respects the
* secondary_count to avoid walking beyond the entry set.
* 6. File extraction: uses a single stack buffer (EXFAT_IO_CHUNK_SIZE)
* instead of per-cluster malloc; respects the NoFatChain flag.
* 7. Added exfat_extract_to_buffer() for in-memory extraction.
* 8. fseeko() used for large-file offsets (> 2 GiB).
*/
#include "exfat_unpacker.h"
#include <errno.h>
#if defined(_MSC_VER)
# include <io.h>
# include <fcntl.h>
# define dup _dup
# define fdopen _fdopen
# define close _close
# define write _write
# define fseeko _fseeki64
# define ftello _ftelli64
# define lseek64 _lseeki64
#else
# include <unistd.h> /* write(), close() */
# define lseek64 lseek
#endif
/* ── Boot sector ────────────────────────────────────────────────────────── */
int exfat_read_boot_sector(exfat_context_t *ctx) {
if (!ctx || !ctx->image_file)
return -1;
if (fseeko(ctx->image_file, 0, SEEK_SET) != 0) {
perror("[exFAT] fseeko boot sector");
return -1;
}
if (fread(&ctx->boot_sector, sizeof(exfat_boot_sector_t), 1,
ctx->image_file) != 1) {
perror("[exFAT] fread boot sector");
return -1;
}
if (exfat_validate_boot_sector(&ctx->boot_sector) != 0)
return -1;
/*
* Derive working constants.
* Shift values are validated by exfat_validate_boot_sector().
*/
ctx->bytes_per_sector = 1U << ctx->boot_sector.bytes_per_sector_shift;
ctx->sectors_per_cluster = 1U << ctx->boot_sector.sectors_per_cluster_shift;
ctx->bytes_per_cluster = ctx->bytes_per_sector * ctx->sectors_per_cluster;
ctx->cluster_heap_offset_bytes =
(uint64_t)ctx->boot_sector.cluster_heap_offset * ctx->bytes_per_sector;
return 0;
}
int exfat_validate_boot_sector(const exfat_boot_sector_t *boot) {
if (!boot)
return -1;
/* FileSystemName must be "EXFAT " (with 3 trailing spaces) */
if (memcmp(boot->fs_name, EXFAT_FS_NAME, EXFAT_FS_NAME_LEN) != 0) {
fprintf(stderr, "[exFAT] invalid filesystem name\n");
return -1;
}
if (boot->boot_signature != EXFAT_BOOT_SIGNATURE) {
fprintf(stderr, "[exFAT] invalid boot signature: 0x%04X\n",
boot->boot_signature);
return -1;
}
/* Per spec §3.1.13: BytesPerSectorShift in [9, 12] */
if (boot->bytes_per_sector_shift < 9 || boot->bytes_per_sector_shift > 12) {
fprintf(stderr, "[exFAT] BytesPerSectorShift=%u out of range [9,12]\n",
boot->bytes_per_sector_shift);
return -1;
}
/* Per spec §3.1.14: BytesPerSectorShift + SectorsPerClusterShift <= 25 */
if ((unsigned)boot->bytes_per_sector_shift +
(unsigned)boot->sectors_per_cluster_shift > 25U) {
fprintf(stderr,
"[exFAT] shift sum %u+%u > 25 (cluster too large)\n",
boot->bytes_per_sector_shift,
boot->sectors_per_cluster_shift);
return -1;
}
return 0;
}
void exfat_print_boot_sector(const exfat_boot_sector_t *boot) {
if (!boot) return;
printf("=== exFAT Boot Sector ===\n");
printf(" FileSystemName : %.8s\n", boot->fs_name);
printf(" FatOffset : %u sectors\n", boot->fat_offset);
printf(" FatLength : %u sectors\n", boot->fat_length);
printf(" ClusterHeapOffset : %u sectors\n", boot->cluster_heap_offset);
printf(" TotalClusters : %u\n", boot->total_clusters);
printf(" RootDirFirstCluster : %u\n", boot->root_dir_first_cluster);
printf(" VolumeSerialNumber : 0x%08X\n", boot->volume_serial);
printf(" FileSystemRevision : 0x%04X\n", boot->fs_revision);
printf(" BytesPerSectorShift : %u (= %u bytes/sector)\n",
boot->bytes_per_sector_shift, 1U << boot->bytes_per_sector_shift);
printf(" SectorsPerClusterShift: %u (= %u sectors/cluster)\n",
boot->sectors_per_cluster_shift,
1U << boot->sectors_per_cluster_shift);
printf(" NumberOfFats : %u\n", boot->num_fats);
printf(" PercentInUse : %u%%\n", boot->percent_in_use);
printf("\n");
}
/* ── FAT ────────────────────────────────────────────────────────────────── */
int exfat_read_fat(exfat_context_t *ctx) {
if (!ctx || !ctx->image_file)
return -1;
uint64_t fat_offset_bytes =
(uint64_t)ctx->boot_sector.fat_offset * ctx->bytes_per_sector;
uint64_t fat_size_bytes =
(uint64_t)ctx->boot_sector.fat_length * ctx->bytes_per_sector;
/* Sanity: FAT must cover at least total_clusters+2 entries */
size_t min_entries = (size_t)ctx->boot_sector.total_clusters + 2U;
size_t available = (size_t)(fat_size_bytes / sizeof(uint32_t));
if (available < min_entries) {
fprintf(stderr,
"[exFAT] FAT too small: %zu entries, need %zu\n",
available, min_entries);
return -1;
}
ctx->fat_table = (uint32_t *)malloc(fat_size_bytes);
if (!ctx->fat_table) {
perror("[exFAT] malloc FAT");
return -1;
}
ctx->fat_entries = available;
if (fseeko(ctx->image_file, (off_t)fat_offset_bytes, SEEK_SET) != 0) {
perror("[exFAT] fseeko FAT");
free(ctx->fat_table);
ctx->fat_table = NULL;
return -1;
}
if (fread(ctx->fat_table, fat_size_bytes, 1, ctx->image_file) != 1) {
perror("[exFAT] fread FAT");
free(ctx->fat_table);
ctx->fat_table = NULL;
return -1;
}
return 0;
}
uint32_t exfat_get_next_cluster(const exfat_context_t *ctx, uint32_t cluster) {
if (!ctx || !ctx->fat_table)
return EXFAT_FAT_END_OF_CHAIN;
if (cluster >= (uint32_t)ctx->fat_entries)
return EXFAT_FAT_END_OF_CHAIN;
return ctx->fat_table[cluster];
}
void exfat_free_fat(exfat_context_t *ctx) {
if (ctx && ctx->fat_table) {
free(ctx->fat_table);
ctx->fat_table = NULL;
ctx->fat_entries = 0;
}
}
/* ── Cluster I/O ────────────────────────────────────────────────────────── */
uint64_t exfat_get_cluster_offset(const exfat_context_t *ctx, uint32_t cluster) {
if (!ctx) return 0;
return ctx->cluster_heap_offset_bytes +
(uint64_t)(cluster - 2U) * ctx->bytes_per_cluster;
}
ssize_t exfat_read_cluster(exfat_context_t *ctx, uint32_t cluster,
uint8_t *buffer, size_t size) {
if (!ctx || !ctx->image_file || !buffer)
return -1;
if (!exfat_is_valid_cluster(cluster)) {
fprintf(stderr, "[exFAT] invalid cluster %u\n", cluster);
return -1;
}
uint64_t offset = exfat_get_cluster_offset(ctx, cluster);
size_t bytes_to_read = (size < ctx->bytes_per_cluster) ?
size : ctx->bytes_per_cluster;
if (fseeko(ctx->image_file, (off_t)offset, SEEK_SET) != 0) {
perror("[exFAT] fseeko cluster");
return -1;
}
size_t got = fread(buffer, 1, bytes_to_read, ctx->image_file);
return (ssize_t)got;
}
/* ── Directory parsing ──────────────────────────────────────────────────── */
int exfat_utf16_to_utf8(const uint16_t *utf16, int nchars,
char *utf8, size_t utf8_size) {
if (!utf16 || !utf8 || utf8_size == 0)
return -1;
size_t out = 0;
for (int i = 0; i < nchars && out < utf8_size - 1U; i++) {
uint16_t ch = utf16[i];
if (ch == 0) break;
if (ch < 0x80U) {
utf8[out++] = (char)ch;
} else if (ch < 0x800U) {
if (out + 2 >= utf8_size) break;
utf8[out++] = (char)(0xC0U | (ch >> 6));
utf8[out++] = (char)(0x80U | (ch & 0x3FU));
} else {
if (out + 3 >= utf8_size) break;
utf8[out++] = (char)(0xE0U | (ch >> 12));
utf8[out++] = (char)(0x80U | ((ch >> 6) & 0x3FU));
utf8[out++] = (char)(0x80U | (ch & 0x3FU));
}
}
utf8[out] = '\0';
return 0;
}
/*
* Parse one complete directory entry set (0x85 + secondaries).
*
* @param data flat buffer containing one or more 32-byte entries
* @param offset byte offset of the 0x85 entry within data[]
* @param data_len total valid bytes in data[]
* @param info output
*
* @return total bytes consumed by this entry set (= (1+secondary_count)*32),
* or -1 if the entry is not a 0x85 file/dir entry or data is short.
*/
int exfat_parse_file_entry(const uint8_t *data, size_t offset, size_t data_len,
exfat_file_info_t *info) {
if (!data || !info || offset >= data_len)
return -1;
const exfat_file_entry_t *fe =
(const exfat_file_entry_t *)(data + offset);
if (fe->entry_type != EXFAT_ENTRY_TYPE_FILE_DIR)
return -1;
uint8_t secondary_count = fe->secondary_count;
size_t total_bytes = (size_t)(1 + secondary_count) * EXFAT_ENTRY_SIZE;
if (offset + total_bytes > data_len)
return -1; /* entry set extends beyond buffer */
memset(info, 0, sizeof(exfat_file_info_t));
info->attributes = fe->file_attributes;
info->create_time = fe->create_time;
info->last_modified_time= fe->last_modified_time;
info->last_accessed_time= fe->last_accessed_time;
info->is_directory = exfat_is_directory(fe->file_attributes);
/* Walk secondary entries ─────────────────────────────────────────── */
int name_chars_collected = 0; /* chars accumulated so far */
int name_length = 0; /* from stream extension */
char name_buf[EXFAT_MAX_FILENAME_LEN * 4 + 1]; /* worst-case UTF-8 */
size_t name_buf_used = 0;
for (int s = 0; s < (int)secondary_count; s++) {
const uint8_t *sec = data + offset + (size_t)(s + 1) * EXFAT_ENTRY_SIZE;
uint8_t stype = sec[0];
if (stype == EXFAT_ENTRY_TYPE_STREAM_EXT) {
const exfat_stream_ext_t *sx = (const exfat_stream_ext_t *)sec;
info->first_cluster = sx->first_cluster;
info->data_length = sx->data_length;
info->no_fat_chain = (sx->flags & EXFAT_FLAG_NO_FAT_CHAIN) ? 1 : 0;
name_length = (int)sx->name_length; /* total chars */
} else if (stype == EXFAT_ENTRY_TYPE_FILENAME) {
const exfat_filename_entry_t *fn =
(const exfat_filename_entry_t *)sec;
/*
* Each filename entry carries up to 15 UTF-16LE code units.
* Collect until we reach name_length chars (from stream ext).
* This correctly handles filenames longer than 15 chars that
* span multiple filename entries.
*/
int chars_remaining = name_length - name_chars_collected;
if (chars_remaining <= 0)
break;
int chars_here = (chars_remaining < 15) ? chars_remaining : 15;
/*
* Copy the packed uint16_t array to a local aligned buffer before
* passing to exfat_utf16_to_utf8(). Taking the address of a packed
* struct member directly causes -Waddress-of-packed-member and
* potential UB on architectures that require aligned 16-bit reads.
*/
uint16_t name_copy[15];
memcpy(name_copy, fn->file_name, (size_t)chars_here * sizeof(uint16_t));
char chunk[15 * 4 + 1];
exfat_utf16_to_utf8(name_copy, chars_here,
chunk, sizeof(chunk));
size_t chunk_len = strlen(chunk);
if (name_buf_used + chunk_len < sizeof(name_buf)) {
memcpy(name_buf + name_buf_used, chunk, chunk_len);
name_buf_used += chunk_len;
name_chars_collected += chars_here;
}
}
}
name_buf[name_buf_used] = '\0';
/* Copy into the fixed-size filename field; snprintf ensures NUL */
snprintf(info->filename, sizeof(info->filename), "%s", name_buf);
return (int)total_bytes;
}
/*
* Read all directory entries from a cluster chain.
*
* FIX: the original only read the first cluster. This implementation
* follows the FAT chain (or advances contiguously for NoFatChain dirs)
* until the chain ends or max_entries is reached.
*
* @note Directories themselves never set NoFatChain in practice, but we
* handle it for correctness.
*/
int exfat_read_directory(exfat_context_t *ctx, uint32_t first_cluster,
exfat_file_info_t *entries, int max_entries) {
if (!ctx || !entries || max_entries <= 0)
return -1;
uint8_t *cluster_buf = (uint8_t *)malloc(ctx->bytes_per_cluster);
if (!cluster_buf) {
perror("[exFAT] malloc directory buffer");
return -1;
}
int entry_count = 0;
uint32_t cluster = first_cluster;
while (exfat_is_valid_cluster(cluster) && entry_count < max_entries) {
ssize_t got = exfat_read_cluster(ctx, cluster, cluster_buf,
ctx->bytes_per_cluster);
if (got <= 0)
break;
size_t offset = 0;
while (offset + EXFAT_ENTRY_SIZE <= (size_t)got &&
entry_count < max_entries) {
uint8_t etype = cluster_buf[offset];
if (etype == EXFAT_ENTRY_TYPE_EOD)
goto done; /* end-of-directory — stop scanning */
if (etype != EXFAT_ENTRY_TYPE_FILE_DIR) {
offset += EXFAT_ENTRY_SIZE;
continue;
}
int consumed = exfat_parse_file_entry(
cluster_buf, offset, (size_t)got,
&entries[entry_count]);
if (consumed <= 0) {
offset += EXFAT_ENTRY_SIZE;
continue;
}
/* Only count entries with a non-empty filename */
if (entries[entry_count].filename[0] != '\0')
entry_count++;
offset += (size_t)consumed;
}
/* Advance to next cluster */
uint32_t next = exfat_get_next_cluster(ctx, cluster);
if (exfat_is_end_of_chain(next))
break;
cluster = next;
}
done:
free(cluster_buf);
return entry_count;
}
/* ── File extraction helpers ────────────────────────────────────────────── */
/*
* Core read loop shared by all three extraction functions.
*
* Reads the file described by `info` in EXFAT_IO_CHUNK_SIZE blocks and
* calls write_cb(write_arg, buf, len) for each. Returns 0 on success.
*
* NoFatChain support: if info->no_fat_chain is set, advance clusters
* sequentially instead of following the FAT.
*/
typedef int (*exfat_write_cb_t)(void *arg, const uint8_t *buf, size_t len);
static int exfat_read_file_data(exfat_context_t *ctx,
const exfat_file_info_t *info,
exfat_write_cb_t write_cb, void *write_arg) {
uint8_t io_buf[EXFAT_IO_CHUNK_SIZE]; /* fixed stack buffer, no malloc */
uint64_t remaining = info->data_length;
uint32_t cluster = info->first_cluster;
if (!exfat_is_valid_cluster(cluster) && remaining > 0) {
fprintf(stderr, "[exFAT] invalid first cluster %u\n", cluster);
return -1;
}
while (remaining > 0 && exfat_is_valid_cluster(cluster)) {
uint64_t cluster_off = exfat_get_cluster_offset(ctx, cluster);
/* How much of this cluster do we still need? */
uint64_t want = (remaining < ctx->bytes_per_cluster) ?
remaining : ctx->bytes_per_cluster;
/* Read in EXFAT_IO_CHUNK_SIZE pieces within the cluster */
uint64_t cluster_done = 0;
while (cluster_done < want) {
uint64_t chunk = want - cluster_done;
if (chunk > EXFAT_IO_CHUNK_SIZE)
chunk = EXFAT_IO_CHUNK_SIZE;
uint64_t abs_off = cluster_off + cluster_done;
if (fseeko(ctx->image_file, (off_t)abs_off, SEEK_SET) != 0) {
perror("[exFAT] fseeko file data");
return -1;
}
size_t got = fread(io_buf, 1, (size_t)chunk, ctx->image_file);
if (got == 0)
return -1; /* short read */
if (write_cb(write_arg, io_buf, got) != 0)
return -1;
cluster_done += got;
}
remaining -= want;
if (remaining == 0)
break;
/* Advance to next cluster */
if (info->no_fat_chain) {
cluster++; /* contiguous layout */
} else {
uint32_t next = exfat_get_next_cluster(ctx, cluster);
if (exfat_is_end_of_chain(next)) {
if (remaining > 0)
fprintf(stderr, "[exFAT] chain ended with %llu bytes left\n",
(unsigned long long)remaining);
break;
}
cluster = next;
}
}
return 0;
}
/* write_cb for FILE* output */
static int write_cb_file(void *arg, const uint8_t *buf, size_t len) {
FILE *f = (FILE *)arg;
return (fwrite(buf, 1, len, f) == len) ? 0 : -1;
}
/* write_cb for fd output */
static int write_cb_fd(void *arg, const uint8_t *buf, size_t len) {
int fd = *(int *)arg;
while (len > 0) {
ssize_t n = write(fd, buf, len);
if (n <= 0) { perror("[exFAT] write"); return -1; }
buf += n;
len -= (size_t)n;
}
return 0;
}
/* write_cb for in-memory output */
typedef struct { uint8_t *buf; size_t pos; size_t cap; } mem_ctx_t;
static int write_cb_mem(void *arg, const uint8_t *buf, size_t len) {
mem_ctx_t *m = (mem_ctx_t *)arg;
if (m->pos + len > m->cap) {
size_t can_copy = m->cap - m->pos;
memcpy(m->buf + m->pos, buf, can_copy);
m->pos += can_copy;
return -1; /* buffer full */
}
memcpy(m->buf + m->pos, buf, len);
m->pos += len;
return 0;
}
int exfat_extract_file(exfat_context_t *ctx, const exfat_file_info_t *info,
const char *output_path) {
if (!ctx || !info || !output_path)
return -1;
FILE *f = fopen(output_path, "wb");
if (!f) { perror("[exFAT] fopen output"); return -1; }
int rc = exfat_read_file_data(ctx, info, write_cb_file, f);
fclose(f);
return rc;
}
int exfat_extract_file_fd(exfat_context_t *ctx, const exfat_file_info_t *info,
int output_fd) {
if (!ctx || !info || output_fd < 0)
return -1;
return exfat_read_file_data(ctx, info, write_cb_fd, &output_fd);
}
/*
* Extract file content directly into a caller-provided buffer.
*
* @return number of bytes written on success, -1 on error (including
* truncation when buf_size < info->data_length).
*/
ssize_t exfat_extract_to_buffer(exfat_context_t *ctx,
const exfat_file_info_t *info,
uint8_t *buf, size_t buf_size) {
if (!ctx || !info || !buf || buf_size == 0)
return -1;
if (info->data_length > buf_size) {
fprintf(stderr, "[exFAT] buffer too small: need %llu, have %zu\n",
(unsigned long long)info->data_length, buf_size);
return -1;
}
mem_ctx_t m = { buf, 0, buf_size };
if (exfat_read_file_data(ctx, info, write_cb_mem, &m) != 0)
return -1;
return (ssize_t)m.pos;
}
/* ── Context lifecycle ──────────────────────────────────────────────────── */
int exfat_init(exfat_context_t *ctx, const char *image_path) {
if (!ctx || !image_path)
return -1;
memset(ctx, 0, sizeof(exfat_context_t));
ctx->image_file = fopen(image_path, "rb");
if (!ctx->image_file) {
perror("[exFAT] fopen image");
return -1;
}
if (exfat_read_boot_sector(ctx) != 0 || exfat_read_fat(ctx) != 0) {
exfat_cleanup(ctx);
return -1;
}
return 0;
}
void exfat_cleanup(exfat_context_t *ctx) {
if (!ctx) return;
if (ctx->image_file) {
fclose(ctx->image_file);
ctx->image_file = NULL;
}
exfat_free_fat(ctx);
}
/* ── Utility ────────────────────────────────────────────────────────────── */
void exfat_print_file_info(const exfat_file_info_t *info) {
if (!info) return;
char sz[32];
exfat_format_size(info->data_length, sz, sizeof(sz));
printf("%s %-40s %10s cluster=%u%s\n",
info->is_directory ? "DIR " : "FILE",
info->filename, sz, info->first_cluster,
info->no_fat_chain ? " [contiguous]" : "");
}
time_t exfat_dos_time_to_unix(uint32_t dos_time) {
struct tm t;
memset(&t, 0, sizeof(t));
t.tm_sec = (int)((dos_time & 0x1FU) * 2);
t.tm_min = (int)((dos_time >> 5) & 0x3FU);
t.tm_hour = (int)((dos_time >> 11) & 0x1FU);
t.tm_mday = (int)((dos_time >> 16) & 0x1FU);
t.tm_mon = (int)(((dos_time >> 21) & 0x0FU) - 1U);
t.tm_year = (int)(((dos_time >> 25) & 0x7FU) + 80U);
t.tm_isdst = -1;
return mktime(&t);
}
void exfat_format_size(uint64_t size, char *buf, size_t buf_size) {
if (!buf || buf_size == 0) return;
static const char *units[] = { "B", "KB", "MB", "GB", "TB" };
double d = (double)size;
int u = 0;
while (d >= 1024.0 && u < 4) { d /= 1024.0; u++; }
snprintf(buf, buf_size, "%.2f %s", d, units[u]);
}
+558
View File
@@ -0,0 +1,558 @@
/*
* GNU GPLv3 License — Copyright (c) 2026 SeregonWar
* See LICENSE for full text.
*/
/*
* pkg_unpacker.c — PS4 PKG archive metadata parser
*
* ── DESIGN RATIONALE ────────────────────────────────────────────────────────
*
* Big-endian parsing via read_be32():
* The original code used expressions of the form `(hdr_buf[N] << 24)`.
* When `char` is signed and the byte value >= 0x80, the uint8_t is promoted
* to `int`, and shifting a positive `int` into the sign bit is undefined
* behaviour (C11 §6.5.7 ¶4). read_be32() casts every byte to uint32_t
* before shifting, eliminating the UB entirely.
*
* All offsets widened to uint64_t before arithmetic:
* PKG offsets and sizes are stored as uint32_t (big-endian) on disk.
* Before any addition or comparison against the 64-bit file size, they are
* widened with an explicit cast to prevent silent wrap-around.
*
* No dynamic allocation after pkg_init():
* pkg_extract_to_buffer() and pkg_extract_file_fd() use only the
* caller-supplied buffer or a stack buffer (COPY_BUF_SIZE bytes).
*
* No fprintf / perror in library code:
* Side-effects on stderr are inappropriate for a library. All diagnostics
* are communicated exclusively through return values (pkg_error_t).
*
* EINTR handling in write(2) loop:
* A signal delivered between the kernel accepting bytes and write(2)
* returning causes it to return -1 / EINTR. The inner write loop retries
* automatically rather than propagating a spurious error.
*
* Partial-read detection in pkg_extract_file_fd():
* The original code checked only `got == 0` as an fread error. A short
* read (0 < got < to_read) silently corrupted the extracted data. We now
* require `got == to_read` and return PKG_ERR_IO otherwise.
*
* Encryption guard on all extraction paths:
* flags1 bit 31 (PKG_ENTRY_FLAG_ENCRYPTED) was parsed in the original
* struct but never populated. We now read flags1/flags2 from each entry
* record and refuse extraction of encrypted entries with PKG_ERR_ENCRYPTED.
*
* ── MISRA C:2012 deviations ─────────────────────────────────────────────────
* Rule 15.5 — Single return point: intentionally violated in validation
* sequences to avoid deeply nested if-else ladders that reduce
* readability without improving safety.
* Dir 4.9 — Function-like macros: not used; all helpers are functions.
*/
#ifndef _FILE_OFFSET_BITS
# define _FILE_OFFSET_BITS 64
#endif
#ifndef _POSIX_C_SOURCE
# define _POSIX_C_SOURCE 200809L
#endif
#include "pkg_unpacker.h"
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <limits.h> /* SSIZE_MAX */
#if defined(_MSC_VER)
# include <io.h>
# define fseeko _fseeki64
# define ftello _ftelli64
# define write _write
# define SSIZE_MAX _I64_MAX
#else
# include <unistd.h>
#endif
/* ── Internal constants ─────────────────────────────────────────────────── */
/**
* Stack-buffer size for the streaming copy in pkg_extract_file_fd().
*
* DESIGN RATIONALE — why 64 KiB:
* Matches one typical OS page-cache read-ahead quantum. Large enough to
* amortise the per-fread overhead; small enough to live on the stack without
* risk of overflow on embedded targets with reduced stack sizes.
*/
#define COPY_BUF_SIZE 65536U
/*
* Byte offsets of the fields we decode within the on-disk header buffer.
* Cross-referenced against psdevwiki.com/ps4/PKG_files and shadPS4 pkg.h.
*
* 0x00 magic (u32_be)
* 0x04 type (u32_be)
* 0x08 unk_0x08 (u32_be)
* 0x0C file_count (u32_be)
* 0x10 entry_count (u32_be) ← HDR_OFF_ENTRY_COUNT
* 0x14 sc_entry_count (u16_be)
* 0x16 entry_count_2 (u16_be)
* 0x18 table_offset (u32_be) ← HDR_OFF_TABLE_OFFSET
* 0x1C entry_data_size (u32_be)
* 0x20 body_offset (u64_be)
* 0x28 body_size (u64_be)
* 0x30 content_offset (u64_be)
* 0x38 content_size (u64_be)
* 0x40 content_id (char[36]) ← HDR_OFF_CONTENT_ID
*/
#define HDR_OFF_MAGIC 0x000U
#define HDR_OFF_ENTRY_COUNT 0x010U
#define HDR_OFF_TABLE_OFFSET 0x018U
#define HDR_OFF_CONTENT_ID 0x040U
/*
* Byte offsets within one 32-byte on-disk entry record.
*
* +00 id (u32_be)
* +04 filename_offset (u32_be)
* +08 flags1 (u32_be) bit 31 = PKG_ENTRY_FLAG_ENCRYPTED
* +12 flags2 (u32_be)
* +16 offset (u32_be)
* +20 size (u32_be)
* +24 padding (u64_be) unused
*/
#define EOFF_ID 0U
#define EOFF_FILENAME_OFFSET 4U
#define EOFF_FLAGS1 8U
#define EOFF_FLAGS2 12U
#define EOFF_OFFSET 16U
#define EOFF_SIZE 20U
/* ── Endianness helper ───────────────────────────────────────────────────── */
/**
* @brief Read a big-endian uint32_t from an unaligned byte buffer.
*
* Each byte is cast to uint32_t before shifting to prevent two sources of
* undefined behaviour in the original code:
* 1. `uint8_t` is implicitly promoted to `int` (which may be 32-bit signed).
* 2. Shifting a signed integer into or past the sign bit is UB (C11 §6.5.7).
*
* @param[in] p Pointer to 4 bytes; must not be NULL (guaranteed by callers).
* @return The decoded value.
*
* @note Thread-safety: pure function, no shared state.
* @note WCET: O(1), no I/O, no branches.
*/
static uint32_t read_be32(const uint8_t *p)
{
return (((uint32_t)p[0]) << 24U) |
(((uint32_t)p[1]) << 16U) |
(((uint32_t)p[2]) << 8U) |
((uint32_t)p[3]);
}
/* ── File-size helper ────────────────────────────────────────────────────── */
/**
* @brief Determine total byte size of an open file without disturbing the
* current file position.
*
* Saves the current position with ftello(), seeks to SEEK_END, queries the
* position, then restores the saved position.
*
* @param[in] fp Open file handle; must not be NULL.
* @param[out] out Receives the file size on PKG_OK; must not be NULL.
*
* @return PKG_OK on success, PKG_ERR_IO if any stdio call fails.
*
* @note Thread-safety: NOT thread-safe (modifies fp's position transiently).
* @note WCET: O(1) on most OS/FS implementations (no I/O read required).
*/
static int get_file_size(FILE *fp, uint64_t *out)
{
off_t saved = ftello(fp);
if (saved == (off_t)-1) {
return PKG_ERR_IO;
}
if (fseeko(fp, (off_t)0, SEEK_END) != 0) {
return PKG_ERR_IO;
}
off_t end = ftello(fp);
/* Restore position before any return, even on error. */
if (fseeko(fp, saved, SEEK_SET) != 0) {
return PKG_ERR_IO;
}
if (end < (off_t)0) {
return PKG_ERR_IO;
}
*out = (uint64_t)end;
return PKG_OK;
}
/* ═══════════════════════════════════════════════════════════════════════════
* Context lifecycle
* ═════════════════════════════════════════════════════════════════════════*/
int pkg_init(pkg_context_t *ctx, const char *pkg_path)
{
if ((ctx == NULL) || (pkg_path == NULL) || (pkg_path[0] == '\0')) {
return PKG_ERR_PARAM;
}
/*
* Zero-initialise first. This ensures pkg_cleanup() is safe to call
* on ctx at any point below, even before all fields are populated.
*/
(void)memset(ctx, 0, sizeof(*ctx));
/* ── Open file ──────────────────────────────────────────────────────── */
ctx->file = fopen(pkg_path, "rb");
if (ctx->file == NULL) {
/* errno is set by fopen; caller may inspect it. */
return PKG_ERR_IO;
}
/* ── Determine file size ────────────────────────────────────────────── */
{
int rc = get_file_size(ctx->file, &ctx->file_size);
if (rc != PKG_OK) {
pkg_cleanup(ctx);
return rc;
}
}
if (ctx->file_size < (uint64_t)PKG_HEADER_SIZE) {
pkg_cleanup(ctx);
return PKG_ERR_FORMAT;
}
/* ── Read the header block ──────────────────────────────────────────── */
uint8_t hdr_buf[PKG_HEADER_SIZE];
if (fread(hdr_buf, 1U, sizeof(hdr_buf), ctx->file) != sizeof(hdr_buf)) {
pkg_cleanup(ctx);
return PKG_ERR_IO;
}
/* ── Validate magic ─────────────────────────────────────────────────── */
uint32_t magic = read_be32(hdr_buf + HDR_OFF_MAGIC);
if ((magic != PKG_MAGIC_CNT) && (magic != PKG_MAGIC_PKG)) {
fprintf(stderr, "[PKG] invalid magic: 0x%08X (expected 0x%08X or 0x%08X)\n",
magic, PKG_MAGIC_CNT, PKG_MAGIC_PKG);
fprintf(stderr, "[PKG] Dump: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X\n",
hdr_buf[0], hdr_buf[1], hdr_buf[2], hdr_buf[3],
hdr_buf[4], hdr_buf[5], hdr_buf[6], hdr_buf[7],
hdr_buf[8], hdr_buf[9], hdr_buf[10], hdr_buf[11],
hdr_buf[12], hdr_buf[13], hdr_buf[14], hdr_buf[15]);
pkg_cleanup(ctx);
return PKG_ERR_FORMAT;
}
ctx->header.magic = magic;
/* ── Decode and validate entry_count ────────────────────────────────── */
uint32_t entry_count = read_be32(hdr_buf + HDR_OFF_ENTRY_COUNT);
if ((entry_count == 0U) || (entry_count > PKG_MAX_ENTRY_COUNT)) {
fprintf(stderr, "[PKG] invalid entry count: %u\n", entry_count);
pkg_cleanup(ctx);
return PKG_ERR_FORMAT;
}
ctx->header.entry_count = entry_count;
/* ── Decode and validate table_offset ──────────────────────────────── */
uint32_t table_offset = read_be32(hdr_buf + HDR_OFF_TABLE_OFFSET);
if (table_offset < PKG_MIN_TABLE_OFFSET) {
/*
* The entry table overlaps the header — either the file is corrupt
* or this is not a real PKG.
*/
pkg_cleanup(ctx);
return PKG_ERR_FORMAT;
}
/*
* Verify the complete table fits inside the file.
*
* All values widened to uint64_t before multiplication and addition to
* prevent uint32_t wrap-around (e.g., table_offset=0xFFFFFF00 +
* entry_count*32 could overflow a u32 silently).
*/
uint64_t table_bytes = (uint64_t)entry_count *
(uint64_t)PKG_ENTRY_RECORD_SIZE;
uint64_t table_end = (uint64_t)table_offset + table_bytes;
if ((table_end < (uint64_t)table_offset) || /* addition overflowed u64 */
(table_end > ctx->file_size)) {
pkg_cleanup(ctx);
return PKG_ERR_FORMAT;
}
ctx->header.table_offset = table_offset;
/* ── Copy content_id; NUL-terminate explicitly ──────────────────────── */
/*
* The on-disk field is exactly PKG_CONTENT_ID_LEN bytes with no NUL.
* The struct provides PKG_CONTENT_ID_LEN+1 bytes; zero-init (above)
* already placed a '\0' at [PKG_CONTENT_ID_LEN], but we write it
* explicitly for clarity and MISRA Rule 9.1 compliance.
*/
(void)memcpy(ctx->header.content_id,
hdr_buf + HDR_OFF_CONTENT_ID,
PKG_CONTENT_ID_LEN);
ctx->header.content_id[PKG_CONTENT_ID_LEN] = '\0';
/* ── Allocate entry array ───────────────────────────────────────────── */
/*
* Overflow analysis:
* entry_count <= PKG_MAX_ENTRY_COUNT == 10 000
* sizeof(pkg_entry_t) == 24 bytes
* max allocation: 240 000 bytes — well within any sane size_t.
* The division check below is a defensive invariant assertion.
*/
size_t alloc_bytes = (size_t)entry_count * sizeof(pkg_entry_t);
if ((alloc_bytes / sizeof(pkg_entry_t)) != (size_t)entry_count) {
/* Should never trigger given the guard above. */
pkg_cleanup(ctx);
return PKG_ERR_RANGE;
}
ctx->entries = (pkg_entry_t *)malloc(alloc_bytes);
if (ctx->entries == NULL) {
pkg_cleanup(ctx);
return PKG_ERR_NOMEM;
}
/* ── Parse each entry record ────────────────────────────────────────── */
if (fseeko(ctx->file, (off_t)table_offset, SEEK_SET) != 0) {
pkg_cleanup(ctx);
return PKG_ERR_IO;
}
for (uint32_t i = 0U; i < entry_count; i++) {
uint8_t e_buf[PKG_ENTRY_RECORD_SIZE];
if (fread(e_buf, 1U, sizeof(e_buf), ctx->file) != sizeof(e_buf)) {
pkg_cleanup(ctx);
return PKG_ERR_IO;
}
pkg_entry_t *e = &ctx->entries[i];
e->id = read_be32(e_buf + EOFF_ID);
e->filename_offset = read_be32(e_buf + EOFF_FILENAME_OFFSET);
e->flags1 = read_be32(e_buf + EOFF_FLAGS1);
e->flags2 = read_be32(e_buf + EOFF_FLAGS2);
e->offset = read_be32(e_buf + EOFF_OFFSET);
e->size = read_be32(e_buf + EOFF_SIZE);
/*
* Validate that this entry's data is fully contained in the file.
* Widen to uint64_t before addition to prevent uint32_t wrap.
*
* NOTE: We validate all entries on init rather than at extraction
* time to fail fast on corrupt files and to avoid redundant checks
* on every call to pkg_extract_*.
*/
uint64_t entry_end = (uint64_t)e->offset + (uint64_t)e->size;
if ((entry_end < (uint64_t)e->offset) || /* addition overflow guard */
(entry_end > ctx->file_size)) {
pkg_cleanup(ctx);
return PKG_ERR_FORMAT;
}
}
ctx->num_entries = (size_t)entry_count;
return PKG_OK;
}
void pkg_cleanup(pkg_context_t *ctx)
{
if (ctx == NULL) {
return;
}
if (ctx->file != NULL) {
(void)fclose(ctx->file);
ctx->file = NULL;
}
if (ctx->entries != NULL) {
free(ctx->entries);
ctx->entries = NULL;
}
ctx->num_entries = 0U;
ctx->file_size = 0U;
}
/* ═══════════════════════════════════════════════════════════════════════════
* Entry retrieval
* ═════════════════════════════════════════════════════════════════════════*/
const pkg_entry_t *pkg_find_entry_by_id(const pkg_context_t *ctx, uint32_t id)
{
if ((ctx == NULL) || (ctx->entries == NULL)) {
return NULL;
}
for (size_t i = 0U; i < ctx->num_entries; i++) {
if (ctx->entries[i].id == id) {
return &ctx->entries[i];
}
}
return NULL;
}
/* ═══════════════════════════════════════════════════════════════════════════
* Extraction
* ═════════════════════════════════════════════════════════════════════════*/
ssize_t pkg_extract_to_buffer(pkg_context_t *ctx, const pkg_entry_t *entry,
uint8_t *buf, size_t buf_size)
{
if ((ctx == NULL) || (ctx->file == NULL) ||
(entry == NULL) || (buf == NULL) || (buf_size == 0U)) {
return (ssize_t)PKG_ERR_PARAM;
}
if (pkg_entry_is_encrypted(entry)) {
return (ssize_t)PKG_ERR_ENCRYPTED;
}
/*
* Bound the in-memory extraction size. This is a defence-in-depth check;
* pkg_init() already validated that entry->offset + entry->size <=
* file_size, so a corrupt entry->size cannot point past the file.
* However, without this guard a 4 GiB size field would cause a
* 4 GiB stack or heap demand on the caller's side.
*/
if (entry->size > PKG_MAX_ENTRY_SIZE) {
return (ssize_t)PKG_ERR_RANGE;
}
if ((size_t)entry->size > buf_size) {
return (ssize_t)PKG_ERR_BUFFER_SMALL;
}
/*
* ssize_t range note:
* entry->size <= PKG_MAX_ENTRY_SIZE (64 MiB) is guaranteed by the
* check above. SSIZE_MAX is at minimum INT32_MAX (~2 GiB) on all
* supported platforms, so the cast on the return statement below is
* always safe. No additional guard is required.
*/
if (entry->size == 0U) {
return (ssize_t)0;
}
if (fseeko(ctx->file, (off_t)entry->offset, SEEK_SET) != 0) {
return (ssize_t)PKG_ERR_IO;
}
size_t got = fread(buf, 1U, (size_t)entry->size, ctx->file);
if (got != (size_t)entry->size) {
/*
* A short read indicates either an I/O error or that the file was
* truncated between pkg_init() and this call. Either way, the
* buffer content is incomplete and must not be used.
*/
return (ssize_t)PKG_ERR_IO;
}
return (ssize_t)got;
}
int pkg_extract_file_fd(pkg_context_t *ctx, const pkg_entry_t *entry,
int output_fd)
{
if ((ctx == NULL) || (ctx->file == NULL) ||
(entry == NULL) || (output_fd < 0)) {
return PKG_ERR_PARAM;
}
if (pkg_entry_is_encrypted(entry)) {
return PKG_ERR_ENCRYPTED;
}
if (entry->size > PKG_MAX_ENTRY_SIZE) {
return PKG_ERR_RANGE;
}
if (fseeko(ctx->file, (off_t)entry->offset, SEEK_SET) != 0) {
return PKG_ERR_IO;
}
/* Stack-allocated copy buffer — no heap allocation in this function. */
uint8_t copy_buf[COPY_BUF_SIZE];
uint32_t remaining = entry->size;
while (remaining > 0U) {
size_t to_read = (remaining < (uint32_t)COPY_BUF_SIZE)
? (size_t)remaining
: (size_t)COPY_BUF_SIZE;
size_t got = fread(copy_buf, 1U, to_read, ctx->file);
if (got == 0U) {
/* EOF or hard read error before we consumed entry->size bytes. */
return PKG_ERR_IO;
}
if (got != to_read) {
/*
* Partial read: the file is shorter than entry->size claimed.
* The original code silently continued with a smaller chunk,
* producing a corrupt output file. We fail fast instead.
*/
return PKG_ERR_IO;
}
/*
* Write the full buffer to output_fd.
*
* DESIGN RATIONALE — inner retry loop for EINTR:
* A signal delivered while write(2) is in progress causes it to
* return -1 with errno == EINTR. This is not a true error; we
* simply retry the write from where we left off. Without this,
* callers running under a debugger or with timers would see
* spurious PKG_ERR_IO failures.
*/
const uint8_t *p = copy_buf;
size_t len = got;
while (len > 0U) {
ssize_t n = write(output_fd, p, len);
if (n < 0) {
if (errno == EINTR) {
continue; /* Signal interrupted; retry this write. */
}
return PKG_ERR_IO;
}
if (n == 0) {
/*
* write(2) returning 0 without error is not specified by
* POSIX for regular files; treat it as a non-recoverable
* error to avoid an infinite spin.
*/
return PKG_ERR_IO;
}
/* Safe casts: 0 < n <= (ssize_t)len, len is a size_t. */
p += (size_t)n;
len -= (size_t)n;
}
/*
* Safe subtraction: got == to_read <= remaining (ensured above),
* so remaining - got >= 0 and no uint32_t underflow is possible.
*/
remaining -= (uint32_t)got;
}
return PKG_OK;
}
+817
View File
@@ -0,0 +1,817 @@
/*
* MIT License — Copyright (c) 2026 SeregonWar
* See LICENSE for full text.
*/
/**
* @file zip_writer.c
* @brief Streaming ZIP writer (STORED entries, ZIP64 aware)
*
* See include/archive/zip_writer.h for the rationale. The writer is a small
* state machine driven by zip_writer_read(): header bytes come from an
* internal buffer, file bytes go straight from pread() into the caller's
* buffer, and the central directory is emitted after the last entry.
*/
#include "archive/zip_writer.h"
#if defined(PLATFORM_PS4) || defined(PLATFORM_PS5)
#include "pal_filesystem.h"
#endif
#include <dirent.h>
#include <errno.h>
#include <pthread.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
#define ZIP_LOCAL_SIG 0x04034b50U
#define ZIP_CENTRAL_SIG 0x02014b50U
#define ZIP_EOCD_SIG 0x06054b50U
#define ZIP64_EOCD_SIG 0x06064b50U
#define ZIP64_LOCATOR_SIG 0x07064b50U
#define ZIP64_EXTRA_ID 0x0001U
#define ZIP_METHOD_STORE 0U
#define ZIP_VERSION_STORE 20U
#define ZIP_VERSION_ZIP64 45U
#define ZIP_FLAG_UTF8 0x0800U
#define ZIP_U32_MAX 0xFFFFFFFFU
#define ZIP_U16_MAX 0xFFFFU
#define ZIP_STATE_LOCAL 0
#define ZIP_STATE_FILE 1
#define ZIP_STATE_DESCRIPTOR 2
#define ZIP_STATE_CENTRAL 3
#define ZIP_STATE_ZIP64_EOCD 4
#define ZIP_STATE_EOCD 5
#define ZIP_STATE_DONE 6
#define ZIP_STATE_ERROR 7
#define ZIP_NAME_MAX 1024
#define ZIP_HEADER_MAX 1280
typedef struct {
char name[ZIP_NAME_MAX]; /* archive path, '/'-separated, dirs end with '/' */
char disk[ZIP_NAME_MAX + 256];
uint64_t size;
uint64_t offset;
uint32_t crc;
uint32_t dos_time;
int is_self; /**< SELF container: read decrypted through the VFS */
} zip_entry_t;
struct zip_writer {
zip_entry_t *entries;
size_t count;
size_t capacity;
int truncated;
size_t index; /* entry currently being emitted */
int state;
int fd; /* open input file for ZIP_STATE_FILE */
uint64_t sent; /* bytes of the current file already streamed */
uint32_t crc; /* running CRC of the current file */
int zip64_needed; /* any offset/size/count past the 32-bit limits */
unsigned char header[ZIP_HEADER_MAX];
size_t header_len;
size_t header_pos;
uint64_t offset; /* bytes of the archive already produced */
uint64_t cd_offset; /* offset where the central directory starts */
int zip64_emitted; /* tail records built once, then served */
int eocd_emitted;
int decrypt; /* stream SELF containers decrypted (dumps) */
int read_failed; /* current entry unreadable: pad its announced size */
#if defined(PLATFORM_PS4) || defined(PLATFORM_PS5)
vfs_node_t self_node;
int self_open;
#endif
};
void zip_writer_set_decrypt(zip_writer_t *zip, int decrypt) {
if (zip != NULL) zip->decrypt = decrypt ? 1 : 0;
}
static void close_current_file(zip_writer_t *zip);
/* Header builders, also used to measure the archive before streaming it. */
static size_t build_local_header(zip_writer_t *zip, const zip_entry_t *entry);
static size_t build_central_header(zip_writer_t *zip, const zip_entry_t *entry);
static size_t build_data_descriptor(zip_writer_t *zip,
const zip_entry_t *entry);
/*===========================================================================*
* CRC-32 (IEEE 802.3, the polynomial ZIP uses)
*===========================================================================*/
static uint32_t g_crc_table[8][256];
static pthread_once_t g_crc_once = PTHREAD_ONCE_INIT;
static void crc_build_table(void) {
for (uint32_t i = 0U; i < 256U; i++) {
uint32_t c = i;
for (int k = 0; k < 8; k++) {
c = (c & 1U) ? (0xEDB88320U ^ (c >> 1U)) : (c >> 1U);
}
g_crc_table[0][i] = c;
}
for (uint32_t i = 0U; i < 256U; i++) {
uint32_t c = g_crc_table[0][i];
for (int slice = 1; slice < 8; slice++) {
c = g_crc_table[0][c & 0xFFU] ^ (c >> 8U);
g_crc_table[slice][i] = c;
}
}
}
static uint32_t crc_update(uint32_t crc, const unsigned char *data, size_t len) {
(void)pthread_once(&g_crc_once, crc_build_table);
uint32_t c = crc ^ 0xFFFFFFFFU;
while (len >= 8U) {
uint32_t word = (uint32_t)data[0] | ((uint32_t)data[1] << 8U) |
((uint32_t)data[2] << 16U) | ((uint32_t)data[3] << 24U);
c ^= word;
c = g_crc_table[7][c & 0xFFU] ^ g_crc_table[6][(c >> 8U) & 0xFFU] ^
g_crc_table[5][(c >> 16U) & 0xFFU] ^ g_crc_table[4][c >> 24U] ^
g_crc_table[3][data[4]] ^ g_crc_table[2][data[5]] ^
g_crc_table[1][data[6]] ^ g_crc_table[0][data[7]];
data += 8U;
len -= 8U;
}
while (len-- > 0U) {
c = g_crc_table[0][(c ^ *data++) & 0xFFU] ^ (c >> 8U);
}
return c ^ 0xFFFFFFFFU;
}
/*===========================================================================*
* Little-endian helpers
*===========================================================================*/
static void put16(unsigned char *p, uint16_t v) {
p[0] = (unsigned char)(v & 0xFFU);
p[1] = (unsigned char)((v >> 8U) & 0xFFU);
}
static void put32(unsigned char *p, uint32_t v) {
p[0] = (unsigned char)(v & 0xFFU);
p[1] = (unsigned char)((v >> 8U) & 0xFFU);
p[2] = (unsigned char)((v >> 16U) & 0xFFU);
p[3] = (unsigned char)((v >> 24U) & 0xFFU);
}
static void put64(unsigned char *p, uint64_t v) {
for (int i = 0; i < 8; i++) p[i] = (unsigned char)((v >> (8 * i)) & 0xFFU);
}
/** MS-DOS date/time, derived from the entry's mtime. */
static void dos_time_from(time_t when, uint32_t *out) {
struct tm tm_buf;
if (localtime_r(&when, &tm_buf) == NULL) {
*out = 0U;
return;
}
int year = tm_buf.tm_year + 1900;
if (year < 1980) year = 1980;
uint32_t date = (uint32_t)(((year - 1980) << 9) | ((tm_buf.tm_mon + 1) << 5) |
tm_buf.tm_mday);
uint32_t time = (uint32_t)((tm_buf.tm_hour << 11) | (tm_buf.tm_min << 5) |
(tm_buf.tm_sec / 2));
*out = (date << 16) | time;
}
/*===========================================================================*
* Entry collection
*===========================================================================*/
static zip_entry_t *entry_push(zip_writer_t *zip) {
if (zip->count >= ZIP_WRITER_MAX_ENTRIES) {
zip->truncated = 1;
return NULL;
}
if (zip->count == zip->capacity) {
size_t next = (zip->capacity == 0U) ? 64U : zip->capacity * 2U;
if (next > ZIP_WRITER_MAX_ENTRIES) next = ZIP_WRITER_MAX_ENTRIES;
zip_entry_t *grown = realloc(zip->entries, next * sizeof(*grown));
if (grown == NULL) {
zip->truncated = 1;
return NULL;
}
zip->entries = grown;
zip->capacity = next;
}
zip_entry_t *entry = &zip->entries[zip->count++];
memset(entry, 0, sizeof(*entry));
return entry;
}
static int name_append(char *dst, size_t dst_size, const char *prefix,
const char *leaf) {
int n = snprintf(dst, dst_size, "%s%s", prefix, leaf);
return (n > 0 && (size_t)n < dst_size) ? 0 : -1;
}
static int zip_cmp(const void *a, const void *b) {
const char *const *lhs = (const char *const *)a;
const char *const *rhs = (const char *const *)b;
return strcmp(*lhs, *rhs);
}
/**
* Add @p disk (file or directory) and, for directories, everything below it.
*
* @param prefix Archive path prefix ending with '/' ("" for the top level).
*/
static void collect_path(zip_writer_t *zip, const char *disk, const char *prefix,
int depth) {
if (depth > 32) return; /* symlink loops / absurd nesting */
struct stat st;
if (lstat(disk, &st) != 0) return;
if (S_ISLNK(st.st_mode)) return; /* never follow links out of the tree */
if (S_ISDIR(st.st_mode)) {
zip_entry_t *entry = entry_push(zip);
if (entry == NULL) return;
if (name_append(entry->name, sizeof(entry->name), prefix, "") != 0) return;
size_t len = strlen(entry->name);
if (len > 0U && entry->name[len - 1U] != '/') {
if (len + 1U >= sizeof(entry->name)) return;
entry->name[len] = '/';
entry->name[len + 1U] = '\0';
}
/* entry_push() can realloc zip->entries while descending into children.
* Keep the parent name on this stack frame before recursing. */
char parent_name[ZIP_NAME_MAX];
(void)memcpy(parent_name, entry->name, strlen(entry->name) + 1U);
(void)snprintf(entry->disk, sizeof(entry->disk), "%s", disk);
entry->size = 0U;
dos_time_from(st.st_mtime, &entry->dos_time);
DIR *dir = opendir(disk);
if (dir == NULL) return;
char **names = NULL;
size_t name_count = 0U, name_cap = 0U;
struct dirent *de;
while ((de = readdir(dir)) != NULL) {
if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0) continue;
if (name_count == name_cap) {
size_t next = (name_cap == 0U) ? 16U : name_cap * 2U;
char **grown = realloc(names, next * sizeof(*grown));
if (grown == NULL) break;
names = grown;
name_cap = next;
}
names[name_count] = strdup(de->d_name);
if (names[name_count] == NULL) break;
name_count++;
}
closedir(dir);
qsort(names, name_count, sizeof(*names), zip_cmp); /* stable output */
for (size_t i = 0U; i < name_count; i++) {
char child_disk[ZIP_NAME_MAX + 256];
char child_name[ZIP_NAME_MAX];
int disk_len = snprintf(child_disk, sizeof(child_disk), "%s/%s", disk, names[i]);
if (disk_len < 0 || (size_t)disk_len >= sizeof(child_disk)) {
free(names[i]);
continue;
}
if (name_append(child_name, sizeof(child_name), parent_name, names[i]) != 0) {
free(names[i]);
continue;
}
collect_path(zip, child_disk, child_name, depth + 1);
free(names[i]);
}
free(names);
return;
}
if (!S_ISREG(st.st_mode)) return;
zip_entry_t *entry = entry_push(zip);
if (entry == NULL) return;
/* Files inside a requested folder already carry their full archive path in
* the prefix; a file requested on its own keeps its own name. */
char leaf[ZIP_NAME_MAX];
const char *entry_name = prefix;
if (prefix[0] == '\0') {
const char *slash = strrchr(disk, '/');
(void)snprintf(leaf, sizeof(leaf), "%s",
(slash != NULL) ? slash + 1 : disk);
entry_name = leaf;
}
if (name_append(entry->name, sizeof(entry->name), entry_name, "") != 0) return;
(void)snprintf(entry->disk, sizeof(entry->disk), "%s", disk);
entry->size = (uint64_t)st.st_size;
dos_time_from(st.st_mtime, &entry->dos_time);
#if defined(PLATFORM_PS4) || defined(PLATFORM_PS5)
/*
* Decrypted dumps: a SELF container streams as its ELF payload, and the
* size must be known here — the archive length is announced before the
* bytes are produced.
*/
if (zip->decrypt != 0) {
vfs_node_t node;
if (psx_vfs_try_open_self(&node, disk) == 1) {
entry->size = vfs_get_size(&node);
entry->is_self = 1;
vfs_close(&node);
}
}
#endif
}
static zip_writer_t *zip_create_impl(const char **paths, size_t count,
int decrypt) {
if (paths == NULL || count == 0U) return NULL;
zip_writer_t *zip = calloc(1U, sizeof(*zip));
if (zip == NULL) return NULL;
zip->state = ZIP_STATE_LOCAL;
zip->fd = -1;
zip->decrypt = (decrypt != 0) ? 1 : 0;
for (size_t i = 0U; i < count; i++) {
if (paths[i] == NULL || paths[i][0] == '\0') continue;
/* Archive names are relative to the parent, so a picked folder keeps its
* own name as the top level entry. */
const char *base = strrchr(paths[i], '/');
base = (base != NULL) ? base + 1 : paths[i];
if (base[0] == '\0') continue;
char prefix[ZIP_NAME_MAX];
if (name_append(prefix, sizeof(prefix), base, "/") != 0) continue;
struct stat st;
if (lstat(paths[i], &st) != 0) continue;
if (S_ISDIR(st.st_mode)) {
collect_path(zip, paths[i], prefix, 0);
} else if (S_ISREG(st.st_mode)) {
collect_path(zip, paths[i], "", 0); /* file: keep its own basename */
}
}
return zip;
}
zip_writer_t *zip_writer_create(const char **paths, size_t count) {
return zip_create_impl(paths, count, 0);
}
zip_writer_t *zip_writer_create_decrypt(const char **paths, size_t count,
int decrypt) {
return zip_create_impl(paths, count, decrypt);
}
size_t zip_writer_entry_count(const zip_writer_t *zip) {
return (zip != NULL) ? zip->count : 0U;
}
uint64_t zip_writer_total_size(zip_writer_t *zip) {
if (zip == NULL || zip->count == 0U) return 0U;
/* Dry run: the same builders that emit the archive measure it, so the
* announced length cannot drift from the bytes produced. Only offsets and
* sizes matter here, and those are known before any file is read. */
uint64_t offset = 0U;
for (size_t i = 0U; i < zip->count; i++) {
zip_entry_t *entry = &zip->entries[i];
entry->offset = offset;
size_t local = build_local_header(zip, entry);
offset += (uint64_t)local + entry->size;
if (entry->size > 0U) {
offset += (uint64_t)build_data_descriptor(zip, entry);
}
if (entry->size >= ZIP_U32_MAX || entry->offset >= ZIP_U32_MAX) {
zip->zip64_needed = 1;
}
}
uint64_t cd_offset = offset;
for (size_t i = 0U; i < zip->count; i++) {
offset += (uint64_t)build_central_header(zip, &zip->entries[i]);
}
int needs_zip64 = zip->zip64_needed || zip->count >= ZIP_U16_MAX ||
offset >= ZIP_U32_MAX;
if (needs_zip64 != 0) offset += 76U; /* zip64 EOCD + locator */
offset += 22U; /* classic EOCD */
zip->cd_offset = cd_offset;
return offset;
}
int zip_writer_truncated(const zip_writer_t *zip) {
return (zip != NULL) ? zip->truncated : 0;
}
void zip_writer_destroy(zip_writer_t *zip) {
if (zip == NULL) return;
close_current_file(zip);
free(zip->entries);
free(zip);
}
/*===========================================================================*
* Header emission
*===========================================================================*/
static size_t build_local_header(zip_writer_t *zip, const zip_entry_t *entry) {
unsigned char *p = zip->header;
size_t name_len = strlen(entry->name);
int zip64 = (entry->size >= ZIP_U32_MAX);
/* The CRC is only known once the bytes streamed, so non-empty entries carry
* the "data descriptor follows" flag and repeat the values after the data. */
int descriptor = (entry->size > 0U);
size_t extra_len = zip64 ? 20U : 0U;
put32(p + 0, ZIP_LOCAL_SIG);
put16(p + 4, zip64 ? ZIP_VERSION_ZIP64 : ZIP_VERSION_STORE);
put16(p + 6, (uint16_t)(ZIP_FLAG_UTF8 | (descriptor ? 0x0008U : 0U)));
put16(p + 8, ZIP_METHOD_STORE);
put32(p + 10, entry->dos_time);
put32(p + 14, 0U); /* CRC only in the descriptor when streaming */
put32(p + 18, zip64 ? ZIP_U32_MAX : (uint32_t)entry->size);
put32(p + 22, zip64 ? ZIP_U32_MAX : (uint32_t)entry->size);
put16(p + 26, (uint16_t)name_len);
put16(p + 28, (uint16_t)extra_len);
memcpy(p + 30, entry->name, name_len);
size_t pos = 30U + name_len;
if (zip64) {
put16(p + pos, ZIP64_EXTRA_ID);
put16(p + pos + 2, 16U);
put64(p + pos + 4, entry->size);
put64(p + pos + 12, entry->size);
pos += 20U;
}
return pos;
}
/** Data descriptor written after a streamed entry (bit 3 in the flags). */
static size_t build_data_descriptor(zip_writer_t *zip, const zip_entry_t *entry) {
unsigned char *p = zip->header;
int zip64 = (entry->size >= ZIP_U32_MAX);
put32(p + 0, 0x08074b50U);
put32(p + 4, entry->crc);
if (zip64) {
put64(p + 8, entry->size);
put64(p + 16, entry->size);
return 24U;
}
put32(p + 8, (uint32_t)entry->size);
put32(p + 12, (uint32_t)entry->size);
return 16U;
}
static size_t build_central_header(zip_writer_t *zip, const zip_entry_t *entry) {
unsigned char *p = zip->header;
size_t name_len = strlen(entry->name);
uint64_t offset = entry->offset;
int zip64 = (entry->size >= ZIP_U32_MAX) || (offset >= ZIP_U32_MAX);
size_t extra_len = 0U;
size_t pos;
put32(p + 0, ZIP_CENTRAL_SIG);
put16(p + 4, zip64 ? ZIP_VERSION_ZIP64 : ZIP_VERSION_STORE); /* made by */
put16(p + 6, zip64 ? ZIP_VERSION_ZIP64 : ZIP_VERSION_STORE); /* needed */
put16(p + 8, ZIP_FLAG_UTF8);
put16(p + 10, ZIP_METHOD_STORE);
put32(p + 12, entry->dos_time);
put32(p + 16, entry->crc);
put32(p + 20, entry->size >= ZIP_U32_MAX ? ZIP_U32_MAX : (uint32_t)entry->size);
put32(p + 24, entry->size >= ZIP_U32_MAX ? ZIP_U32_MAX : (uint32_t)entry->size);
put16(p + 28, (uint16_t)name_len);
put16(p + 30, 0U); /* extra length, patched below */
put16(p + 32, 0U); /* comment length */
put16(p + 34, 0U); /* disk number */
put16(p + 36, 0U); /* internal attributes */
put32(p + 38, 0U); /* external attributes */
put32(p + 42, offset >= ZIP_U32_MAX ? ZIP_U32_MAX : (uint32_t)offset);
memcpy(p + 46, entry->name, name_len);
pos = 46U + name_len;
if (zip64) {
size_t field = 0U;
field += (entry->size >= ZIP_U32_MAX) ? 8U : 0U;
field += (entry->size >= ZIP_U32_MAX) ? 8U : 0U;
field += (offset >= ZIP_U32_MAX) ? 8U : 0U;
put16(p + pos, ZIP64_EXTRA_ID);
put16(p + pos + 2, (uint16_t)field);
size_t q = pos + 4U;
if (entry->size >= ZIP_U32_MAX) {
put64(p + q, entry->size);
q += 8U;
put64(p + q, entry->size);
q += 8U;
}
if (offset >= ZIP_U32_MAX) {
put64(p + q, offset);
q += 8U;
}
extra_len = 4U + field;
pos = q;
put16(p + 30, (uint16_t)extra_len);
zip->zip64_needed = 1;
}
return pos;
}
/*===========================================================================*
* Producer
*===========================================================================*/
static int open_next_file(zip_writer_t *zip, const zip_entry_t *entry) {
if (zip->fd >= 0) {
close(zip->fd);
zip->fd = -1;
}
zip->sent = 0U;
zip->crc = 0U;
#if defined(PLATFORM_PS4) || defined(PLATFORM_PS5)
if (entry->is_self != 0 && zip->decrypt != 0) {
if (psx_vfs_try_open_self(&zip->self_node, entry->disk) == 1) {
zip->self_open = 1;
return 0;
}
return -1; /* the container stopped being readable: skip it */
}
#endif
zip->fd = open(entry->disk, O_RDONLY);
return (zip->fd >= 0) ? 0 : -1;
}
/** True when the current entry is already being read through the VFS. */
static int zip_self_open(const zip_writer_t *zip) {
#if defined(PLATFORM_PS4) || defined(PLATFORM_PS5)
return (zip->self_open != 0);
#else
(void)zip;
return 0;
#endif
}
/** Release whatever the current entry had open. */
static void close_current_file(zip_writer_t *zip) {
if (zip->fd >= 0) {
close(zip->fd);
zip->fd = -1;
}
#if defined(PLATFORM_PS4) || defined(PLATFORM_PS5)
if (zip->self_open != 0) {
vfs_close(&zip->self_node);
zip->self_open = 0;
}
#endif
}
static size_t serve_header(zip_writer_t *zip, void *buf, size_t len) {
size_t available = zip->header_len - zip->header_pos;
size_t take = (len < available) ? len : available;
memcpy(buf, zip->header + zip->header_pos, take);
zip->header_pos += take;
zip->offset += take;
return take;
}
/**
* Close the current entry: record its CRC and queue the data descriptor that
* carries the values the local header had to leave blank.
*/
static int finish_entry(zip_writer_t *zip) {
zip_entry_t *entry = &zip->entries[zip->index];
entry->crc = zip->crc;
close_current_file(zip);
if (entry->size >= ZIP_U32_MAX || entry->offset >= ZIP_U32_MAX) {
zip->zip64_needed = 1;
}
zip->header_len = (entry->size > 0U) ? build_data_descriptor(zip, entry) : 0U;
zip->header_pos = 0U;
zip->state = ZIP_STATE_DESCRIPTOR;
return 0;
}
ssize_t zip_writer_read(zip_writer_t *zip, void *buf, size_t len) {
if (zip == NULL || buf == NULL || len == 0U) return -1;
unsigned char *out = (unsigned char *)buf;
size_t produced = 0U;
while (produced < len) {
switch (zip->state) {
case ZIP_STATE_LOCAL: {
if (zip->index >= zip->count) {
zip->cd_offset = zip->offset; /* central directory starts here */
zip->state = ZIP_STATE_CENTRAL;
zip->index = 0U;
continue;
}
zip_entry_t *entry = &zip->entries[zip->index];
entry->offset = zip->offset;
zip->header_len = build_local_header(zip, entry);
zip->header_pos = 0U;
zip->sent = 0U; /* per-entry state: never inherit the previous file */
zip->crc = 0U;
zip->read_failed = 0;
zip->state = ZIP_STATE_FILE;
break;
}
case ZIP_STATE_FILE: {
zip_entry_t *entry = &zip->entries[zip->index];
if ((zip->header_pos == 0U) && (zip->fd < 0) && !zip_self_open(zip) &&
(zip->read_failed == 0) && (entry->size > 0U) &&
(open_next_file(zip, entry) != 0)) {
if (entry->is_self != 0 && zip->decrypt != 0) {
return -1; /* never present an encrypted SELF as an all-zero ELF */
}
/*
* Unreadable file. The length of this archive was announced before
* streaming, so the entry cannot simply disappear: it is padded with
* zeros to the size stated in the headers. A client then gets a
* complete, well-formed archive instead of a truncated transfer.
*/
zip->read_failed = 1;
}
if (zip->header_pos < zip->header_len) {
produced += serve_header(zip, out + produced, len - produced);
continue;
}
uint64_t remaining = entry->size - zip->sent;
if (remaining == 0U) {
(void)finish_entry(zip);
continue;
}
if (zip->read_failed != 0) {
size_t pad = (size_t)((remaining < (uint64_t)(len - produced))
? remaining
: (uint64_t)(len - produced));
memset(out + produced, 0, pad);
zip->crc = crc_update(zip->crc, out + produced, pad);
zip->sent += (uint64_t)pad;
zip->offset += (uint64_t)pad;
produced += pad;
break;
}
size_t want = (size_t)((remaining < (uint64_t)(len - produced))
? remaining
: (uint64_t)(len - produced));
ssize_t got;
#if defined(PLATFORM_PS4) || defined(PLATFORM_PS5)
if (zip->self_open != 0) {
got = psx_vfs_read(&zip->self_node, out + produced, want);
} else {
got = pread(zip->fd, out + produced, want, (off_t)zip->sent);
}
#else
got = pread(zip->fd, out + produced, want, (off_t)zip->sent);
#endif
if (got < 0 && errno == EINTR) continue;
if (got <= 0) {
if (entry->is_self != 0 && zip->decrypt != 0) {
if (got == 0) errno = EIO;
return -1;
}
/* Premature end (file shrank, SELF became unreadable): pad the rest of
* the announced size so the archive length stays exact. */
zip->read_failed = 1;
continue;
}
zip->crc = crc_update(zip->crc, out + produced, (size_t)got);
zip->sent += (uint64_t)got;
zip->offset += (uint64_t)got;
produced += (size_t)got;
break;
}
case ZIP_STATE_DESCRIPTOR: {
if (zip->header_pos < zip->header_len) {
produced += serve_header(zip, out + produced, len - produced);
continue;
}
zip->index++;
zip->state = ZIP_STATE_LOCAL;
break;
}
case ZIP_STATE_CENTRAL: {
if (zip->header_pos < zip->header_len) {
produced += serve_header(zip, out + produced, len - produced);
continue;
}
if (zip->index >= zip->count) {
zip->state = ZIP_STATE_ZIP64_EOCD;
continue;
}
zip->header_len = build_central_header(zip, &zip->entries[zip->index]);
zip->header_pos = 0U;
zip->index++;
break;
}
case ZIP_STATE_ZIP64_EOCD: {
if (zip->zip64_emitted == 0) {
zip->zip64_emitted = 1;
int needs_zip64 = zip->zip64_needed || zip->count >= ZIP_U16_MAX ||
zip->offset >= ZIP_U32_MAX;
unsigned char *p = zip->header;
size_t cd_size = (size_t)(zip->offset - zip->cd_offset);
if (needs_zip64 != 0) {
put32(p + 0, ZIP64_EOCD_SIG);
put64(p + 4, 44U);
put16(p + 12, ZIP_VERSION_ZIP64);
put16(p + 14, ZIP_VERSION_ZIP64);
put32(p + 16, 0U);
put32(p + 20, 0U);
put64(p + 24, (uint64_t)zip->count);
put64(p + 32, (uint64_t)zip->count);
put64(p + 40, (uint64_t)cd_size);
put64(p + 48, zip->cd_offset);
put32(p + 56, ZIP64_LOCATOR_SIG);
put32(p + 60, 0U);
put64(p + 64, zip->offset); /* offset of the ZIP64 EOCD itself */
put32(p + 72, 1U);
zip->header_len = 76U;
} else {
zip->header_len = 0U;
}
zip->header_pos = 0U;
}
if (zip->header_pos < zip->header_len) {
produced += serve_header(zip, out + produced, len - produced);
continue;
}
zip->state = ZIP_STATE_EOCD;
break;
}
case ZIP_STATE_EOCD: {
if (zip->eocd_emitted == 0) {
zip->eocd_emitted = 1;
unsigned char *p = zip->header;
uint64_t cd_size = zip->offset - zip->cd_offset;
/* "zip64" here means the zip64 EOCD was written, so the classic
* record carries the 0xFFFF/0xFFFFFFFF sentinels. */
int zip64 = zip->zip64_emitted != 0 && zip->header_len == 76U;
put32(p + 0, ZIP_EOCD_SIG);
put16(p + 4, 0U);
put16(p + 6, 0U);
put16(p + 8, zip64 ? ZIP_U16_MAX : (uint16_t)zip->count);
put16(p + 10, zip64 ? ZIP_U16_MAX : (uint16_t)zip->count);
put32(p + 12, (zip64 || cd_size >= ZIP_U32_MAX) ? ZIP_U32_MAX
: (uint32_t)cd_size);
put32(p + 16, (zip64 || zip->cd_offset >= ZIP_U32_MAX)
? ZIP_U32_MAX
: (uint32_t)zip->cd_offset);
put16(p + 20, 0U);
zip->header_len = 22U;
zip->header_pos = 0U;
}
if (zip->header_pos < zip->header_len) {
produced += serve_header(zip, out + produced, len - produced);
continue;
}
zip->state = ZIP_STATE_DONE;
break;
}
case ZIP_STATE_DONE:
case ZIP_STATE_ERROR:
default:
return (ssize_t)produced;
}
}
return (ssize_t)produced;
}
+157
View File
@@ -0,0 +1,157 @@
/*
MIT License
Copyright (c) 2026 Seregon
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
/** @file control.c @brief FTP authentication, control and navigation commands. */
#include "ftp_commands.h"
#include "ftp_path.h"
#include "ftp_session.h"
#include "pal_filesystem.h"
#include "pal_fileio.h"
#include <stdatomic.h>
#include <stdio.h>
#include <string.h>
ftp_error_t cmd_USER(ftp_session_t *session, const char *args) {
if ((session == NULL) || (args == NULL)) {
return FTP_ERR_INVALID_PARAM;
}
/*
* Open server — accept any username and log in immediately.
*
* Android apps (File Manager+, SuperFTP) expect 230 right
* after USER and never send PASS. GoldHEN/ftpsrv does the
* same: USER always returns 230.
*
* Clients that DO send PASS (FileZilla, WinSCP) will just
* receive a harmless 230 from cmd_PASS too.
*/
(void)args;
session->user_ok = 1U;
session->authenticated = 1U;
session->auth_attempts = 0U;
atomic_store(&session->state, FTP_STATE_AUTHENTICATED);
return ftp_session_send_reply(session, FTP_REPLY_230_LOGGED_IN, NULL);
}
ftp_error_t cmd_PASS(ftp_session_t *session, const char *args) {
(void)args;
if (session == NULL) {
return FTP_ERR_INVALID_PARAM;
}
/* Already logged in from USER — accept PASS as harmless no-op */
session->authenticated = 1U;
session->auth_attempts = 0U;
atomic_store(&session->state, FTP_STATE_AUTHENTICATED);
return ftp_session_send_reply(session, FTP_REPLY_230_LOGGED_IN, NULL);
}
ftp_error_t cmd_QUIT(ftp_session_t *session, const char *args) {
(void)args; /* Unused */
if (session == NULL) {
return FTP_ERR_INVALID_PARAM;
}
return ftp_session_send_reply(session, FTP_REPLY_221_GOODBYE, NULL);
}
ftp_error_t cmd_NOOP(ftp_session_t *session, const char *args) {
(void)args; /* Unused */
if (session == NULL) {
return FTP_ERR_INVALID_PARAM;
}
return ftp_session_send_reply(session, FTP_REPLY_200_OK, NULL);
}
ftp_error_t cmd_CWD(ftp_session_t *session, const char *args) {
if ((session == NULL) || (args == NULL)) {
return FTP_ERR_INVALID_PARAM;
}
char resolved[FTP_PATH_MAX];
ftp_error_t err = ftp_path_resolve(session, args, resolved, sizeof(resolved));
if (err != FTP_OK) {
return ftp_session_send_reply(session, FTP_REPLY_550_FILE_ERROR,
"Invalid path.");
}
int is_dir = pal_path_is_directory(resolved);
if (is_dir != 1) {
return ftp_session_send_reply(session, FTP_REPLY_550_FILE_ERROR,
"Not a directory.");
}
size_t len = strlen(resolved);
if (len >= sizeof(session->cwd)) {
return ftp_session_send_reply(session, FTP_REPLY_550_FILE_ERROR,
"Path too long.");
}
memcpy(session->cwd, resolved, len + 1U);
return ftp_session_send_reply(session, FTP_REPLY_250_FILE_ACTION_OK,
"Directory changed.");
}
ftp_error_t cmd_CDUP(ftp_session_t *session, const char *args) {
(void)args; /* Unused */
if (session == NULL) {
return FTP_ERR_INVALID_PARAM;
}
return cmd_CWD(session, "..");
}
ftp_error_t cmd_PWD(ftp_session_t *session, const char *args) {
(void)args; /* Unused */
if (session == NULL) {
return FTP_ERR_INVALID_PARAM;
}
/* Format: 257 "pathname" — truncate path to fit the fixed-size reply buffer.
* FTP_PATH_MAX (4096 on Linux) may exceed FTP_REPLY_BUFFER_SIZE (1024). */
char reply[FTP_REPLY_BUFFER_SIZE];
size_t cwd_len = strlen(session->cwd);
size_t max_path = sizeof(reply) - 24; /* "..." is current directory. + NUL */
if (cwd_len > max_path) { cwd_len = max_path; }
int n = snprintf(reply, sizeof(reply), "\"%.*s\" is current directory.",
(int)cwd_len, session->cwd);
if ((n < 0) || ((size_t)n >= sizeof(reply))) {
return FTP_ERR_INVALID_PARAM;
}
return ftp_session_send_reply(session, FTP_REPLY_257_PATH_CREATED, reply);
}
+198
View File
@@ -0,0 +1,198 @@
/*
MIT License
Copyright (c) 2026 Seregon
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
/** @file data_connection.c @brief FTP active and passive data-channel commands. */
#include "ftp_commands.h"
#include "ftp_log.h"
#include "ftp_session.h"
#include "pal_network.h"
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#ifndef FTP_PORT_ALLOW_FOREIGN_IP
#define FTP_PORT_ALLOW_FOREIGN_IP 0
#endif
ftp_error_t cmd_PORT(ftp_session_t *session, const char *args) {
if ((session == NULL) || (args == NULL)) {
return FTP_ERR_INVALID_PARAM;
}
unsigned int h1, h2, h3, h4, p1, p2;
int consumed = 0;
if (sscanf(args, "%u,%u,%u,%u,%u,%u%n", &h1, &h2, &h3, &h4, &p1, &p2,
&consumed) != 6) {
return ftp_session_send_reply(session, FTP_REPLY_501_SYNTAX_ARGS,
"Invalid PORT format.");
}
while (args[consumed] == ' ' || args[consumed] == '\t') consumed++;
if (args[consumed] != '\0') {
return ftp_session_send_reply(session, FTP_REPLY_501_SYNTAX_ARGS,
"Invalid PORT format.");
}
if ((h1 > 255U) || (h2 > 255U) || (h3 > 255U) || (h4 > 255U) || (p1 > 255U) ||
(p2 > 255U)) {
return ftp_session_send_reply(session, FTP_REPLY_501_SYNTAX_ARGS,
"Invalid PORT values.");
}
char ip[INET_ADDRSTRLEN];
snprintf(ip, sizeof(ip), "%u.%u.%u.%u", h1, h2, h3, h4);
uint16_t port = (uint16_t)((p1 << 8) | p2);
ftp_error_t err = pal_make_sockaddr(ip, port, &session->data_addr);
if (err != FTP_OK) {
return ftp_session_send_reply(session, FTP_REPLY_501_SYNTAX_ARGS,
"Invalid address.");
}
/* RFC 2577: block FTP bounce attacks by default. */
#if !FTP_PORT_ALLOW_FOREIGN_IP
if (strcmp(ip, session->client_ip) != 0) {
char dbg[128];
snprintf(dbg, sizeof(dbg),
"[SEC] PORT IP mismatch: client=%s port=%s (rejected)",
session->client_ip, ip);
ftp_log_line(FTP_LOG_INFO, dbg);
return ftp_session_send_reply(session, FTP_REPLY_501_SYNTAX_ARGS,
"PORT address mismatch.");
}
#endif
session->data_mode = FTP_DATA_MODE_ACTIVE;
return ftp_session_send_reply(session, FTP_REPLY_200_OK,
"PORT command successful.");
}
static ftp_error_t open_passive_listener(ftp_session_t *session,
uint32_t *out_ip,
uint16_t *out_port) {
if (session == NULL || out_ip == NULL || out_port == NULL) {
return FTP_ERR_INVALID_PARAM;
}
if (session->pasv_fd >= 0) {
PAL_CLOSE(session->pasv_fd);
session->pasv_fd = -1;
}
int fd = PAL_SOCKET(AF_INET, SOCK_STREAM, 0);
if (fd < 0) {
return ftp_session_send_reply(session, FTP_REPLY_425_CANT_OPEN_DATA,
"Cannot create socket.");
}
(void)pal_socket_set_reuseaddr(fd);
/* FreeBSD/OrbisOS inherit SO_RCVBUF from the listener into accepted
* sockets. Set it before bind/listen to prevent STOR zero-window stalls.
* SO_SNDBUF remains unset so TCP send auto-tuning stays enabled. */
int rcvbuf = (int)FTP_TCP_RCVBUF;
(void)PAL_SETSOCKOPT(fd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf));
uint32_t ip = 0U;
struct sockaddr_in local;
socklen_t local_len = (socklen_t)sizeof(local);
memset(&local, 0, sizeof(local));
if (PAL_GETSOCKNAME(session->ctrl_fd, (struct sockaddr *)&local,
&local_len) == 0) {
ip = PAL_NTOHL(local.sin_addr.s_addr);
}
if (ip == 0U) {
char ip_str[INET_ADDRSTRLEN];
struct in_addr ia;
if (pal_network_get_primary_ip(ip_str, sizeof(ip_str)) == FTP_OK &&
PAL_INET_PTON(AF_INET, ip_str, &ia) == 1) {
ip = PAL_NTOHL(ia.s_addr);
}
}
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = (ip != 0U) ? PAL_HTONL(ip) : PAL_HTONL(INADDR_ANY);
addr.sin_port = 0;
if (PAL_BIND(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
PAL_CLOSE(fd);
return ftp_session_send_reply(session, FTP_REPLY_425_CANT_OPEN_DATA,
"Bind failed.");
}
if (PAL_LISTEN(fd, 1) < 0) {
PAL_CLOSE(fd);
return ftp_session_send_reply(session, FTP_REPLY_425_CANT_OPEN_DATA,
"Listen failed.");
}
struct sockaddr_in passive_addr;
socklen_t addr_len = (socklen_t)sizeof(passive_addr);
memset(&passive_addr, 0, sizeof(passive_addr));
if (PAL_GETSOCKNAME(fd, (struct sockaddr *)&passive_addr, &addr_len) < 0) {
PAL_CLOSE(fd);
return ftp_session_send_reply(session, FTP_REPLY_425_CANT_OPEN_DATA,
"Cannot get socket name.");
}
session->pasv_fd = fd;
session->data_mode = FTP_DATA_MODE_PASSIVE;
*out_ip = (ip != 0U) ? ip : PAL_NTOHL(passive_addr.sin_addr.s_addr);
*out_port = PAL_NTOHS(passive_addr.sin_port);
return FTP_OK;
}
ftp_error_t cmd_PASV(ftp_session_t *session, const char *args) {
(void)args;
if (session == NULL) return FTP_ERR_INVALID_PARAM;
uint32_t ip = 0U;
uint16_t port = 0U;
ftp_error_t err = open_passive_listener(session, &ip, &port);
if (err != FTP_OK) return err;
char reply[FTP_REPLY_BUFFER_SIZE];
(void)snprintf(reply, sizeof(reply),
"Entering Passive Mode (%u,%u,%u,%u,%u,%u).",
(ip >> 24) & 0xFFU, (ip >> 16) & 0xFFU,
(ip >> 8) & 0xFFU, ip & 0xFFU,
(port >> 8) & 0xFFU, port & 0xFFU);
return ftp_session_send_reply(session, FTP_REPLY_227_PASV_MODE, reply);
}
ftp_error_t cmd_EPSV(ftp_session_t *session, const char *args) {
(void)args;
if (session == NULL) return FTP_ERR_INVALID_PARAM;
uint32_t ip = 0U;
uint16_t port = 0U;
ftp_error_t err = open_passive_listener(session, &ip, &port);
if (err != FTP_OK) return err;
(void)ip;
char reply[FTP_REPLY_BUFFER_SIZE];
(void)snprintf(reply, sizeof(reply),
"Entering Extended Passive Mode (|||%u|).",
(unsigned)port);
return ftp_session_send_reply(session, FTP_REPLY_229_EPSV_MODE, reply);
}
+187
View File
@@ -0,0 +1,187 @@
/*
MIT License
Copyright (c) 2026 Seregon
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
/** @file extensions.c @brief FTP OPTS, SITE and client-extension commands. */
#include "ftp_commands.h"
#include "ftp_path.h"
#include "ftp_session.h"
#include "pal_fileio.h"
#include <ctype.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
/* RFC 2389 options used by modern clients. */
ftp_error_t cmd_OPTS(ftp_session_t *session, const char *args) {
if (session == NULL) {
return FTP_ERR_INVALID_PARAM;
}
if (args == NULL) {
return ftp_session_send_reply(session, FTP_REPLY_501_SYNTAX_ARGS,
"OPTS requires an argument.");
}
char upper[64];
size_t len = strlen(args);
if (len >= sizeof(upper)) {
len = sizeof(upper) - 1U;
}
for (size_t i = 0U; i < len; i++) {
upper[i] = (char)toupper((unsigned char)args[i]);
}
upper[len] = '\0';
if ((len >= 4U) && (strncmp(upper, "UTF8", 4) == 0) &&
(len == 4U || strcmp(upper + 4, " ON") == 0)) {
return ftp_session_send_reply(session, FTP_REPLY_200_OK,
"UTF8 mode enabled.");
}
if (strncmp(upper, "MLST", 4) == 0) {
return ftp_session_send_reply(session, FTP_REPLY_200_OK, "MLST OPTS set.");
}
return ftp_session_send_reply(session, FTP_REPLY_501_SYNTAX_ARGS,
"Option not recognized.");
}
/* SITE CHMOD reports the effective mode because some console mounts ignore chmod bits. */
ftp_error_t cmd_SITE(ftp_session_t *session, const char *args) {
if (session == NULL) {
return FTP_ERR_INVALID_PARAM;
}
if (args == NULL || args[0] == '\0') {
return ftp_session_send_reply(session, FTP_REPLY_501_SYNTAX_ARGS,
"SITE requires a command.");
}
char verb[8];
size_t vi = 0U;
while ((args[vi] != '\0') && (args[vi] != ' ') && (args[vi] != '\t') &&
(vi + 1U < sizeof(verb))) {
verb[vi] = (char)toupper((unsigned char)args[vi]);
vi++;
}
verb[vi] = '\0';
if (strcmp(verb, "CHMOD") != 0) {
return ftp_session_send_reply(session, FTP_REPLY_502_NOT_IMPLEMENTED,
"SITE command not supported.");
}
const char *p = args + vi;
while ((*p == ' ') || (*p == '\t')) {
p++;
}
if (*p == '\0') {
return ftp_session_send_reply(session, FTP_REPLY_501_SYNTAX_ARGS,
"SITE CHMOD requires mode and path.");
}
char *end = NULL;
errno = 0;
unsigned long mode_ul = strtoul(p, &end, 8);
if ((end == p) || (errno == ERANGE) || (mode_ul > 07777UL)) {
return ftp_session_send_reply(session, FTP_REPLY_501_SYNTAX_ARGS,
"Invalid CHMOD mode (octal 0-7777).");
}
while ((*end == ' ') || (*end == '\t')) {
end++;
}
if (*end == '\0') {
return ftp_session_send_reply(session, FTP_REPLY_501_SYNTAX_ARGS,
"SITE CHMOD requires a path.");
}
const char *path_arg = end;
size_t path_len = strlen(path_arg);
char path_buf[FTP_PATH_MAX];
if ((path_len >= 2U) &&
(((path_arg[0] == '"') && (path_arg[path_len - 1U] == '"')) ||
((path_arg[0] == '\'') && (path_arg[path_len - 1U] == '\'')))) {
if (path_len - 2U >= sizeof(path_buf)) {
return ftp_session_send_reply(session, FTP_REPLY_550_FILE_ERROR,
"Path too long.");
}
memcpy(path_buf, path_arg + 1, path_len - 2U);
path_buf[path_len - 2U] = '\0';
path_arg = path_buf;
}
char resolved[FTP_PATH_MAX];
ftp_error_t err =
ftp_path_resolve(session, path_arg, resolved, sizeof(resolved));
if (err != FTP_OK) {
return ftp_session_send_reply(session, FTP_REPLY_550_FILE_ERROR,
"Invalid path.");
}
mode_t mode = (mode_t)(mode_ul & 07777UL);
err = pal_file_chmod(resolved, mode);
if (err == FTP_ERR_NOT_FOUND) {
return ftp_session_send_reply(session, FTP_REPLY_550_FILE_ERROR,
"File not found.");
}
if (err == FTP_ERR_PERMISSION) {
return ftp_session_send_reply(session, FTP_REPLY_550_FILE_ERROR,
"Permission denied or CHMOD unsupported "
"on this filesystem.");
}
if (err != FTP_OK) {
return ftp_session_send_reply(session, FTP_REPLY_550_FILE_ERROR,
"Cannot change permissions.");
}
struct stat st;
if (pal_file_stat(resolved, &st) == FTP_OK) {
mode_t applied = (mode_t)(st.st_mode & 07777);
if (applied != mode) {
char msg[96];
(void)snprintf(msg, sizeof(msg),
"CHMOD accepted but filesystem reports %04o "
"(requested %04o).",
(unsigned)applied, (unsigned)mode);
return ftp_session_send_reply(session, FTP_REPLY_200_OK, msg);
}
}
return ftp_session_send_reply(session, FTP_REPLY_200_OK,
"CHMOD command successful.");
}
ftp_error_t cmd_CLNT(ftp_session_t *session, const char *args) {
(void)args;
if (session == NULL) {
return FTP_ERR_INVALID_PARAM;
}
return ftp_session_send_reply(session, FTP_REPLY_200_OK, "Noted.");
}
+435
View File
@@ -0,0 +1,435 @@
/*
MIT License
Copyright (c) 2026 Seregon
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
/** @file filesystem.c @brief FTP filesystem mutation and asynchronous copy commands. */
#include "ftp_commands.h"
#include "ftp_log.h"
#include "ftp_path.h"
#include "ftp_session.h"
#include "pal_fileio.h"
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static ftp_error_t start_async_copy(ftp_session_t *session,
const char *src_ftp_path,
const char *dst_ftp_path, int is_move);
ftp_error_t cmd_DELE(ftp_session_t *session, const char *args) {
if ((session == NULL) || (args == NULL)) {
return FTP_ERR_INVALID_PARAM;
}
char resolved[FTP_PATH_MAX];
ftp_error_t err = ftp_path_resolve(session, args, resolved, sizeof(resolved));
if (err != FTP_OK) {
return ftp_session_send_reply(session, FTP_REPLY_550_FILE_ERROR,
"Invalid path.");
}
err = pal_file_delete(resolved);
if (err != FTP_OK) {
return ftp_session_send_reply(session, FTP_REPLY_550_FILE_ERROR,
"Cannot delete file.");
}
return ftp_session_send_reply(session, FTP_REPLY_250_FILE_ACTION_OK,
"File deleted.");
}
ftp_error_t cmd_RMD(ftp_session_t *session, const char *args) {
if ((session == NULL) || (args == NULL)) {
return FTP_ERR_INVALID_PARAM;
}
char resolved[FTP_PATH_MAX];
ftp_error_t err = ftp_path_resolve(session, args, resolved, sizeof(resolved));
if (err != FTP_OK) {
return ftp_session_send_reply(session, FTP_REPLY_550_FILE_ERROR,
"Invalid path.");
}
err = pal_dir_remove(resolved);
if (err != FTP_OK) {
return ftp_session_send_reply(session, FTP_REPLY_550_FILE_ERROR,
"Cannot remove directory.");
}
return ftp_session_send_reply(session, FTP_REPLY_250_FILE_ACTION_OK,
"Directory removed.");
}
ftp_error_t cmd_MKD(ftp_session_t *session, const char *args) {
if ((session == NULL) || (args == NULL)) {
return FTP_ERR_INVALID_PARAM;
}
char resolved[FTP_PATH_MAX];
ftp_error_t err = ftp_path_resolve(session, args, resolved, sizeof(resolved));
if (err != FTP_OK) {
return ftp_session_send_reply(session, FTP_REPLY_550_FILE_ERROR,
"Invalid path.");
}
err = pal_dir_create(resolved, DIR_PERM);
if (err != FTP_OK) {
if ((err == FTP_ERR_DIR_EXISTS) && (pal_path_is_directory(resolved) == 1)) {
/*
* +---------------------------------------------------------+
* | CONCURRENCY HANDLING |
* | Directory was just created by another active thread. |
* | We treat this EEXIST as a success to prevent FileZilla |
* | from aborting the entire directory tree upload. |
* +---------------------------------------------------------+
*/
} else {
return ftp_session_send_reply(session, FTP_REPLY_550_FILE_ERROR,
"Cannot create directory.");
}
}
char reply[FTP_REPLY_BUFFER_SIZE];
size_t res_len = strlen(resolved);
size_t max_path = sizeof(reply) - 14; /* "..." created. + NUL */
if (res_len > max_path) { res_len = max_path; }
int n = snprintf(reply, sizeof(reply), "\"%.*s\" created.",
(int)res_len, resolved);
/* VULN-05 fix: check for truncation (same as cmd_PWD) */
if ((n < 0) || ((size_t)n >= sizeof(reply))) {
return ftp_session_send_reply(session, FTP_REPLY_257_PATH_CREATED,
"Directory created.");
}
return ftp_session_send_reply(session, FTP_REPLY_257_PATH_CREATED, reply);
}
ftp_error_t cmd_RNFR(ftp_session_t *session, const char *args) {
if ((session == NULL) || (args == NULL)) {
return FTP_ERR_INVALID_PARAM;
}
char resolved[FTP_PATH_MAX];
ftp_error_t err = ftp_path_resolve(session, args, resolved, sizeof(resolved));
if (err != FTP_OK) {
return ftp_session_send_reply(session, FTP_REPLY_550_FILE_ERROR,
"Invalid path.");
}
if (pal_path_exists(resolved) != 1) {
return ftp_session_send_reply(session, FTP_REPLY_550_FILE_ERROR,
"File not found.");
}
size_t len = strlen(resolved);
if (len >= sizeof(session->rename_from)) {
return ftp_session_send_reply(session, FTP_REPLY_550_FILE_ERROR,
"Path too long.");
}
memcpy(session->rename_from, resolved, len + 1U);
return ftp_session_send_reply(session, FTP_REPLY_350_PENDING,
"Ready for RNTO.");
}
ftp_error_t cmd_RNTO(ftp_session_t *session, const char *args) {
if ((session == NULL) || (args == NULL)) {
return FTP_ERR_INVALID_PARAM;
}
if (session->rename_from[0] == '\0') {
return ftp_session_send_reply(session, FTP_REPLY_503_BAD_SEQUENCE,
"RNFR required first.");
}
char resolved[FTP_PATH_MAX];
ftp_error_t err = ftp_path_resolve(session, args, resolved, sizeof(resolved));
if (err != FTP_OK) {
session->rename_from[0] = '\0';
return ftp_session_send_reply(session, FTP_REPLY_550_FILE_ERROR,
"Invalid path.");
}
err = pal_file_rename(session->rename_from, resolved);
if (err == FTP_ERR_CROSS_DEVICE) {
ftp_error_t async_err =
start_async_copy(session, session->rename_from, args, 1);
session->rename_from[0] = '\0';
return async_err;
}
session->rename_from[0] = '\0';
if (err != FTP_OK) {
/*
* Map ftp_error_t to a human-readable detail so the FTP client
* (FileZilla, WinSCP, etc.) shows something actionable instead
* of the opaque "Rename failed.".
*
* 550 Permission denied.
* 550 Source not found.
* 550 Path too long.
* ...
*/
const char *detail;
switch (err) {
case FTP_ERR_NOT_FOUND:
detail = "Source not found.";
break;
case FTP_ERR_PERMISSION:
detail = "Permission denied.";
break;
case FTP_ERR_PATH_TOO_LONG:
detail = "Path too long.";
break;
case FTP_ERR_OUT_OF_MEMORY:
detail = "Out of memory.";
break;
case FTP_ERR_DIR_OPEN:
detail = "Cannot open directory.";
break;
case FTP_ERR_FILE_OPEN:
detail = "Cannot open file.";
break;
case FTP_ERR_FILE_READ:
detail = "Read error during copy.";
break;
default: {
static _Thread_local char buf[64];
snprintf(buf, sizeof(buf), "Rename failed (err=%d).", (int)err);
detail = buf;
break;
}
}
return ftp_session_send_reply(session, FTP_REPLY_550_FILE_ERROR, detail);
}
return ftp_session_send_reply(session, FTP_REPLY_250_FILE_ACTION_OK,
"File renamed.");
}
typedef struct {
ftp_session_t *session;
char src_path[FTP_PATH_MAX];
char dst_path[FTP_PATH_MAX];
int is_move;
} ftp_copy_task_t;
static void *ftp_copy_thread_func(void *arg) {
ftp_copy_task_t *task = (ftp_copy_task_t *)arg;
ftp_session_t *session = task->session;
ftp_log_line(FTP_LOG_INFO, "[COPY] Background task started");
/* Use pal_file_copy_recursive_ex so the OS errno is captured and visible
* in the failure log. Previously pal_file_copy_recursive was called with
* no errno output, causing all error logs to show errno=0. */
int copy_errno = 0;
ftp_error_t err = pal_file_copy_recursive_ex(task->src_path, task->dst_path,
!task->is_move, NULL, NULL,
&copy_errno);
pthread_mutex_lock(&session->copy_mutex);
session->copy_in_progress = 0;
pthread_mutex_unlock(&session->copy_mutex);
if (err == FTP_OK) {
ftp_log_line(FTP_LOG_INFO, "[COPY] Background task completed successfully");
} else {
char msg[256];
snprintf(msg, sizeof(msg), "[COPY] Background task failed: err=%d errno=%d",
(int)err, copy_errno);
ftp_log_line(FTP_LOG_WARN, msg);
}
free(task);
return NULL;
}
static ftp_error_t start_async_copy(ftp_session_t *session,
const char *src_ftp_path,
const char *dst_ftp_path, int is_move) {
pthread_mutex_lock(&session->copy_mutex);
if (session->copy_in_progress) {
pthread_mutex_unlock(&session->copy_mutex);
return ftp_session_send_reply(session, FTP_REPLY_450_FILE_UNAVAILABLE,
"Operation already in progress.");
}
char src_resolved[FTP_PATH_MAX];
char dst_resolved[FTP_PATH_MAX];
if (ftp_path_resolve(session, src_ftp_path, src_resolved,
sizeof(src_resolved)) != FTP_OK ||
ftp_path_resolve(session, dst_ftp_path, dst_resolved,
sizeof(dst_resolved)) != FTP_OK) {
pthread_mutex_unlock(&session->copy_mutex);
return ftp_session_send_reply(session, FTP_REPLY_550_FILE_ERROR,
"Invalid path.");
}
if (strcmp(src_resolved, dst_resolved) == 0) {
pthread_mutex_unlock(&session->copy_mutex);
return ftp_session_send_reply(session, FTP_REPLY_553_FILENAME_INVALID,
"Source and destination are the same.");
}
ftp_copy_task_t *task = malloc(sizeof(ftp_copy_task_t));
if (task == NULL) {
pthread_mutex_unlock(&session->copy_mutex);
return ftp_session_send_reply(session, FTP_REPLY_451_LOCAL_ERROR,
"Memory allocation failed.");
}
task->session = session;
strncpy(task->src_path, src_resolved, sizeof(task->src_path) - 1);
task->src_path[sizeof(task->src_path) - 1] = '\0';
strncpy(task->dst_path, dst_resolved, sizeof(task->dst_path) - 1);
task->dst_path[sizeof(task->dst_path) - 1] = '\0';
task->is_move = is_move;
session->copy_in_progress = 1;
if (session->copy_thread_valid) {
pthread_join(session->copy_thread, NULL);
}
if (pthread_create(&session->copy_thread, NULL, ftp_copy_thread_func, task) !=
0) {
session->copy_in_progress = 0;
session->copy_thread_valid = 0;
pthread_mutex_unlock(&session->copy_mutex);
free(task);
return ftp_session_send_reply(session, FTP_REPLY_451_LOCAL_ERROR,
"Failed to create background thread.");
}
session->copy_thread_valid = 1;
pthread_mutex_unlock(&session->copy_mutex);
return ftp_session_send_reply(session, FTP_REPLY_250_FILE_ACTION_OK,
is_move ? "Move started in background."
: "Copy started in background.");
}
ftp_error_t cmd_CPFR(ftp_session_t *session, const char *args) {
if ((session == NULL) || (args == NULL)) {
return FTP_ERR_INVALID_PARAM;
}
if (args[0] == '\0') {
return ftp_session_send_reply(session, FTP_REPLY_501_SYNTAX_ARGS,
"Syntax: CPFR <path>");
}
char resolved[FTP_PATH_MAX];
ftp_error_t err = ftp_path_resolve(session, args, resolved, sizeof(resolved));
if (err != FTP_OK) {
return ftp_session_send_reply(session, FTP_REPLY_550_FILE_ERROR,
"Invalid source path.");
}
if (!pal_path_exists(resolved)) {
return ftp_session_send_reply(session, FTP_REPLY_550_FILE_ERROR,
"Source does not exist.");
}
strncpy(session->copy_from, args, sizeof(session->copy_from) - 1);
session->copy_from[sizeof(session->copy_from) - 1] = '\0';
return ftp_session_send_reply(session, FTP_REPLY_350_PENDING,
"File exists, ready for destination name.");
}
ftp_error_t cmd_CPTO(ftp_session_t *session, const char *args) {
if ((session == NULL) || (args == NULL)) {
return FTP_ERR_INVALID_PARAM;
}
if (args[0] == '\0') {
return ftp_session_send_reply(session, FTP_REPLY_501_SYNTAX_ARGS,
"Syntax: CPTO <path>");
}
if (session->copy_from[0] == '\0') {
return ftp_session_send_reply(session, FTP_REPLY_503_BAD_SEQUENCE,
"Bad sequence of commands (use CPFR first).");
}
ftp_error_t result = start_async_copy(session, session->copy_from, args, 0);
session->copy_from[0] = '\0';
return result;
}
ftp_error_t cmd_COPY(ftp_session_t *session, const char *args) {
if ((session == NULL) || (args == NULL)) {
return FTP_ERR_INVALID_PARAM;
}
char src_arg[FTP_PATH_MAX];
char dst_arg[FTP_PATH_MAX];
const char *space = strchr(args, ' ');
if (space == NULL) {
return ftp_session_send_reply(session, FTP_REPLY_501_SYNTAX_ARGS,
"Syntax: COPY <src> <dst>");
}
size_t src_len = (size_t)(space - args);
if (src_len >= sizeof(src_arg)) {
return ftp_session_send_reply(session, FTP_REPLY_501_SYNTAX_ARGS,
"Paths too long.");
}
strncpy(src_arg, args, src_len);
src_arg[src_len] = '\0';
const char *dst_start = space + 1;
while (*dst_start == ' ')
dst_start++; /* skip extra spaces */
if (*dst_start == '\0') {
return ftp_session_send_reply(session, FTP_REPLY_501_SYNTAX_ARGS,
"Syntax: COPY <src> <dst>");
}
strncpy(dst_arg, dst_start, sizeof(dst_arg) - 1);
dst_arg[sizeof(dst_arg) - 1] = '\0';
return start_async_copy(session, src_arg, dst_arg, 0);
}
+199
View File
@@ -0,0 +1,199 @@
/*
MIT License
Copyright (c) 2026 Seregon
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
/** @file information.c @brief FTP metadata, feature and help commands. */
#include "ftp_commands.h"
#include "ftp_instance.h"
#include "ftp_path.h"
#include "ftp_session.h"
#include "pal_fileio.h"
#include "pal_filesystem.h"
#include <stdio.h>
#include <sys/stat.h>
#include <time.h>
ftp_error_t cmd_SIZE(ftp_session_t *session, const char *args) {
if ((session == NULL) || (args == NULL)) {
return FTP_ERR_INVALID_PARAM;
}
char resolved[FTP_PATH_MAX];
ftp_error_t err = ftp_path_resolve(session, args, resolved, sizeof(resolved));
if (err != FTP_OK) {
return ftp_session_send_reply(session, FTP_REPLY_550_FILE_ERROR,
"Invalid path.");
}
vfs_stat_t st;
err = vfs_stat(resolved, &st);
if (err != FTP_OK) {
return ftp_session_send_reply(session, FTP_REPLY_550_FILE_ERROR,
"File not found.");
}
char reply[64];
snprintf(reply, sizeof(reply), "%llu", (unsigned long long)st.size);
return ftp_session_send_reply(session, FTP_REPLY_213_FILE_STATUS, reply);
}
ftp_error_t cmd_MDTM(ftp_session_t *session, const char *args) {
if ((session == NULL) || (args == NULL)) {
return FTP_ERR_INVALID_PARAM;
}
char resolved[FTP_PATH_MAX];
ftp_error_t err = ftp_path_resolve(session, args, resolved, sizeof(resolved));
if (err != FTP_OK) {
return ftp_session_send_reply(session, FTP_REPLY_550_FILE_ERROR,
"Invalid path.");
}
struct stat st;
err = pal_file_stat(resolved, &st);
if (err != FTP_OK) {
return ftp_session_send_reply(session, FTP_REPLY_550_FILE_ERROR,
"File not found.");
}
/* Format: YYYYMMDDhhmmss */
struct tm tm_time;
gmtime_r(&st.st_mtime, &tm_time);
char reply[32];
snprintf(reply, sizeof(reply), "%04d%02d%02d%02d%02d%02d",
tm_time.tm_year + 1900, tm_time.tm_mon + 1, tm_time.tm_mday,
tm_time.tm_hour, tm_time.tm_min, tm_time.tm_sec);
return ftp_session_send_reply(session, FTP_REPLY_213_FILE_STATUS, reply);
}
ftp_error_t cmd_STAT(ftp_session_t *session, const char *args) {
(void)args;
if (session == NULL) {
return FTP_ERR_INVALID_PARAM;
}
{
char msg[128];
(void)snprintf(msg, sizeof(msg),
"Server status OK. Instance %016llx",
(unsigned long long)ftp_daemon_instance_id());
return ftp_session_send_reply(session, FTP_REPLY_211_SYSTEM_STATUS, msg);
}
}
ftp_error_t cmd_SYST(ftp_session_t *session, const char *args) {
(void)args;
if (session == NULL) {
return FTP_ERR_INVALID_PARAM;
}
return ftp_session_send_reply(session, FTP_REPLY_215_SYSTEM_TYPE, NULL);
}
ftp_error_t cmd_FEAT(ftp_session_t *session, const char *args) {
(void)args;
if (session == NULL) {
return FTP_ERR_INVALID_PARAM;
}
/*
* FEAT reply (RFC 2389)
*
* 211-Extensions supported:
* SIZE
* MDTM
* REST STREAM
* APPE
* UTF8
* XZFTPD INSTANCE <hex>
* 211 End
*/
char feat_instance[48];
(void)snprintf(feat_instance, sizeof(feat_instance),
" XZFTPD INSTANCE %016llx",
(unsigned long long)ftp_daemon_instance_id());
const char *features[] = {"Extensions supported:",
#if FTP_ENABLE_SIZE
" SIZE",
#endif
#if FTP_ENABLE_MDTM
" MDTM",
#endif
#if FTP_ENABLE_REST
" REST STREAM",
#endif
" APPE",
" EPSV",
#if FTP_ENABLE_UTF8
" UTF8",
#endif
#if FTP_ENABLE_MLST
" MLSD",
" MLST type*;size*;modify*;unix.mode*;",
#endif
#if FTP_ENABLE_CRYPTO
" XCRYPT",
#endif
" CPFR",
" CPTO",
" COPY",
" SITE CHMOD",
feat_instance,
"End"};
return ftp_session_send_multiline_reply(
session, FTP_REPLY_211_SYSTEM_STATUS, features,
sizeof(features) / sizeof(features[0]));
}
ftp_error_t cmd_HELP(ftp_session_t *session, const char *args) {
(void)args;
if (session == NULL) {
return FTP_ERR_INVALID_PARAM;
}
const char *lines[] = {"Supported commands:",
" USER PASS QUIT NOOP CWD CDUP PWD",
" LIST NLST MLSD MLST",
" RETR STOR APPE REST",
" DELE RMD MKD RNFR RNTO",
" PORT PASV SIZE MDTM STAT",
" SYST FEAT HELP TYPE MODE STRU",
" SITE CHMOD",
"End"};
return ftp_session_send_multiline_reply(session, FTP_REPLY_214_HELP, lines,
sizeof(lines) / sizeof(lines[0]));
}
+476
View File
@@ -0,0 +1,476 @@
/*
MIT License
Copyright (c) 2026 Seregon
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
/** @file listing.c @brief FTP LIST/NLST/MLSD/MLST commands. */
#include "ftp_commands.h"
#include "ftp_log.h"
#include "ftp_path.h"
#include "ftp_session.h"
#include "pal_filesystem.h"
#include "pal_network.h"
#include <dirent.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
#if defined(__APPLE__) || (defined(__FreeBSD__) && !defined(PLATFORM_PS4) && !defined(PLATFORM_PS5))
#include <sys/mount.h>
#endif
static ftp_error_t send_control_raw(ftp_session_t *session,
const char *payload) {
if ((session == NULL) || (payload == NULL)) {
return FTP_ERR_INVALID_PARAM;
}
if (session->ctrl_fd < 0) {
return FTP_ERR_SOCKET_SEND;
}
size_t len = strlen(payload);
if (len == 0U) {
return FTP_OK;
}
ssize_t sent = pal_send_all(session->ctrl_fd, payload, len, 0);
if (sent != (ssize_t)len) {
return FTP_ERR_SOCKET_SEND;
}
session->last_activity = time(NULL);
return FTP_OK;
}
static const char *mlsx_type_from_mode(uint32_t mode) {
return (((mode & (uint32_t)S_IFMT) == (uint32_t)S_IFDIR)) ? "dir" : "file";
}
static void mlsx_format_modify_time(int64_t mtime, char *buffer,
size_t size) {
if ((buffer == NULL) || (size == 0U)) {
return;
}
time_t unix_time = (time_t)mtime;
struct tm tm_time;
memset(&tm_time, 0, sizeof(tm_time));
gmtime_r(&unix_time, &tm_time);
(void)strftime(buffer, size, "%Y%m%d%H%M%S", &tm_time);
}
static ftp_error_t ftp_path_to_client_path(const ftp_session_t *session,
const char *resolved,
char *output, size_t size) {
if ((session == NULL) || (resolved == NULL) || (output == NULL)) {
return FTP_ERR_INVALID_PARAM;
}
if (size < 2U) {
return FTP_ERR_PATH_TOO_LONG;
}
if (strcmp(session->root_path, "/") == 0) {
size_t len = strlen(resolved);
if ((len + 1U) > size) {
return FTP_ERR_PATH_TOO_LONG;
}
memcpy(output, resolved, len + 1U);
return FTP_OK;
}
size_t root_len = strlen(session->root_path);
if (strncmp(resolved, session->root_path, root_len) != 0) {
return FTP_ERR_PATH_INVALID;
}
const char *suffix = resolved + root_len;
if (*suffix == '\0') {
output[0] = '/';
output[1] = '\0';
return FTP_OK;
}
if (*suffix == '/') {
size_t len = strlen(suffix);
if ((len + 1U) > size) {
return FTP_ERR_PATH_TOO_LONG;
}
memcpy(output, suffix, len + 1U);
return FTP_OK;
}
int n = snprintf(output, size, "/%s", suffix);
if ((n < 0) || ((size_t)n >= size)) {
return FTP_ERR_PATH_TOO_LONG;
}
return FTP_OK;
}
static ftp_error_t mlsx_format_line(const vfs_stat_t *st,
const char *display_name,
int leading_space, char *buffer,
size_t size) {
if ((st == NULL) || (display_name == NULL) || (buffer == NULL) ||
(size == 0U)) {
return FTP_ERR_INVALID_PARAM;
}
char timebuf[20];
memset(timebuf, 0, sizeof(timebuf));
mlsx_format_modify_time(st->mtime, timebuf, sizeof(timebuf));
const char *type_str = mlsx_type_from_mode(st->mode);
unsigned long long size_value =
(((st->mode & (uint32_t)S_IFMT) == (uint32_t)S_IFDIR))
? 0ULL
: (unsigned long long)st->size;
unsigned mode_value = (unsigned)(st->mode & 07777U);
const char *prefix = (leading_space != 0) ? " " : "";
int n = snprintf(buffer, size,
"%stype=%s;size=%llu;modify=%s;unix.mode=%04o; %.*s\r\n",
prefix, type_str, size_value, timebuf, mode_value,
(int)(size > 50 ? size - 50 : 0), display_name);
if ((n < 0) || ((size_t)n >= size)) {
return FTP_ERR_PATH_TOO_LONG;
}
return FTP_OK;
}
static ftp_error_t send_directory_listing(ftp_session_t *session,
const char *path, int detailed) {
DIR *dir = opendir(path);
if (dir == NULL) {
return FTP_ERR_DIR_OPEN;
}
char line_buffer[FTP_LIST_LINE_SIZE];
struct dirent *entry;
int skip_stat = 0;
if (FTP_LIST_SAFE_MODE != 0) {
if ((strncmp(path, "/dev", 4) == 0) &&
((path[4] == '\0') || (path[4] == '/'))) {
skip_stat = 1;
} else if ((strncmp(path, "/proc", 5) == 0) &&
((path[5] == '\0') || (path[5] == '/'))) {
skip_stat = 1;
} else if ((strncmp(path, "/sys", 4) == 0) &&
((path[4] == '\0') || (path[4] == '/'))) {
skip_stat = 1;
}
#if defined(__APPLE__) || \
(defined(__FreeBSD__) && !defined(PLATFORM_PS4) && !defined(PLATFORM_PS5))
if (skip_stat == 0) {
struct statfs sfs;
if (statfs(path, &sfs) == 0) {
const char *t = sfs.f_fstypename;
if ((t != NULL) &&
((strcmp(t, "devfs") == 0) || (strcmp(t, "procfs") == 0) ||
(strcmp(t, "fdescfs") == 0) || (strcmp(t, "sysfs") == 0) ||
(strcmp(t, "linsysfs") == 0))) {
skip_stat = 1;
}
}
}
#endif
}
while ((entry = readdir(dir)) != NULL) {
/* Skip . and .. */
if ((strcmp(entry->d_name, ".") == 0) ||
(strcmp(entry->d_name, "..") == 0)) {
continue;
}
if (detailed != 0) {
/* Detailed listing (ls -l format) */
vfs_stat_t st;
int have_stat = 0;
if (skip_stat == 0) {
char fullpath[FTP_PATH_MAX];
int max_path_len = (int)(sizeof(fullpath) - 2 - strlen(entry->d_name));
if (max_path_len < 0) { continue; }
int n =
snprintf(fullpath, sizeof(fullpath), "%.*s/%s", max_path_len, path, entry->d_name);
if ((n >= 0) && ((size_t)n < sizeof(fullpath))) {
if (vfs_stat(fullpath, &st) == FTP_OK) {
have_stat = 1;
}
}
}
if (have_stat == 0) {
memset(&st, 0, sizeof(st));
if (entry->d_type == DT_DIR) {
st.mode = (uint32_t)S_IFDIR;
} else {
st.mode = (uint32_t)S_IFREG;
}
}
/* Format: -rw-r--r-- 1 user group size date filename */
char perms[11];
perms[0] = (((st.mode & S_IFMT) == S_IFDIR)) ? 'd' : '-';
perms[1] = ((st.mode & S_IRUSR) != 0U) ? 'r' : '-';
perms[2] = ((st.mode & S_IWUSR) != 0U) ? 'w' : '-';
perms[3] = ((st.mode & S_IXUSR) != 0U) ? 'x' : '-';
perms[4] = ((st.mode & S_IRGRP) != 0U) ? 'r' : '-';
perms[5] = ((st.mode & S_IWGRP) != 0U) ? 'w' : '-';
perms[6] = ((st.mode & S_IXGRP) != 0U) ? 'x' : '-';
perms[7] = ((st.mode & S_IROTH) != 0U) ? 'r' : '-';
perms[8] = ((st.mode & S_IWOTH) != 0U) ? 'w' : '-';
perms[9] = ((st.mode & S_IXOTH) != 0U) ? 'x' : '-';
perms[10] = '\0';
/* Format time */
struct tm tm_time;
time_t mtime = (time_t)st.mtime;
gmtime_r(&mtime, &tm_time);
char time_str[32];
strftime(time_str, sizeof(time_str), "%b %d %H:%M", &tm_time);
int n = snprintf(line_buffer, sizeof(line_buffer),
"%s 1 ftp ftp %10lld %s %s\r\n", perms,
(long long)st.size, time_str, entry->d_name);
if ((n < 0) || ((size_t)n >= sizeof(line_buffer))) {
continue;
}
} else {
/* Simple listing (names only) */
int n =
snprintf(line_buffer, sizeof(line_buffer), "%s\r\n", entry->d_name);
if ((n < 0) || ((size_t)n >= sizeof(line_buffer))) {
continue;
}
}
/* Send line */
(void)ftp_session_send_data(session, line_buffer, strlen(line_buffer));
}
closedir(dir);
return FTP_OK;
}
static ftp_error_t cmd_list_common(ftp_session_t *session, const char *args,
int detailed) {
if (session == NULL) {
return FTP_ERR_INVALID_PARAM;
}
char scratch[FTP_PATH_MAX];
const char *path_arg;
if (ftp_path_has_list_flag(args, session->cwd, scratch, sizeof(scratch))) {
path_arg = ftp_path_skip_list_flag(args, session->cwd);
} else {
path_arg = (args != NULL && *args != '\0') ? args : session->cwd;
}
char resolved[FTP_PATH_MAX];
ftp_error_t err = ftp_path_resolve(session, path_arg, resolved, sizeof(resolved));
if (err != FTP_OK) {
return ftp_session_send_reply(session, FTP_REPLY_550_FILE_ERROR,
"Invalid path.");
}
if (detailed != 0) {
ftp_log_line(FTP_LOG_INFO, "[DBG] LIST: opening data connection...");
}
err = ftp_session_open_data_connection(session);
if (err != FTP_OK) {
if (detailed != 0) {
char dbg[64];
(void)snprintf(dbg, sizeof(dbg),
"[DBG] LIST: data conn FAILED err=%d", (int)err);
ftp_log_line(FTP_LOG_INFO, dbg);
}
return ftp_session_send_reply(session, FTP_REPLY_425_CANT_OPEN_DATA, NULL);
}
if (detailed != 0) {
ftp_log_line(FTP_LOG_INFO, "[DBG] LIST: data conn OK, sending 150");
}
(void)ftp_session_send_reply(session, FTP_REPLY_150_FILE_OK, NULL);
usleep(50000);
err = send_directory_listing(session, resolved, detailed);
ftp_session_close_data_connection(session);
if (err != FTP_OK) {
return ftp_session_send_reply(session, FTP_REPLY_451_LOCAL_ERROR,
"Error reading directory.");
}
return ftp_session_send_reply(session, FTP_REPLY_226_TRANSFER_COMPLETE, NULL);
}
ftp_error_t cmd_LIST(ftp_session_t *session, const char *args) {
return cmd_list_common(session, args, 1);
}
ftp_error_t cmd_NLST(ftp_session_t *session, const char *args) {
return cmd_list_common(session, args, 0);
}
/* RFC 3659 machine-readable directory listing. */
ftp_error_t cmd_MLSD(ftp_session_t *session, const char *args) {
if (session == NULL) {
return FTP_ERR_INVALID_PARAM;
}
const char *path_arg = (args != NULL) ? args : session->cwd;
char resolved[FTP_PATH_MAX];
ftp_error_t err =
ftp_path_resolve(session, path_arg, resolved, sizeof(resolved));
if (err != FTP_OK) {
return ftp_session_send_reply(session, FTP_REPLY_550_FILE_ERROR,
"Invalid path.");
}
err = ftp_session_open_data_connection(session);
if (err != FTP_OK) {
return ftp_session_send_reply(session, FTP_REPLY_425_CANT_OPEN_DATA, NULL);
}
ftp_session_send_reply(session, FTP_REPLY_150_FILE_OK, NULL);
/* Keep 150 and 226 in separate control-channel reads for fragile clients. */
usleep(50000);
DIR *dir = opendir(resolved);
if (dir != NULL) {
struct dirent *entry;
char line_buffer[FTP_LIST_LINE_SIZE];
while ((entry = readdir(dir)) != NULL) {
if ((strcmp(entry->d_name, ".") == 0) ||
(strcmp(entry->d_name, "..") == 0)) {
continue;
}
vfs_stat_t st;
int have_stat = 0;
char fullpath[FTP_PATH_MAX];
int max_ml = (int)(sizeof(fullpath) - 2 - strlen(entry->d_name));
if (max_ml < 0) { continue; }
int n = snprintf(fullpath, sizeof(fullpath), "%.*s/%s", max_ml, resolved,
entry->d_name);
if ((n >= 0) && ((size_t)n < sizeof(fullpath))) {
if (vfs_stat(fullpath, &st) == FTP_OK) {
have_stat = 1;
}
}
if (have_stat == 0) {
memset(&st, 0, sizeof(st));
if (entry->d_type == DT_DIR) {
st.mode = (uint32_t)S_IFDIR;
} else {
st.mode = (uint32_t)S_IFREG;
}
}
if (mlsx_format_line(&st, entry->d_name, 0, line_buffer,
sizeof(line_buffer)) == FTP_OK) {
size_t line_len = strlen(line_buffer);
(void)ftp_session_send_data(session, line_buffer, line_len);
}
}
closedir(dir);
}
ftp_session_close_data_connection(session);
return ftp_session_send_reply(session, FTP_REPLY_226_TRANSFER_COMPLETE, NULL);
}
ftp_error_t cmd_MLST(ftp_session_t *session, const char *args) {
if (session == NULL) {
return FTP_ERR_INVALID_PARAM;
}
const char *path_arg = ((args != NULL) && (args[0] != '\0')) ? args
: session->cwd;
char resolved[FTP_PATH_MAX];
ftp_error_t err =
ftp_path_resolve(session, path_arg, resolved, sizeof(resolved));
if (err != FTP_OK) {
return ftp_session_send_reply(session, FTP_REPLY_550_FILE_ERROR,
"Invalid path.");
}
vfs_stat_t st;
err = vfs_stat(resolved, &st);
if (err != FTP_OK) {
return ftp_session_send_reply(session, FTP_REPLY_550_FILE_ERROR,
"File not found.");
}
char client_path[FTP_PATH_MAX];
err = ftp_path_to_client_path(session, resolved, client_path,
sizeof(client_path));
if (err != FTP_OK) {
return ftp_session_send_reply(session, FTP_REPLY_550_FILE_ERROR,
"Invalid path.");
}
char listing_line[FTP_LIST_LINE_SIZE];
err = mlsx_format_line(&st, client_path, 1, listing_line,
sizeof(listing_line));
if (err != FTP_OK) {
return ftp_session_send_reply(session, FTP_REPLY_451_LOCAL_ERROR,
"MLST formatting failed.");
}
char reply[FTP_REPLY_BUFFER_SIZE];
int n = snprintf(reply, sizeof(reply), "250-Listing %.*s\r\n",
(int)(sizeof(reply) - 17), client_path);
/* 250-Listing + \r\n + NUL = 17 bytes reserved */
if ((n < 0) || ((size_t)n >= sizeof(reply))) {
return ftp_session_send_reply(session, FTP_REPLY_451_LOCAL_ERROR,
"MLST formatting failed.");
}
err = send_control_raw(session, reply);
if (err != FTP_OK) {
return err;
}
err = send_control_raw(session, listing_line);
if (err != FTP_OK) {
return err;
}
return send_control_raw(session, "250 End\r\n");
}
+744
View File
@@ -0,0 +1,744 @@
/*
MIT License
Copyright (c) 2026 Seregon
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
/** @file transfer.c @brief FTP RETR/STOR/APPE/REST transfer commands. */
#include "ftp_commands.h"
#include "ftp_buffer_pool.h"
#include "ftp_log.h"
#include "ftp_path.h"
#include "ftp_session.h"
#include "pal_fileio.h"
#include "pal_filesystem.h"
#include "pal_network.h"
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
#if defined(__APPLE__) || (defined(__FreeBSD__) && !defined(PLATFORM_PS4) && !defined(PLATFORM_PS5))
#include <sys/mount.h>
#endif
#if defined(PLATFORM_PS4) || defined(PLATFORM_PS5)
#include <sys/mount.h>
extern int _fstatfs(int, struct statfs *);
#endif
/* PFS serializes inode creation on PS4/PS5. Serialize only O_CREAT opens
* in userspace to avoid multi-session journal contention and client timeouts. */
#if defined(PLATFORM_PS4) || defined(PLATFORM_PS5)
static pthread_mutex_t g_pfs_create_mtx = PTHREAD_MUTEX_INITIALIZER;
/* pthread_mutex_timedlock() is absent from the OrbisOS (PS4/PS5) SDK.
* This helper polls with trylock + nanosleep to emulate a timed lock.
* Returns 0 on success, ETIMEDOUT if the deadline passes. */
static int pfs_mutex_lock_timeout(pthread_mutex_t *mtx, int timeout_s) {
struct timespec sleep_ts;
sleep_ts.tv_sec = 0;
sleep_ts.tv_nsec = 10000000; /* 10 ms */
int elapsed_ms = 0;
const int limit_ms = timeout_s * 1000;
while (elapsed_ms < limit_ms) {
if (pthread_mutex_trylock(mtx) == 0) {
return 0;
}
nanosleep(&sleep_ts, NULL);
elapsed_ms += 10;
}
return ETIMEDOUT;
}
/* exFAT/msdosfs/ufs bypass the PFS creation lock; unknown filesystems
* use the conservative serialized path. */
static int pfs_needs_serialisation(const char *file_path) {
/* Extract parent directory */
char dir[FTP_PATH_MAX];
const char *slash = strrchr(file_path, '/');
if (slash != NULL) {
size_t dlen = (size_t)(slash - file_path);
if (dlen == 0U) {
/* path like "/file" — parent is "/" */
dir[0] = '/';
dir[1] = '\0';
} else if (dlen >= sizeof(dir)) {
return 1;
} else {
memcpy(dir, file_path, dlen);
dir[dlen] = '\0';
}
} else {
return 1;
}
/* _fstatfs() is the OrbisOS fd-based statfs; more reliable than
* path-based statfs() on PS4/PS5 for certain mount types. */
int dfd = open(dir, O_RDONLY);
if (dfd < 0) {
return 1;
}
struct statfs sfs;
memset(&sfs, 0, sizeof(sfs));
int sfs_ok = (_fstatfs(dfd, &sfs) == 0);
close(dfd);
if (sfs_ok == 0) {
return 1;
}
if (sfs.f_fstypename[0] != '\0') {
if (strcmp(sfs.f_fstypename, "exfat") == 0 ||
strcmp(sfs.f_fstypename, "msdosfs") == 0 ||
strcmp(sfs.f_fstypename, "ufs") == 0) {
return 0;
}
}
return 1;
}
#endif
static ftp_error_t upload_open_file(ftp_session_t *session,
const char *path, int flags,
const char *failure_message,
int *out_fd) {
if (session == NULL || path == NULL || failure_message == NULL ||
out_fd == NULL) {
return FTP_ERR_INVALID_PARAM;
}
#if defined(PLATFORM_PS4) || defined(PLATFORM_PS5)
int held_pfs_mtx = 0;
if ((flags & O_CREAT) != 0 && pfs_needs_serialisation(path)) {
if (pfs_mutex_lock_timeout(&g_pfs_create_mtx, 10) != 0) {
session->restart_offset = 0;
return ftp_session_send_reply(session, FTP_REPLY_451_LOCAL_ERROR,
"Server busy, please retry.");
}
held_pfs_mtx = 1;
}
#endif
int fd = pal_file_open(path, flags, FILE_PERM);
#if defined(PLATFORM_PS4) || defined(PLATFORM_PS5)
if (held_pfs_mtx != 0) {
pthread_mutex_unlock(&g_pfs_create_mtx);
}
#endif
if (fd < 0) {
session->restart_offset = 0;
return ftp_session_send_reply(session, FTP_REPLY_550_FILE_ERROR,
failure_message);
}
*out_fd = fd;
return FTP_OK;
}
static int upload_receive_single(ftp_session_t *session, int fd, void *buffer,
size_t buffer_size, uint64_t *total_received,
int *fail_stage, int *saved_errno) {
if (buffer == NULL) {
*fail_stage = 1;
return -1;
}
for (;;) {
ssize_t n = ftp_session_recv_data(session, buffer, buffer_size);
if (n < 0) {
if (errno == EINTR) continue;
*saved_errno = errno;
*fail_stage = 2;
return -1;
}
if (n == 0) return 0;
if (pal_file_write_all(fd, buffer, (size_t)n) != n) {
*saved_errno = errno;
*fail_stage = 3;
return -1;
}
*total_received += (uint64_t)n;
session->last_activity = time(NULL);
}
}
static void upload_sync_file(int fd, int success) {
#if defined(PLATFORM_PS4) || defined(PLATFORM_PS5)
(void)fd;
(void)success;
#elif defined(__linux__)
if (success != 0 && fd >= 0) (void)fdatasync(fd);
#else
if (success != 0 && fd >= 0) (void)fsync(fd);
#endif
}
static ftp_error_t upload_failure_reply(ftp_session_t *session,
const char *event,
uint64_t bytes_received,
int fail_stage, int saved_errno) {
ftp_log_session_event(session, event, FTP_ERR_UNKNOWN, bytes_received);
char detail[128];
if (fail_stage == 2) {
(void)snprintf(detail, sizeof(detail),
"Transfer failed: network receive error (errno=%d).",
saved_errno);
} else if (fail_stage == 3) {
(void)snprintf(detail, sizeof(detail),
"Transfer failed: disk write error (errno=%d).",
saved_errno);
} else {
(void)snprintf(detail, sizeof(detail), "Transfer failed.");
}
return ftp_session_send_reply(session, FTP_REPLY_426_TRANSFER_ABORTED,
detail);
}
ftp_error_t cmd_RETR(ftp_session_t *session, const char *args) {
if ((session == NULL) || (args == NULL)) {
return FTP_ERR_INVALID_PARAM;
}
char resolved[FTP_PATH_MAX];
ftp_error_t err = ftp_path_resolve(session, args, resolved, sizeof(resolved));
if (err != FTP_OK) {
return ftp_session_send_reply(session, FTP_REPLY_550_FILE_ERROR,
"Invalid path.");
}
vfs_node_t node;
err = vfs_open(&node, resolved);
if (err != FTP_OK) {
return ftp_session_send_reply(session, FTP_REPLY_550_FILE_ERROR,
"Cannot open file.");
}
uint64_t file_size = vfs_get_size(&node);
vfs_stat_t st;
int have_stat = 0;
if (vfs_stat(resolved, &st) == FTP_OK) {
have_stat = 1;
}
if (have_stat != 0) {
uint32_t fmt = (uint32_t)(st.mode & (uint32_t)S_IFMT);
if (fmt != (uint32_t)S_IFREG) {
vfs_close(&node);
session->restart_offset = 0;
return ftp_session_send_reply(session, FTP_REPLY_550_FILE_ERROR,
"Not a regular file.");
}
}
off_t offset = session->restart_offset;
if ((offset < 0) || ((uint64_t)offset > file_size)) {
vfs_close(&node);
session->restart_offset = 0;
return ftp_session_send_reply(session, FTP_REPLY_550_FILE_ERROR,
"Invalid offset.");
}
vfs_set_offset(&node, (uint64_t)offset);
ftp_session_send_reply(session, FTP_REPLY_150_FILE_OK, NULL);
err = ftp_session_open_data_connection(session);
if (err != FTP_OK) {
vfs_close(&node);
return ftp_session_send_reply(session, FTP_REPLY_425_CANT_OPEN_DATA, NULL);
}
size_t remaining = (size_t)(file_size - (uint64_t)offset);
uint64_t bytes_sent = 0U;
/* Encryption and throttling require userspace, so this RETR path cannot use them. */
int use_sendfile = ((vfs_get_caps(&node) & VFS_CAP_SENDFILE) != 0U) &&
(FTP_TRANSFER_RATE_LIMIT_BPS == 0U);
{
char diag[320];
/* Limit filename length to fit the fixed-size log buffer.
* FTP_PATH_MAX (4096) exceeds sizeof(diag) so GCC warns about
* potential truncation; use %.*s to bound the output explicitly. */
const char *short_name = strrchr(resolved, '/');
if (short_name != NULL) { short_name++; } else { short_name = resolved; }
#if defined(PLATFORM_PS4) || defined(PLATFORM_PS5)
struct statfs sfs;
const char *fstype = "unknown";
if (_fstatfs(node.fd, &sfs) == 0) { fstype = sfs.f_fstypename; }
int max_fn = (int)(sizeof(diag) - 140);
if (max_fn < 0) { max_fn = 0; }
snprintf(diag, sizeof(diag),
"[RETR] file=%.*s size=%llu fs=%s sendfile=%d "
"chunk=%u eagain_sleep=%u sndbuf=%u",
max_fn, short_name, (unsigned long long)file_size, fstype, use_sendfile,
(unsigned)FTP_RETR_SENDFILE_CHUNK,
(unsigned)FTP_SENDFILE_EAGAIN_SLEEP_US,
(unsigned)FTP_TCP_DATA_SNDBUF);
#else
int max_fn = (int)(sizeof(diag) - 80);
if (max_fn < 0) { max_fn = 0; }
snprintf(diag, sizeof(diag),
"[RETR] file=%.*s size=%llu sendfile=%d chunk=%u",
max_fn, short_name, (unsigned long long)file_size, use_sendfile,
(unsigned)FTP_RETR_SENDFILE_CHUNK);
#endif
ftp_log_line(FTP_LOG_INFO, diag);
}
#if FTP_ENABLE_CRYPTO
if (session->crypto.active != 0U) {
use_sendfile = 0;
}
#endif
if (use_sendfile == 0) {
vfs_close(&node);
ftp_session_close_data_connection(session);
session->restart_offset = 0;
return ftp_session_send_reply(session, FTP_REPLY_426_TRANSFER_ABORTED,
"sendfile unavailable (crypto/rate-limit active).");
}
/* The PAL uses zero-copy where safe and buffered I/O on console filesystems
* whose vnode pagers cannot support sendfile. */
pal_socket_cork(session->data_fd);
while (remaining > 0U) {
size_t chunk = remaining > (size_t)FTP_RETR_SENDFILE_CHUNK
? (size_t)FTP_RETR_SENDFILE_CHUNK
: remaining;
ssize_t sent = pal_sendfile_retry(
session->data_fd, node.fd, &offset, chunk,
FTP_SENDFILE_EAGAIN_RETRIES, FTP_SENDFILE_EAGAIN_SLEEP_US);
if (sent <= 0) break;
remaining -= (size_t)sent;
bytes_sent += (uint64_t)sent;
session->last_activity = time(NULL);
atomic_fetch_add(&session->stats.bytes_sent, (uint64_t)sent);
#if defined(POSIX_FADV_DONTNEED) && !defined(PLATFORM_PS4) && !defined(PS4)
off_t evict_start = offset - (off_t)sent;
if (node.fd >= 0 && evict_start >= 0)
(void)posix_fadvise(node.fd, evict_start, (off_t)sent, POSIX_FADV_DONTNEED);
#endif
}
pal_socket_uncork(session->data_fd);
vfs_close(&node);
ftp_session_close_data_connection(session);
session->restart_offset = 0;
if (remaining == 0U) {
atomic_fetch_add(&session->stats.files_sent, 1U);
ftp_log_session_event(session, "RETR_OK", FTP_OK, bytes_sent);
return ftp_session_send_reply(session, FTP_REPLY_226_TRANSFER_COMPLETE,
NULL);
}
ftp_log_session_event(session, "RETR_FAIL", FTP_ERR_UNKNOWN, bytes_sent);
return ftp_session_send_reply(session, FTP_REPLY_426_TRANSFER_ABORTED,
"Transfer failed.");
}
/* Non-console STOR overlaps network receive with disk writes using two buffers. */
typedef struct {
void *buf[2];
size_t len[2];
int active;
int fd;
int error;
int done;
uint64_t written;
pthread_mutex_t mtx;
pthread_cond_t cv_ready;
pthread_cond_t cv_free;
} stor_pipe_t;
static void *stor_writer_thread(void *arg) {
stor_pipe_t *p = (stor_pipe_t *)arg;
pthread_mutex_lock(&p->mtx);
for (;;) {
while ((p->len[1 - p->active] == 0U) && (p->done == 0)) {
pthread_cond_wait(&p->cv_ready, &p->mtx);
}
int drain = 1 - p->active;
size_t nbytes = p->len[drain];
if ((nbytes == 0U) && (p->done != 0)) {
break;
}
pthread_mutex_unlock(&p->mtx);
ssize_t w = pal_file_write_all(p->fd, p->buf[drain], nbytes);
int write_ok = (w == (ssize_t)nbytes) ? 1 : 0;
pthread_mutex_lock(&p->mtx);
if (write_ok != 0) {
p->written += (uint64_t)nbytes;
} else {
p->error = errno;
}
p->len[drain] = 0U;
pthread_cond_signal(&p->cv_free);
if (write_ok == 0) {
break;
}
}
pthread_mutex_unlock(&p->mtx);
return NULL;
}
ftp_error_t cmd_STOR(ftp_session_t *session, const char *args) {
if ((session == NULL) || (args == NULL)) {
return FTP_ERR_INVALID_PARAM;
}
char resolved[FTP_PATH_MAX];
ftp_error_t err = ftp_path_resolve(session, args, resolved, sizeof(resolved));
if (err != FTP_OK) {
session->restart_offset = 0;
return ftp_session_send_reply(session, FTP_REPLY_550_FILE_ERROR,
"Invalid path.");
}
/* Fresh non-console uploads use a temporary sibling and atomic rename. */
char tmp_path[FTP_PATH_MAX];
int was_fresh_upload = (session->restart_offset == 0) ? 1 : 0;
#if defined(PLATFORM_PS5) || defined(PLATFORM_PS4)
/* Temp-file writes add significant PFS latency on consoles; write directly there. */
int use_atomic = 0;
#else
int use_atomic = (session->restart_offset == 0) ? 1 : 0;
#endif
if (use_atomic != 0) {
const char *slash = strrchr(resolved, '/');
if (slash != NULL) {
size_t dir_len = (size_t)(slash - resolved);
size_t tail_len = strlen(slash + 1);
/* Bound basename arithmetic before constructing the temporary path. */
const size_t overhead = sizeof("/.zftpd.tmp.");
if (dir_len < sizeof(tmp_path) - overhead) {
size_t max_tail = sizeof(tmp_path) - dir_len - overhead;
if (tail_len > max_tail) { tail_len = max_tail; }
} else {
tail_len = 0;
}
int n = snprintf(tmp_path, sizeof(tmp_path), "%.*s/.zftpd.tmp.%.*s",
(int)dir_len, resolved, (int)tail_len, slash + 1);
(void)n;
} else {
size_t name_len = strlen(resolved);
size_t max_name = sizeof(tmp_path) - sizeof(".zftpd.tmp.");
if (name_len > max_name) { name_len = max_name; }
int n = snprintf(tmp_path, sizeof(tmp_path), ".zftpd.tmp.%.*s",
(int)name_len, resolved);
(void)n;
}
}
const char *write_path = (use_atomic != 0) ? tmp_path : resolved;
int open_flags = O_WRONLY | O_CREAT;
if (session->restart_offset == 0) {
open_flags |= O_TRUNC;
}
/* Open before 150/accept: PFS O_CREAT can block long enough to fill the
* client-facing TCP receive window if data transfer has already started. */
int fd = -1;
err = upload_open_file(session, write_path, open_flags,
"Cannot create file.", &fd);
if (err != FTP_OK || fd < 0) {
if (use_atomic != 0) (void)unlink(tmp_path);
return err != FTP_OK ? err : FTP_ERR_FILE_OPEN;
}
if (session->restart_offset > 0) {
if (lseek(fd, session->restart_offset, SEEK_SET) < 0) {
pal_file_close(fd);
if (use_atomic != 0) {
(void)unlink(tmp_path);
} else if (was_fresh_upload != 0) {
(void)unlink(write_path);
}
session->restart_offset = 0;
return ftp_session_send_reply(session, FTP_REPLY_451_LOCAL_ERROR,
"Seek failed.");
}
}
ftp_session_send_reply(session, FTP_REPLY_150_FILE_OK, NULL);
err = ftp_session_open_data_connection(session);
if (err != FTP_OK) {
pal_file_close(fd);
if (use_atomic != 0) {
(void)unlink(tmp_path);
} else if (was_fresh_upload != 0) {
(void)unlink(write_path);
}
session->restart_offset = 0;
return ftp_session_send_reply(session, FTP_REPLY_425_CANT_OPEN_DATA, NULL);
}
/* OrbisOS receive buffers cannot reliably absorb double-buffer writer stalls;
* consoles therefore keep recv/write single-buffered. */
#if defined(PLATFORM_PS4) || defined(PLATFORM_PS5)
void *buf0 = ftp_buffer_acquire();
void *buf1 = NULL;
#else
void *buf0 = ftp_buffer_acquire();
void *buf1 = ftp_buffer_acquire();
#endif
size_t buf_sz = ftp_buffer_size();
uint64_t total_received = 0U;
int ok = 1;
int fail_stage = 0; /* 1 = no buffer, 2 = recv error, 3 = write error */
int saved_errno = 0;
if ((buf0 == NULL) || (buf1 == NULL)) {
void *buffer = (buf0 != NULL) ? buf0 : buf1;
ftp_buffer_release((buf0 != NULL) ? buf1 : buf0);
if (upload_receive_single(session, fd, buffer, buf_sz,
&total_received, &fail_stage,
&saved_errno) != 0) {
ok = 0;
}
ftp_buffer_release(buffer);
} else {
stor_pipe_t pipe;
pipe.buf[0] = buf0;
pipe.buf[1] = buf1;
pipe.len[0] = 0U;
pipe.len[1] = 0U;
pipe.active = 0;
pipe.fd = fd;
pipe.error = 0;
pipe.done = 0;
pipe.written = 0U;
pthread_mutex_init(&pipe.mtx, NULL);
pthread_cond_init(&pipe.cv_ready, NULL);
pthread_cond_init(&pipe.cv_free, NULL);
pthread_t writer;
int thread_ok =
(pthread_create(&writer, NULL, stor_writer_thread, &pipe) == 0) ? 1 : 0;
if (thread_ok == 0) {
if (upload_receive_single(session, fd, buf0, buf_sz,
&total_received, &fail_stage,
&saved_errno) != 0) {
ok = 0;
}
} else {
while (1) {
pthread_mutex_lock(&pipe.mtx);
while (pipe.len[pipe.active] != 0U) {
if (pipe.error != 0) {
pthread_mutex_unlock(&pipe.mtx);
goto recv_done;
}
pthread_cond_wait(&pipe.cv_free, &pipe.mtx);
}
if (pipe.error != 0) {
pthread_mutex_unlock(&pipe.mtx);
break;
}
int fill_idx = pipe.active;
pthread_mutex_unlock(&pipe.mtx);
ssize_t n = ftp_session_recv_data(session, pipe.buf[fill_idx], buf_sz);
if (n < 0) {
if (errno == EINTR) {
continue;
}
saved_errno = errno;
fail_stage = 2;
ok = 0;
break;
}
if (n == 0) {
break;
}
total_received += (uint64_t)n;
session->last_activity = time(NULL);
pthread_mutex_lock(&pipe.mtx);
pipe.len[fill_idx] = (size_t)n;
pipe.active = 1 - fill_idx;
pthread_cond_signal(&pipe.cv_ready);
pthread_mutex_unlock(&pipe.mtx);
}
recv_done:
pthread_mutex_lock(&pipe.mtx);
pipe.done = 1;
pthread_cond_signal(&pipe.cv_ready);
pthread_mutex_unlock(&pipe.mtx);
(void)pthread_join(writer, NULL);
if (pipe.error != 0) {
saved_errno = pipe.error;
fail_stage = 3;
ok = 0;
}
}
pthread_mutex_destroy(&pipe.mtx);
pthread_cond_destroy(&pipe.cv_ready);
pthread_cond_destroy(&pipe.cv_free);
ftp_buffer_release(buf0);
ftp_buffer_release(buf1);
}
upload_sync_file(fd, ok);
/* Avoid POSIX_FADV_DONTNEED on writable PFS files: FreeBSD may turn it
* into a synchronous crypto-backed flush with severe tail latency. */
pal_file_close(fd);
ftp_session_close_data_connection(session);
session->restart_offset = 0;
if (ok != 0) {
/* rename() exposes the completed file atomically. */
if (use_atomic != 0) {
if (rename(tmp_path, resolved) != 0) {
(void)unlink(tmp_path);
return ftp_session_send_reply(session, FTP_REPLY_451_LOCAL_ERROR,
"Rename to final path failed.");
}
}
atomic_fetch_add(&session->stats.files_received, 1U);
ftp_log_session_event(session, "STOR_OK", FTP_OK, total_received);
return ftp_session_send_reply(session, FTP_REPLY_226_TRANSFER_COMPLETE,
NULL);
}
if (use_atomic != 0) {
(void)unlink(tmp_path);
} else if (was_fresh_upload != 0) {
/* Remove failed fresh uploads; keep resumed partials for REST continuation. */
(void)unlink(write_path);
}
return upload_failure_reply(session, "STOR_FAIL", total_received,
fail_stage, saved_errno);
}
/* APPE seeks to REST offset when set; otherwise it uses O_APPEND. */
ftp_error_t cmd_APPE(ftp_session_t *session, const char *args) {
if (session == NULL || args == NULL) return FTP_ERR_INVALID_PARAM;
char resolved[FTP_PATH_MAX];
ftp_error_t err = ftp_path_resolve(session, args, resolved, sizeof(resolved));
if (err != FTP_OK) {
session->restart_offset = 0;
return ftp_session_send_reply(session, FTP_REPLY_550_FILE_ERROR,
"Invalid path.");
}
int open_flags = O_WRONLY | O_CREAT;
if (session->restart_offset == 0) open_flags |= O_APPEND;
int fd = -1;
err = upload_open_file(session, resolved, open_flags, "Cannot open file.", &fd);
if (err != FTP_OK || fd < 0)
return err != FTP_OK ? err : FTP_ERR_FILE_OPEN;
if (session->restart_offset > 0 &&
lseek(fd, session->restart_offset, SEEK_SET) < 0) {
pal_file_close(fd);
session->restart_offset = 0;
return ftp_session_send_reply(session, FTP_REPLY_451_LOCAL_ERROR,
"Seek failed.");
}
(void)ftp_session_send_reply(session, FTP_REPLY_150_FILE_OK, NULL);
err = ftp_session_open_data_connection(session);
if (err != FTP_OK) {
pal_file_close(fd);
session->restart_offset = 0;
return ftp_session_send_reply(session, FTP_REPLY_425_CANT_OPEN_DATA, NULL);
}
void *buffer = ftp_buffer_acquire();
uint64_t total_received = 0U;
int fail_stage = 0;
int saved_errno = 0;
int ok = upload_receive_single(session, fd, buffer, ftp_buffer_size(),
&total_received, &fail_stage,
&saved_errno) == 0;
upload_sync_file(fd, ok);
pal_file_close(fd);
ftp_buffer_release(buffer);
ftp_session_close_data_connection(session);
session->restart_offset = 0;
if (ok != 0) {
ftp_log_session_event(session, "APPE_OK", FTP_OK, total_received);
return ftp_session_send_reply(session, FTP_REPLY_226_TRANSFER_COMPLETE,
NULL);
}
return upload_failure_reply(session, "APPE_FAIL", total_received,
fail_stage, saved_errno);
}
ftp_error_t cmd_REST(ftp_session_t *session, const char *args) {
if ((session == NULL) || (args == NULL)) {
return FTP_ERR_INVALID_PARAM;
}
char *endptr;
long long offset = strtoll(args, &endptr, 10);
if ((*endptr != '\0') || (offset < 0)) {
return ftp_session_send_reply(session, FTP_REPLY_501_SYNTAX_ARGS,
"Invalid offset.");
}
session->restart_offset = (off_t)offset;
return ftp_session_send_reply(session, FTP_REPLY_350_PENDING,
"Restart position accepted.");
}
+158
View File
@@ -0,0 +1,158 @@
/*
MIT License
Copyright (c) 2026 Seregon
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
/** @file transfer_params.c @brief FTP transfer parameters and optional AUTH XCRYPT. */
#include "ftp_commands.h"
#include "ftp_crypto.h"
#include "ftp_log.h"
#include "ftp_session.h"
#include "pal_fileio.h"
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
ftp_error_t cmd_TYPE(ftp_session_t *session, const char *args) {
if ((session == NULL) || (args == NULL)) {
return FTP_ERR_INVALID_PARAM;
}
if ((args[0] == 'A') || (args[0] == 'a')) {
session->transfer_type = FTP_TYPE_ASCII;
} else if ((args[0] == 'I') || (args[0] == 'i')) {
session->transfer_type = FTP_TYPE_BINARY;
} else {
return ftp_session_send_reply(session, FTP_REPLY_504_NOT_IMPL_PARAM,
"Type not supported.");
}
return ftp_session_send_reply(session, FTP_REPLY_200_OK, "Type set.");
}
ftp_error_t cmd_MODE(ftp_session_t *session, const char *args) {
if ((session == NULL) || (args == NULL)) {
return FTP_ERR_INVALID_PARAM;
}
if ((args[0] == 'S') || (args[0] == 's')) {
session->transfer_mode = FTP_MODE_STREAM;
return ftp_session_send_reply(session, FTP_REPLY_200_OK,
"Mode set to Stream.");
}
return ftp_session_send_reply(session, FTP_REPLY_504_NOT_IMPL_PARAM,
"Only Stream mode supported.");
}
ftp_error_t cmd_STRU(ftp_session_t *session, const char *args) {
if ((session == NULL) || (args == NULL)) {
return FTP_ERR_INVALID_PARAM;
}
if ((args[0] == 'F') || (args[0] == 'f')) {
session->file_structure = FTP_STRU_FILE;
return ftp_session_send_reply(session, FTP_REPLY_200_OK,
"Structure set to File.");
}
return ftp_session_send_reply(session, FTP_REPLY_504_NOT_IMPL_PARAM,
"Only File structure supported.");
}
/* AUTH XCRYPT derives a per-session ChaCha20 key from the PSK and a 96-bit nonce. */
#if FTP_ENABLE_CRYPTO
static char nibble_to_hex(uint8_t n) {
return (n < 10U) ? (char)('0' + n) : (char)('a' + (n - 10U));
}
static int generate_nonce(uint8_t *buf, size_t len) {
int fd = pal_file_open("/dev/urandom", O_RDONLY, 0);
if (fd < 0) return -1;
size_t offset = 0U;
while (offset < len) {
ssize_t n = pal_file_read(fd, buf + offset, len - offset);
if (n <= 0) {
pal_file_close(fd);
return -1;
}
offset += (size_t)n;
}
pal_file_close(fd);
return 0;
}
ftp_error_t cmd_AUTH(ftp_session_t *session, const char *args) {
if ((session == NULL) || (args == NULL)) {
return FTP_ERR_INVALID_PARAM;
}
if ((strcmp(args, "XCRYPT") != 0) && (strcmp(args, "xcrypt") != 0)) {
return ftp_session_send_reply(session, FTP_REPLY_504_NOT_IMPL_PARAM,
"Unsupported AUTH mechanism.");
}
if (session->crypto.active != 0U) {
return ftp_session_send_reply(session, FTP_REPLY_503_BAD_SEQUENCE,
"Already encrypted.");
}
uint8_t nonce[12];
if (generate_nonce(nonce, sizeof(nonce)) != 0) {
return ftp_session_send_reply(session, FTP_REPLY_451_LOCAL_ERROR,
"Secure random source unavailable.");
}
static const uint8_t psk[32] = FTP_CRYPTO_PSK;
uint8_t session_key[32];
ftp_crypto_derive_key(psk, nonce, session_key);
char hex_nonce[25]; /* 24 hex chars + NUL */
for (size_t i = 0U; i < 12U; i++) {
hex_nonce[i * 2U] = nibble_to_hex((nonce[i] >> 4U) & 0x0FU);
hex_nonce[(i * 2U) + 1U] = nibble_to_hex(nonce[i] & 0x0FU);
}
hex_nonce[24] = '\0';
char reply_msg[64];
(void)snprintf(reply_msg, sizeof(reply_msg), "XCRYPT %s", hex_nonce);
ftp_error_t err =
ftp_session_send_reply(session, FTP_REPLY_234_AUTH_OK, reply_msg);
if (err == FTP_OK) {
ftp_crypto_init(&session->crypto, session_key, nonce);
ftp_log_session_event(session, "CRYPTO_ON", FTP_OK, 0U);
}
volatile uint8_t *vk = (volatile uint8_t *)session_key;
for (size_t i = 0U; i < sizeof(session_key); i++) {
vk[i] = 0U;
}
return err;
}
#endif /* FTP_ENABLE_CRYPTO */
+115
View File
@@ -0,0 +1,115 @@
/*
MIT License
Copyright (c) 2026 Seregon
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
/**
* @file ftp_buffer_pool.h
* @brief FTP stream buffer pool
*
* @author Seregon
* @version 1.0.0
*
* PLATFORMS: FreeBSD (PS4/PS5 kqueue), Linux (epoll)
* DESIGN: Single-threaded, non-blocking I/O
*
*/
#include "ftp_buffer_pool.h"
#include "ftp_config.h"
#include <stdatomic.h>
#include <stdint.h>
#ifndef FTP_STREAM_BUFFER_SIZE
#define FTP_STREAM_BUFFER_SIZE 65536U
#endif
#ifndef FTP_STREAM_BUFFER_COUNT
#define FTP_STREAM_BUFFER_COUNT FTP_MAX_SESSIONS
#endif
static _Alignas(64) uint8_t
g_stream_buffers[FTP_STREAM_BUFFER_COUNT][FTP_STREAM_BUFFER_SIZE];
static atomic_uint_fast32_t g_stream_buffer_mask = ATOMIC_VAR_INIT(0U);
/*
* VULN-06 fix: guard against 1U << i undefined behavior
*
* FTP_MAX_SESSIONS up to 256 is allowed by ftp_config.h,
* but the bitmask is uint_fast32_t (may be 32-bit on ARM).
* If BUFFER_COUNT > bit width, slots beyond bit 31 are
* invisible and those sessions can never acquire a buffer.
*
* This assert fires at compile time if misconfigured.
*/
_Static_assert(FTP_STREAM_BUFFER_COUNT <= (sizeof(uint_fast32_t) * 8U),
"FTP_STREAM_BUFFER_COUNT exceeds atomic bitmask width "
"(reduce FTP_MAX_SESSIONS or widen the mask type)");
void *ftp_buffer_acquire(void) {
for (;;) {
uint_fast32_t mask = atomic_load(&g_stream_buffer_mask);
uint_fast32_t found = 0U;
for (uint_fast32_t i = 0U; i < (uint_fast32_t)FTP_STREAM_BUFFER_COUNT;
i++) {
uint_fast32_t bit = 1U << i;
if ((mask & bit) != 0U) {
continue;
}
uint_fast32_t expected = mask;
uint_fast32_t desired = mask | bit;
if (atomic_compare_exchange_strong(&g_stream_buffer_mask, &expected,
desired)) {
return (void *)g_stream_buffers[i];
}
found = 1U;
break; /* CAS failed, re-read mask from outer loop */
}
/*
* If CAS failed (found==1), the outer loop retries with a fresh mask.
* If no free slot was found (found==0), check if mask changed
* since we read it; if unchanged, all buffers are truly occupied.
*/
if ((found == 0U) && (mask == atomic_load(&g_stream_buffer_mask))) {
return NULL;
}
}
}
void ftp_buffer_release(void *buffer) {
if (buffer == NULL) {
return;
}
for (uint_fast32_t i = 0U; i < (uint_fast32_t)FTP_STREAM_BUFFER_COUNT; i++) {
if ((void *)g_stream_buffers[i] == buffer) {
uint_fast32_t bit = 1U << i;
(void)atomic_fetch_and(&g_stream_buffer_mask, ~bit);
return;
}
}
}
size_t ftp_buffer_size(void) { return (size_t)FTP_STREAM_BUFFER_SIZE; }
File renamed without changes.
+181
View File
@@ -0,0 +1,181 @@
/*
MIT License
Copyright (c) 2026 Seregon
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
/**
* @file ftp_instance.c
* @brief Process-lifetime daemon identity for rest-mode reconnect
*/
#include "ftp_instance.h"
#include <errno.h>
#include <limits.h>
#include <stdatomic.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
static atomic_uint_fast64_t g_daemon_instance_id = ATOMIC_VAR_INIT(0);
static atomic_uint_fast64_t g_daemon_start_ns = ATOMIC_VAR_INIT(0);
static uint64_t monotonic_ns_now(void) {
struct timespec ts;
if (clock_gettime(CLOCK_MONOTONIC, &ts) == 0) {
return ((uint64_t)ts.tv_sec * 1000000000ULL) + (uint64_t)ts.tv_nsec;
}
/* Fallback: wall clock (still unique enough for instance seed). */
return (uint64_t)time(NULL) * 1000000000ULL;
}
void ftp_instance_init(void) {
(void)ftp_daemon_instance_id();
}
uint64_t ftp_daemon_instance_id(void) {
uint64_t id = atomic_load(&g_daemon_instance_id);
if (id != 0U) {
return id;
}
uint64_t start_ns = monotonic_ns_now();
uint64_t seed = start_ns ^ (uint64_t)(uintptr_t)&g_daemon_instance_id ^
(uint64_t)getpid();
/* Mix seed; keep non-zero so clients can distinguish "unknown" (0). */
seed = seed * 6364136223846793005ULL + 1442695040888963407ULL;
if (seed == 0U) {
seed = 1ULL;
}
uint64_t expected = 0U;
if (atomic_compare_exchange_strong(&g_daemon_instance_id, &expected, seed)) {
atomic_store(&g_daemon_start_ns, start_ns);
return seed;
}
return atomic_load(&g_daemon_instance_id);
}
uint64_t ftp_daemon_start_monotonic_ns(void) {
(void)ftp_daemon_instance_id();
return atomic_load(&g_daemon_start_ns);
}
int ftp_instance_identity_format(char *out, size_t capacity, int pid,
uint64_t token) {
if (out == NULL || capacity == 0U || pid <= 1) {
return -1;
}
int written = snprintf(out, capacity, "%d %016llx\n", pid,
(unsigned long long)token);
if (written <= 0 || (size_t)written >= capacity) {
return -1;
}
return 0;
}
int ftp_instance_identity_parse(const char *text, int *pid, uint64_t *token) {
if (text == NULL) {
return -1;
}
errno = 0;
char *pid_end = NULL;
long parsed_pid = strtol(text, &pid_end, 10);
if (pid_end == text || errno != 0 || parsed_pid <= 1L || parsed_pid > INT_MAX) {
return -1;
}
if (*pid_end != ' ' && *pid_end != '\t') {
return -1;
}
const char *token_start = pid_end + 1;
while (*token_start == ' ' || *token_start == '\t') {
token_start++;
}
errno = 0;
char *token_end = NULL;
unsigned long long parsed_token = strtoull(token_start, &token_end, 16);
if (token_end == token_start || errno != 0) {
return -1;
}
while (*token_end == ' ' || *token_end == '\t') {
token_end++;
}
if (*token_end != '\0' && *token_end != '\n' && *token_end != '\r') {
return -1;
}
if (pid != NULL) {
*pid = (int)parsed_pid;
}
if (token != NULL) {
*token = (uint64_t)parsed_token;
}
return 0;
}
int ftp_instance_stop_command_format(char *out, size_t capacity,
uint64_t token) {
if (out == NULL || capacity == 0U) {
return -1;
}
int written = snprintf(out, capacity, "STOP %016llx\n",
(unsigned long long)token);
if (written <= 0 || (size_t)written >= capacity) {
return -1;
}
return 0;
}
int ftp_instance_stop_command_parse(const char *line, uint64_t *token) {
static const char prefix[] = "STOP ";
if (line == NULL) {
return -1;
}
if (strncmp(line, prefix, sizeof(prefix) - 1U) != 0) {
return -1;
}
const char *start = line + sizeof(prefix) - 1U;
errno = 0;
char *end = NULL;
unsigned long long parsed = strtoull(start, &end, 16);
if (end == start || errno != 0) {
return -1;
}
while (*end == ' ' || *end == '\t') {
end++;
}
if (*end != '\0' && *end != '\n' && *end != '\r') {
return -1;
}
if (token != NULL) {
*token = (uint64_t)parsed;
}
return 0;
}
View File
File renamed without changes.
+54 -1
View File
@@ -265,7 +265,9 @@ ftp_error_t ftp_path_resolve(const ftp_session_t *session,
}
char joined[FTP_PATH_MAX];
int nn = snprintf(joined, sizeof(joined), "%s/%s", dir_real, base_buf);
int max_dir = (int)(sizeof(joined) - 2 - strlen(base_buf));
if (max_dir < 0) { return FTP_ERR_PATH_TOO_LONG; }
int nn = snprintf(joined, sizeof(joined), "%.*s/%s", max_dir, dir_real, base_buf);
if ((nn < 0) || ((size_t)nn >= sizeof(joined))) {
return FTP_ERR_PATH_TOO_LONG;
}
@@ -500,3 +502,54 @@ ftp_error_t ftp_path_join(const char *base,
/* Normalize the joined path */
return ftp_path_normalize(temp, output, size);
}
/*
* LIST flag disambiguation.
* curlftpfs sends "LIST -a" by default; treat "-a"/"-l"/"-la"/"-al"
* as flags (case-insensitive), stat single tokens to tell flag from path.
*/
static int is_list_flag_char(int c) {
return (c == 'a' || c == 'A' || c == 'l' || c == 'L');
}
static int is_list_flag(const char *s) {
if (s == NULL || *s != '-') return 0;
for (const char *p = s + 1; *p != '\0' && *p != ' '; p++)
if (!is_list_flag_char((unsigned char)*p)) return 0;
return s[1] != '\0';
}
static int has_multi_tokens(const char *s) {
while (*s != '\0' && *s != ' ') s++;
while (*s == ' ') s++;
return *s != '\0';
}
int ftp_path_has_list_flag(const char *args, const char *cwd,
char *scratch, size_t scratch_size) {
if (args == NULL || *args == '\0') return 0;
if (!is_list_flag(args)) return 0;
/* args like "-a /data" → definitely a flag, strip it */
if (has_multi_tokens(args)) return 1;
/* Single token like "-a": stat(CWD/-a).
* Exists → path (e.g. a directory actually named "-a")
* Absent → flag (curlftpfs default) */
size_t cwd_len = strlen(cwd);
size_t arg_len = strlen(args);
size_t needed = cwd_len + 1U + arg_len + 1U;
if (needed > scratch_size) return 1; /* buffer too small, assume flag */
memcpy(scratch, cwd, cwd_len);
if (cwd_len > 0U && cwd[cwd_len - 1U] != '/') scratch[cwd_len++] = '/';
memcpy(scratch + cwd_len, args, arg_len);
scratch[cwd_len + arg_len] = '\0';
return pal_path_exists(scratch) ? 0 : 1; /* exists=path, absent=flag */
}
const char *ftp_path_skip_list_flag(const char *args, const char *cwd) {
while (*args != '\0' && *args != ' ') args++;
while (*args == ' ') args++;
return (*args == '\0') ? cwd : args;
}
@@ -84,6 +84,11 @@ static const ftp_command_entry_t command_table[] = {
{"RNFR", cmd_RNFR, FTP_ARGS_REQUIRED},
{"RNTO", cmd_RNTO, FTP_ARGS_REQUIRED},
/* Async Copy */
{"CPFR", cmd_CPFR, FTP_ARGS_REQUIRED},
{"CPTO", cmd_CPTO, FTP_ARGS_REQUIRED},
{"COPY", cmd_COPY, FTP_ARGS_REQUIRED},
/* Data connection */
{"PORT", cmd_PORT, FTP_ARGS_REQUIRED},
{"PASV", cmd_PASV, FTP_ARGS_NONE},
+155 -23
View File
@@ -33,8 +33,10 @@ SOFTWARE.
*/
#include "ftp_server.h"
#include "ftp_instance.h"
#include "ftp_session.h"
#include "pal_network.h"
#include "pal_resilient_server.h"
#include <string.h>
#include <unistd.h>
#include <signal.h>
@@ -73,6 +75,9 @@ ftp_error_t ftp_server_init(ftp_server_context_t *ctx,
if (err != FTP_OK) {
return err;
}
/* Process-lifetime identity for rest-mode reconnect clients */
ftp_instance_init();
/* Create listen socket */
int fd = PAL_SOCKET(AF_INET, SOCK_STREAM, 0);
@@ -188,25 +193,90 @@ ftp_error_t ftp_server_start(ftp_server_context_t *ctx)
}
/**
* @brief Stop FTP server
* @brief Stop FTP server (graceful shutdown with bounded wait)
*
* SHUTDOWN SEQUENCE (order matters):
*
* 1. Clear the running flag so the accept thread exits its loop check.
*
* 2. Close listen_fd immediately.
* WHY: PAL_ACCEPT() is a blocking syscall. The accept thread only checks
* ctx->running AFTER accept() returns. Without closing the socket, the
* thread blocks forever — even though running == 0. Closing the fd
* makes accept() return EBADF/EINVAL immediately, allowing the thread to
* observe running == 0 and exit.
*
* 3. Force shutdown(SHUT_RDWR) on every active session's ctrl_fd.
* WHY: Session threads block inside recv() waiting for the next FTP
* command. They only check for server shutdown when recv() returns.
* shutdown() injects an EOF into the socket without closing the fd,
* making recv() return 0 so the session loop exits cleanly.
* (close() would race with the session thread which also closes ctrl_fd
* in ftp_session_cleanup().)
*
* 4. Wait up to SERVER_STOP_TIMEOUT_MS for active_sessions to reach 0.
* WHY: Each session thread calls ftp_server_release_session() at exit,
* which decrements active_sessions. The timeout is a safety net: if a
* session thread is stuck (e.g. blocked in a long PFS write), we do not
* hang the entire PS5 shutdown sequence — the kernel will collect the
* remaining resources via SIGKILL/forced unmount anyway, but an orderly
* decrement is strongly preferred.
*/
void ftp_server_stop(ftp_server_context_t *ctx)
{
if (ctx == NULL) {
return;
}
/* Signal server to stop */
/* Step 1 — signal stop */
atomic_store(&ctx->running, 0);
/* Wait for all sessions to complete */
while (atomic_load(&ctx->active_sessions) > 0U) {
usleep(100000); /* 100ms */
/* Step 2 — unblock PAL_ACCEPT() in the accept thread */
if (ctx->listen_fd >= 0) {
PAL_CLOSE(ctx->listen_fd);
ctx->listen_fd = -1; /* ftp_server_cleanup() guards against double-close */
}
/* Step 3 — interrupt blocking recv() in each session thread */
pthread_mutex_lock(&ctx->session_lock);
for (size_t i = 0U; i < FTP_MAX_SESSIONS; i++) {
int state = atomic_load(&ctx->sessions[i].state);
if ((state == FTP_STATE_CONNECTED) ||
(state == FTP_STATE_AUTHENTICATED)||
(state == FTP_STATE_TRANSFERRING)) {
int cfd = ctx->sessions[i].ctrl_fd;
if (cfd >= 0) {
/*
* shutdown() injects EOF without closing the fd.
* The session thread's recv() returns 0 → loop exits →
* ftp_session_cleanup() closes the fd normally.
* Using close() here would race with ftp_session_cleanup().
*/
(void)shutdown(cfd, SHUT_RDWR);
}
}
}
pthread_mutex_unlock(&ctx->session_lock);
/* Step 4 — wait for sessions to drain, with a hard timeout */
#define SERVER_STOP_TIMEOUT_MS 5000U
#define SERVER_STOP_POLL_MS 100U
uint32_t waited_ms = 0U;
while ((atomic_load(&ctx->active_sessions) > 0U) &&
(waited_ms < SERVER_STOP_TIMEOUT_MS)) {
usleep(SERVER_STOP_POLL_MS * 1000U);
waited_ms += SERVER_STOP_POLL_MS;
}
#undef SERVER_STOP_TIMEOUT_MS
#undef SERVER_STOP_POLL_MS
}
/**
* @brief Cleanup server resources
*
* @note listen_fd may already be closed by ftp_server_stop() — guard
* against double-close with the >= 0 check.
*/
void ftp_server_cleanup(ftp_server_context_t *ctx)
{
@@ -214,7 +284,7 @@ void ftp_server_cleanup(ftp_server_context_t *ctx)
return;
}
/* Close listen socket */
/* Close listen socket if not already closed by ftp_server_stop() */
if (ctx->listen_fd >= 0) {
PAL_CLOSE(ctx->listen_fd);
ctx->listen_fd = -1;
@@ -247,17 +317,14 @@ static void* server_accept_thread(void *arg)
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);
int client_fd = PAL_ACCEPT(ctx->listen_fd,
(struct sockaddr*)&client_addr,
&addr_len);
int client_fd = pal_resilient_accept(&ctx->listen_fd,
&ctx->listen_addr,
(struct sockaddr*)&client_addr,
&addr_len,
&ctx->running);
if (client_fd < 0) {
/* Accept failed - check if server is stopping */
if (atomic_load(&ctx->running) == 0) {
break;
}
continue; /* Try again */
continue; /* pal_resilient_accept handled errors and shutdown check internally */
}
/* Configure client socket */
@@ -275,7 +342,7 @@ static void* server_accept_thread(void *arg)
/* Initialize session */
static atomic_uint_fast32_t session_counter = ATOMIC_VAR_INIT(0);
uint32_t session_id = atomic_fetch_add(&session_counter, 1U);
uint32_t session_id = (uint32_t)atomic_fetch_add(&session_counter, 1U);
ftp_error_t err = ftp_session_init(session, client_fd, &client_addr,
session_id, ctx->root_path);
@@ -286,6 +353,19 @@ static void* server_accept_thread(void *arg)
atomic_fetch_add(&ctx->stats.total_errors, 1U);
continue;
}
/*
* Store the back-pointer to the owning server context.
*
* WHY: The session thread calls ftp_server_release_session() on exit
* to decrement active_sessions and mark the slot as free. Without
* this pointer the thread cannot reach the server context.
*
* Written here — before pthread_create() — so it is visible to the
* new thread without any additional synchronisation (pthread_create
* acts as a memory barrier).
*/
session->server_ctx = ctx;
/* Create session thread */
pthread_attr_t sess_attr;
@@ -321,7 +401,7 @@ static void* server_accept_thread(void *arg)
}
/*===========================================================================*
* SESSION MANAGEMENT
* SESSION POOL MANAGEMENT
*===========================================================================*/
/**
@@ -355,7 +435,26 @@ static ftp_session_t* allocate_session(ftp_server_context_t *ctx)
}
/**
* @brief Free session back to pool
* @brief Free session back to pool (internal error-path helper).
*
* Used ONLY in server_accept_thread for early error paths — BEFORE
* pthread_create() succeeds and BEFORE active_sessions is incremented.
*
* WHY NO DECREMENT:
* active_sessions is incremented at line:
* atomic_fetch_add(&ctx->active_sessions, 1U);
* which occurs AFTER pthread_create() returns 0. All calls to
* free_session() happen in error branches that execute BEFORE that
* line (ftp_session_init failure, pthread_create failure).
* Decrementing here would underflow the counter from 0 to UINT_FAST32_MAX,
* which would permanently prevent ftp_server_stop() from exiting even
* after all real sessions have finished.
*
* The only correct place to decrement active_sessions is
* ftp_server_release_session(), which is called from inside
* ftp_session_thread() — i.e. after the thread (and the increment) exist.
*
* @note Thread-safety: Protected by ctx->session_lock
*/
static void free_session(ftp_server_context_t *ctx, ftp_session_t *session)
{
@@ -365,12 +464,45 @@ static void free_session(ftp_server_context_t *ctx, ftp_session_t *session)
pthread_mutex_lock(&ctx->session_lock);
/* Mark session as free */
/* Reset slot state so it can be reused — no counter decrement here */
atomic_store(&session->state, FTP_STATE_INIT);
/* Update active count */
pthread_mutex_unlock(&ctx->session_lock);
}
/**
* @brief Release a session slot back to the server pool (public API).
*
* Called by ftp_session_thread() as its very last action, after
* ftp_session_cleanup() has closed all file descriptors.
*
* INVARIANT: active_sessions was already incremented (in server_accept_thread,
* after pthread_create() returned 0) before this thread started running.
* This function performs the matching decrement. free_session() (the internal
* error-path helper) deliberately does NOT decrement — it is only called from
* code paths that execute BEFORE the increment.
*
* @pre Called only from the session's own thread
* @pre ftp_session_cleanup() already called (all FDs closed)
* @pre ctx->active_sessions > 0
*/
void ftp_server_release_session(ftp_server_context_t *ctx,
ftp_session_t *session)
{
if ((ctx == NULL) || (session == NULL)) {
return;
}
/*
* NULL out the back-pointer before releasing the slot.
* Prevents stale pointer access if the slot is immediately reused.
*/
session->server_ctx = NULL;
/* Reset slot state and decrement the active-session counter */
pthread_mutex_lock(&ctx->session_lock);
atomic_store(&session->state, FTP_STATE_INIT);
atomic_fetch_sub(&ctx->active_sessions, 1U);
pthread_mutex_unlock(&ctx->session_lock);
}
+139 -12
View File
@@ -33,6 +33,7 @@ SOFTWARE.
*/
#include "ftp_session.h"
#include "ftp_server.h" /* ftp_server_release_session() — called at thread exit */
#include "ftp_crypto.h"
#include "ftp_log.h"
#include "ftp_path.h"
@@ -119,6 +120,10 @@ ftp_error_t ftp_session_init(ftp_session_t *session, int ctrl_fd,
}
session->rename_from[0] = '\0';
session->copy_from[0] = '\0';
pthread_mutex_init(&session->copy_mutex, NULL);
session->copy_in_progress = 0;
session->copy_thread_valid = 0;
/* Authentication */
session->auth_attempts = 0U;
@@ -176,6 +181,12 @@ void ftp_session_cleanup(ftp_session_t *session) {
/* Set state to terminating */
atomic_store(&session->state, FTP_STATE_TERMINATING);
/* Wait for active copy to finish and destroy mutex */
if (session->copy_thread_valid) {
pthread_join(session->copy_thread, NULL);
}
pthread_mutex_destroy(&session->copy_mutex);
/* Close data connection */
ftp_session_close_data_connection(session);
@@ -262,6 +273,40 @@ void *ftp_session_thread(void *arg) {
ftp_log_session_event(session, "DISCONNECT", FTP_OK, 0U);
/*
* Release this session slot back to the server pool.
*
* WHY THIS IS THE CRITICAL FIX:
* ftp_server_stop() blocks on (active_sessions > 0) waiting for all
* sessions to finish. Previously, active_sessions was incremented in
* server_accept_thread when a new session thread was spawned, but was
* NEVER decremented when the thread exited — free_session() existed but
* was only called from error paths before the thread was created.
*
* Without this call:
* - active_sessions grows monotonically and never returns to 0
* - ftp_server_stop() loops infinitely
* - PS5 shutdown waits, extends the timer 7 times, then sends SIGKILL
* - zftpd dies with 300+ open file descriptors
* - M2 filesystem cannot unmount cleanly → forced unmount → kernel panic
*
* With this call:
* - Each thread decrements the counter on exit
* - ftp_server_stop() unblocks once all threads have exited
* - All FDs are closed before the process exits
* - M2 unmounts cleanly → no kernel panic
*
* ORDERING: Must be called AFTER ftp_session_cleanup() so all file
* descriptors are already closed before the slot is marked as available.
* This prevents a race where a new session grabs the slot while the old
* session's FDs are still open.
*
* session->server_ctx is guaranteed non-NULL here: it is set in
* server_accept_thread before pthread_create(), and this function only
* runs inside the created thread.
*/
ftp_server_release_session(session->server_ctx, session);
return NULL;
}
@@ -355,22 +400,29 @@ static int wait_fd_ready(int fd, int for_write, uint32_t timeout_ms) {
return -1;
}
struct timeval tv;
tv.tv_sec = (time_t)(timeout_ms / 1000U);
tv.tv_usec = (suseconds_t)((timeout_ms % 1000U) * 1000U);
fd_set rfds;
fd_set wfds;
FD_ZERO(&rfds);
FD_ZERO(&wfds);
if (for_write != 0) {
FD_SET(fd, &wfds);
} else {
FD_SET(fd, &rfds);
}
while (1) {
/* Re-initialize fd_set and tv on every iteration.
* POSIX select() modifies both: fd_sets are undefined after return,
* and on Linux tv is decremented in place. Failing to reinitialize
* them on EINTR retry causes select() to see an empty set and return
* 0 immediately, which is misinterpreted as a timeout — closing the
* passive listener before the client's TCP SYN arrives. */
struct timeval tv;
tv.tv_sec = (time_t)(timeout_ms / 1000U);
tv.tv_usec = (suseconds_t)((timeout_ms % 1000U) * 1000U);
FD_ZERO(&rfds);
FD_ZERO(&wfds);
if (for_write != 0) {
FD_SET(fd, &wfds);
} else {
FD_SET(fd, &rfds);
}
int rc = select(fd + 1, (for_write != 0) ? NULL : &rfds,
(for_write != 0) ? &wfds : NULL, NULL, &tv);
if (rc == 0) {
@@ -497,6 +549,16 @@ ftp_error_t ftp_session_open_data_connection(ftp_session_t *session) {
tv.tv_usec = 0;
(void)PAL_SETSOCKOPT(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
/* Set SO_RCVBUF before connect() — pre-connect avoids the
* post-connect kern.ipc.maxsockbuf cap on OrbisOS/FreeBSD.
* (Passive mode handles this on the listening socket in cmd_PASV.) */
{
int rcvbuf = (int)FTP_TCP_RCVBUF;
(void)PAL_SETSOCKOPT(fd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf));
}
/* SO_SNDBUF intentionally NOT set — see cmd_PASV comment on auto-tuning. */
{
char dbg[128];
snprintf(dbg, sizeof(dbg), "[DBG] ACTIVE connect to %s:%u ...",
@@ -545,6 +607,61 @@ ftp_error_t ftp_session_open_data_connection(ftp_session_t *session) {
/* Close passive listener (one connection only) */
PAL_CLOSE(session->pasv_fd);
session->pasv_fd = -1;
/*
* SO_RCVBUF post-accept: conditional bump only.
*
* cmd_PASV sets SO_RCVBUF = FTP_TCP_RCVBUF on the LISTENING socket
* before bind/listen. On OrbisOS/FreeBSD, the kernel propagates this
* value into every accepted connection during the 3-way handshake,
* bypassing the normal kern.ipc.maxsockbuf cap (~1 MB on OrbisOS).
*
* PROBLEM with an unconditional setsockopt here:
* When the accepted socket has already INHERITED 4 MB (from the
* listening socket), calling setsockopt(SO_RCVBUF) on it re-applies
* the maxsockbuf cap, downgrading the buffer from 4 MB back to 1 MB.
* This is precisely why STOR stalls after exactly 1 MB.
*
* CORRECT strategy — read before write:
* 1. getsockopt: read the currently effective receive buffer size.
* 2. If it is already >= FTP_TCP_RCVBUF, the inheritance worked.
* Leave it alone to preserve the cap-bypassed value.
* 3. If it is < FTP_TCP_RCVBUF, the inheritance was unreliable
* (observed on some PS5 firmware builds). Apply the 2× trick:
* FreeBSD/OrbisOS halves the requested value when storing it, so
* requesting 2 × FTP_TCP_RCVBUF yields the desired FTP_TCP_RCVBUF.
*
* @pre fd is a freshly accepted, not-yet-connected socket.
* @post SO_RCVBUF >= FTP_TCP_RCVBUF, without ever downgrading an
* already-correct inherited value.
*/
{
int current_rcvbuf = 0;
socklen_t optlen = (socklen_t)sizeof(current_rcvbuf);
const int target_rcvbuf = (int)FTP_TCP_RCVBUF;
if (PAL_GETSOCKOPT(fd, SOL_SOCKET, SO_RCVBUF,
&current_rcvbuf, &optlen) != 0) {
current_rcvbuf = 0; /* treat read failure as "buffer unknown" */
}
if (current_rcvbuf < target_rcvbuf) {
/*
* Inheritance was unreliable on this firmware build.
* Request 2× to compensate for OrbisOS/FreeBSD halving.
*/
int rcvbuf = target_rcvbuf * 2;
(void)PAL_SETSOCKOPT(fd, SOL_SOCKET, SO_RCVBUF,
&rcvbuf, sizeof(rcvbuf));
}
/* else: inherited value is adequate — do NOT call setsockopt,
* which would downgrade the cap-bypassed buffer. */
}
/*
* SO_SNDBUF is intentionally NOT set here.
* See cmd_PASV for the full rationale (auto-tuning vs. explicit value).
*/
}
/* Configure data socket for bulk transfer
@@ -554,6 +671,10 @@ ftp_error_t ftp_session_open_data_connection(ftp_session_t *session) {
*/
(void)pal_socket_configure_data(session->data_fd);
if (atomic_load(&session->state) != FTP_STATE_TERMINATING) {
atomic_store(&session->state, FTP_STATE_TRANSFERRING);
}
return FTP_OK;
}
@@ -585,6 +706,12 @@ void ftp_session_close_data_connection(ftp_session_t *session) {
session->data_mode = FTP_DATA_MODE_NONE;
session->restart_offset = 0;
if (atomic_load(&session->state) != FTP_STATE_TERMINATING) {
atomic_store(&session->state,
(session->authenticated != 0U) ? FTP_STATE_AUTHENTICATED
: FTP_STATE_CONNECTED);
}
}
/**
-98
View File
@@ -1,98 +0,0 @@
/*
MIT License
Copyright (c) 2026 Seregon
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
/**
* @file ftp_buffer_pool.h
* @brief FTP stream buffer pool
*
* @author Seregon
* @version 1.0.0
*
* PLATFORMS: FreeBSD (PS4/PS5 kqueue), Linux (epoll)
* DESIGN: Single-threaded, non-blocking I/O
*
*/
#include "ftp_buffer_pool.h"
#include "ftp_config.h"
#include <stdatomic.h>
#include <stdint.h>
#ifndef FTP_STREAM_BUFFER_SIZE
#define FTP_STREAM_BUFFER_SIZE 65536U
#endif
#ifndef FTP_STREAM_BUFFER_COUNT
#define FTP_STREAM_BUFFER_COUNT FTP_MAX_SESSIONS
#endif
static _Alignas(64) uint8_t g_stream_buffers[FTP_STREAM_BUFFER_COUNT][FTP_STREAM_BUFFER_SIZE];
static atomic_uint_fast32_t g_stream_buffer_mask = ATOMIC_VAR_INIT(0U);
void *ftp_buffer_acquire(void)
{
for (;;) {
uint_fast32_t mask = atomic_load(&g_stream_buffer_mask);
for (uint_fast32_t i = 0U; i < (uint_fast32_t)FTP_STREAM_BUFFER_COUNT; i++) {
uint_fast32_t bit = 1U << i;
if ((mask & bit) != 0U) {
continue;
}
uint_fast32_t expected = mask;
uint_fast32_t desired = mask | bit;
if (atomic_compare_exchange_strong(&g_stream_buffer_mask, &expected, desired)) {
return (void *)g_stream_buffers[i];
}
goto retry;
}
retry:
if (mask == atomic_load(&g_stream_buffer_mask)) {
return NULL;
}
continue;
}
}
void ftp_buffer_release(void *buffer)
{
if (buffer == NULL) {
return;
}
for (uint_fast32_t i = 0U; i < (uint_fast32_t)FTP_STREAM_BUFFER_COUNT; i++) {
if ((void *)g_stream_buffers[i] == buffer) {
uint_fast32_t bit = 1U << i;
(void)atomic_fetch_and(&g_stream_buffer_mask, ~bit);
return;
}
}
}
size_t ftp_buffer_size(void)
{
return (size_t)FTP_STREAM_BUFFER_SIZE;
}
-1891
View File
File diff suppressed because it is too large. Load diff
+767
View File
@@ -0,0 +1,767 @@
#include "games_internal.h"
#include "http_config.h" /* HTTP_DEFAULT_PORT */
#include "../http_api_internal.h"
#include "ftp_config.h"
#include <ctype.h>
#include <dirent.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#ifndef ENABLE_PKG_INSTALL
#define ENABLE_PKG_INSTALL 0
#endif
static http_response_t *api_games_installed(const http_request_t *request) {
(void)request;
enum { GAMES_BODY_CAP = 512U * 1024U };
char *body = (char *)malloc(GAMES_BODY_CAP);
if (body == NULL) {
return http_api_error_json(HTTP_STATUS_500_INTERNAL_ERROR, "Out of memory");
}
size_t pos = 0U;
if (http_buf_append_cstr(body, GAMES_BODY_CAP, &pos,
"{\"ok\":true,\"entries\":[") != 0) {
free(body);
return http_api_error_json(HTTP_STATUS_500_INTERNAL_ERROR, "Out of memory");
}
int first = 1;
size_t count_added = 0U;
const char *bases[] = {"/user/app", "/system_ex/app", "/mnt/ext0/user/app",
NULL};
for (size_t bi = 0; bases[bi] != NULL; bi++) {
if (games_append_installed_entries(bases[bi], body, GAMES_BODY_CAP,
&pos, &first,
&count_added) != 0) {
free(body);
return http_api_error_json(HTTP_STATUS_500_INTERNAL_ERROR, "Response too large");
}
}
if (http_buf_append_cstr(body, GAMES_BODY_CAP, &pos, "]}") != 0) {
free(body);
return http_api_error_json(HTTP_STATUS_500_INTERNAL_ERROR, "Response too large");
}
http_response_t *resp = http_response_create(HTTP_STATUS_200_OK);
if (resp == NULL) {
free(body);
return http_api_error_json(HTTP_STATUS_500_INTERNAL_ERROR, "Out of memory");
}
http_response_add_header(resp, "Content-Type", "application/json");
http_response_add_header(resp, "Cache-Control", "no-store");
if (http_response_set_body_owned(resp, body, pos) != 0) {
free(body);
http_response_destroy(resp);
return http_api_error_json(HTTP_STATUS_500_INTERNAL_ERROR, "Body allocation failed");
}
return resp;
}
static http_response_t *api_games_icon(const http_request_t *request) {
const char *query = strchr(request->uri, '?');
if (query == NULL) {
return http_api_error_json(HTTP_STATUS_404_NOT_FOUND, "Icon not found");
}
char title_id[64] = {0};
if (http_api_parse_query_param(query, "id", title_id, sizeof(title_id)) != 0) {
title_id[0] = '\0';
}
if (title_id[0] != '\0') {
for (size_t i = 0; title_id[i] != '\0'; i++) {
unsigned char c = (unsigned char)title_id[i];
if (!(isalnum(c) || c == '_' || c == '-')) {
title_id[0] = '\0';
break;
}
title_id[i] = (char)toupper(c);
}
}
/*
* The app-directory hint only shortens the lookup: an unusable one is
* dropped and resolution falls back to the per-title appmeta paths.
*
* Every failure answers 404 so the browser fires onerror and the UI can
* draw its placeholder; a "200 + 1x1 transparent PNG" made every failed
* icon look like an empty box with no way to tell the two apart.
*/
char path_hint[FTP_PATH_MAX] = {0};
char path_param[FTP_PATH_MAX] = {0};
if ((http_api_parse_query_param(query, "path", path_param,
sizeof(path_param)) == 0) &&
(path_param[0] != '\0')) {
char safe[FTP_PATH_MAX];
if (http_api_validate_path(path_param, safe, sizeof(safe))) {
(void)snprintf(path_hint, sizeof(path_hint), "%s", safe);
}
}
char icon_path[FTP_PATH_MAX] = {0};
if (games_resolve_installed_icon((title_id[0] != '\0') ? title_id : NULL,
(path_hint[0] != '\0') ? path_hint : NULL,
icon_path, sizeof(icon_path)) != 0) {
return http_api_error_json(HTTP_STATUS_404_NOT_FOUND, "Icon not found");
}
FILE *fp = fopen(icon_path, "rb");
if (fp == NULL) {
return http_api_error_json(HTTP_STATUS_404_NOT_FOUND, "Icon not found");
}
if (fseek(fp, 0, SEEK_END) != 0) {
fclose(fp);
return http_api_error_json(HTTP_STATUS_404_NOT_FOUND, "Icon not found");
}
long flen = ftell(fp);
if (flen <= 0 || flen > (8 * 1024 * 1024)) {
fclose(fp);
return http_api_error_json(HTTP_STATUS_404_NOT_FOUND, "Icon not found");
}
if (fseek(fp, 0, SEEK_SET) != 0) {
fclose(fp);
return http_api_error_json(HTTP_STATUS_404_NOT_FOUND, "Icon not found");
}
uint8_t *buf = (uint8_t *)malloc((size_t)flen);
if (buf == NULL) {
fclose(fp);
return http_api_error_json(HTTP_STATUS_500_INTERNAL_ERROR, "Out of memory");
}
size_t got = fread(buf, 1, (size_t)flen, fp);
fclose(fp);
if (got != (size_t)flen) {
free(buf);
return http_api_error_json(HTTP_STATUS_404_NOT_FOUND, "Icon not found");
}
http_response_t *resp = http_response_create(HTTP_STATUS_200_OK);
http_response_add_header(resp, "Content-Type", "image/png");
/* Icons are fetched once per title and do not change while installed. */
http_response_add_header(resp, "Cache-Control", "public, max-age=300");
if (http_response_set_body_owned(resp, buf, got) != 0) {
free(buf);
http_response_destroy(resp);
return http_api_error_json(HTTP_STATUS_500_INTERNAL_ERROR,
"Body allocation failed");
}
return resp;
}
static http_response_t *api_games_repair_visibility(const http_request_t *request) {
char body[2048];
size_t pos = 0U;
int first = 1;
size_t count_added = 0U;
int repaired_titles = 0;
int repaired_tables = 0;
int repaired_rows = 0;
int sqlite_available = 0;
char requested_id[32] = {0};
const char *query = strchr(request->uri, '?');
if (query != NULL) {
(void)http_api_parse_query_param(query, "id", requested_id, sizeof(requested_id));
for (size_t i = 0; requested_id[i] != '\0'; i++) {
requested_id[i] = (char)toupper((unsigned char)requested_id[i]);
}
}
if (http_buf_append_cstr(body, sizeof(body), &pos,
"{\"ok\":true,\"message\":\"Visibility reindex completed\",\"scanned\":[") !=
0) {
return http_api_error_json(HTTP_STATUS_500_INTERNAL_ERROR, "Out of memory");
}
const char *bases[] = {"/user/app", "/system_ex/app", "/mnt/ext0/user/app",
"/user/appmeta", "/system_data/priv/appmeta", NULL};
#if defined(PLATFORM_PS4) || defined(PLATFORM_PS5)
if (requested_id[0] != '\0') {
int valid = 1;
for (size_t i = 0; requested_id[i] != '\0'; i++) {
unsigned char c = (unsigned char)requested_id[i];
if (!(isalnum(c) || c == '_' || c == '-')) {
valid = 0;
break;
}
}
if (valid) {
int touched_tables = 0;
int touched_rows = 0;
int r =
games_psx_repair_appdb_visibility(requested_id, &touched_tables,
&touched_rows);
if (r == 0) {
sqlite_available = 1;
repaired_titles = 1;
repaired_tables += touched_tables;
repaired_rows += touched_rows;
}
}
} else {
const char *repair_bases[] = {"/user/app", "/mnt/ext0/user/app",
"/system_ex/app", NULL};
for (size_t bi = 0; repair_bases[bi] != NULL; bi++) {
DIR *d = opendir(repair_bases[bi]);
if (d == NULL) {
continue;
}
struct dirent *e;
while ((e = readdir(d)) != NULL) {
if (e->d_name[0] == '.') {
continue;
}
int valid = 1;
size_t len = strlen(e->d_name);
if (len < 9U || len > 31U) {
valid = 0;
}
for (size_t i = 0; valid && i < len; i++) {
unsigned char c = (unsigned char)e->d_name[i];
if (!(isalnum(c) || c == '_' || c == '-')) {
valid = 0;
break;
}
}
if (!valid) {
continue;
}
char title_id[32];
memset(title_id, 0, sizeof(title_id));
for (size_t i = 0; i < len && i < (sizeof(title_id) - 1U); i++) {
title_id[i] = (char)toupper((unsigned char)e->d_name[i]);
}
int touched_tables = 0;
int touched_rows = 0;
int r =
games_psx_repair_appdb_visibility(title_id, &touched_tables,
&touched_rows);
if (r == 0) {
sqlite_available = 1;
repaired_titles++;
repaired_tables += touched_tables;
repaired_rows += touched_rows;
}
}
closedir(d);
}
}
#endif
for (size_t i = 0; bases[i] != NULL; i++) {
if (!first) {
(void)http_buf_append_cstr(body, sizeof(body), &pos, ",");
}
first = 0;
(void)http_buf_append_cstr(body, sizeof(body), &pos, "\"");
(void)http_json_escape_append(body, sizeof(body), &pos, bases[i]);
(void)http_buf_append_cstr(body, sizeof(body), &pos, "\"");
DIR *d = opendir(bases[i]);
if (d != NULL) {
struct dirent *e;
while ((e = readdir(d)) != NULL) {
if (e->d_name[0] == '.') {
continue;
}
count_added++;
}
closedir(d);
}
}
(void)http_buf_append_cstr(body, sizeof(body), &pos, "],\"items_seen\":");
{
char num[32];
int n = snprintf(num, sizeof(num), "%zu", count_added);
if (n > 0 && (size_t)n < sizeof(num)) {
(void)http_buf_append_bytes(body, sizeof(body), &pos, num, (size_t)n);
}
}
(void)http_buf_append_cstr(body, sizeof(body), &pos, ",\"hints\":[");
(void)http_buf_append_cstr(body, sizeof(body), &pos,
"\"Use Refresh Installed in Games tab\",");
(void)http_buf_append_cstr(body, sizeof(body), &pos,
"\"If titles still missing, restart shell/console\"");
(void)http_buf_append_cstr(body, sizeof(body), &pos, "],\"sqlite_repair\":{");
(void)http_buf_append_cstr(body, sizeof(body), &pos, "\"available\":");
(void)http_buf_append_cstr(body, sizeof(body), &pos,
sqlite_available ? "true" : "false");
(void)http_buf_append_cstr(body, sizeof(body), &pos, ",\"titles\":");
{
char num[32];
int n = snprintf(num, sizeof(num), "%d", repaired_titles);
if (n > 0 && (size_t)n < sizeof(num)) {
(void)http_buf_append_bytes(body, sizeof(body), &pos, num, (size_t)n);
}
}
(void)http_buf_append_cstr(body, sizeof(body), &pos, ",\"tables\":");
{
char num[32];
int n = snprintf(num, sizeof(num), "%d", repaired_tables);
if (n > 0 && (size_t)n < sizeof(num)) {
(void)http_buf_append_bytes(body, sizeof(body), &pos, num, (size_t)n);
}
}
(void)http_buf_append_cstr(body, sizeof(body), &pos, ",\"rows\":");
{
char num[32];
int n = snprintf(num, sizeof(num), "%d", repaired_rows);
if (n > 0 && (size_t)n < sizeof(num)) {
(void)http_buf_append_bytes(body, sizeof(body), &pos, num, (size_t)n);
}
}
(void)http_buf_append_cstr(body, sizeof(body), &pos, "}}");
http_response_t *resp = http_response_create(HTTP_STATUS_200_OK);
http_response_add_header(resp, "Content-Type", "application/json");
http_response_add_header(resp, "Cache-Control", "no-store");
http_response_set_body(resp, body, strlen(body));
return resp;
}
static http_response_t *api_games_install_status(const http_request_t *request) {
(void)request;
games_install_snapshot_t state;
(void)games_install_state_refresh(&state);
const size_t capacity = FTP_PATH_MAX * 6U + 2048U;
char *body = malloc(capacity);
if (body == NULL) return NULL;
size_t pos = 0U;
int valid =
http_buf_append_cstr(body, capacity, &pos, "{\"ok\":true,\"active\":") == 0 &&
http_buf_append_cstr(body, capacity, &pos,
state.active ? "true" : "false") == 0 &&
http_buf_append_cstr(body, capacity, &pos, ",\"task_id\":") == 0 &&
http_buf_append_i32(body, capacity, &pos, state.task_id) == 0 &&
http_buf_append_cstr(body, capacity, &pos, ",\"progress\":") == 0 &&
http_buf_append_i32(body, capacity, &pos, state.last_percent) == 0 &&
http_buf_append_cstr(body, capacity, &pos, ",\"error\":") == 0 &&
http_buf_append_i32(body, capacity, &pos, state.last_error) == 0 &&
http_buf_append_cstr(body, capacity, &pos, ",\"length\":") == 0 &&
http_buf_append_u64(body, capacity, &pos, state.last_length) == 0 &&
http_buf_append_cstr(body, capacity, &pos, ",\"transferred\":") == 0 &&
http_buf_append_u64(body, capacity, &pos, state.last_transferred) == 0 &&
http_buf_append_cstr(body, capacity, &pos, ",\"title_id\":\"") == 0 &&
http_json_escape_append(body, capacity, &pos, state.title_id) == 0 &&
http_buf_append_cstr(body, capacity, &pos, "\",\"path\":\"") == 0 &&
http_json_escape_append(body, capacity, &pos, state.path) == 0 &&
http_buf_append_cstr(body, capacity, &pos, "\",\"detail\":\"") == 0 &&
http_json_escape_append(body, capacity, &pos, state.detail) == 0 &&
http_buf_append_cstr(body, capacity, &pos, "\",\"needs_confirm\":") == 0 &&
http_buf_append_cstr(body, capacity, &pos,
state.needs_confirm ? "true" : "false") == 0 &&
http_buf_append_cstr(body, capacity, &pos, ",\"slot\":") == 0 &&
http_buf_append_i32(body, capacity, &pos, state.slot) == 0 &&
http_buf_append_cstr(body, capacity, &pos, "}") == 0;
if (!valid) {
free(body);
return http_api_error_json(HTTP_STATUS_500_INTERNAL_ERROR,
"Install status is too large");
}
http_response_t *resp = http_response_create(HTTP_STATUS_200_OK);
if (resp == NULL) { free(body); return NULL; }
http_response_add_header(resp, "Content-Type", "application/json");
http_response_add_header(resp, "Cache-Control", "no-store");
if (http_response_set_body_owned(resp, body, pos) != 0) {
free(body);
http_response_destroy(resp);
return NULL;
}
return resp;
}
static http_response_t *api_games_uninstall(const http_request_t *request) {
if ((request->method != HTTP_METHOD_POST) &&
(request->method != HTTP_METHOD_GET)) {
return http_api_error_json(HTTP_STATUS_405_METHOD_NOT_ALLOWED, "Use POST or GET");
}
const char *query = strchr(request->uri, '?');
if (query == NULL) {
return http_api_error_json(HTTP_STATUS_400_BAD_REQUEST, "Missing query string");
}
char title_id[64] = {0};
if (http_api_parse_query_param(query, "id", title_id, sizeof(title_id)) != 0 ||
!games_is_valid_title_id(title_id)) {
return http_api_error_json(HTTP_STATUS_400_BAD_REQUEST, "Missing or invalid title id");
}
#if defined(PLATFORM_PS4) || defined(PLATFORM_PS5)
int rc = -1;
if (games_psx_uninstall(title_id, &rc) != 0) {
return http_api_status_json_200(0, "Uninstall API unavailable", -1);
}
if (rc < 0) {
char msg[96];
(void)snprintf(msg, sizeof(msg), "Uninstall failed: 0x%08X", (unsigned)rc);
return http_api_status_json_200(0, msg, rc);
}
char body[192];
int n = snprintf(body, sizeof(body),
"{\"ok\":true,\"message\":\"Uninstalled\",\"id\":\"%s\"}",
title_id);
http_response_t *resp = http_response_create(HTTP_STATUS_200_OK);
http_response_add_header(resp, "Content-Type", "application/json");
http_response_set_body(resp, body, (size_t)n);
return resp;
#else
(void)title_id;
return http_api_status_json_200(0, "Uninstall only available on PS4/PS5", -1);
#endif
}
#if ENABLE_PKG_INSTALL && \
(defined(PLATFORM_PS4) || defined(PLATFORM_PS5))
/**
* Decodes a percent-encoded query value in place.
*/
static void games_url_decode(char *value) {
char *out = value;
for (const char *in = value; *in != '\0'; in++) {
if (*in == '%' && isxdigit((unsigned char)in[1]) && isxdigit((unsigned char)in[2])) {
char hex[3] = {in[1], in[2], '\0'};
*out++ = (char)strtol(hex, NULL, 16);
in += 2;
} else if (*in == '+') {
*out++ = ' ';
} else {
*out++ = *in;
}
}
*out = '\0';
}
/**
* Percent-encodes a filesystem path so it can ride inside a query string.
*/
static void games_url_encode_path(const char *in, char *out, size_t out_size) {
static const char *hex = "0123456789ABCDEF";
size_t o = 0U;
for (size_t i = 0U; in[i] != '\0' && (o + 4U) < out_size; i++) {
unsigned char ch = (unsigned char)in[i];
if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') ||
(ch >= '0' && ch <= '9') || ch == '-' || ch == '_' || ch == '.' ||
ch == '/') {
out[o++] = (char)ch;
} else {
out[o++] = '%';
out[o++] = hex[ch >> 4];
out[o++] = hex[ch & 0x0FU];
}
}
out[o] = '\0';
}
#endif /* ENABLE_PKG_INSTALL */
static http_response_t *api_games_install(const http_request_t *request) {
#if !ENABLE_PKG_INSTALL
(void)request;
return http_api_error_json(HTTP_STATUS_409_CONFLICT,
"PKG installation is disabled for this build");
#else
if ((request->method != HTTP_METHOD_POST) &&
(request->method != HTTP_METHOD_GET)) {
return http_api_error_json(HTTP_STATUS_405_METHOD_NOT_ALLOWED, "Use POST or GET");
}
const char *query = strchr(request->uri, '?');
if (query == NULL) {
return http_api_error_json(HTTP_STATUS_400_BAD_REQUEST, "Missing query string");
}
/*
* Source: a package already on the console (path) or a link the installer
* pulls by itself (url). Nothing is uploaded first - the console downloads
* the package as part of installing it, and the task shows up in transfers.
*/
char source_url[768] = {0};
#if defined(PLATFORM_PS4) || defined(PLATFORM_PS5)
const char *url_param = strstr(query, "url=");
if (url_param != NULL) {
url_param += 4;
size_t n = strcspn(url_param, "&");
if (n >= sizeof(source_url)) {
n = sizeof(source_url) - 1U;
}
memcpy(source_url, url_param, n);
source_url[n] = '\0';
games_url_decode(source_url);
if (strncmp(source_url, "http://", 7) != 0 &&
strncmp(source_url, "https://", 8) != 0) {
return http_api_error_json(HTTP_STATUS_400_BAD_REQUEST,
"Install links must be http or https");
}
}
#endif
char path[FTP_PATH_MAX] = {0};
char safe[FTP_PATH_MAX] = {0};
if (source_url[0] == '\0') {
if (http_api_parse_path_param(query, path, sizeof(path)) != 0) {
return http_api_error_json(HTTP_STATUS_400_BAD_REQUEST,
"Missing path or url parameter");
}
if (!http_api_validate_path(path, safe, sizeof(safe))) {
return http_api_error_json(HTTP_STATUS_403_FORBIDDEN, "Path traversal blocked");
}
if (!games_has_pkg_extension(safe)) {
return http_api_error_json(HTTP_STATUS_400_BAD_REQUEST,
"Install supports only PKG/FPKG files");
}
struct stat package_stat;
if (stat(safe, &package_stat) != 0 || !S_ISREG(package_stat.st_mode) ||
package_stat.st_size <= 0) {
return http_api_error_json(HTTP_STATUS_400_BAD_REQUEST,
"Package file is missing or empty");
}
}
#if defined(PLATFORM_PS4) || defined(PLATFORM_PS5)
games_install_snapshot_t current_install;
(void)games_install_state_refresh(&current_install);
if (current_install.active) {
return http_api_error_json(HTTP_STATUS_409_CONFLICT,
"A package installation is already running");
}
int install_rc = -1;
int task_id = -1;
char title_id[64] = {0};
int overwrite = (strstr(query, "overwrite=1") != NULL) ? 1 : 0;
/*
* Replacing an installation destroys data, so it is never implicit: when the
* package is already installed the caller is told, and only a request that
* says so explicitly goes through (the interface asks the user first).
*/
if (safe[0] != '\0' &&
games_psx_pkg_title_id(safe, title_id, sizeof(title_id)) == 0 &&
title_id[0] != '\0') {
char app_dir[FTP_PATH_MAX];
if (games_resolve_installed_app_dir(title_id, app_dir, sizeof(app_dir)) == 0) {
if (!overwrite) {
char body[384];
int n = snprintf(body, sizeof(body),
"{\"ok\":false,\"error\":\"already_installed\","
"\"title_id\":\"%s\",\"message\":\"%s is already "
"installed\",\"can_overwrite\":true}",
title_id, title_id);
http_response_t *conflict = http_response_create(HTTP_STATUS_409_CONFLICT);
if (conflict == NULL) {
return NULL;
}
http_response_add_header(conflict, "Content-Type", "application/json");
http_response_set_body(conflict, (const uint8_t *)body, (size_t)n);
return conflict;
}
/* Let the system installer decide whether replacement is supported.
* Removing a working title before validating its replacement would
* leave the console without either version if the PKG is rejected. */
}
}
/* Destination: internal storage unless the caller asks for extended. */
int slot = (strstr(query, "dest=extended") != NULL) ? 1 : 0;
#if defined(PLATFORM_PS5)
if (slot != 0) {
return http_api_error_json(HTTP_STATUS_400_BAD_REQUEST,
"PS5 system installer does not expose an extended-storage selection");
}
#endif
/*
* BGFT downloads the package, so a file living on console storage is served
* by this same server over loopback: the payload never holds the bytes.
* The port comes from the request, falling back to the configured default.
*/
/* The installer is on the same console: loop back to this server. */
const int port = HTTP_DEFAULT_PORT;
char encoded[FTP_PATH_MAX * 2U];
char content_url[FTP_PATH_MAX * 2U + sizeof(source_url)];
if (source_url[0] != '\0') {
(void)snprintf(content_url, sizeof(content_url), "%s", source_url);
} else {
games_url_encode_path(safe, encoded, sizeof(encoded));
(void)snprintf(content_url, sizeof(content_url),
"http://127.0.0.1:%d/api/file/get?path=%s", port, encoded);
}
/* Uploads staged by the web interface are disposable, user files are not. */
#if defined(PLATFORM_PS4)
int delete_source = (safe[0] != '\0' &&
strncmp(safe, "/data/zftpd/pkg/", 16) == 0) ? 1 : 0;
#endif
#if defined(PLATFORM_PS5)
#if defined(ZFTPD_INSTALL_HELPER)
if (games_ps5_helper_install(content_url,
(safe[0] != '\0') ? safe : NULL,
slot, overwrite, title_id,
sizeof(title_id), &task_id,
&install_rc) != 0) {
return http_api_status_json_200(0, "PS5 installer helper unavailable", -1);
}
#else
return http_api_status_json_200(0, "PS5 installer helper not built", -1);
#endif
#else
if (games_psx_install_bgft(content_url, "Remote PKG Install", slot,
delete_source, title_id,
sizeof(title_id), &task_id,
&install_rc) == 0) {
if (install_rc == 0 && task_id >= 0) {
games_install_state_begin(task_id, title_id[0] ? title_id : "", safe);
}
} else {
return http_api_status_json_200(0, "PS4 BGFT installer unavailable", -1);
}
#endif
if (install_rc < 0) {
char msg[96];
(void)snprintf(msg, sizeof(msg), "Install failed: 0x%08X",
(unsigned)install_rc);
return http_api_status_json_200(0, msg, install_rc);
}
const size_t capacity = FTP_PATH_MAX * 6U + 512U;
char *body = malloc(capacity);
if (body == NULL) return NULL;
size_t pos = 0U;
int valid =
http_buf_append_cstr(body, capacity, &pos,
"{\"ok\":true,\"message\":\"Install started\",\"title_id\":\"") == 0 &&
http_json_escape_append(body, capacity, &pos, title_id) == 0 &&
http_buf_append_cstr(body, capacity, &pos, "\",\"path\":\"") == 0 &&
http_json_escape_append(body, capacity, &pos, safe) == 0 &&
http_buf_append_cstr(body, capacity, &pos, "\",\"task_id\":") == 0 &&
http_buf_append_i32(body, capacity, &pos, task_id) == 0 &&
http_buf_append_cstr(body, capacity, &pos, ",\"task_based\":") == 0 &&
http_buf_append_cstr(body, capacity, &pos,
task_id >= 0 ? "true}" : "false}") == 0;
if (!valid) { free(body); return NULL; }
http_response_t *resp = http_response_create(HTTP_STATUS_200_OK);
if (resp == NULL) { free(body); return NULL; }
http_response_add_header(resp, "Content-Type", "application/json");
if (http_response_set_body_owned(resp, body, pos) != 0) {
free(body);
http_response_destroy(resp);
return NULL;
}
return resp;
#else
(void)safe;
return http_api_status_json_200(0, "Install only available on PS4/PS5", -1);
#endif
#endif
}
static http_response_t *api_games_reinstall(const http_request_t *request) {
#if !ENABLE_PKG_INSTALL
(void)request;
return http_api_error_json(HTTP_STATUS_409_CONFLICT,
"PKG installation is disabled for this build");
#else
if ((request->method != HTTP_METHOD_POST) &&
(request->method != HTTP_METHOD_GET)) {
return http_api_error_json(HTTP_STATUS_405_METHOD_NOT_ALLOWED, "Use POST or GET");
}
const char *query = strchr(request->uri, '?');
if (query == NULL) {
return http_api_error_json(HTTP_STATUS_400_BAD_REQUEST, "Missing query string");
}
char path[FTP_PATH_MAX] = {0};
if (http_api_parse_path_param(query, path, sizeof(path)) != 0) {
return http_api_error_json(HTTP_STATUS_400_BAD_REQUEST, "Missing path parameter");
}
char safe[FTP_PATH_MAX];
if (!http_api_validate_path(path, safe, sizeof(safe))) {
return http_api_error_json(HTTP_STATUS_403_FORBIDDEN, "Path traversal blocked");
}
if (!games_has_pkg_extension(safe)) {
return http_api_error_json(HTTP_STATUS_400_BAD_REQUEST,
"Reinstall supports only PKG/FPKG files");
}
char title_id[64] = {0};
(void)games_extract_title_id_from_image(safe, title_id, sizeof(title_id));
#if defined(PLATFORM_PS4) || defined(PLATFORM_PS5)
int uninstall_rc = -1;
if (title_id[0] != '\0') {
if (games_psx_uninstall(title_id, &uninstall_rc) != 0) {
uninstall_rc = -1;
}
}
int install_rc = -1;
int task_id = -1;
char install_title[64] = {0};
if (games_psx_install_bgft(safe, "Remote PKG Reinstall", 0, 0, install_title,
sizeof(install_title), &task_id,
&install_rc) == 0) {
if (install_rc == 0 && task_id >= 0) {
games_install_state_begin(task_id, install_title[0] ? install_title : title_id, safe);
}
} else if (games_psx_install_path(safe, install_title, sizeof(install_title),
&install_rc) != 0) {
return http_api_status_json_200(0, "Install API unavailable", -1);
}
if (install_rc < 0) {
char msg[96];
(void)snprintf(msg, sizeof(msg), "Reinstall failed: 0x%08X",
(unsigned)install_rc);
return http_api_status_json_200(0, msg, install_rc);
}
char body[576];
int n = snprintf(
body, sizeof(body),
"{\"ok\":true,\"message\":\"Reinstall started\",\"title_id\":\"%s\",\"uninstall_rc\":%d,\"task_id\":%d,\"task_based\":%s}",
install_title[0] ? install_title : title_id, uninstall_rc, task_id,
(task_id >= 0) ? "true" : "false");
http_response_t *resp = http_response_create(HTTP_STATUS_200_OK);
http_response_add_header(resp, "Content-Type", "application/json");
http_response_set_body(resp, body, (size_t)n);
return resp;
#else
return http_api_status_json_200(0, "Reinstall only available on PS4/PS5", -1);
#endif
#endif
}
http_response_t *http_games_admin_handle(const http_request_t *request) {
if (request == NULL) return NULL;
if (http_api_route_is(request->uri, "/api/admin/games/installed")) return api_games_installed(request);
if (http_api_route_is(request->uri, "/api/admin/games/icon")) return api_games_icon(request);
if (http_api_route_is(request->uri, "/api/admin/games/repair_visibility")) return api_games_repair_visibility(request);
if (http_api_route_is(request->uri, "/api/admin/games/uninstall")) return api_games_uninstall(request);
if (http_api_route_is(request->uri, "/api/admin/games/install_status")) return api_games_install_status(request);
if (http_api_route_is(request->uri, "/api/admin/games/install")) return api_games_install(request);
if (http_api_route_is(request->uri, "/api/admin/games/reinstall")) return api_games_reinstall(request);
if (http_api_route_is(request->uri, "/api/admin/launch")) return http_games_launch(request);
return NULL;
}
+221
View File
@@ -0,0 +1,221 @@
#include "games_internal.h"
#include "../http_api_internal.h"
#include "ftp_config.h"
#include "exfat_unpacker.h"
#include "pkg_unpacker.h"
#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/stat.h>
int games_append_installed_entries(const char *base,
char *body,
size_t cap,
size_t *pos,
int *first,
size_t *count_added) {
if (!base || !body || !pos || !first || !count_added) {
return -1;
}
DIR *dir = opendir(base);
if (dir == NULL) {
return 0;
}
struct dirent *ent;
while ((ent = readdir(dir)) != NULL) {
if ((strcmp(ent->d_name, ".") == 0) || (strcmp(ent->d_name, "..") == 0)) {
continue;
}
char app_dir[FTP_PATH_MAX];
int n = snprintf(app_dir, sizeof(app_dir), "%s/%s", base, ent->d_name);
if (n < 0 || (size_t)n >= sizeof(app_dir)) {
continue;
}
struct stat st;
if (stat(app_dir, &st) != 0 || !S_ISDIR(st.st_mode)) {
continue;
}
char title_id[64] = {0};
char title_name[256] = {0};
(void)snprintf(title_id, sizeof(title_id), "%.63s", ent->d_name);
(void)games_read_installed_sfo(app_dir, title_id, sizeof(title_id), title_name,
sizeof(title_name));
if (title_name[0] == '\0') {
(void)snprintf(title_name, sizeof(title_name), "%s", title_id);
}
char icon_path[FTP_PATH_MAX] = {0};
int has_icon = (games_resolve_installed_icon(title_id, app_dir, icon_path,
sizeof(icon_path)) == 0);
if (!*first) {
if (http_buf_append_cstr(body, cap, pos, ",") != 0) {
closedir(dir);
return -1;
}
}
*first = 0;
(*count_added)++;
if (http_buf_append_cstr(body, cap, pos, "{\"id\":\"") != 0 ||
http_json_escape_append(body, cap, pos, title_id) != 0 ||
http_buf_append_cstr(body, cap, pos, "\",\"name\":\"") != 0 ||
http_json_escape_append(body, cap, pos, title_name) != 0 ||
http_buf_append_cstr(body, cap, pos, "\",\"path\":\"") != 0 ||
http_json_escape_append(body, cap, pos, app_dir) != 0 ||
http_buf_append_cstr(body, cap, pos, "\",\"source\":\"") != 0 ||
http_json_escape_append(body, cap, pos, base) != 0 ||
http_buf_append_cstr(body, cap, pos, "\",\"has_icon\":") != 0 ||
http_buf_append_cstr(body, cap, pos, has_icon ? "true" : "false") != 0 ||
http_buf_append_cstr(body, cap, pos, "}") != 0) {
closedir(dir);
return -1;
}
}
closedir(dir);
return 0;
}
int games_extract_title_id_from_image(const char *safe_path,
char *title_id,
size_t title_id_size) {
if ((safe_path == NULL) || (title_id == NULL) || (title_id_size < 10U)) {
return -1;
}
title_id[0] = '\0';
/* 1) Try PKG */
pkg_context_t pkg_ctx;
if (pkg_init(&pkg_ctx, safe_path) == PKG_OK) {
const pkg_entry_t *sfo_entry =
pkg_find_entry_by_id(&pkg_ctx, PKG_ENTRY_ID_PARAM_SFO);
if (sfo_entry && sfo_entry->size > 0U && sfo_entry->size <= 65536U) {
uint8_t *sfo_data = (uint8_t *)malloc((size_t)sfo_entry->size);
if (sfo_data != NULL) {
if (pkg_extract_to_buffer(&pkg_ctx, sfo_entry, sfo_data,
(size_t)sfo_entry->size) > 0) {
(void)games_sfo_get_string(sfo_data, (size_t)sfo_entry->size, "TITLE_ID",
title_id, title_id_size);
if (title_id[0] == '\0') {
char cid[64] = "";
(void)games_sfo_get_string(sfo_data, (size_t)sfo_entry->size,
"CONTENT_ID", cid, sizeof(cid));
(void)games_title_id_from_content_id(cid, title_id, title_id_size);
}
}
free(sfo_data);
}
}
if (title_id[0] == '\0') {
(void)games_title_id_from_content_id(pkg_ctx.header.content_id,
title_id, title_id_size);
}
pkg_cleanup(&pkg_ctx);
return (title_id[0] != '\0') ? 0 : -1;
}
/* 2) Try exFAT image */
exfat_context_t ctx;
if (exfat_init(&ctx, safe_path) != 0) {
return -1;
}
exfat_file_info_t root_entries[256];
int root_count = exfat_read_directory(&ctx,
ctx.boot_sector.root_dir_first_cluster,
root_entries, 256);
for (int i = 0; i < root_count; i++) {
if (!root_entries[i].is_directory) {
continue;
}
if (strcasecmp(root_entries[i].filename, "sce_sys") != 0) {
continue;
}
exfat_file_info_t sce_entries[64];
int sce_count = exfat_read_directory(&ctx,
root_entries[i].first_cluster,
sce_entries, 64);
for (int j = 0; j < sce_count; j++) {
if (sce_entries[j].is_directory) {
continue;
}
if (strcasecmp(sce_entries[j].filename, "param.sfo") == 0 &&
sce_entries[j].data_length > 0U &&
sce_entries[j].data_length <= 65536U) {
size_t slen = (size_t)sce_entries[j].data_length;
uint8_t *sbuf = (uint8_t *)malloc(slen);
if (sbuf != NULL) {
ssize_t got = exfat_extract_to_buffer(&ctx, &sce_entries[j], sbuf,
slen);
if (got > 0) {
(void)games_sfo_get_string(sbuf, (size_t)got, "TITLE_ID", title_id,
title_id_size);
if (title_id[0] == '\0') {
char cid[64] = "";
(void)games_sfo_get_string(sbuf, (size_t)got, "CONTENT_ID", cid,
sizeof(cid));
(void)games_title_id_from_content_id(cid, title_id, title_id_size);
}
}
free(sbuf);
}
}
if (title_id[0] == '\0' &&
strcasecmp(sce_entries[j].filename, "param.json") == 0 &&
sce_entries[j].data_length > 0U &&
sce_entries[j].data_length <= (256U * 1024U)) {
size_t plen = (size_t)sce_entries[j].data_length;
uint8_t *pbuf = (uint8_t *)malloc(plen + 1U);
if (pbuf != NULL) {
ssize_t got = exfat_extract_to_buffer(&ctx, &sce_entries[j], pbuf,
plen);
if (got > 0) {
pbuf[got] = '\0';
(void)games_json_get_string((char *)pbuf, "titleId", title_id,
title_id_size);
if (title_id[0] == '\0') {
(void)games_json_get_string((char *)pbuf, "title_id", title_id,
title_id_size);
}
if (title_id[0] == '\0') {
char cid[64] = "";
(void)games_json_get_string((char *)pbuf, "contentId", cid,
sizeof(cid));
if (cid[0] == '\0') {
(void)games_json_get_string((char *)pbuf, "content_id", cid,
sizeof(cid));
}
(void)games_title_id_from_content_id(cid, title_id, title_id_size);
}
}
free(pbuf);
}
}
if (title_id[0] != '\0') {
break;
}
}
break;
}
exfat_cleanup(&ctx);
return (title_id[0] != '\0') ? 0 : -1;
}
+525
View File
@@ -0,0 +1,525 @@
#include "games_internal.h"
#include "ftp_config.h"
#include "pkg_unpacker.h"
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/stat.h>
#include <unistd.h>
#ifndef ENABLE_PKG_INSTALL
#define ENABLE_PKG_INSTALL 0
#endif
/* Simple SFO string extraction */
static uint16_t le16(const uint8_t *p) { return (uint16_t)(p[0] | ((uint16_t)p[1] << 8)); }
static uint32_t le32(const uint8_t *p) { return (uint32_t)(p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24)); }
int games_sfo_get_string(const uint8_t *sfo, size_t size,
const char *req_key, char *out, size_t out_max) {
if (sfo == NULL || req_key == NULL || out == NULL || out_max == 0U ||
size < 20U || le32(sfo) != 0x46535000U) {
return -1;
}
out[0] = '\0';
const size_t key_table = (size_t)le32(sfo + 0x08);
const size_t data_table = (size_t)le32(sfo + 0x0C);
const size_t count = (size_t)le32(sfo + 0x10);
if (key_table >= size || data_table >= size ||
count > (size - 0x14U) / 16U) {
return -1;
}
const size_t req_len = strlen(req_key);
for (size_t i = 0U; i < count; i++) {
const uint8_t *entry = sfo + 0x14U + i * 16U;
const size_t key_off = (size_t)le16(entry);
const uint16_t fmt = le16(entry + 2U);
const size_t data_len = (size_t)le32(entry + 4U);
const size_t data_off = (size_t)le32(entry + 12U);
if (key_off > size - key_table) return -1;
const size_t key_pos = key_table + key_off;
if (key_pos >= size) return -1;
const char *key = (const char *)(sfo + key_pos);
const size_t key_avail = size - key_pos;
const char *key_end = (const char *)memchr(key, '\0', key_avail);
if (key_end == NULL) return -1;
const size_t key_len = (size_t)(key_end - key);
if (key_len != req_len || memcmp(key, req_key, req_len) != 0) continue;
if (fmt != 0x0204U && fmt != 0x0004U && fmt != 0x0000U &&
fmt != 0x0404U) {
return -1;
}
if (data_off > size - data_table) return -1;
const size_t data_pos = data_table + data_off;
if (data_len > size - data_pos) return -1;
const uint8_t *data = sfo + data_pos;
size_t text_len = data_len;
const uint8_t *nul = (const uint8_t *)memchr(data, '\0', data_len);
if (nul != NULL) text_len = (size_t)(nul - data);
const size_t copy_len = (text_len < out_max - 1U) ? text_len : out_max - 1U;
if (copy_len > 0U) memcpy(out, data, copy_len);
out[copy_len] = '\0';
return 0;
}
return -1;
}
/* Simple JSON string extraction: get value for "key":"value" */
int games_json_get_string(const char *json, const char *key,
char *out, size_t out_size) {
if (json == NULL || key == NULL || out == NULL || out_size == 0U) return -1;
out[0] = '\0';
char needle[128];
int n = snprintf(needle, sizeof(needle), "\"%s\"", key);
if (n < 0 || (size_t)n >= sizeof(needle)) return -1;
const char *pos = strstr(json, needle);
if (pos == NULL) return -1;
pos += (size_t)n;
while (*pos == ' ' || *pos == '\t' || *pos == '\n' || *pos == '\r') pos++;
if (*pos++ != ':') return -1;
while (*pos == ' ' || *pos == '\t' || *pos == '\n' || *pos == '\r') pos++;
if (*pos++ != '"') return -1;
size_t used = 0U;
while (*pos != '\0' && *pos != '"') {
unsigned char c = (unsigned char)*pos++;
if (c == '\\') {
if (*pos == '\0') return -1;
c = (unsigned char)*pos++;
switch (c) {
case 'n': c = '\n'; break;
case 'r': c = '\r'; break;
case 't': c = '\t'; break;
case 'b': c = '\b'; break;
case 'f': c = '\f'; break;
case '"': case '\\': case '/': break;
default: return -1; /* \uXXXX is intentionally not accepted here. */
}
}
if (used + 1U >= out_size) return -1;
out[used++] = (char)c;
}
if (*pos != '"') return -1;
out[used] = '\0';
return 0;
}
int games_title_id_from_content_id(const char *content_id,
char *out, size_t out_size) {
if ((content_id == NULL) || (out == NULL) || (out_size < 10U)) {
return -1;
}
size_t n = strlen(content_id);
for (size_t i = 0; (i + 9U) <= n; i++) {
if (isalpha((unsigned char)content_id[i + 0]) &&
isalpha((unsigned char)content_id[i + 1]) &&
isalpha((unsigned char)content_id[i + 2]) &&
isalpha((unsigned char)content_id[i + 3]) &&
isdigit((unsigned char)content_id[i + 4]) &&
isdigit((unsigned char)content_id[i + 5]) &&
isdigit((unsigned char)content_id[i + 6]) &&
isdigit((unsigned char)content_id[i + 7]) &&
isdigit((unsigned char)content_id[i + 8])) {
for (size_t j = 0; j < 9U; j++) {
out[j] = (char)toupper((unsigned char)content_id[i + j]);
}
out[9] = '\0';
return 0;
}
}
return -1;
}
int games_read_file(const char *path, uint8_t **out_data,
size_t *out_size, size_t max_size) {
if ((path == NULL) || (out_data == NULL) || (out_size == NULL)) {
return -1;
}
FILE *fp = fopen(path, "rb");
if (fp == NULL) {
return -1;
}
if (fseek(fp, 0, SEEK_END) != 0) {
fclose(fp);
return -1;
}
long flen = ftell(fp);
if (flen <= 0 || (size_t)flen > max_size) {
fclose(fp);
return -1;
}
if (fseek(fp, 0, SEEK_SET) != 0) {
fclose(fp);
return -1;
}
uint8_t *buf = (uint8_t *)malloc((size_t)flen);
if (buf == NULL) {
fclose(fp);
return -1;
}
size_t got = fread(buf, 1, (size_t)flen, fp);
fclose(fp);
if (got != (size_t)flen) {
free(buf);
return -1;
}
*out_data = buf;
*out_size = got;
return 0;
}
/** Cheap magic probe so a non-package app.pkg never reaches the parser. */
static int games_is_pkg_image(const char *path) {
FILE *fp = fopen(path, "rb");
if (fp == NULL) {
return 0;
}
uint8_t magic[4] = {0};
size_t got = fread(magic, 1U, sizeof(magic), fp);
fclose(fp);
if (got != sizeof(magic)) {
return 0;
}
uint32_t be = ((uint32_t)magic[0] << 24) | ((uint32_t)magic[1] << 16) |
((uint32_t)magic[2] << 8) | (uint32_t)magic[3];
return (be == PKG_MAGIC_CNT) || (be == PKG_MAGIC_PKG);
}
/**
* Load the param.sfo of an installed title.
*
* Titles installed as a directory tree keep it in sce_sys/param.sfo; the ones
* delivered as a single package (fpkg installers leave only app.pkg with
* app.json/app.pbm beside it) keep it inside the package image, which is why
* the entry is read from app.pkg when the plain file is missing.
*
* @return 0 with *out_data malloc()ed (caller frees) on success, -1 otherwise.
*/
static int games_load_installed_sfo(const char *app_dir, uint8_t **out_data,
size_t *out_size) {
if (app_dir == NULL || out_data == NULL || out_size == NULL) {
return -1;
}
*out_data = NULL;
*out_size = 0U;
char sfo_path[FTP_PATH_MAX];
int n = snprintf(sfo_path, sizeof(sfo_path), "%s/sce_sys/param.sfo", app_dir);
if (n > 0 && (size_t)n < sizeof(sfo_path) && access(sfo_path, R_OK) == 0 &&
games_read_file(sfo_path, out_data, out_size, 65536U) == 0) {
return 0;
}
n = snprintf(sfo_path, sizeof(sfo_path), "%s/param.sfo", app_dir);
if (n > 0 && (size_t)n < sizeof(sfo_path) && access(sfo_path, R_OK) == 0 &&
games_read_file(sfo_path, out_data, out_size, 65536U) == 0) {
return 0;
}
char pkg_path[FTP_PATH_MAX];
n = snprintf(pkg_path, sizeof(pkg_path), "%s/app.pkg", app_dir);
if (n <= 0 || (size_t)n >= sizeof(pkg_path) ||
!games_is_pkg_image(pkg_path)) {
return -1;
}
pkg_context_t pkg;
if (pkg_init(&pkg, pkg_path) != PKG_OK) {
return -1;
}
int rc = -1;
const pkg_entry_t *entry = pkg_find_entry_by_id(&pkg, PKG_ENTRY_ID_PARAM_SFO);
if (entry != NULL && entry->size > 0U && entry->size <= 65536U &&
!pkg_entry_is_encrypted(entry)) {
uint8_t *buf = (uint8_t *)malloc((size_t)entry->size);
if (buf != NULL) {
if (pkg_extract_to_buffer(&pkg, entry, buf, (size_t)entry->size) > 0) {
*out_data = buf;
*out_size = (size_t)entry->size;
rc = 0;
} else {
free(buf);
}
}
}
pkg_cleanup(&pkg);
return rc;
}
/**
* Display name of a title whose package cannot be read.
*
* PS5 packages (console format, magic 0x7F "FIH") hold no readable param.sfo,
* but the system keeps the metadata in the appmeta directory: param.json with
* the localized name in plain UTF-8, the first entry belonging to the default
* language.
*
* @return 0 when a name was extracted.
*/
static int games_read_appmeta_title(const char *title_id, char *title_name,
size_t title_name_size) {
static const char *const roots[] = {"/user/appmeta",
"/system_data/priv/appmeta", NULL};
if (title_id == NULL || title_id[0] == '\0' || title_name == NULL ||
title_name_size < 2U) {
return -1;
}
for (size_t i = 0U; roots[i] != NULL; i++) {
char path[FTP_PATH_MAX];
int n = snprintf(path, sizeof(path), "%s/%s/param.json", roots[i], title_id);
if (n <= 0 || (size_t)n >= sizeof(path)) {
continue;
}
uint8_t *raw = NULL;
size_t size = 0U;
if (games_read_file(path, &raw, &size, 65536U) != 0) {
continue;
}
/* games_json_get_string() needs a terminated buffer. */
char *text = (char *)malloc(size + 1U);
if (text == NULL) {
free(raw);
return -1;
}
memcpy(text, raw, size);
text[size] = '\0';
free(raw);
int ok = games_json_get_string(text, "titleName", title_name,
title_name_size) == 0 &&
title_name[0] != '\0';
free(text);
if (ok) {
return 0;
}
}
return -1;
}
int games_read_installed_sfo(const char *app_dir, char *title_id,
size_t title_id_size, char *title_name,
size_t title_name_size) {
if ((app_dir == NULL) || (title_id == NULL) || (title_name == NULL)) {
return -1;
}
title_name[0] = '\0';
int have_sfo = 0;
uint8_t *sfo = NULL;
size_t sfo_size = 0U;
if (games_load_installed_sfo(app_dir, &sfo, &sfo_size) == 0) {
have_sfo = 1;
/* The title id is already known from the directory name; a missing
* TITLE_ID must not erase it. */
char id_fallback[64] = {0};
(void)snprintf(id_fallback, sizeof(id_fallback), "%.63s", title_id);
(void)games_sfo_get_string(sfo, sfo_size, "TITLE_ID", title_id,
title_id_size);
if (title_id[0] == '\0') {
(void)snprintf(title_id, title_id_size, "%s", id_fallback);
}
(void)games_sfo_get_string(sfo, sfo_size, "TITLE", title_name,
title_name_size);
if (title_name[0] == '\0') {
(void)games_sfo_get_string(sfo, sfo_size, "TITLE_01", title_name,
title_name_size);
}
free(sfo);
}
/* Packages whose param.sfo cannot be read (PS5 console format) still have
* their name in the system metadata. */
if (title_name[0] == '\0') {
(void)games_read_appmeta_title(title_id, title_name, title_name_size);
}
return (have_sfo || title_name[0] != '\0') ? 0 : -1;
}
#if defined(PLATFORM_PS4) || defined(PLATFORM_PS5)
int games_read_installed_sfo_field(const char *app_dir,
const char *key,
char *out,
size_t out_size) {
if ((app_dir == NULL) || (key == NULL) || (out == NULL) || (out_size < 2U)) {
return -1;
}
out[0] = '\0';
uint8_t *sfo = NULL;
size_t sfo_size = 0U;
if (games_load_installed_sfo(app_dir, &sfo, &sfo_size) != 0) {
return -1;
}
int rc = games_sfo_get_string(sfo, sfo_size, key, out, out_size);
free(sfo);
return (rc == 0 && out[0] != '\0') ? 0 : -1;
}
#endif
int games_resolve_installed_icon(const char *title_id,
const char *app_dir,
char *out_path,
size_t out_size) {
if (!out_path || out_size < 2U) {
return -1;
}
out_path[0] = '\0';
if (app_dir && app_dir[0] != '\0') {
int n = snprintf(out_path, out_size, "%s/sce_sys/icon0.png", app_dir);
if (n > 0 && (size_t)n < out_size && access(out_path, R_OK) == 0) {
return 0;
}
n = snprintf(out_path, out_size, "%s/icon0.png", app_dir);
if (n > 0 && (size_t)n < out_size && access(out_path, R_OK) == 0) {
return 0;
}
}
if (title_id && title_id[0] != '\0') {
int n = snprintf(out_path, out_size, "/user/appmeta/%s/icon0.png", title_id);
if (n > 0 && (size_t)n < out_size && access(out_path, R_OK) == 0) {
return 0;
}
n = snprintf(out_path, out_size, "/system_data/priv/appmeta/%s/icon0.png",
title_id);
if (n > 0 && (size_t)n < out_size && access(out_path, R_OK) == 0) {
return 0;
}
}
return -1;
}
int games_resolve_installed_app_dir(const char *title_id,
char *out_path,
size_t out_size) {
if ((title_id == NULL) || (title_id[0] == '\0') || (out_path == NULL) ||
(out_size < 2U)) {
return -1;
}
out_path[0] = '\0';
const char *bases[] = {
"/user/app",
"/mnt/ext0/user/app",
"/system_ex/app",
"/system/app",
NULL,
};
for (size_t i = 0; bases[i] != NULL; i++) {
int n = snprintf(out_path, out_size, "%s/%s", bases[i], title_id);
if (n <= 0 || (size_t)n >= out_size) {
continue;
}
struct stat st;
if (stat(out_path, &st) == 0 && S_ISDIR(st.st_mode)) {
return 0;
}
}
out_path[0] = '\0';
return -1;
}
int games_extract_title_id_from_app_dir(const char *safe_path,
char *title_id,
size_t title_id_size) {
if ((safe_path == NULL) || (title_id == NULL) || (title_id_size < 10U)) {
return -1;
}
title_id[0] = '\0';
struct stat st;
if (stat(safe_path, &st) != 0 || !S_ISDIR(st.st_mode)) {
return -1;
}
char title_name[128] = {0};
if (games_read_installed_sfo(safe_path, title_id, title_id_size, title_name,
sizeof(title_name)) == 0 &&
title_id[0] != '\0') {
return 0;
}
const char *base = strrchr(safe_path, '/');
if (base && base[1] != '\0') {
base++;
size_t len = strlen(base);
if (len >= 9U && len < title_id_size) {
int valid = 1;
for (size_t i = 0; i < len; i++) {
unsigned char c = (unsigned char)base[i];
if (!(isalnum(c) || c == '_' || c == '-')) {
valid = 0;
break;
}
}
if (valid) {
(void)snprintf(title_id, title_id_size, "%s", base);
return 0;
}
}
}
return -1;
}
int games_is_valid_title_id(const char *title_id) {
if (title_id == NULL) {
return 0;
}
size_t len = strlen(title_id);
if (len < 4U || len > 16U) {
return 0;
}
for (size_t i = 0; i < len; i++) {
unsigned char c = (unsigned char)title_id[i];
if (!(isalnum(c) || c == '_' || c == '-')) {
return 0;
}
}
return 1;
}
#if ENABLE_PKG_INSTALL
int games_has_pkg_extension(const char *path) {
if (path == NULL) {
return 0;
}
const char *dot = strrchr(path, '.');
if (dot == NULL) {
return 0;
}
dot++;
return (strcasecmp(dot, "pkg") == 0 || strcasecmp(dot, "fpkg") == 0 ||
strcasecmp(dot, "ffpkg") == 0);
}
#endif
Loaded 100 of 253 files, more files were not shown because too many files have changed in this diff. Show more