Compare commits

..
17 Commits
Author SHA1 Message Date
Roman Tarasov 7e48103149 Updated readme 2026-03-16 22:04:49 +03:00
Roman Tarasov 124e757199 Disable kstuff auto-pause when kstuff is not installed 2026-03-16 20:44:34 +03:00
Roman Tarasov 0fcc970a20 Create /data/.kstuff_noautomount 2026-03-16 01:18:56 +03:00
Roman Tarasov 7684e8431d Increased block size to 64kb for UFS 2026-03-15 14:28:24 +03:00
Roman Tarasov 3166434db1 The start notification has been fixed and its sound has been disabled. 2026-03-15 12:27:11 +03:00
Roman Tarasov c785daaddf Changed image mount directory to /mnt/shadowmnt 2026-03-14 23:41:28 +03:00
Roman Tarasov 2db6b49439 Fix /dev/lvd attach 2026-03-14 23:18:19 +03:00
Roman Tarasov aff2fc0e6c Remove stale duplicate-tracking entries when sources disappear 2026-03-14 20:08:29 +03:00
Roman Tarasov ae36dd5871 unionfs options 2026-03-13 02:57:57 +03:00
Roman Tarasov cc3e07d201 Update compatibility note in README.md 2026-03-10 10:24:30 +03:00
Roman Tarasov 2c03ede25c Update README.md 2026-03-10 02:43:18 +03:00
Roman Tarasov a212a883b3 1.6b4 2026-03-10 02:24:03 +03:00
Roman Tarasov 99a2956c28 upd .gitingore 2026-03-10 02:23:13 +03:00
Roman Tarasov 93071924b5 wait /dev/lvd1 -> /dev/lvd2 2026-03-04 23:47:40 +03:00
Roman Tarasov 47e47477d1 Add macOS exFAT image builder and improve LVD startup wait handling 2026-02-28 20:21:46 +03:00
Roman Tarasov 31c28e1896 Block mounting of images with too small a cluster size. 2026-02-27 22:20:16 +03:00
Roman Tarasov c1f34ffa22 Version bump 2026-02-27 01:09:34 +03:00
56 changed files with 11900 additions and 3769 deletions

No files matched your search

+3
View File
@@ -3,3 +3,6 @@
daemon.lock
debug.log
notify.txt
src/notify_icon_asset.c
refs/*
docs/*
+20 -5
View File
@@ -1,22 +1,37 @@
PS5_PAYLOAD_SDK ?= /opt/ps5-payload-sdk
include $(PS5_PAYLOAD_SDK)/toolchain/prospero.mk
VERSION_TAG := $(shell git describe --abbrev=6 --dirty --always --tags 2>/dev/null || echo unknown)
# Standard Flags (No extra libraries)
CFLAGS := -O2 -Wall -D_BSD_SOURCE -std=gnu11 -Isrc
CFLAGS := -O2 -Wall -Wextra -Wstrict-prototypes -Wmissing-prototypes -Werror=strict-prototypes -Werror=missing-prototypes -D_BSD_SOURCE -std=gnu11 -Iinclude -Isrc
CFLAGS += -DSHADOWMOUNT_VERSION=\"$(VERSION_TAG)\"
# Linker
LDFLAGS :=
# Standard Libraries Only
LIBS := -lkernel_sys -lSceSystemService -lSceUserService -lSceAppInstUtil -lsqlite3
LIBS := -lkernel_sys -lSceNotification -lSceSystemService -lSceUserService -lSceAppInstUtil -lsqlite3
ASSET_SRCS := src/notify_icon_asset.c
SRCS := src/main.c $(wildcard src/sm_*.c) $(ASSET_SRCS)
OBJS := $(SRCS:.c=.o)
HEADERS := $(wildcard include/*.h)
# Targets
all: shadowmountplus.elf
# Build Daemon
shadowmountplus.elf: src/main.c
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $< $(LIBS)
shadowmountplus.elf: $(OBJS)
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(OBJS) $(LIBS)
$(PS5_PAYLOAD_SDK)/bin/prospero-strip --strip-debug $@
rm -f src/notify_icon_asset.c
src/notify_icon_asset.c: smp_icon.png
xxd -i $< > $@
src/%.o: src/%.c $(HEADERS)
$(CC) $(CFLAGS) -c -o $@ $<
clean:
rm -f shadowmountplus.elf kill.elf src/*.o
rm -f shadowmountplus.elf kill.elf src/*.o src/notify_icon_asset.c
+61 -33
View File
@@ -1,6 +1,6 @@
# ShadowMountPlus (PS5)
**Version:** `1.6beta1`
**Version:** `1.6beta5`
**Repository:** https://github.com/drakmor/shadowMountPlus
@@ -10,7 +10,7 @@
**ShadowMountPlus** is a fully automated, background "Auto-Mounter" payload for Jailbroken PlayStation 5 consoles. It streamlines the game mounting process by eliminating the need for manual configuration or external tools (such as DumpRunner or Itemzflow). ShadowMountPlus automatically detects, mounts, and installs game dumps from both **internal and external storage**.
**Compatibility:** Supports all Jailbroken PS5 firmwares running **Kstuff v1.6.7**.
**Compatibility:** Supports all Jailbroken PS5 firmwares running **Kstuff v1.6.6+ (Recommended 1.6.6)** or **Kstuff-lite v1.0+**.
## Current image support
@@ -19,19 +19,22 @@
| Extension | Mounted FS | Attach backend | Status |
| --- | --- | --- | --- |
| `.exfat` | `exfatfs` | `LVD` or `MD` (configurable) | Optimal |
| `.ffpkg` | `ufs` | `LVD` or `MD` (configurable) | Legacy |
| `.ffpkg` | `ufs` | `LVD` or `MD` (configurable) | Recommended |
| `.exfat` | `exfatfs` | `LVD` or `MD` (configurable) | Compatibility / external-drive-only titles |
| `.ffpfs` | `pfs` | `LVD` | Experimental |
Notes:
- Backend, read-only mode, and sector size can be configured via `/data/shadowmount/config.ini`.
- Debug logging is enabled by default (`debug=1`) and writes to console plus `/data/shadowmount/debug.log` (set `debug=0` to disable).
- **exFAT is the preferred image filesystem, including on 4.xx firmware, with no known reboot/shutdown issues in typical use.**
- **UFS (`.ffpkg`) is the recommended image format for normal use.**
- **Use exFAT (`.exfat`) only for titles that need external-drive-style compatibility.**
- **When building exFAT images manually, keep the cluster size at `64 KB`; smaller clusters can reduce performance.**
## Recommended FS choice
- Prefer **exFAT (`.exfat`)** in most cases: it is generally more performant, case-insensitive, and does not have reboot/shutdown issues in typical use.
- Use **UFS (`.ffpkg`)** only when specifically needed for compatibility with your game/setup.
- Prefer **UFS (`.ffpkg`)** in most cases: this is the recommended default image format for ShadowMountPlus.
- Use **exFAT (`.exfat`)** only for games that do not work correctly unless they are handled like external-drive content.
- If you create an **exFAT (`.exfat`)** image manually, use a **`64 KB` cluster size**. Smaller clusters can cause a noticeable performance loss.
## Runtime config (`/data/shadowmount/config.ini`)
@@ -39,16 +42,22 @@ This file is optional. If it does not exist, built-in defaults are used.
Supported keys (all optional):
- `debug=1|0` (`1` enables `log_debug` output to console + `/data/shadowmount/debug.log`; default is `1`)
- `quiet_mode=1|0` (`1` suppresses plain informational popups but keeps rich toasts; default is `0`)
- `mount_read_only=1|0` (default: `1`)
- `force_mount=1|0` (mounting even damaged file systems; default: `0`)
- `image_ro=<image_filename>` (repeatable; force read-only mode for this image filename)
- `image_rw=<image_filename>` (repeatable; force read-write mode for this image filename)
- `recursive_scan=1|0` (`0` = scan only first-level subfolders, `1` = recursive scan without depth limit; default: `0`)
- `backports_path=<absolute_path>` (default: `/data/backports`; if `<backports_path>/<TITLE_ID>` exists, it is overlaid on mounted image via `unionfs`)
- `scan_interval_seconds=<1..3600>` (full scan loop interval; default: `10`)
- `stability_wait_seconds=<0..3600>` (minimum source age before processing; default: `10`)
- `exfat_backend=lvd|md` (default: `lvd`)
- `ufs_backend=lvd|md` (default: `lvd`)
- `backport_fakelib=1|0` (`1` mounts sandbox `fakelib` overlays for running games; default: `1`)
- `kstuff_game_auto_toggle=1|0` (`1` pauses kstuff after tracked game launches and resumes it on stop; default: `1`)
- `kstuff_pause_delay_image_seconds=<0..3600>` (delay before pausing kstuff for image-backed launches; default: `20`)
- `kstuff_pause_delay_direct_seconds=<0..3600>` (delay before pausing kstuff for direct/non-image launches; default: `10`)
- `kstuff_no_pause=<TITLE_ID>` (repeatable; keeps kstuff enabled for matching titles)
- `kstuff_delay=<TITLE_ID>:<0..3600>` (repeatable; per-title pause delay override, last matching rule wins)
- `scanpath=<absolute_path>` (can be repeated on multiple lines; default: built-in scan path list below)
- `lvd_exfat_sector_size=<value>` (default: `512`)
- `lvd_ufs_sector_size=<value>` (default: `4096`)
@@ -71,15 +80,29 @@ image_ro=legacy_dump.ffpkg
Scan path behavior:
- If at least one `scanpath=...` is present, only those custom paths are used.
- `/data/imgmnt/{ufsmnt,exfatmnt,pfsmnt}` are always added automatically, even with custom paths.
- `/mnt/shadowmnt` is always added automatically, even with custom paths.
- With `recursive_scan=0` (default), only first-level subfolders are checked.
- With `recursive_scan=1`, subfolders are scanned recursively.
- Full scan loop runs every `scan_interval_seconds` (default: `10`).
- Sources newer than `stability_wait_seconds` are deferred until stable (default: `10`).
Backport overlay behavior:
- If `${backports_path}/${TITLE_ID}` exists, ShadowMount+ mounts it over the game root with `unionfs`.
- Files from backport folder override files from the mounted image.
- For each `scanpath`, use:
- `<scanpath>/backports/<TITLE_ID>/`
- The `backports` folder is ignored during normal game scanning.
- A backport is applied automatically to the matching mounted game from the same scan path.
- If `/mnt/sandbox/<TITLE_ID>_XXX/app0/fakelib` exists while the game is running, ShadowMount+ also mounts it into that game's sandbox `common/lib`.
- `backport_fakelib=0` disables the sandbox `fakelib` watcher.
- For `backport_fakelib` to work correctly, the standalone `BackPork` payload must be disabled. Running both at the same time will conflict.
Kstuff game lifecycle behavior:
- When `kstuff_game_auto_toggle=1`, ShadowMount watches game `exec/exit` events in the background.
- Image-backed launches use `kstuff_pause_delay_image_seconds`; direct/non-image launches use `kstuff_pause_delay_direct_seconds`.
- `kstuff_no_pause` skips auto-pause entirely for matching title IDs.
- `kstuff_delay` overrides the pause delay for matching title IDs, regardless of image/direct launch type.
- If both kinds of rule target the same title, `kstuff_no_pause` takes priority.
- When the last tracked game stops, ShadowMount immediately enables kstuff again if it was the component that disabled it.
Validation:
- See `config.ini.example` for a ready-to-use template.
@@ -88,9 +111,7 @@ Validation:
Image mountpoints are created under:
`/data/imgmnt/<fs>/<image_name>`
Where `<fs>` is one of: `ufsmnt`, `exfatmnt`, `pfsmnt`.
`/mnt/shadowmnt/<image_name>_<hash>`
Image layout requirement (`.ffpkg`, `.exfat`, `.ffpfs`):
- Game files must be placed at the image root.
@@ -112,7 +133,7 @@ Default scan locations:
- `/mnt/usb0` .. `/mnt/usb7`
- `/mnt/ext0`
- `/mnt/ext1`
- `/data/imgmnt/ufsmnt`, `/data/imgmnt/exfatmnt`, `/data/imgmnt/pfsmnt` (mounted image content scan)
- `/mnt/shadowmnt` (mounted image content scan)
You can override scan roots with `scanpath=...` entries in `/data/shadowmount/config.ini`.
@@ -120,14 +141,19 @@ Recommended folder structure:
- Default mode (`recursive_scan=0`):
- `/data/homebrew/<TITLE_ID>/`
- `/data/etaHEN/games/<TITLE_ID>/`
- `/data/homebrew/backports/<TITLE_ID>/`
- `/data/etaHEN/games/backports/<TITLE_ID>/`
- Recursive mode (`recursive_scan=1`):
- `/data/homebrew/PS5/<AnyFolder>/<TITLE_ID>/`
- `/mnt/ext0/etaHEN/games/<Collection>/<TITLE_ID>/`
- `/mnt/ext0/etaHEN/games/backports/<TITLE_ID>/`
## Creating an exFAT image
Recommended only for titles that need external-drive-style compatibility. For general use, prefer `.ffpkg`.
Linux (Ubuntu/Debian):
- Required components installation:
- `sudo apt-get update && sudo apt-get install -y exfatprogs exfat-fuse fuse3 rsync`
@@ -139,6 +165,7 @@ Linux (Ubuntu/Debian):
- Notes:
- Source folder must be the game root and contain `eboot.bin`.
- Auto-calculates image size using rounded file allocation + metadata + safety margin.
- For manual exFAT builds, keep the cluster size at `64 KB` or you may lose performance.
- Automatically selects exFAT cluster profile:
- Large-file profile: `64K`
- Small/mixed-file profile: `32K`
@@ -149,6 +176,7 @@ Windows:
- Install OSFMount: https://www.osforensics.com/tools/mount-disk-images.html.
- Keep `make_image.bat` and `New-OsfExfatImage.ps1` in the same folder.
- Run `cmd.exe` as Administrator.
- If you build an exFAT image manually instead of using the script, keep the cluster size at `64 KB` or you may lose performance.
- Usage:
- `make_image.bat "C:\images\game.exfat" "C:\payload\APPXXXX"`
- Behavior:
@@ -170,14 +198,20 @@ FreeBSD:
- Source folder must be the game root and contain `eboot.bin`.
- The script auto-calculates image size using rounded file allocation + metadata + safety margin.
- Recommended `newfs` parameters for UFS2:
- Large-file profile: `newfs -O 2 -b 65536 -f 4096 -m 0 -i 262144`
- Small/mixed-file profile: `newfs -O 2 -b 32768 -f 4096 -m 0 -i 262144`
- `mkufs2.sh` selects between these two profiles automatically (based on average file size).
- `newfs -O 2 -b 65536 -f 65536 -m 0 -S 4096`
- `mkufs2.sh` keeps this fixed block/fragment/sector profile and auto-tunes `-i` based on source file/directory count.
- Rough manual `-i` estimate for manual builds:
- `target_inodes ~= file_count + dir_count + 2048`
- `bytes_per_inode ~= image_size_bytes / target_inodes`
- Round `bytes_per_inode` down to a multiple of `4096`, then keep it in the practical range `65536..262144`.
- Practical rule of thumb: use `262144` for normal game dumps, `131072` for tens of thousands of files, and `65536` only for very file-dense images.
- Example: for an `8 GiB` image with `60000` files and `4000` directories, `-i ~= 8*1024^3 / (60000 + 4000 + 2048) ~= 130312`, so use `-i 131072`.
Windows:
- You can create UFS2 images with **UFS2Tool** https://github.com/SvenGDK/UFS2Tool.
- Example:
- `UFS2Tool.exe newfs -O 2 -b 32768 -f 4096 -m 0 -i 262144 -D ./APPXXXX ./PPSA12345.ffpkg`
- `UFS2Tool.exe newfs -O 2 -b 65536 -f 65536 -m 0 -S 4096 -i 262144 -D ./APPXXXX ./PPSA12345.ffpkg`
- For manual builds, use `-i 262144` as the baseline and lower it for images with many small files.
## Installation and usage
@@ -186,23 +220,16 @@ Windows:
### Method 1: Manual Payload Injection (Port 9021)
Use a payload sender (such as NetCat GUI or a web-based loader) to send the files to **Port 9021**.
1. Send `notify.elf` (Optional).
* *Only send this if you want graphical pop-ups. Skip if you prefer standard notifications.*
2. Send `shadowmountplus.elf`.
3. Wait for the notification: *"ShadowMount+"*.
1. Send `shadowmountplus.elf`.
2. Wait for the notification: *"ShadowMount+"*.
### Method 2: PLK Autoloader (Recommended)
Add ShadowMountPlus to your `autoload.txt` for **plk-autoloader** to ensure it starts automatically on every boot.
**Sample Configuration:**
```ini
!1000
notify.elf ; Optional - Remove this line if you do not want Rich Toasts
!1000
shadowmountplus.elf
!3000
ps5-backpork.elf
!1000
kstuff.elf
```
@@ -215,7 +242,7 @@ If a game is not mounted:
- Check `/data/shadowmount/debug.log` and system notifications from ShadowMount+.
- Verify scan roots:
- if `scanpath=...` is set, only these paths are scanned;
- `/data/imgmnt/{ufsmnt,exfatmnt,pfsmnt}` are always scanned.
- `/mnt/shadowmnt` is always scanned.
- Verify scan depth:
- `recursive_scan=0` scans only first-level subfolders;
- `recursive_scan=1` scans recursively.
@@ -223,7 +250,7 @@ If a game is not mounted:
- Verify game structure:
- folder game: `<GAME_DIR>/sce_sys/param.json`;
- image game (`.ffpkg` / `.exfat` / `.ffpfs`): `sce_sys/param.json` must be at image root (no extra top-level folder).
- If you see `missing/invalid param.json` for an image, check via FTP that files are present under `/data/imgmnt/<fs>/<image_name>/` and include `sce_sys/param.json`.
- If you see `missing/invalid param.json` for an image, check via FTP that files are present under `/mnt/shadowmnt/<image_name>_<hash>/` and include `sce_sys/param.json`.
- If you see image mount failure, check image integrity and filesystem type (`.ffpkg`=UFS, `.exfat`=exFAT, `.ffpfs`=PFS).
- If you see duplicate titleId notification, keep only one source per `<TITLE_ID>`.
@@ -240,10 +267,11 @@ If a game is mounted but does not start:
* **Drakmor** - Evolution of ShadowMount to ShadowMountPlus
* **Special Thanks:**
* VoidWhisper for ShadowMount [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/voidwhisper)
* EchoStretch
* VoidWhisper for ShadowMount
* BestPig for BackPort
* EchoStretch for kstuff-toggle and etc
* Gezine
* earthonion
* LightningMods
* john-tornblom
* john-tornblom for SDK
* PS5 R&D Community
+3583
View File
File diff suppressed because it is too large. Load diff
+34 -10
View File
@@ -9,6 +9,11 @@
# 0/false/no/off -> disable log_debug output
debug=1
# Quiet mode:
# 1/true/yes/on -> only show the startup version popup and runtime errors
# 0/false/no/off -> also show normal informational popups (default)
quiet_mode=0
# Global mount mode:
# 1/true/yes/on/ro -> read-only mounts
# 0/false/no/off/rw -> read-write mounts
@@ -30,10 +35,6 @@ force_mount=0
# 0/false/no/off -> scan only first-level subdirectories (default)
recursive_scan=0
# Optional backport overlay root:
# If "<backports_path>/<TITLE_ID>" exists, it is mounted over image root via unionfs.
backports_path=/data/backports
# Full scan loop interval (seconds), range: 1..3600
scan_interval_seconds=10
@@ -47,12 +48,35 @@ stability_wait_seconds=10
exfat_backend=lvd
ufs_backend=lvd
# Backend-specific overrides (optional):
# lvd_exfat_sector_size=512
# lvd_ufs_sector_size=4096
# lvd_pfs_sector_size=32768
# md_exfat_sector_size=512
# md_ufs_sector_size=512
# Sandbox fakelib backport watcher:
# 1/true/yes/on -> mount /mnt/sandbox/<TITLE_ID>_XXX/app0/fakelib into common/lib
# 0/false/no/off -> disable fakelib watcher
backport_fakelib=1
# Auto-manage kstuff around game launches:
# 1/true/yes/on -> pause kstuff after game launch, resume immediately on stop
# 0/false/no/off -> leave kstuff state untouched
kstuff_game_auto_toggle=1
# Delay before pausing kstuff for image-backed titles.
kstuff_pause_delay_image_seconds=20
# Delay before pausing kstuff for direct/non-image launches.
kstuff_pause_delay_direct_seconds=10
# Keep kstuff enabled for specific title IDs (can be repeated).
# kstuff_no_pause=PPSA12345
# Override pause delay for specific title IDs (can be repeated).
# Format: TITLEID:SECONDS
# kstuff_delay=PPSA12345:30
# Backend-specific sector size overrides:
lvd_exfat_sector_size=512
lvd_ufs_sector_size=4096
lvd_pfs_sector_size=32768
md_exfat_sector_size=512
md_ufs_sector_size=512
# Optional custom scan roots (can be repeated):
# If at least one scanpath=... is present, only these paths are used.
+20
View File
@@ -0,0 +1,20 @@
#ifndef SM_APPDB_H
#define SM_APPDB_H
#include <stdbool.h>
struct AppDbTitleList;
// Close the cached app.db handle and release related resources.
void shutdown_app_db(void);
// Update snd0 metadata for a registered title.
int update_snd0info(const char *title_id);
// Check whether a title ID exists in a cached app.db title list.
bool app_db_title_list_contains(const struct AppDbTitleList *list,
const char *title_id);
// Drop the cached app.db title list so it is reloaded on next access.
void invalidate_app_db_title_cache(void);
// Return the cached app.db title list, loading it if needed.
bool get_app_db_title_list_cached(const struct AppDbTitleList **list_out);
#endif
+27
View File
@@ -0,0 +1,27 @@
#ifndef SM_CONFIG_MOUNT_H
#define SM_CONFIG_MOUNT_H
#include <stdbool.h>
#include <stdint.h>
typedef struct runtime_config runtime_config_t;
// Ensure runtime configuration is loaded before use.
void ensure_runtime_config_ready(void);
// Load runtime configuration from disk and apply defaults.
bool load_runtime_config(void);
// Return the current runtime configuration.
const runtime_config_t *runtime_config(void);
// Return the number of configured scan roots.
int get_scan_path_count(void);
// Return a scan root by index, or NULL if out of range.
const char *get_scan_path(int index);
// Resolve a per-image read-only override from the file name.
bool get_image_mode_override(const char *filename, bool *mount_read_only_out);
// Return true when kstuff auto-pause is disabled for the given title ID.
bool is_kstuff_pause_disabled_for_title(const char *title_id);
// Resolve a per-title kstuff pause-delay override in seconds.
bool get_kstuff_pause_delay_override_for_title(const char *title_id,
uint32_t *delay_seconds_out);
#endif
+17
View File
@@ -0,0 +1,17 @@
#ifndef SM_FAKELIB_H
#define SM_FAKELIB_H
#include <stdbool.h>
#include <stdint.h>
#include <sys/types.h>
// Return true when per-game fakelib backport automation is enabled.
bool sm_fakelib_game_feature_enabled(void);
// Track a supported game launch and mount its fakelib overlay immediately.
void sm_fakelib_game_on_exec(pid_t pid, const char *title_id);
// Remove a tracked fakelib overlay for a stopped game.
void sm_fakelib_game_on_exit(pid_t pid);
// Unmount all tracked overlays during watcher shutdown.
void sm_fakelib_game_shutdown(void);
#endif
+45
View File
@@ -0,0 +1,45 @@
#ifndef SM_FILESYSTEM_H
#define SM_FILESYSTEM_H
#include <dirent.h>
#include <stdbool.h>
#include <stddef.h>
// Check whether a title is present in the installed app set.
bool is_installed(const char *title_id);
// Check whether a title currently has mounted data.
bool is_data_mounted(const char *title_id);
// Read the mount.lnk source path for an installed title.
bool read_mount_link(const char *title_id, char *out, size_t out_size);
// Validate a /user/app/<TITLE_ID> entry and optionally build its path.
bool resolve_title_app_dir(const struct dirent *entry, char *app_dir,
size_t app_dir_size);
// Recover staged mount links and warm image source mappings on startup.
void cleanup_staged_mount_links(void);
// Remove duplicated managed mount layers left from previous runs.
void cleanup_duplicate_title_mounts(void);
// Remount /system_ex with the expected flags.
int remount_system_ex(void);
// Mount a title source into /system_ex/app/<title_id> via nullfs.
bool mount_title_nullfs(const char *title_id, const char *src_path);
// Reconcile the title mount stack against the expected source/backport state.
bool reconcile_title_backport_mount(const char *title_id, const char *src_path,
const char *expected_backport_path,
bool *overlay_active_out);
// Mount a prepared backport overlay on top of an already mounted title.
void mount_backport_overlay(const char *mount_point,
const char *backport_path,
const char *title_id);
// Unmount all managed /system_ex/app/<title_id> mount stacks on shutdown.
void shutdown_title_mounts(void);
// Return true when path is equal to root or is under it.
bool path_matches_root_or_child(const char *path, const char *root);
// Remove stale mount links and optionally restore image-backed mounts.
void cleanup_mount_links(const char *removed_source_root,
bool unmount_system_ex_bind);
// Recursively copy a directory tree.
int copy_dir(const char *src, const char *dst);
// Copy a single file.
int copy_file(const char *src, const char *dst);
#endif
+17
View File
@@ -0,0 +1,17 @@
#ifndef SM_GAME_CACHE_H
#define SM_GAME_CACHE_H
#include <stdbool.h>
// Cache resolved metadata for a mounted or discovered game.
void cache_game_entry(const char *path, const char *title_id,
const char *title_name);
// Drop invalid or stale entries from the game cache.
void prune_game_cache(void);
// Look up a cached game entry by path or title ID.
bool find_cached_game(const char *path, const char *title_id,
const char **existing_path_out);
// Remove a game cache entry by path.
void clear_cached_game(const char *path);
#endif
+13
View File
@@ -0,0 +1,13 @@
#ifndef SM_GAME_LIFECYCLE_H
#define SM_GAME_LIFECYCLE_H
#include <stdbool.h>
// Start the shared game lifecycle watcher used by backport fakelib and kstuff.
bool start_game_lifecycle_watcher(void);
// Wake the shared watcher so pending lifecycle work is processed immediately.
void wake_game_lifecycle_watcher(void);
// Stop the shared watcher and let lifecycle modules clean up tracked state.
void stop_game_lifecycle_watcher(void);
#endif
+13
View File
@@ -0,0 +1,13 @@
#ifndef SM_GAMEINFO_H
#define SM_GAMEINFO_H
#include <stdbool.h>
#include <sys/stat.h>
// Read title ID and title name from a mounted game directory.
bool get_game_info(const char *base_path, const struct stat *param_st,
char *out_id, char *out_name);
// Check whether a directory contains sce_sys/param.json and stat it.
bool directory_has_param_json(const char *dir_path, struct stat *param_st_out);
#endif
+16
View File
@@ -0,0 +1,16 @@
#ifndef SM_HASH_H
#define SM_HASH_H
#include <stdint.h>
// Return a stable 32-bit FNV-1a hash for a NUL-terminated string.
static inline uint32_t sm_fnv1a32(const char *s) {
uint32_t h = 2166136261u;
while (s && *s) {
h ^= (uint8_t)(*s++);
h *= 16777619u;
}
return h;
}
#endif
+23
View File
@@ -0,0 +1,23 @@
#ifndef SM_IMAGE_H
#define SM_IMAGE_H
#include <stdbool.h>
#include "sm_types.h"
// Log filesystem statistics for a mounted path.
void log_fs_stats(const char *tag, const char *path, const char *type_hint);
// Attach and mount an image file to its runtime mount point.
bool mount_image(const char *file_path, image_fs_type_t fs_type);
// Unmount an image mount point and detach its backing device.
bool unmount_image(const char *file_path, int unit_id, attach_backend_t backend);
// Reconcile cached image mounts with current sources and remount if needed.
void cleanup_stale_image_mounts(void);
// Unmount every cached image mount during shutdown.
bool shutdown_image_mounts(void);
// Remove empty directories left under the image mount root.
void cleanup_mount_dirs(void);
// Mount an image file if it is stable and not currently rate-limited.
void maybe_mount_image_file(const char *full_path, const char *name);
#endif
+34
View File
@@ -0,0 +1,34 @@
#ifndef SM_IMAGE_CACHE_H
#define SM_IMAGE_CACHE_H
#include <stdbool.h>
#include "sm_types.h"
typedef struct {
char path[MAX_PATH];
char mount_point[MAX_PATH];
int unit_id;
attach_backend_t backend;
} image_cache_entry_t;
// Cache a successful image mount and its attached device.
// Returns false when no free cache slot is available.
bool cache_image_mount(const char *path, const char *mount_point,
int unit_id, attach_backend_t backend);
// Cache an image source mapping without attach metadata.
bool cache_image_source_mapping(const char *path, const char *mount_point);
// Return a cached image mount entry by index.
bool get_image_cache_entry(int index, image_cache_entry_t *entry_out);
// Mark a cached image mount entry as invalid.
void invalidate_image_cache_entry(int index);
// Resolve a device mapping from the in-memory mount cache.
bool resolve_device_from_mount_cache(const char *mount_point,
attach_backend_t *backend_out,
int *unit_out);
// Resolve the original image file path for a cached runtime mount point.
bool resolve_image_source_from_mount_cache(const char *mount_point,
char *path_out,
size_t path_out_size);
#endif
+10
View File
@@ -0,0 +1,10 @@
#ifndef SM_INSTALL_H
#define SM_INSTALL_H
typedef struct scan_candidate scan_candidate_t;
// Install or remount the collected scan candidates.
void process_scan_candidates(const scan_candidate_t *candidates,
int candidate_count);
#endif
+35
View File
@@ -0,0 +1,35 @@
#ifndef SM_KSTUFF_H
#define SM_KSTUFF_H
#include <stdbool.h>
#include <stdint.h>
#include <sys/types.h>
// Resolve firmware-specific kstuff sysentvec addresses and initialize state.
void sm_kstuff_init(void);
// Shut down runtime kstuff control state.
void sm_kstuff_shutdown(void);
// Return true when firmware-specific kstuff control is available.
bool sm_kstuff_is_supported(void);
// Return true when both tracked kstuff sysentvecs are enabled.
bool sm_kstuff_is_enabled(void);
// Set both tracked kstuff sysentvecs and return the resulting enabled state.
bool sm_kstuff_set_enabled(bool enabled, bool notify_user);
// Return true when game-driven kstuff automation should run.
bool sm_kstuff_game_feature_enabled(void);
// Track a supported game launch for delayed auto-pause handling.
void sm_kstuff_game_on_exec(pid_t pid, const char *title_id, uint32_t app_id,
uint64_t exec_time_us);
// Publish an AppFocus change for processing on the lifecycle/kstuff thread.
void sm_kstuff_note_app_focus(uint32_t app_id);
// Return the next wake deadline in monotonic microseconds, or 0 when idle.
uint64_t sm_kstuff_game_next_wake_us(uint64_t now_us);
// Forget a tracked game and restore kstuff when no paused entries remain.
void sm_kstuff_game_on_exit(pid_t pid);
// Apply any delayed kstuff pause whose deadline has elapsed.
void sm_kstuff_game_poll(void);
// Clear tracked game state and restore kstuff if the watcher paused it.
void sm_kstuff_game_shutdown(void);
#endif
+45
View File
@@ -0,0 +1,45 @@
#ifndef SM_LIMITS_H
#define SM_LIMITS_H
#define DEFAULT_SCAN_INTERVAL_US 10000000u
#define DEFAULT_STABILITY_WAIT_SECONDS 10u
#define DEFAULT_KSTUFF_PAUSE_DELAY_IMAGE_SECONDS 20u
#define DEFAULT_KSTUFF_PAUSE_DELAY_DIRECT_SECONDS 10u
#define MAX_PENDING 512
#define MAX_IMAGE_MOUNTS 64
#define MAX_IMAGE_MODE_RULES 128
#define MAX_KSTUFF_TITLE_RULES 128
#define PATH_STATE_CAPACITY MAX_PENDING
#define TITLE_STATE_CAPACITY MAX_PENDING
#define STATE_HASH_SIZE 1024u
#define MAX_SCAN_PATHS 256
#define MAX_FAILED_MOUNT_ATTEMPTS 2
#define MAX_REGISTER_ATTEMPTS 2
#define MAX_MISSING_PARAM_SCAN_ATTEMPTS 3
#define MAX_IMAGE_MOUNT_ATTEMPTS 3
#define MAX_LAYERED_UNMOUNT_ATTEMPTS 4
#define LVD_RELEASE_WAIT_MAX_US 15000000u
#define LVD_RELEASE_WAIT_POLL_US 500000u
#define GAME_LIFECYCLE_POLL_INTERVAL_US 250000u
#define GAME_APPINFO_LOOKUP_TIMEOUT_US 1000000u
#define KSTUFF_RESTORE_RETRY_US 1000000u
#define SHELLCORE_FLAG_POLL_INTERVAL_US 100000u
#define MIN_SCAN_INTERVAL_SECONDS 1u
#define MAX_SCAN_INTERVAL_SECONDS 3600u
#define MAX_STABILITY_WAIT_SECONDS 3600u
#define MAX_KSTUFF_PAUSE_DELAY_SECONDS 3600u
#define APP_DB_QUERY_BUSY_RETRIES 3
#define APP_DB_UPDATE_BUSY_RETRIES 25
#define APP_DB_PREPARE_BUSY_RETRIES 25
#define APP_DB_BUSY_RETRY_SLEEP_US 200000u
#define APP_DB_BUSY_TIMEOUT_MS 5000
#define MAX_PATH 1024
#define MAX_TITLE_ID 32
#define MAX_TITLE_NAME 256
#endif
+34
View File
@@ -0,0 +1,34 @@
#ifndef SM_LOG_H
#define SM_LOG_H
#include <stdbool.h>
typedef struct sm_error sm_error_t;
// Write a formatted line to the debug log.
void log_debug(const char *fmt, ...);
// Prepare notification assets such as the packaged icon file.
void sm_notifications_init(void);
// Send a rich toast notification with packaged icon and version header.
void notify_system_rich(bool allow_in_quiet_mode, const char *fmt, ...);
// Send the "game installed" rich toast for the given title ID.
void notify_game_installed_rich(const char *title_id);
// Send an informational system notification (suppressed in quiet mode).
void notify_system_info(const char *fmt, ...);
// Send a system notification with formatted text.
void notify_system(const char *fmt, ...);
// Clear the last recorded subsystem error.
void sm_error_clear(void);
// Store the last subsystem error with formatted details.
void sm_error_set(const char *subsystem, int code, const char *path,
const char *fmt, ...);
// Return the last recorded subsystem error.
const sm_error_t *sm_last_error(void);
// Return whether the current error was already notified.
bool sm_error_notified(void);
// Mark the current error as already shown to the user.
void sm_error_mark_notified(void);
// Notify the user about an image mount failure once per error state.
void notify_image_mount_failed(const char *path, int mount_err);
#endif
+80
View File
@@ -0,0 +1,80 @@
#ifndef SM_MOUNT_DEFS_H
#define SM_MOUNT_DEFS_H
#define IMAGE_MOUNT_READ_ONLY 1
// 1 = use legacy /dev/mdctl backend for .exfat images, 0 = use LVD for all.
#define EXFAT_ATTACH_USE_MDCTL 0
// 1 = allow mounting .ffpkg images via /dev/mdctl, 0 = keep UFS on LVD.
#define UFS_ATTACH_USE_MDCTL 0
// --- LVD Definitions ---
// Kernel exposes base attach (V0), extended attach (V1/Attach2) and detach.
// This code only builds V0 layer descriptors, so it uses the base attach path.
// Wrapper-side raw attach presets:
// - 0x8/0x9 -> single-image/save-data family (normalized 0x14/0x1C)
// - 0xC/0xD -> download-data/LWFS family (normalized 0x16/0x1E)
// raw bit3 (0x8) sets the DONT_MAP_BM-style mode, raw bit2 (0x4) switches to
// the DD/LWFS family, and raw bit0 (0x1) selects the alternate preset inside a
// family. Firmware pairs bit0 with read-only mount variants, but this is not a
// standalone LVD readonly bit by itself.
// image_type values accepted by validator: 0..0xC (13 values total).
// layer source_type observed: 1=file, 2=device/special source (/dev/sbram0, char/block).
// layer descriptor flag bit0 is "no bitmap file specified".
#define LVD_CTRL_PATH "/dev/lvdctl"
#define MD_CTRL_PATH "/dev/mdctl"
#define SCE_LVD_IOC_ATTACH_V0 0xC0286D00
#define SCE_LVD_IOC_ATTACH_V1 0xC0286D09
#define SCE_LVD_IOC_DETACH 0xC0286D01
#define LVD_ATTACH_IO_VERSION_V0 0u
#define LVD_ATTACH_IO_VERSION_V1 1u
#define LVD_ATTACH_RAW_FLAGS_SINGLE_RO 0x9
#define LVD_ATTACH_RAW_FLAGS_SINGLE_RW 0x8
#define LVD_ATTACH_RAW_FLAGS_DD_RO 0xD
#define LVD_ATTACH_RAW_FLAGS_DD_RW 0xC
#
#define LVD_SECTOR_SIZE_EXFAT 512u
#define LVD_SECTOR_SIZE_UFS 4096u
#define LVD_SECTOR_SIZE_PFS 4096u
#define LVD_SECONDARY_UNIT_SINGLE_IMAGE 0x10000u
#define MD_SECTOR_SIZE_EXFAT 512u
#define MD_SECTOR_SIZE_UFS 512u
// Raw option bits are normalized by sceFsLvdAttachCommon before validation:
// raw:0x1->norm:0x08, raw:0x2->norm:0x80, raw:0x4->norm:0x02, raw:0x8->norm:0x10.
// The normalized masks are then checked against validator constraints (0x82/0x92).
#define LVD_ATTACH_IMAGE_TYPE_SINGLE 0
#define LVD_ATTACH_IMAGE_TYPE_UFS_DOWNLOAD_DATA 7
#define LVD_ATTACH_IMAGE_TYPE_PFS_SAVE_DATA 5
#define LVD_ATTACH_LAYER_COUNT 1
#define LVD_ATTACH_LAYER_ARRAY_SIZE 3
#define LVD_ENTRY_TYPE_FILE 1
#define LVD_ENTRY_TYPE_SPECIAL 2
#define LVD_ENTRY_FLAG_NO_BITMAP 0x1
#define LVD_NODE_WAIT_US 100000
#define LVD_NODE_WAIT_RETRIES 100
#define UFS_NMOUNT_FLAG_RW 0x10000000u
#define UFS_NMOUNT_FLAG_RO 0x10000001u
// --- devpfs/pfs option defaults ---
// PFS nmount key/value variants observed in refs:
// - fstype: "pfs", "transaction_pfs", "ppr_pfs"
// - mkeymode: "SD"
// - budgetid: "game"/"system" in init paths
// - sigverify/playgo/disc: "0" or "1"
// - optional keys in specific flows: ekpfs/eekpfs, eekc, pubkey_ver, key_ver,
// finalized, ppkg_opt, sblock_offset, maxpkgszingib
#define DEVPFS_BUDGET_GAME "game"
#define DEVPFS_BUDGET_SYSTEM "system"
#define DEVPFS_MKEYMODE_SD "SD"
#define DEVPFS_MKEYMODE_GD "GD"
#define DEVPFS_MKEYMODE_AC "AC"
#define PFS_MOUNT_BUDGET_ID DEVPFS_BUDGET_GAME
#define PFS_MOUNT_MKEYMODE DEVPFS_MKEYMODE_SD
#define PFS_MOUNT_SIGVERIFY 0
#define PFS_MOUNT_PLAYGO 0
#define PFS_MOUNT_DISC 0
// 4x64-bit PFS key encoded as 64 hex chars.
#define PFS_ZERO_EKPFS_KEY_HEX \
"0000000000000000000000000000000000000000000000000000000000000000"
#endif
+23
View File
@@ -0,0 +1,23 @@
#ifndef SM_MOUNT_DEVICE_H
#define SM_MOUNT_DEVICE_H
#include <stdbool.h>
#include "sm_types.h"
// Return a printable name for an attach backend enum value.
const char *attach_backend_name(attach_backend_t backend);
// Check whether a source path has aged enough to be mounted safely.
bool is_source_stable_for_mount(const char *path, const char *name,
const char *tag);
// Resolve backend and unit ID for a mounted image path.
bool resolve_device_from_mount(const char *mount_point,
attach_backend_t *backend_out, int *unit_out);
// Return true if a path is an active mount point.
bool is_active_image_mount_point(const char *path);
// Wait until /dev/lvd2 is no longer mounted during startup.
bool wait_for_lvd_release(void);
// Detach a previously attached MD or LVD unit.
bool detach_attached_unit(attach_backend_t backend, int unit_id);
#endif
+30
View File
@@ -0,0 +1,30 @@
#ifndef SM_PATH_STATE_H
#define SM_PATH_STATE_H
#include <stdbool.h>
#include <stdint.h>
#include <sys/stat.h>
// Load cached game info if the param file state still matches.
bool load_cached_game_info(const char *path, const struct stat *param_st,
char *out_id, char *out_name, bool *valid_out);
// Store parsed game info together with the current param file state.
void store_cached_game_info(const char *path, const struct stat *param_st,
bool valid, const char *title_id,
const char *title_name);
// Drop expired entries from path-based retry and metadata state.
void prune_path_state(void);
// Return whether scans for a path are temporarily suppressed after failures.
bool is_missing_param_scan_limited(const char *path);
// Record a missing param.json failure for scan backoff.
void record_missing_param_failure(const char *path);
// Clear missing-param backoff state for a path.
void clear_missing_param_entry(const char *path);
// Return whether image mount retries are temporarily suppressed for a path.
bool is_image_mount_limited(const char *path);
// Increment and return the image mount retry counter for a path.
uint8_t bump_image_mount_attempts(const char *path);
// Clear image mount retry state for a path.
void clear_image_mount_attempts(const char *path);
#endif
+16
View File
@@ -0,0 +1,16 @@
#ifndef SM_PATH_UTILS_H
#define SM_PATH_UTILS_H
#include <stdbool.h>
#include <stddef.h>
#include "sm_types.h"
// Return the last path component without modifying the input string.
const char *get_filename_component(const char *path);
// Return true when a path lives under the image mount root.
bool is_under_image_mount_base(const char *path);
// Build "<scan_path>/backports" for a managed scan root.
bool build_backports_root_path(const char *scan_path, char out[MAX_PATH]);
#endif
+40
View File
@@ -0,0 +1,40 @@
#ifndef SM_PATHS_H
#define SM_PATHS_H
#define IMAGE_MOUNT_BASE "/mnt/shadowmnt"
#define DEFAULT_BACKPORTS_DIR_NAME "backports"
#define LOG_DIR "/data/shadowmount"
#define LOG_FILE "/data/shadowmount/debug.log"
#define LOG_FILE_PREV "/data/shadowmount/debug.log.1"
#define CONFIG_FILE "/data/shadowmount/config.ini"
#define KSTUFF_NOAUTOMOUNT_FILE "/data/.kstuff_noautomount"
#define KILL_FILE "/data/shadowmount/STOP"
#define TOAST_FILE "/data/shadowmount/notify.txt"
#define NOTIFY_ICON_DIR "/user/data/shadowmount"
#define NOTIFY_ICON_FILE "/user/data/shadowmount/smp_icon.png"
#define APP_DB_PATH "/system_data/priv/mms/app.db"
/* Compile-time default scan roots (used when config has no scanpath entries). */
#define SM_DEFAULT_SCAN_PATHS_INITIALIZER \
{ \
/* Internal */ \
"/data/homebrew", "/data/etaHEN/games", \
/* Extended Storage */ \
"/mnt/ext0/homebrew", "/mnt/ext0/etaHEN/games", \
/* M.2 Drive */ \
"/mnt/ext1/homebrew", "/mnt/ext1/etaHEN/games", \
/* USB Subfolders */ \
"/mnt/usb0/homebrew", "/mnt/usb1/homebrew", "/mnt/usb2/homebrew", \
"/mnt/usb3/homebrew", "/mnt/usb4/homebrew", "/mnt/usb5/homebrew", \
"/mnt/usb6/homebrew", "/mnt/usb7/homebrew", \
"/mnt/usb0/etaHEN/games", "/mnt/usb1/etaHEN/games", \
"/mnt/usb2/etaHEN/games", "/mnt/usb3/etaHEN/games", \
"/mnt/usb4/etaHEN/games", "/mnt/usb5/etaHEN/games", \
"/mnt/usb6/etaHEN/games", "/mnt/usb7/etaHEN/games", \
/* USB Root Paths */ \
"/mnt/usb0", "/mnt/usb1", "/mnt/usb2", "/mnt/usb3", "/mnt/usb4", \
"/mnt/usb5", "/mnt/usb6", "/mnt/usb7", "/mnt/ext0", "/mnt/ext1", NULL \
}
#endif
+59
View File
@@ -0,0 +1,59 @@
#ifndef SM_PLATFORM_H
#define SM_PLATFORM_H
#include <ctype.h>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <signal.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/file.h>
#include <sys/ioctl.h>
#include <sys/mdioctl.h>
#include <sys/mount.h>
#include <sys/param.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/uio.h>
#include <time.h>
#include <unistd.h>
#include <ps5/kernel.h>
typedef struct {
uint32_t app_id;
uint64_t unknown1;
char title_id[14];
char unknown2[0x3c];
} app_info_t;
// --- SDK Imports ---
int sceAppInstUtilInitialize(void);
int sceAppInstUtilAppInstallTitleDir(const char *title_id,
const char *install_path, void *reserved);
int sceKernelGetAppInfo(pid_t pid, app_info_t *info);
int sceKernelUsleep(unsigned int microseconds);
int sceUserServiceInitialize(void *);
void sceUserServiceTerminate(void);
// Standard Notification
typedef struct notify_request {
char unused[45];
char message[3075];
} notify_request_t;
int sceKernelSendNotificationRequest(int, notify_request_t *, size_t, int);
#define IOVEC_ENTRY(x) \
{(void *)(x), \
((const char *)(x) == NULL ? 0u : (size_t)(strlen((const char *)(x)) + 1u))}
#define IOVEC_SIZE(x) (sizeof(x) / sizeof(struct iovec))
#endif
+22
View File
@@ -0,0 +1,22 @@
#ifndef SM_RUNTIME_H
#define SM_RUNTIME_H
#include <stdbool.h>
#include <sys/types.h>
// Install process signal handlers used for graceful shutdown.
void install_signal_handlers(void);
// Return process pid with the given name, or 0 if not found.
pid_t find_pid_by_name(const char *name, bool exclude_self);
// Return true when shutdown was requested by signal or kill file.
bool should_stop_requested(void);
// Request graceful shutdown with a descriptive source string.
void request_shutdown_stop(const char *reason);
// Request an immediate scan cycle with a descriptive source string.
void request_scan_now(const char *reason);
// Consume a pending immediate scan request, returning true if one was pending.
bool consume_scan_now_request(const char **reason_out);
// Sleep in chunks and stop early if shutdown was requested.
bool sleep_with_stop_check(unsigned int total_us);
#endif
+14
View File
@@ -0,0 +1,14 @@
#ifndef SM_SCAN_H
#define SM_SCAN_H
typedef struct scan_candidate scan_candidate_t;
// Unmount and clean up mounts whose backing sources disappeared.
void cleanup_lost_sources_before_scan(void);
// Scan configured roots and collect install candidates.
int collect_scan_candidates(scan_candidate_t *candidates, int max_candidates,
int *total_found_out);
// Mount stable per-root backport overlays for already mounted titles.
void mount_backport_overlays(void);
#endif
+9
View File
@@ -0,0 +1,9 @@
#ifndef SM_SHELLCORE_FLAGS_H
#define SM_SHELLCORE_FLAGS_H
#include <stdbool.h>
bool sm_shellcore_flags_start(void);
void sm_shellcore_flags_stop(void);
#endif
+12
View File
@@ -0,0 +1,12 @@
#ifndef SM_STABILITY_H
#define SM_STABILITY_H
#include <stdbool.h>
// Check whether a path is old enough to be treated as stable.
bool is_path_stable_now(const char *path, double *root_diff_out,
int *stat_errno_out);
// Wait briefly for a path to become stable before giving up.
bool wait_for_stability_fast(const char *path, const char *name);
#endif
+9
View File
@@ -0,0 +1,9 @@
#ifndef SM_TIME_H
#define SM_TIME_H
#include <stdint.h>
// Return a monotonic timestamp in microseconds, or 0 on failure.
uint64_t monotonic_time_us(void);
#endif
+25
View File
@@ -0,0 +1,25 @@
#ifndef SM_TITLE_STATE_H
#define SM_TITLE_STATE_H
#include <stdbool.h>
#include <stdint.h>
// Return whether registration was already attempted for a title.
bool was_register_attempted(const char *title_id);
// Return the number of registration attempts for a title.
uint8_t get_register_attempts(const char *title_id);
// Record a registration attempt for a title.
void mark_register_attempted(const char *title_id);
// Notify once about duplicate title IDs from different paths.
void notify_duplicate_title_once(const char *title_id, const char *path_a,
const char *path_b);
// Clear the one-shot duplicate notification state for a title.
void clear_duplicate_title_notification(const char *title_id);
// Return the number of failed install/remount attempts for a title.
uint8_t get_failed_mount_attempts(const char *title_id);
// Clear failed install/remount attempts for a title.
void clear_failed_mount_attempts(const char *title_id);
// Increment and return failed install/remount attempts for a title.
uint8_t bump_failed_mount_attempts(const char *title_id);
#endif
+141
View File
@@ -0,0 +1,141 @@
#ifndef SM_TYPES_H
#define SM_TYPES_H
#include <stdbool.h>
#include <stdint.h>
#include "sm_limits.h"
typedef struct {
// +0x00: 1 -> mount read-only, 0 -> allow write.
uint32_t ro;
// +0x04: reserved in observed Shell/FSMP callers.
uint32_t reserved0;
// +0x08: logical budget/domain string, usually "game" or "system".
const char *budget_id;
// +0x10: reserved in observed Shell/FSMP callers.
uint32_t reserved1;
// +0x14: bitmask consumed by devpfs mount logic.
uint32_t flags;
// +0x18: optional "maxpkgszingib" value (GiB), 0 means not set.
uint64_t max_pkg_gib;
} devpfs_mount_opt_t;
typedef struct {
// Human-readable profile id for logs.
const char *name;
// Raw option payload mapped to FSMP mount behavior.
devpfs_mount_opt_t opt;
} devpfs_mount_profile_t;
typedef struct {
// Source object class (observed: 1=file, 2=device-like/special source).
uint16_t source_type;
// Layer descriptor flags (observed bit0 = no bitmap file path).
uint16_t flags;
// Must be zero.
uint32_t reserved0;
// Backing file or device path.
const char *path;
// Data start offset in backing object (bytes).
uint64_t offset;
// Data size exposed via this layer (bytes).
uint64_t size;
// Optional bitmap file path.
const char *bitmap_path;
// Bitmap offset in bitmap file (bytes).
uint64_t bitmap_offset;
// Bitmap size (bytes), 0 when bitmap is unused.
uint64_t bitmap_size;
} lvd_ioctl_layer_v0_t;
typedef struct {
// Protocol version for /dev/lvdctl ioctl payload (0 = V0/base attach).
uint32_t io_version;
// Input: usually -1 for auto-assign. Output: created lvd unit id.
int32_t device_id;
// User-visible sector size exported by /dev/lvdN.
uint32_t sector_size;
// Secondary unit/granularity validated against sector_size.
uint32_t secondary_unit;
// Normalized attach flags produced from wrapper raw options.
uint16_t flags;
// LVD image type id (validator accepts 0..0xC).
uint16_t image_type;
// Number of valid entries in layers_ptr.
uint32_t layer_count;
// Total exported virtual size (bytes).
uint64_t device_size;
// Pointer to V0 layer array in user payload.
lvd_ioctl_layer_v0_t *layers_ptr;
} lvd_ioctl_attach_v0_t;
typedef struct {
// Must be zero.
uint32_t reserved0;
// Target lvd unit id to detach.
int32_t device_id;
// Reserved padding required by kernel ABI.
uint8_t reserved[0x20];
} lvd_ioctl_detach_t;
struct AppDbTitleList {
char(*ids)[MAX_TITLE_ID];
int count;
int capacity;
};
typedef struct scan_candidate {
char path[MAX_PATH];
char title_id[MAX_TITLE_ID];
char title_name[MAX_TITLE_NAME];
bool installed;
bool in_app_db;
} scan_candidate_t;
typedef enum {
ATTACH_BACKEND_NONE = 0,
// /dev/lvdctl -> /dev/lvdN
ATTACH_BACKEND_LVD,
// /dev/mdctl -> /dev/mdN
ATTACH_BACKEND_MD,
} attach_backend_t;
typedef struct runtime_config {
bool debug_enabled;
bool quiet_mode;
bool mount_read_only;
bool force_mount;
bool recursive_scan;
bool backport_fakelib_enabled;
bool kstuff_game_auto_toggle;
uint32_t scan_interval_us;
uint32_t stability_wait_seconds;
uint32_t kstuff_pause_delay_image_seconds;
uint32_t kstuff_pause_delay_direct_seconds;
attach_backend_t exfat_backend;
attach_backend_t ufs_backend;
uint32_t lvd_sector_exfat;
uint32_t lvd_sector_ufs;
uint32_t lvd_sector_pfs;
uint32_t md_sector_exfat;
uint32_t md_sector_ufs;
} runtime_config_t;
typedef enum {
IMAGE_FS_UNKNOWN = 0,
IMAGE_FS_UFS,
IMAGE_FS_EXFAT,
IMAGE_FS_PFS,
} image_fs_type_t;
typedef struct sm_error {
int code;
char subsystem[16];
char message[256];
char path[MAX_PATH];
bool valid;
bool notified;
} sm_error_t;
#endif
+137
View File
@@ -0,0 +1,137 @@
#!/bin/sh
# macOS exFAT image builder — requires macOS 10.6.5+ (exFAT and rsync built-in)
# Usage: mkexfat_macos.sh <input_dir> [output_file]
# (c) @drakmor,@betmoar
set -e
# ── argument validation ───────────────────────────────────────────────────────
if [ -z "$1" ]; then
echo "Usage: $0 <input_dir> [output_file]"
exit 1
fi
INPUT_DIR="$1"
OUTPUT="${2:-test.exfat}"
# newfs_exfat interprets relative paths as device names under /dev/ — resolve to absolute
OUTPUT="$(cd "$(dirname "$OUTPUT")" && pwd)/$(basename "$OUTPUT")"
if [ ! -d "$INPUT_DIR" ]; then
echo "Error: input directory not found: $INPUT_DIR"
exit 1
fi
if [ ! -f "$INPUT_DIR/eboot.bin" ]; then
echo "Error: eboot.bin not found in source directory: $INPUT_DIR"
exit 1
fi
# ── sizing constants ──────────────────────────────────────────────────────────
CLUSTER_SIZE=32768
LARGE_FILE_THRESHOLD=$((1024 * 1024))
META_FIXED=$((32 * 1024 * 1024)) # boot region, upcase, root and misc
MIN_SLACK=$((64 * 1024 * 1024)) # minimum copy/runtime safety margin
SPARE_MIN=$((64 * 1024 * 1024)) # lower bound for dynamic headroom
SPARE_MAX=$((512 * 1024 * 1024)) # upper bound for dynamic headroom
ENTRY_META_BYTES=256
# Single filesystem traversal: collect all file sizes into a temp file.
# FILE_COUNT and RAW_FILE_BYTES are derived from it in one awk pass.
# DIR_COUNT needs a separate traversal (different -type).
SIZES_FILE=$(mktemp)
MOUNT_POINT=""
LOOP_DEVICE=""
cleanup() {
if [ -n "$MOUNT_POINT" ] && mount | grep -q "on $MOUNT_POINT "; then
umount "$MOUNT_POINT" 2>/dev/null || true
fi
if [ -n "$LOOP_DEVICE" ]; then
hdiutil detach "$LOOP_DEVICE" 2>/dev/null || true
fi
if [ -n "$MOUNT_POINT" ]; then
rmdir "$MOUNT_POINT" 2>/dev/null || true
fi
rm -f "$SIZES_FILE"
}
trap cleanup EXIT INT TERM
find "$INPUT_DIR" -type f -exec stat -f '%z' {} \; > "$SIZES_FILE"
read -r FILE_COUNT RAW_FILE_BYTES <<EOF
$(awk '{c++; s += $1} END {print c+0, s+0}' "$SIZES_FILE")
EOF
DIR_COUNT=$(find "$INPUT_DIR" -type d | wc -l | tr -d ' ')
AVG_FILE_BYTES=0
if [ "$FILE_COUNT" -gt 0 ]; then
AVG_FILE_BYTES=$((RAW_FILE_BYTES / FILE_COUNT))
fi
# exFAT profile selection:
# - large-file sets: 64K cluster
# - small/mixed-file sets: 32K cluster
if [ "$AVG_FILE_BYTES" -ge "$LARGE_FILE_THRESHOLD" ]; then
CLUSTER_SIZE=65536
fi
# DATA_BYTES re-reads the already-collected sizes from the temp file — no second traversal
DATA_BYTES=$(awk -v cls="$CLUSTER_SIZE" \
'{s += int(($1 + cls - 1) / cls) * cls} END {print s + 0}' "$SIZES_FILE")
rm -f "$SIZES_FILE"
DATA_CLUSTERS=$(( (DATA_BYTES + CLUSTER_SIZE - 1) / CLUSTER_SIZE ))
FAT_BYTES=$((DATA_CLUSTERS * 4))
BITMAP_BYTES=$(( (DATA_CLUSTERS + 7) / 8 ))
ENTRY_BYTES=$(( (FILE_COUNT + DIR_COUNT) * ENTRY_META_BYTES ))
BASE_TOTAL=$((DATA_BYTES + FAT_BYTES + BITMAP_BYTES + ENTRY_BYTES + META_FIXED))
SPARE_BYTES=$((BASE_TOTAL / 200)) # ~0.5%
if [ "$SPARE_BYTES" -lt "$SPARE_MIN" ]; then SPARE_BYTES=$SPARE_MIN; fi
if [ "$SPARE_BYTES" -gt "$SPARE_MAX" ]; then SPARE_BYTES=$SPARE_MAX; fi
TOTAL=$((BASE_TOTAL + SPARE_BYTES))
MIN_TOTAL=$((RAW_FILE_BYTES + MIN_SLACK))
if [ "$TOTAL" -lt "$MIN_TOTAL" ]; then TOTAL=$MIN_TOTAL; fi
# Round up to nearest MB
MB=$(( (TOTAL + 1024*1024 - 1) / (1024*1024) ))
echo "Input size (raw files): $RAW_FILE_BYTES bytes"
echo "Input size (exFAT alloc): $DATA_BYTES bytes"
echo "Files: $FILE_COUNT, Dirs: $DIR_COUNT"
echo "exFAT profile: -b ${CLUSTER_SIZE} (avg file=$AVG_FILE_BYTES bytes)"
echo "Image size: ${MB}MB"
# ── create, attach, format, mount ────────────────────────────────────────────
MOUNT_POINT=$(mktemp -d)
# mkfile -n creates a sparse file without writing zeros (equivalent to Linux truncate)
mkfile -n "${MB}m" "$OUTPUT"
# Attach as a block device first — newfs_exfat requires real sector geometry via ioctl
# and cannot format a raw file directly
ATTACH_OUTPUT=$(hdiutil attach \
-imagekey diskimage-class=CRawDiskImage \
-nomount "$OUTPUT")
LOOP_DEVICE=$(printf '%s\n' "$ATTACH_OUTPUT" | awk 'NR==1 {print $1; exit}')
if [ -z "$LOOP_DEVICE" ]; then
echo "Error: failed to determine attached device: $OUTPUT"
exit 1
fi
# newfs_exfat -b takes bytes-per-cluster directly
newfs_exfat -b "$CLUSTER_SIZE" "$LOOP_DEVICE"
mount -t exfat "$LOOP_DEVICE" "$MOUNT_POINT"
rsync -r --progress "$INPUT_DIR"/ "$MOUNT_POINT"/
umount "$MOUNT_POINT"
hdiutil detach "$LOOP_DEVICE"
LOOP_DEVICE="" # prevent double-detach in trap
echo "Created $OUTPUT"
+25 -18
View File
@@ -23,14 +23,17 @@ if [ ! -f "$INPUT_DIR/eboot.bin" ]; then
fi
# More accurate sizing for UFS2:
# - file payload rounded to fragment size (-f)
# - file payload rounded to the actual allocation unit (-f)
# - minimal per-directory allocation
# - inode table estimate
# - fixed filesystem metadata headroom
BLOCK_SIZE=32768
FRAG_SIZE=4096
BLOCK_SIZE=65536
FRAG_SIZE=65536
SECTOR_SIZE=4096
MINFREE_PERCENT=0
BYTES_PER_INODE=262144
DEFAULT_BYTES_PER_INODE=$((4 * FRAG_SIZE))
MIN_BYTES_PER_INODE=$FRAG_SIZE
INODE_SIZE=256
INODE_SPARE=2048
META_FIXED=$((64 * 1024 * 1024)) # superblocks, cg metadata, allocator slack
@@ -43,18 +46,6 @@ DIR_COUNT=$(find "$INPUT_DIR" -type d | wc -l | tr -d ' ')
RAW_FILE_BYTES=$(find "$INPUT_DIR" -type f -exec stat -f '%z' {} + | \
awk '{s += $1} END {print s + 0}')
AVG_FILE_BYTES=0
if [ "$FILE_COUNT" -gt 0 ]; then
AVG_FILE_BYTES=$((RAW_FILE_BYTES / FILE_COUNT))
fi
# UFS profile selection:
# - large-file sets: 64K block
# - small/mixed-file sets: 32K block (safer/denser default)
if [ "$AVG_FILE_BYTES" -ge $((1024 * 1024)) ]; then
BLOCK_SIZE=65536
fi
DATA_BYTES=$(find "$INPUT_DIR" -type f -exec stat -f '%z' {} + | \
awk -v frag="$FRAG_SIZE" '{s += int(($1 + frag - 1) / frag) * frag} END {print s + 0}')
DIR_BYTES=$((DIR_COUNT * FRAG_SIZE))
@@ -77,17 +68,33 @@ fi
# Round up to nearest MB
MB=$(( (TOTAL + 1024*1024 - 1) / (1024*1024) ))
IMAGE_BYTES=$((MB * 1024 * 1024))
# Tune inode density for the actual tree size. Keep the documented default
# unless the source contains enough files/directories to require denser inodes.
BYTES_PER_INODE=$DEFAULT_BYTES_PER_INODE
if [ "$INODE_COUNT" -gt 0 ]; then
AUTO_BYTES_PER_INODE=$((IMAGE_BYTES / INODE_COUNT))
AUTO_BYTES_PER_INODE=$(( (AUTO_BYTES_PER_INODE / SECTOR_SIZE) * SECTOR_SIZE ))
if [ "$AUTO_BYTES_PER_INODE" -lt "$MIN_BYTES_PER_INODE" ]; then
AUTO_BYTES_PER_INODE=$MIN_BYTES_PER_INODE
fi
if [ "$AUTO_BYTES_PER_INODE" -lt "$BYTES_PER_INODE" ]; then
BYTES_PER_INODE=$AUTO_BYTES_PER_INODE
fi
fi
echo "Input size (raw files): $RAW_FILE_BYTES bytes"
echo "Input size (UFS alloc): $DATA_BYTES bytes"
echo "Files: $FILE_COUNT, Dirs: $DIR_COUNT"
echo "UFS profile: -b $BLOCK_SIZE -f $FRAG_SIZE -m $MINFREE_PERCENT -i $BYTES_PER_INODE (avg file=$AVG_FILE_BYTES bytes)"
echo "Inode target: $INODE_COUNT"
echo "UFS profile: -b $BLOCK_SIZE -f $FRAG_SIZE -m $MINFREE_PERCENT -S $SECTOR_SIZE -i $BYTES_PER_INODE"
echo "Image size: ${MB}MB"
truncate -s "${MB}M" "$OUTPUT"
MD=$(mdconfig -a -t vnode -f "$(realpath "$OUTPUT")")
newfs -O 2 -b "$BLOCK_SIZE" -f "$FRAG_SIZE" -m "$MINFREE_PERCENT" -i "$BYTES_PER_INODE" /dev/${MD}
newfs -O 2 -b "$BLOCK_SIZE" -f "$FRAG_SIZE" -m "$MINFREE_PERCENT" -S "$SECTOR_SIZE" -i "$BYTES_PER_INODE" /dev/${MD}
mkdir -p /mnt
mount /dev/${MD} /mnt
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

+318 -3703
View File
File diff suppressed because it is too large. Load diff
+268
View File
@@ -0,0 +1,268 @@
#include "sm_platform.h"
#include <sqlite3.h>
#include "sm_runtime.h"
#include "sm_types.h"
#include "sm_appdb.h"
#include "sm_limits.h"
#include "sm_log.h"
#include "sm_paths.h"
static sqlite3 *g_app_db;
static sqlite3_stmt *g_app_db_stmt_update_snd0;
static struct AppDbTitleList g_app_db_title_cache;
static bool g_app_db_title_cache_ready = false;
static time_t g_app_db_title_cache_mtime = 0;
static void close_app_db(void) {
if (g_app_db_stmt_update_snd0) {
sqlite3_finalize(g_app_db_stmt_update_snd0);
g_app_db_stmt_update_snd0 = NULL;
}
if (g_app_db) {
sqlite3_close(g_app_db);
g_app_db = NULL;
}
}
static void free_app_db_title_list(struct AppDbTitleList *list) {
if (!list)
return;
free(list->ids);
list->ids = NULL;
list->count = 0;
list->capacity = 0;
}
void shutdown_app_db(void) {
free_app_db_title_list(&g_app_db_title_cache);
g_app_db_title_cache_ready = false;
g_app_db_title_cache_mtime = 0;
close_app_db();
}
static bool ensure_app_db_open(void) {
if (!g_app_db) {
if (sqlite3_open_v2(APP_DB_PATH, &g_app_db, SQLITE_OPEN_READWRITE, NULL) !=
SQLITE_OK) {
log_debug(" [DB] open failed: %s",
(g_app_db ? sqlite3_errmsg(g_app_db) : APP_DB_PATH));
close_app_db();
return false;
}
(void)sqlite3_busy_timeout(g_app_db, APP_DB_BUSY_TIMEOUT_MS);
}
return true;
}
static bool app_db_wait_retry(int rc, int attempt, int max_attempts,
bool close_before_retry) {
if (rc != SQLITE_BUSY && rc != SQLITE_LOCKED)
return false;
if (attempt + 1 >= max_attempts || should_stop_requested())
return false;
if (close_before_retry)
close_app_db();
sceKernelUsleep(APP_DB_BUSY_RETRY_SLEEP_US);
return true;
}
static int app_db_prepare_with_retry(const char *sql, sqlite3_stmt **stmt_out,
int max_attempts, const char *label) {
for (int attempt = 0; attempt < max_attempts; attempt++) {
if (!ensure_app_db_open())
return -1;
int rc = sqlite3_prepare_v2(g_app_db, sql, -1, stmt_out, NULL);
if (rc == SQLITE_OK)
return SQLITE_OK;
if (app_db_wait_retry(rc, attempt, max_attempts, true))
continue;
log_debug(" [DB] prepare failed for %s: rc=%d err=%s", label, rc,
(g_app_db ? sqlite3_errmsg(g_app_db) : "unknown"));
close_app_db();
return rc;
}
close_app_db();
return -1;
}
int update_snd0info(const char *title_id) {
if (!title_id || title_id[0] == '\0')
return -1;
if (!g_app_db_stmt_update_snd0) {
const char *sql =
"UPDATE tbl_contentinfo "
"SET snd0info = '/user/appmeta/' || ?1 || '/snd0.at9' "
"WHERE titleId = ?1;";
int prep_rc = app_db_prepare_with_retry(sql, &g_app_db_stmt_update_snd0,
APP_DB_PREPARE_BUSY_RETRIES,
"snd0info update");
if (prep_rc != SQLITE_OK)
return -1;
}
for (int attempt = 0; attempt < APP_DB_UPDATE_BUSY_RETRIES; attempt++) {
sqlite3_reset(g_app_db_stmt_update_snd0);
sqlite3_clear_bindings(g_app_db_stmt_update_snd0);
if (sqlite3_bind_text(g_app_db_stmt_update_snd0, 1, title_id, -1,
SQLITE_TRANSIENT) != SQLITE_OK) {
if (g_app_db) {
log_debug(" [DB] bind failed for snd0info update: %s",
sqlite3_errmsg(g_app_db));
}
close_app_db();
return -1;
}
int rc = sqlite3_step(g_app_db_stmt_update_snd0);
if (rc == SQLITE_DONE) {
int changes = sqlite3_changes(g_app_db);
close_app_db();
return changes;
}
if (app_db_wait_retry(rc, attempt, APP_DB_UPDATE_BUSY_RETRIES, false))
continue;
if (g_app_db) {
log_debug(" [DB] step failed for snd0info update: rc=%d err=%s", rc,
sqlite3_errmsg(g_app_db));
}
close_app_db();
return -1;
}
close_app_db();
return -1;
}
static bool append_app_db_title(struct AppDbTitleList *list,
const char *title_id) {
if (!list || !title_id || title_id[0] == '\0')
return true;
if (list->count >= list->capacity) {
int new_capacity = (list->capacity > 0) ? (list->capacity * 2) : 1024;
char(*new_ids)[MAX_TITLE_ID] =
realloc(list->ids, (size_t)new_capacity * sizeof(*list->ids));
if (!new_ids)
return false;
list->ids = new_ids;
list->capacity = new_capacity;
}
(void)strlcpy(list->ids[list->count], title_id, MAX_TITLE_ID);
list->count++;
return true;
}
static int compare_title_id_str(const void *a, const void *b) {
return strcmp((const char *)a, (const char *)b);
}
bool app_db_title_list_contains(const struct AppDbTitleList *list,
const char *title_id) {
if (!list || !title_id || title_id[0] == '\0')
return false;
return bsearch(title_id, list->ids, (size_t)list->count, sizeof(*list->ids),
compare_title_id_str) != NULL;
}
static bool load_app_db_title_list(struct AppDbTitleList *list) {
if (!list)
return false;
free_app_db_title_list(list);
const char *sql =
"SELECT DISTINCT titleId "
"FROM tbl_contentinfo "
"WHERE titleId != '' "
"ORDER BY titleId;";
sqlite3_stmt *stmt = NULL;
int prep_rc = app_db_prepare_with_retry(sql, &stmt, APP_DB_PREPARE_BUSY_RETRIES,
"title list query");
if (prep_rc != SQLITE_OK) {
close_app_db();
return false;
}
int busy_attempts = 0;
while (!should_stop_requested()) {
int rc = sqlite3_step(stmt);
if (rc == SQLITE_ROW) {
const char *title_id = (const char *)sqlite3_column_text(stmt, 0);
if (title_id && title_id[0] != '\0') {
if (!append_app_db_title(list, title_id)) {
log_debug(" [DB] title list allocation failed");
sqlite3_finalize(stmt);
close_app_db();
free_app_db_title_list(list);
return false;
}
}
continue;
}
if (rc == SQLITE_DONE) {
sqlite3_finalize(stmt);
close_app_db();
log_debug(" [DB] loaded app.db title list: %d entries", list->count);
return true;
}
if ((rc == SQLITE_BUSY || rc == SQLITE_LOCKED) &&
busy_attempts + 1 < APP_DB_QUERY_BUSY_RETRIES) {
busy_attempts++;
sceKernelUsleep(APP_DB_BUSY_RETRY_SLEEP_US);
continue;
}
log_debug(" [DB] title list query failed: rc=%d err=%s", rc,
(g_app_db ? sqlite3_errmsg(g_app_db) : "unknown"));
sqlite3_finalize(stmt);
close_app_db();
free_app_db_title_list(list);
return false;
}
sqlite3_finalize(stmt);
close_app_db();
free_app_db_title_list(list);
return false;
}
void invalidate_app_db_title_cache(void) {
g_app_db_title_cache_ready = false;
g_app_db_title_cache_mtime = 0;
}
bool get_app_db_title_list_cached(const struct AppDbTitleList **list_out) {
if (!list_out)
return false;
*list_out = NULL;
struct stat st;
int app_db_stat_rc = stat(APP_DB_PATH, &st);
if (!g_app_db_title_cache_ready ||
(app_db_stat_rc == 0 && g_app_db_title_cache_mtime != st.st_mtime)) {
struct AppDbTitleList fresh = {0};
if (app_db_stat_rc == 0 && load_app_db_title_list(&fresh)) {
free_app_db_title_list(&g_app_db_title_cache);
g_app_db_title_cache = fresh;
g_app_db_title_cache_mtime = st.st_mtime;
g_app_db_title_cache_ready = true;
} else {
free_app_db_title_list(&fresh);
if (!g_app_db_title_cache_ready)
return false;
}
}
*list_out = &g_app_db_title_cache;
return true;
}
+694
View File
@@ -0,0 +1,694 @@
#include "sm_platform.h"
#include "sm_config_mount.h"
#include "sm_types.h"
#include "sm_limits.h"
#include "sm_log.h"
#include "sm_mount_defs.h"
#include "sm_mount_device.h"
#include "sm_path_utils.h"
#include "sm_paths.h"
static const char *const k_default_scan_paths[] = SM_DEFAULT_SCAN_PATHS_INITIALIZER;
typedef struct {
char filename[MAX_PATH];
bool mount_read_only;
bool valid;
} image_mode_rule_t;
typedef struct {
char title_id[MAX_TITLE_ID];
uint32_t delay_seconds;
bool valid;
} kstuff_delay_rule_t;
static runtime_config_t g_runtime_cfg;
static bool g_runtime_cfg_ready = false;
static const char *g_scan_paths[MAX_SCAN_PATHS + 1];
static char g_scan_path_storage[MAX_SCAN_PATHS][MAX_PATH];
static int g_scan_path_count = 0;
static image_mode_rule_t g_image_mode_rules[MAX_IMAGE_MODE_RULES];
static char g_kstuff_no_pause_title_ids[MAX_KSTUFF_TITLE_RULES][MAX_TITLE_ID];
static int g_kstuff_no_pause_title_count = 0;
static kstuff_delay_rule_t g_kstuff_delay_rules[MAX_KSTUFF_TITLE_RULES];
static char *trim_ascii(char *s);
static bool normalize_title_id_value(const char *value,
char out[MAX_TITLE_ID]);
static attach_backend_t default_exfat_backend(void) {
#if EXFAT_ATTACH_USE_MDCTL
return ATTACH_BACKEND_MD;
#else
return ATTACH_BACKEND_LVD;
#endif
}
static attach_backend_t default_ufs_backend(void) {
#if UFS_ATTACH_USE_MDCTL
return ATTACH_BACKEND_MD;
#else
return ATTACH_BACKEND_LVD;
#endif
}
static void clear_runtime_scan_paths(void) {
g_scan_path_count = 0;
memset(g_scan_paths, 0, sizeof(g_scan_paths));
memset(g_scan_path_storage, 0, sizeof(g_scan_path_storage));
}
static bool add_runtime_scan_path(const char *path) {
if (!path)
return false;
while (*path && isspace((unsigned char)*path))
path++;
size_t len = strlen(path);
while (len > 0 && isspace((unsigned char)path[len - 1]))
len--;
if (len == 0 || len >= MAX_PATH)
return false;
char normalized[MAX_PATH];
memcpy(normalized, path, len);
normalized[len] = '\0';
while (len > 1 && normalized[len - 1] == '/') {
normalized[len - 1] = '\0';
len--;
}
for (int i = 0; i < g_scan_path_count; i++) {
if (g_scan_paths[i] && strcmp(g_scan_paths[i], normalized) == 0)
return true;
}
if (g_scan_path_count >= MAX_SCAN_PATHS)
return false;
(void)strlcpy(g_scan_path_storage[g_scan_path_count], normalized,
sizeof(g_scan_path_storage[g_scan_path_count]));
g_scan_paths[g_scan_path_count] = g_scan_path_storage[g_scan_path_count];
g_scan_path_count++;
g_scan_paths[g_scan_path_count] = NULL;
return true;
}
static void init_runtime_scan_paths_defaults(void) {
clear_runtime_scan_paths();
for (int i = 0; k_default_scan_paths[i] != NULL; i++)
(void)add_runtime_scan_path(k_default_scan_paths[i]);
(void)add_runtime_scan_path(IMAGE_MOUNT_BASE);
}
static void clear_kstuff_title_rules(void) {
g_kstuff_no_pause_title_count = 0;
memset(g_kstuff_no_pause_title_ids, 0, sizeof(g_kstuff_no_pause_title_ids));
memset(g_kstuff_delay_rules, 0, sizeof(g_kstuff_delay_rules));
}
static void init_runtime_config_defaults(void) {
g_runtime_cfg.debug_enabled = true;
g_runtime_cfg.quiet_mode = false;
g_runtime_cfg.mount_read_only = (IMAGE_MOUNT_READ_ONLY != 0);
g_runtime_cfg.force_mount = false;
g_runtime_cfg.recursive_scan = false;
g_runtime_cfg.backport_fakelib_enabled = true;
g_runtime_cfg.kstuff_game_auto_toggle = true;
g_runtime_cfg.scan_interval_us = DEFAULT_SCAN_INTERVAL_US;
g_runtime_cfg.stability_wait_seconds = DEFAULT_STABILITY_WAIT_SECONDS;
g_runtime_cfg.kstuff_pause_delay_image_seconds =
DEFAULT_KSTUFF_PAUSE_DELAY_IMAGE_SECONDS;
g_runtime_cfg.kstuff_pause_delay_direct_seconds =
DEFAULT_KSTUFF_PAUSE_DELAY_DIRECT_SECONDS;
g_runtime_cfg.exfat_backend = default_exfat_backend();
g_runtime_cfg.ufs_backend = default_ufs_backend();
g_runtime_cfg.lvd_sector_exfat = LVD_SECTOR_SIZE_EXFAT;
g_runtime_cfg.lvd_sector_ufs = LVD_SECTOR_SIZE_UFS;
g_runtime_cfg.lvd_sector_pfs = LVD_SECTOR_SIZE_PFS;
g_runtime_cfg.md_sector_exfat = MD_SECTOR_SIZE_EXFAT;
g_runtime_cfg.md_sector_ufs = MD_SECTOR_SIZE_UFS;
memset(g_image_mode_rules, 0, sizeof(g_image_mode_rules));
clear_kstuff_title_rules();
init_runtime_scan_paths_defaults();
g_runtime_cfg_ready = true;
}
void ensure_runtime_config_ready(void) {
if (!g_runtime_cfg_ready)
init_runtime_config_defaults();
}
const runtime_config_t *runtime_config(void) {
ensure_runtime_config_ready();
return &g_runtime_cfg;
}
int get_scan_path_count(void) {
ensure_runtime_config_ready();
return g_scan_path_count;
}
const char *get_scan_path(int index) {
ensure_runtime_config_ready();
if (index < 0 || index >= g_scan_path_count)
return NULL;
return g_scan_paths[index];
}
bool get_image_mode_override(const char *filename, bool *mount_read_only_out) {
ensure_runtime_config_ready();
if (!filename || !mount_read_only_out)
return false;
for (int k = 0; k < MAX_IMAGE_MODE_RULES; k++) {
if (!g_image_mode_rules[k].valid)
continue;
if (strcasecmp(g_image_mode_rules[k].filename, filename) != 0)
continue;
*mount_read_only_out = g_image_mode_rules[k].mount_read_only;
return true;
}
return false;
}
bool is_kstuff_pause_disabled_for_title(const char *title_id) {
ensure_runtime_config_ready();
char normalized[MAX_TITLE_ID];
if (!normalize_title_id_value(title_id, normalized))
return false;
for (int i = 0; i < g_kstuff_no_pause_title_count; ++i) {
if (strcmp(g_kstuff_no_pause_title_ids[i], normalized) == 0)
return true;
}
return false;
}
bool get_kstuff_pause_delay_override_for_title(const char *title_id,
uint32_t *delay_seconds_out) {
ensure_runtime_config_ready();
if (!delay_seconds_out)
return false;
char normalized[MAX_TITLE_ID];
if (!normalize_title_id_value(title_id, normalized))
return false;
for (int i = 0; i < MAX_KSTUFF_TITLE_RULES; ++i) {
if (!g_kstuff_delay_rules[i].valid)
continue;
if (strcmp(g_kstuff_delay_rules[i].title_id, normalized) != 0)
continue;
*delay_seconds_out = g_kstuff_delay_rules[i].delay_seconds;
return true;
}
return false;
}
static char *trim_ascii(char *s) {
while (*s == ' ' || *s == '\t' || *s == '\r' || *s == '\n')
s++;
size_t n = strlen(s);
while (n > 0) {
char c = s[n - 1];
if (c != ' ' && c != '\t' && c != '\r' && c != '\n')
break;
s[n - 1] = '\0';
n--;
}
return s;
}
static bool normalize_title_id_value(const char *value,
char out[MAX_TITLE_ID]) {
if (!value || !out)
return false;
char local[MAX_TITLE_ID];
(void)strlcpy(local, value, sizeof(local));
char *trimmed = trim_ascii(local);
size_t len = strlen(trimmed);
if (len == 0 || len >= MAX_TITLE_ID)
return false;
for (size_t i = 0; i < len; ++i) {
unsigned char ch = (unsigned char)trimmed[i];
if (!isalnum(ch))
return false;
out[i] = (char)toupper(ch);
}
out[len] = '\0';
return true;
}
static bool parse_bool_ini(const char *value, bool *out) {
if (!value || !out)
return false;
if (strcmp(value, "1") == 0 || strcasecmp(value, "true") == 0 ||
strcasecmp(value, "yes") == 0 || strcasecmp(value, "on") == 0 ||
strcasecmp(value, "ro") == 0) {
*out = true;
return true;
}
if (strcmp(value, "0") == 0 || strcasecmp(value, "false") == 0 ||
strcasecmp(value, "no") == 0 || strcasecmp(value, "off") == 0 ||
strcasecmp(value, "rw") == 0) {
*out = false;
return true;
}
return false;
}
static bool parse_backend_ini(const char *value, attach_backend_t *out) {
if (!value || !out)
return false;
if (strcasecmp(value, "lvd") == 0) {
*out = ATTACH_BACKEND_LVD;
return true;
}
if (strcasecmp(value, "md") == 0 || strcasecmp(value, "mdctl") == 0) {
*out = ATTACH_BACKEND_MD;
return true;
}
return false;
}
static bool parse_u32_ini(const char *value, uint32_t *out) {
if (!value || !out)
return false;
errno = 0;
char *end = NULL;
unsigned long v = strtoul(value, &end, 0);
if (errno != 0 || end == value || *end != '\0' || v > UINT32_MAX)
return false;
*out = (uint32_t)v;
return true;
}
static bool is_valid_sector_size(uint32_t size) {
if (size < 512u || size > 1024u * 1024u)
return false;
return (size & (size - 1u)) == 0u;
}
static bool set_image_mode_rule(const char *path, bool mount_read_only) {
const char *filename = get_filename_component(path);
if (filename[0] == '\0')
return false;
for (int k = 0; k < MAX_IMAGE_MODE_RULES; k++) {
if (!g_image_mode_rules[k].valid)
continue;
if (strcasecmp(g_image_mode_rules[k].filename, filename) != 0)
continue;
g_image_mode_rules[k].mount_read_only = mount_read_only;
return true;
}
for (int k = 0; k < MAX_IMAGE_MODE_RULES; k++) {
if (g_image_mode_rules[k].valid)
continue;
(void)strlcpy(g_image_mode_rules[k].filename, filename,
sizeof(g_image_mode_rules[k].filename));
g_image_mode_rules[k].mount_read_only = mount_read_only;
g_image_mode_rules[k].valid = true;
return true;
}
return false;
}
static bool add_kstuff_no_pause_title_rule(const char *value) {
char normalized[MAX_TITLE_ID];
if (!normalize_title_id_value(value, normalized))
return false;
for (int i = 0; i < g_kstuff_no_pause_title_count; ++i) {
if (strcmp(g_kstuff_no_pause_title_ids[i], normalized) == 0)
return true;
}
if (g_kstuff_no_pause_title_count >= MAX_KSTUFF_TITLE_RULES)
return false;
(void)strlcpy(g_kstuff_no_pause_title_ids[g_kstuff_no_pause_title_count],
normalized,
sizeof(g_kstuff_no_pause_title_ids[g_kstuff_no_pause_title_count]));
g_kstuff_no_pause_title_count++;
return true;
}
static bool set_kstuff_pause_delay_override_rule(const char *value) {
if (!value)
return false;
char local[128];
(void)strlcpy(local, value, sizeof(local));
char *sep = strchr(local, ':');
if (!sep)
return false;
*sep = '\0';
char *title_id = trim_ascii(local);
char *delay_value = trim_ascii(sep + 1);
char normalized[MAX_TITLE_ID];
uint32_t delay_seconds = 0;
if (!normalize_title_id_value(title_id, normalized))
return false;
if (!parse_u32_ini(delay_value, &delay_seconds) ||
delay_seconds > MAX_KSTUFF_PAUSE_DELAY_SECONDS) {
return false;
}
for (int i = 0; i < MAX_KSTUFF_TITLE_RULES; ++i) {
if (!g_kstuff_delay_rules[i].valid)
continue;
if (strcmp(g_kstuff_delay_rules[i].title_id, normalized) != 0)
continue;
g_kstuff_delay_rules[i].delay_seconds = delay_seconds;
return true;
}
for (int i = 0; i < MAX_KSTUFF_TITLE_RULES; ++i) {
if (g_kstuff_delay_rules[i].valid)
continue;
(void)strlcpy(g_kstuff_delay_rules[i].title_id, normalized,
sizeof(g_kstuff_delay_rules[i].title_id));
g_kstuff_delay_rules[i].delay_seconds = delay_seconds;
g_kstuff_delay_rules[i].valid = true;
return true;
}
return false;
}
bool load_runtime_config(void) {
init_runtime_config_defaults();
FILE *f = fopen(CONFIG_FILE, "r");
if (!f) {
if (errno != ENOENT) {
log_debug(" [CFG] open failed: %s (%s)", CONFIG_FILE, strerror(errno));
} else {
log_debug(" [CFG] not found, using defaults");
}
return false;
}
char line[512];
int line_no = 0;
bool has_custom_scanpaths = false;
while (fgets(line, sizeof(line), f)) {
line_no++;
char *s = trim_ascii(line);
if (s[0] == '\0' || s[0] == '#' || s[0] == ';' || s[0] == '[')
continue;
char *eq = strchr(s, '=');
if (!eq) {
log_debug(" [CFG] invalid line %d (missing '=')", line_no);
continue;
}
*eq = '\0';
char *key = trim_ascii(s);
char *value = trim_ascii(eq + 1);
char *comment = strchr(value, '#');
if (comment) {
*comment = '\0';
value = trim_ascii(value);
}
comment = strchr(value, ';');
if (comment) {
*comment = '\0';
value = trim_ascii(value);
}
if (key[0] == '\0' || value[0] == '\0')
continue;
bool bval = false;
uint32_t u32 = 0;
attach_backend_t backend = ATTACH_BACKEND_NONE;
if (strcasecmp(key, "debug") == 0) {
if (!parse_bool_ini(value, &bval)) {
log_debug(" [CFG] invalid bool at line %d: %s=%s", line_no, key, value);
continue;
}
g_runtime_cfg.debug_enabled = bval;
continue;
}
if (strcasecmp(key, "quiet_mode") == 0) {
if (!parse_bool_ini(value, &bval)) {
log_debug(" [CFG] invalid bool at line %d: %s=%s", line_no, key, value);
continue;
}
g_runtime_cfg.quiet_mode = bval;
continue;
}
if (strcasecmp(key, "mount_read_only") == 0 ||
strcasecmp(key, "read_only") == 0) {
if (!parse_bool_ini(value, &bval)) {
log_debug(" [CFG] invalid bool at line %d: %s=%s", line_no, key, value);
continue;
}
g_runtime_cfg.mount_read_only = bval;
continue;
}
if (strcasecmp(key, "force_mount") == 0) {
if (!parse_bool_ini(value, &bval)) {
log_debug(" [CFG] invalid bool at line %d: %s=%s", line_no, key, value);
continue;
}
g_runtime_cfg.force_mount = bval;
continue;
}
if (strcasecmp(key, "image_ro") == 0 ||
strcasecmp(key, "image_rw") == 0) {
bool rule_read_only = (strcasecmp(key, "image_ro") == 0);
if (!set_image_mode_rule(value, rule_read_only)) {
log_debug(" [CFG] invalid image mode rule at line %d: %s=%s", line_no,
key, value);
}
continue;
}
if (strcasecmp(key, "recursive_scan") == 0) {
if (!parse_bool_ini(value, &bval)) {
log_debug(" [CFG] invalid bool at line %d: %s=%s", line_no, key, value);
continue;
}
g_runtime_cfg.recursive_scan = bval;
continue;
}
if (strcasecmp(key, "backport_fakelib") == 0) {
if (!parse_bool_ini(value, &bval)) {
log_debug(" [CFG] invalid bool at line %d: %s=%s", line_no, key, value);
continue;
}
g_runtime_cfg.backport_fakelib_enabled = bval;
continue;
}
if (strcasecmp(key, "kstuff_game_auto_toggle") == 0) {
if (!parse_bool_ini(value, &bval)) {
log_debug(" [CFG] invalid bool at line %d: %s=%s", line_no, key, value);
continue;
}
g_runtime_cfg.kstuff_game_auto_toggle = bval;
continue;
}
if (strcasecmp(key, "kstuff_no_pause") == 0) {
if (!add_kstuff_no_pause_title_rule(value)) {
log_debug(" [CFG] invalid kstuff no-pause title rule at line %d: "
"%s=%s", line_no, key, value);
}
continue;
}
if (strcasecmp(key, "kstuff_delay") == 0) {
if (!set_kstuff_pause_delay_override_rule(value)) {
log_debug(" [CFG] invalid kstuff pause override at line %d: %s=%s "
"(format: TITLEID:SECONDS, max: %u)",
line_no, key, value,
(unsigned)MAX_KSTUFF_PAUSE_DELAY_SECONDS);
}
continue;
}
if (strcasecmp(key, "scan_interval_seconds") == 0 ||
strcasecmp(key, "scan_interval_sec") == 0) {
if (!parse_u32_ini(value, &u32) || u32 < MIN_SCAN_INTERVAL_SECONDS ||
u32 > MAX_SCAN_INTERVAL_SECONDS) {
log_debug(" [CFG] invalid scan interval at line %d: %s=%s (range: %u..%u)",
line_no, key, value, (unsigned)MIN_SCAN_INTERVAL_SECONDS,
(unsigned)MAX_SCAN_INTERVAL_SECONDS);
continue;
}
g_runtime_cfg.scan_interval_us = u32 * 1000000u;
continue;
}
if (strcasecmp(key, "stability_wait_seconds") == 0 ||
strcasecmp(key, "stability_wait_sec") == 0) {
if (!parse_u32_ini(value, &u32) || u32 > MAX_STABILITY_WAIT_SECONDS) {
log_debug(" [CFG] invalid stability wait at line %d: %s=%s (max: %u)",
line_no, key, value, (unsigned)MAX_STABILITY_WAIT_SECONDS);
continue;
}
g_runtime_cfg.stability_wait_seconds = u32;
continue;
}
if (strcasecmp(key, "kstuff_pause_delay_image_seconds") == 0 ||
strcasecmp(key, "kstuff_pause_delay_image_sec") == 0) {
if (!parse_u32_ini(value, &u32) || u32 > MAX_KSTUFF_PAUSE_DELAY_SECONDS) {
log_debug(" [CFG] invalid image kstuff pause delay at line %d: %s=%s "
"(max: %u)", line_no, key, value,
(unsigned)MAX_KSTUFF_PAUSE_DELAY_SECONDS);
continue;
}
g_runtime_cfg.kstuff_pause_delay_image_seconds = u32;
continue;
}
if (strcasecmp(key, "kstuff_pause_delay_direct_seconds") == 0 ||
strcasecmp(key, "kstuff_pause_delay_direct_sec") == 0) {
if (!parse_u32_ini(value, &u32) || u32 > MAX_KSTUFF_PAUSE_DELAY_SECONDS) {
log_debug(" [CFG] invalid direct kstuff pause delay at line %d: %s=%s "
"(max: %u)", line_no, key, value,
(unsigned)MAX_KSTUFF_PAUSE_DELAY_SECONDS);
continue;
}
g_runtime_cfg.kstuff_pause_delay_direct_seconds = u32;
continue;
}
if (strcasecmp(key, "exfat_backend") == 0) {
if (!parse_backend_ini(value, &backend)) {
log_debug(" [CFG] invalid backend at line %d: %s=%s", line_no, key,
value);
continue;
}
g_runtime_cfg.exfat_backend = backend;
continue;
}
if (strcasecmp(key, "ufs_backend") == 0) {
if (!parse_backend_ini(value, &backend)) {
log_debug(" [CFG] invalid backend at line %d: %s=%s", line_no, key,
value);
continue;
}
g_runtime_cfg.ufs_backend = backend;
continue;
}
if (strcasecmp(key, "scanpath") == 0) {
if (!has_custom_scanpaths) {
clear_runtime_scan_paths();
has_custom_scanpaths = true;
}
if (!add_runtime_scan_path(value)) {
log_debug(" [CFG] invalid scanpath at line %d: %s=%s", line_no, key,
value);
}
continue;
}
bool is_sector_key =
(strcasecmp(key, "lvd_exfat_sector_size") == 0) ||
(strcasecmp(key, "lvd_ufs_sector_size") == 0) ||
(strcasecmp(key, "lvd_pfs_sector_size") == 0) ||
(strcasecmp(key, "md_exfat_sector_size") == 0) ||
(strcasecmp(key, "md_ufs_sector_size") == 0);
if (!is_sector_key) {
log_debug(" [CFG] unknown key at line %d: %s", line_no, key);
continue;
}
if (!parse_u32_ini(value, &u32) || !is_valid_sector_size(u32)) {
log_debug(" [CFG] invalid sector size at line %d: %s=%s", line_no, key,
value);
continue;
}
if (strcasecmp(key, "lvd_exfat_sector_size") == 0) {
g_runtime_cfg.lvd_sector_exfat = u32;
} else if (strcasecmp(key, "lvd_ufs_sector_size") == 0) {
g_runtime_cfg.lvd_sector_ufs = u32;
} else if (strcasecmp(key, "lvd_pfs_sector_size") == 0) {
g_runtime_cfg.lvd_sector_pfs = u32;
} else if (strcasecmp(key, "md_exfat_sector_size") == 0) {
g_runtime_cfg.md_sector_exfat = u32;
} else if (strcasecmp(key, "md_ufs_sector_size") == 0) {
g_runtime_cfg.md_sector_ufs = u32;
}
}
fclose(f);
if (has_custom_scanpaths && g_scan_path_count == 0) {
log_debug(" [CFG] no valid scanpath entries, using defaults");
init_runtime_scan_paths_defaults();
} else {
(void)add_runtime_scan_path(IMAGE_MOUNT_BASE);
}
int image_rule_count = 0;
for (int k = 0; k < MAX_IMAGE_MODE_RULES; k++) {
if (g_image_mode_rules[k].valid)
image_rule_count++;
}
int kstuff_delay_rule_count = 0;
for (int k = 0; k < MAX_KSTUFF_TITLE_RULES; k++) {
if (g_kstuff_delay_rules[k].valid)
kstuff_delay_rule_count++;
}
log_debug(" [CFG] loaded: debug=%d quiet=%d ro=%d force=%d recursive_scan=%d "
"backport_fakelib=%d kstuff_game_auto_toggle=%d "
"kstuff_pause_delay_image_s=%u kstuff_pause_delay_direct_s=%u "
"exfat_backend=%s ufs_backend=%s "
"lvd_sec(exfat=%u ufs=%u pfs=%u) md_sec(exfat=%u ufs=%u) "
"scan_interval_s=%u stability_wait_s=%u scan_paths=%d image_rules=%d "
"kstuff_no_pause=%d kstuff_delay_rules=%d",
g_runtime_cfg.debug_enabled ? 1 : 0,
g_runtime_cfg.quiet_mode ? 1 : 0,
g_runtime_cfg.mount_read_only ? 1 : 0,
g_runtime_cfg.force_mount ? 1 : 0,
g_runtime_cfg.recursive_scan ? 1 : 0,
g_runtime_cfg.backport_fakelib_enabled ? 1 : 0,
g_runtime_cfg.kstuff_game_auto_toggle ? 1 : 0,
g_runtime_cfg.kstuff_pause_delay_image_seconds,
g_runtime_cfg.kstuff_pause_delay_direct_seconds,
attach_backend_name(g_runtime_cfg.exfat_backend),
attach_backend_name(g_runtime_cfg.ufs_backend),
g_runtime_cfg.lvd_sector_exfat, g_runtime_cfg.lvd_sector_ufs,
g_runtime_cfg.lvd_sector_pfs, g_runtime_cfg.md_sector_exfat,
g_runtime_cfg.md_sector_ufs,
g_runtime_cfg.scan_interval_us / 1000000u,
g_runtime_cfg.stability_wait_seconds, g_scan_path_count,
image_rule_count, g_kstuff_no_pause_title_count,
kstuff_delay_rule_count);
return true;
}
+179
View File
@@ -0,0 +1,179 @@
#include "sm_platform.h"
#include "sm_fakelib.h"
#include "sm_config_mount.h"
#include "sm_log.h"
#include "sm_types.h"
typedef struct {
bool active;
pid_t pid;
char mount_path[MAX_PATH];
} fakelib_mount_t;
static fakelib_mount_t g_fakelib_mount;
bool sm_fakelib_game_feature_enabled(void) {
return runtime_config()->backport_fakelib_enabled;
}
static bool mount_fakelib_overlay(const char *title_id,
const char *source_path,
const char *mount_path) {
struct iovec overlay_iov[] = {
IOVEC_ENTRY("fstype"), IOVEC_ENTRY("unionfs"),
IOVEC_ENTRY("from"), IOVEC_ENTRY(source_path),
IOVEC_ENTRY("fspath"), IOVEC_ENTRY(mount_path),
IOVEC_ENTRY("copymode"), IOVEC_ENTRY("transparent"),
IOVEC_ENTRY("notime"), IOVEC_ENTRY(NULL),
IOVEC_ENTRY("fnodup"), IOVEC_ENTRY(NULL)};
if (nmount(overlay_iov, IOVEC_SIZE(overlay_iov), 0) == 0) {
notify_system_info("Game backported: %s", title_id);
log_debug(" [FAKELIB] libraries mounted for %s: %s -> %s", title_id,
source_path, mount_path);
return true;
}
log_debug(" [FAKELIB] mount failed for %s (%s -> %s): %s", title_id,
source_path, mount_path, strerror(errno));
return false;
}
static bool unmount_fakelib_overlay(const char *mount_path) {
if (unmount(mount_path, MNT_FORCE) == 0 || errno == ENOENT ||
errno == EINVAL) {
return true;
}
log_debug(" [FAKELIB] unmount failed for %s: %s", mount_path,
strerror(errno));
return false;
}
static bool resolve_sandbox_fakelib_context(const char *title_id,
char source_path[MAX_PATH],
char mount_path[MAX_PATH]) {
char sandbox_id[MAX_TITLE_ID];
DIR *d = opendir("/mnt/sandbox");
if (!d)
return false;
size_t title_len = strlen(title_id);
int best_index = -1;
bool found = false;
struct dirent *entry;
while ((entry = readdir(d)) != NULL) {
if (entry->d_name[0] == '.')
continue;
if (strncmp(entry->d_name, title_id, title_len) != 0)
continue;
if (entry->d_name[title_len] != '_')
continue;
const char *suffix = entry->d_name + title_len + 1u;
if (!isdigit((unsigned char)suffix[0]))
continue;
int idx = atoi(suffix);
if (idx < best_index)
continue;
best_index = idx;
(void)strlcpy(sandbox_id, entry->d_name, sizeof(sandbox_id));
found = true;
}
closedir(d);
if (!found)
return false;
snprintf(source_path, MAX_PATH, "/mnt/sandbox/%s/app0/fakelib", sandbox_id);
struct stat st;
if (stat(source_path, &st) != 0 || !S_ISDIR(st.st_mode))
return false;
char sandbox_root[MAX_PATH];
snprintf(sandbox_root, sizeof(sandbox_root), "/mnt/sandbox/%s", sandbox_id);
d = opendir(sandbox_root);
if (!d)
return false;
found = false;
while ((entry = readdir(d)) != NULL) {
if (entry->d_name[0] == '.')
continue;
if (strcmp(entry->d_name, "app0") == 0)
continue;
snprintf(mount_path, MAX_PATH, "%s/%s/common/lib", sandbox_root,
entry->d_name);
if (stat(mount_path, &st) != 0 || !S_ISDIR(st.st_mode))
continue;
found = true;
break;
}
closedir(d);
return found;
}
static bool cleanup_fakelib_mount(void) {
if (!g_fakelib_mount.active)
return true;
if (!unmount_fakelib_overlay(g_fakelib_mount.mount_path)) {
return false;
}
memset(&g_fakelib_mount, 0, sizeof(g_fakelib_mount));
return true;
}
void sm_fakelib_game_on_exec(pid_t pid, const char *title_id) {
if (!sm_fakelib_game_feature_enabled())
return;
if (g_fakelib_mount.active && g_fakelib_mount.pid == pid) {
log_debug(" [FAKELIB] already tracking pid=%ld for %s", (long)pid,
title_id);
return;
}
if (g_fakelib_mount.active && g_fakelib_mount.pid != pid) {
log_debug(" [FAKELIB] handoff active mount pid=%ld -> pid=%ld (%s)",
(long)g_fakelib_mount.pid, (long)pid, title_id);
if (!cleanup_fakelib_mount()) {
log_debug(" [FAKELIB] handoff cleanup failed for pid=%ld, skipping %s",
(long)g_fakelib_mount.pid, title_id);
return;
}
}
char source_path[MAX_PATH];
char mount_path[MAX_PATH];
if (!resolve_sandbox_fakelib_context(title_id, source_path, mount_path))
return;
if (!mount_fakelib_overlay(title_id, source_path, mount_path))
return;
g_fakelib_mount.active = true;
g_fakelib_mount.pid = pid;
(void)strlcpy(g_fakelib_mount.mount_path, mount_path,
sizeof(g_fakelib_mount.mount_path));
}
void sm_fakelib_game_on_exit(pid_t pid) {
if (!g_fakelib_mount.active || g_fakelib_mount.pid != pid)
return;
log_debug(" [FAKELIB] game stopped: pid=%ld mount=%s", (long)pid,
g_fakelib_mount.mount_path);
cleanup_fakelib_mount();
}
void sm_fakelib_game_shutdown(void) {
(void)cleanup_fakelib_mount();
}
+966
View File
@@ -0,0 +1,966 @@
#include "sm_platform.h"
#include "sm_runtime.h"
#include "sm_filesystem.h"
#include "sm_config_mount.h"
#include "sm_limits.h"
#include "sm_log.h"
#include "sm_image_cache.h"
#include "sm_image.h"
#include "sm_path_utils.h"
#include "sm_paths.h"
// --- FILESYSTEM ---
bool is_installed(const char *title_id) {
char path[MAX_PATH];
snprintf(path, sizeof(path), "/user/app/%s", title_id);
struct stat st;
return (stat(path, &st) == 0);
}
bool is_data_mounted(const char *title_id) {
char path[MAX_PATH];
snprintf(path, sizeof(path), "/system_ex/app/%s/sce_sys/param.json",
title_id);
return (access(path, F_OK) == 0);
}
static bool read_mount_link_file(const char *lnk_path, char *out,
size_t out_size) {
if (!lnk_path || out_size == 0)
return false;
out[0] = '\0';
FILE *f = fopen(lnk_path, "r");
if (!f)
return false;
if (!fgets(out, (int)out_size, f)) {
fclose(f);
out[0] = '\0';
return false;
}
fclose(f);
size_t len = strcspn(out, "\r\n");
out[len] = '\0';
return out[0] != '\0';
}
static void build_title_link_path(const char *title_id, const char *filename,
char out[MAX_PATH]) {
snprintf(out, MAX_PATH, "/user/app/%s/%s", title_id, filename);
}
typedef struct {
char mount_link[MAX_PATH];
char staged_mount_link[MAX_PATH];
char mount_image_link[MAX_PATH];
} title_link_paths_t;
static void build_title_link_paths(const char *title_id,
title_link_paths_t *paths) {
build_title_link_path(title_id, "mount.lnk", paths->mount_link);
build_title_link_path(title_id, "mount.lnk.cleanup",
paths->staged_mount_link);
build_title_link_path(title_id, "mount_img.lnk", paths->mount_image_link);
}
typedef bool (*title_app_dir_iter_fn)(const char *title_id,
const title_link_paths_t *paths,
void *ctx);
static void for_each_title_app_dir(const char *open_error_context,
bool stop_on_signal,
title_app_dir_iter_fn fn, void *ctx) {
DIR *d = opendir("/user/app");
if (!d) {
if (errno != ENOENT)
log_debug(" [LINK] open /user/app failed%s: %s",
open_error_context ? open_error_context : "", strerror(errno));
return;
}
struct dirent *entry;
while ((entry = readdir(d)) != NULL) {
if (stop_on_signal && should_stop_requested())
break;
if (!resolve_title_app_dir(entry, NULL, 0))
continue;
title_link_paths_t paths;
build_title_link_paths(entry->d_name, &paths);
if (!fn(entry->d_name, &paths, ctx))
break;
}
closedir(d);
}
bool read_mount_link(const char *title_id, char *out, size_t out_size) {
if (out_size == 0)
return false;
out[0] = '\0';
title_link_paths_t paths;
build_title_link_paths(title_id, &paths);
return read_mount_link_file(paths.mount_link, out, out_size);
}
bool resolve_title_app_dir(const struct dirent *entry, char *app_dir,
size_t app_dir_size) {
if (!entry)
return false;
if (entry->d_name[0] == '.')
return false;
if (strlen(entry->d_name) != 9)
return false;
char path_buf[MAX_PATH];
char *path = path_buf;
size_t path_size = sizeof(path_buf);
if (app_dir && app_dir_size > 0) {
path = app_dir;
path_size = app_dir_size;
}
int written = snprintf(path, path_size, "/user/app/%s", entry->d_name);
if (written < 0 || (size_t)written >= path_size) {
if (app_dir && app_dir_size > 0)
app_dir[0] = '\0';
return false;
}
if (entry->d_type != DT_DIR) {
if (entry->d_type != DT_UNKNOWN)
return false;
struct stat st;
if (stat(path, &st) != 0 || !S_ISDIR(st.st_mode))
return false;
}
return true;
}
static bool source_path_needs_cleanup(const char *source_path,
bool *tried_image_recovery) {
if (is_under_image_mount_base(source_path)) {
char image_source_path[MAX_PATH];
bool has_image_source = resolve_image_source_from_mount_cache(
source_path, image_source_path, sizeof(image_source_path));
if (has_image_source && access(image_source_path, F_OK) != 0)
return true;
}
if (access(source_path, F_OK) != 0)
return true;
if (path_matches_root_or_child(source_path, "/system_ex/app"))
return true;
char eboot_path[MAX_PATH];
snprintf(eboot_path, sizeof(eboot_path), "%s/eboot.bin", source_path);
if (access(eboot_path, F_OK) == 0)
return false;
if (!*tried_image_recovery && is_under_image_mount_base(source_path)) {
cleanup_stale_image_mounts();
*tried_image_recovery = true;
}
return access(eboot_path, F_OK) != 0;
}
static bool cleanup_staged_mount_links_entry(const char *title_id,
const title_link_paths_t *paths,
void *ctx) {
(void)title_id;
(void)ctx;
struct stat staged_st;
if (stat(paths->staged_mount_link, &staged_st) == 0) {
struct stat link_st;
if (stat(paths->mount_link, &link_st) == 0) {
if (unlink(paths->staged_mount_link) == 0) {
log_debug(" [LINK] removed stale staged mount link: %s",
paths->staged_mount_link);
} else if (errno != ENOENT) {
log_debug(" [LINK] staged cleanup failed for %s: %s",
paths->staged_mount_link, strerror(errno));
}
} else if (errno != ENOENT) {
log_debug(" [LINK] staged cleanup failed for %s: %s",
paths->staged_mount_link, strerror(errno));
} else if (rename(paths->staged_mount_link, paths->mount_link) == 0) {
log_debug(" [LINK] restored interrupted mount link: %s",
paths->mount_link);
} else {
log_debug(" [LINK] restore failed for %s: %s", paths->mount_link,
strerror(errno));
return true;
}
} else if (errno != ENOENT) {
log_debug(" [LINK] staged cleanup failed for %s: %s",
paths->staged_mount_link, strerror(errno));
return true;
}
char source_path[MAX_PATH];
if (!read_mount_link_file(paths->mount_link, source_path, sizeof(source_path))) {
if (unlink(paths->mount_image_link) != 0 && errno != ENOENT) {
log_debug(" [LINK] remove failed for %s: %s", paths->mount_image_link,
strerror(errno));
}
return true;
}
if (!is_under_image_mount_base(source_path)) {
if (unlink(paths->mount_image_link) != 0 && errno != ENOENT) {
log_debug(" [LINK] remove failed for %s: %s", paths->mount_image_link,
strerror(errno));
}
return true;
}
char image_source_path[MAX_PATH];
if (!read_mount_link_file(paths->mount_image_link, image_source_path,
sizeof(image_source_path))) {
return true;
}
if (!cache_image_source_mapping(image_source_path, source_path)) {
log_debug(" [LINK] image source cache warmup failed: %s -> %s",
source_path, image_source_path);
}
return true;
}
void cleanup_staged_mount_links(void) {
for_each_title_app_dir(" for staged cleanup", false,
cleanup_staged_mount_links_entry, NULL);
}
static bool get_top_mount(const char *path, struct statfs *mount_st_out) {
if (statfs(path, mount_st_out) == 0 &&
strcmp(mount_st_out->f_mntonname, path) == 0) {
return true;
}
struct statfs *mntbuf = NULL;
int mntcount = getmntinfo(&mntbuf, MNT_NOWAIT);
if (mntcount <= 0 || !mntbuf)
return false;
const struct statfs *best_mount = NULL;
for (int i = 0; i < mntcount; i++) {
if (strcmp(mntbuf[i].f_mntonname, path) != 0)
continue;
if (!best_mount ||
(strcmp(mntbuf[i].f_fstypename, "unionfs") == 0 &&
strcmp(best_mount->f_fstypename, "unionfs") != 0)) {
best_mount = &mntbuf[i];
}
}
if (!best_mount)
return false;
*mount_st_out = *best_mount;
return true;
}
typedef enum {
TITLE_STACK_TOP_NONE = 0,
TITLE_STACK_TOP_OTHER,
TITLE_STACK_TOP_NULLFS,
TITLE_STACK_TOP_BACKPORT,
} title_stack_top_kind_t;
typedef struct {
bool mounted;
bool has_our_nullfs;
bool has_nullfs_from_root;
bool has_our_backport;
bool top_is_our_nullfs;
int our_nullfs_count;
int our_backport_count;
title_stack_top_kind_t top_kind;
char system_ex_path[MAX_PATH];
char top_from[MAX_PATH];
} title_mount_state_t;
static bool path_is_managed_backport_for_title(const char *title_id,
const char *path) {
if (strncmp(path, "<above>:", 8) == 0)
path += 8;
char root[MAX_PATH];
int scan_path_count = get_scan_path_count();
for (int i = 0; i < scan_path_count; i++) {
if (!build_backports_root_path(get_scan_path(i), root))
continue;
size_t root_len = strlen(root);
if (strncmp(path, root, root_len) != 0)
continue;
if (path[root_len] != '/')
continue;
if (strcmp(path + root_len + 1u, title_id) == 0)
return true;
}
return false;
}
static bool inspect_title_stack(const char *title_id, const char *source_path,
const char *source_root,
title_mount_state_t *state_out) {
memset(state_out, 0, sizeof(*state_out));
snprintf(state_out->system_ex_path, sizeof(state_out->system_ex_path),
"/system_ex/app/%s", title_id);
struct statfs statfs_mount;
bool statfs_ok = false;
int inspect_errno = 0;
int statfs_res = statfs(state_out->system_ex_path, &statfs_mount);
if (statfs_res == 0 &&
strcmp(statfs_mount.f_mntonname, state_out->system_ex_path) == 0) {
statfs_ok = true;
} else if (statfs_res != 0) {
inspect_errno = errno;
}
struct statfs *mntbuf = NULL;
int mntcount = getmntinfo(&mntbuf, MNT_NOWAIT);
const struct statfs *best_mount = NULL;
bool found_mount_entry = false;
if (mntcount > 0 && mntbuf) {
for (int i = 0; i < mntcount; i++) {
if (strcmp(mntbuf[i].f_mntonname, state_out->system_ex_path) != 0)
continue;
found_mount_entry = true;
if (!best_mount ||
(strcmp(mntbuf[i].f_fstypename, "unionfs") == 0 &&
strcmp(best_mount->f_fstypename, "unionfs") != 0)) {
best_mount = &mntbuf[i];
}
if (strcmp(mntbuf[i].f_fstypename, "nullfs") == 0) {
if (source_path && source_path[0] != '\0' &&
strcmp(mntbuf[i].f_mntfromname, source_path) == 0) {
state_out->has_our_nullfs = true;
state_out->our_nullfs_count++;
}
if (source_root && source_root[0] != '\0' &&
path_matches_root_or_child(mntbuf[i].f_mntfromname, source_root)) {
state_out->has_nullfs_from_root = true;
}
continue;
}
if (strcmp(mntbuf[i].f_fstypename, "unionfs") == 0 &&
path_is_managed_backport_for_title(title_id,
mntbuf[i].f_mntfromname)) {
state_out->has_our_backport = true;
state_out->our_backport_count++;
}
}
}
const struct statfs *top_mount = NULL;
if (statfs_ok) {
top_mount = &statfs_mount;
} else if (best_mount) {
top_mount = best_mount;
if (inspect_errno == EEXIST) {
log_debug(" [LINK] inspect recovered from statfs(EEXIST) for %s",
state_out->system_ex_path);
}
} else if (inspect_errno != 0 && inspect_errno != ENOENT &&
inspect_errno != EINVAL) {
log_debug(" [LINK] inspect statfs failed for %s: %s",
state_out->system_ex_path, strerror(inspect_errno));
if (mntcount <= 0 || !mntbuf) {
log_debug(" [LINK] mount table unavailable for %s",
state_out->system_ex_path);
} else if (!found_mount_entry) {
log_debug(" [LINK] no mount entries for %s", state_out->system_ex_path);
}
for (int i = 0; i < mntcount; i++) {
if (strcmp(mntbuf[i].f_mntonname, state_out->system_ex_path) != 0)
continue;
log_debug(" [LINK] mount entry for %s: type=%s from=%s flags=0x%lX",
state_out->system_ex_path, mntbuf[i].f_fstypename,
mntbuf[i].f_mntfromname, (unsigned long)mntbuf[i].f_flags);
}
return false;
}
if (!top_mount)
return true;
const char *top_from = top_mount->f_mntfromname;
if (strcmp(top_mount->f_fstypename, "unionfs") == 0 &&
strncmp(top_from, "<above>:", 8) == 0) {
top_from += 8;
}
state_out->mounted = true;
(void)strlcpy(state_out->top_from, top_from, sizeof(state_out->top_from));
state_out->top_kind = TITLE_STACK_TOP_OTHER;
if (strcmp(top_mount->f_fstypename, "nullfs") == 0) {
state_out->top_kind = TITLE_STACK_TOP_NULLFS;
if (source_path && source_path[0] != '\0' &&
strcmp(top_from, source_path) == 0) {
state_out->top_is_our_nullfs = true;
state_out->has_our_nullfs = true;
}
} else if (strcmp(top_mount->f_fstypename, "unionfs") == 0 &&
path_is_managed_backport_for_title(title_id, top_from)) {
state_out->top_kind = TITLE_STACK_TOP_BACKPORT;
state_out->has_our_backport = true;
}
return true;
}
static bool unmount_top_controlled_layer(const char *path) {
struct statfs mount_st;
if (!get_top_mount(path, &mount_st))
return true;
bool controlled_top =
(strcmp(mount_st.f_fstypename, "unionfs") == 0) ||
(strcmp(mount_st.f_fstypename, "nullfs") == 0);
if (!controlled_top) {
log_debug(" [LINK] mount point busy for %s: type=%s from=%s flags=0x%lX",
path, mount_st.f_fstypename, mount_st.f_mntfromname,
(unsigned long)mount_st.f_flags);
return false;
}
if (unmount(path, 0) == 0 || errno == ENOENT || errno == EINVAL)
return true;
if (unmount(path, MNT_FORCE) == 0 || errno == ENOENT || errno == EINVAL)
return true;
log_debug(" [LINK] unmount failed for %s: %s", path, strerror(errno));
return false;
}
bool reconcile_title_backport_mount(const char *title_id, const char *src_path,
const char *expected_backport_path,
bool *overlay_active_out) {
if (overlay_active_out)
*overlay_active_out = false;
if (!title_id || title_id[0] == '\0' || !src_path || src_path[0] == '\0' ||
!expected_backport_path || expected_backport_path[0] == '\0') {
return false;
}
struct stat backport_st;
bool backport_present = stat(expected_backport_path, &backport_st) == 0 &&
S_ISDIR(backport_st.st_mode);
title_mount_state_t state;
if (!inspect_title_stack(title_id, src_path, NULL, &state)) {
return false;
}
if (!state.has_our_nullfs)
return false;
if (state.top_is_our_nullfs)
return backport_present;
if (!state.has_our_backport || state.top_kind != TITLE_STACK_TOP_BACKPORT)
return false;
if (strcmp(state.top_from, expected_backport_path) == 0 && backport_present) {
if (overlay_active_out)
*overlay_active_out = true;
return true;
}
if (!unmount_top_controlled_layer(state.system_ex_path))
return false;
log_debug(" [IMG] backport overlay removed: %s -> %s", state.top_from,
state.system_ex_path);
return backport_present;
}
void mount_backport_overlay(const char *mount_point,
const char *backport_path,
const char *title_id) {
struct stat backport_st;
if (stat(backport_path, &backport_st) != 0 || !S_ISDIR(backport_st.st_mode))
return;
struct statfs mounted_sfs;
if (statfs(mount_point, &mounted_sfs) != 0 ||
strcmp(mounted_sfs.f_mntonname, mount_point) != 0) {
return;
}
if (strcmp(mounted_sfs.f_fstypename, "unionfs") == 0)
return;
bool mount_read_only = ((mounted_sfs.f_flags & MNT_RDONLY) != 0);
struct iovec overlay_iov[] = {
IOVEC_ENTRY("fstype"), IOVEC_ENTRY("unionfs"),
IOVEC_ENTRY("from"), IOVEC_ENTRY(backport_path),
IOVEC_ENTRY("fspath"), IOVEC_ENTRY(mount_point),
IOVEC_ENTRY("copymode"), IOVEC_ENTRY("transparent"),
IOVEC_ENTRY("notime"), IOVEC_ENTRY(NULL),
IOVEC_ENTRY("fnodup"), IOVEC_ENTRY(NULL)};
int overlay_flags = mount_read_only ? MNT_RDONLY : 0;
if (nmount(overlay_iov, IOVEC_SIZE(overlay_iov), overlay_flags) == 0) {
log_debug(" [IMG] backport overlay mounted (%s): %s -> %s",
mount_read_only ? "ro" : "rw", backport_path, mount_point);
return;
}
int overlay_err = errno;
log_debug(" [IMG] backport overlay failed: %s -> %s (%s)", backport_path,
mount_point, strerror(overlay_err));
notify_system("Backport overlay failed: %s\n%s\n0x%08X", title_id,
backport_path, (uint32_t)overlay_err);
}
static bool unmount_controlled_mount_stack(const char *path) {
for (int i = 0; i < MAX_LAYERED_UNMOUNT_ATTEMPTS * 4; i++) {
struct statfs mount_st;
if (!get_top_mount(path, &mount_st))
return true;
if (!unmount_top_controlled_layer(path))
return false;
}
struct statfs mount_st;
return !get_top_mount(path, &mount_st);
}
static bool cleanup_duplicate_title_mounts_entry(const char *title_id,
const title_link_paths_t *paths,
void *ctx) {
(void)ctx;
char source_path[MAX_PATH];
if (!read_mount_link_file(paths->mount_link, source_path, sizeof(source_path)))
return true;
title_mount_state_t state;
if (!inspect_title_stack(title_id, source_path, NULL, &state))
return true;
if ((state.our_nullfs_count <= 1 && state.our_backport_count <= 1) ||
!state.has_our_nullfs) {
return true;
}
log_debug(
" [LINK] duplicate managed mount layers for %s: nullfs=%d backport=%d. "
"Resetting stack.",
state.system_ex_path, state.our_nullfs_count, state.our_backport_count);
if (!unmount_controlled_mount_stack(state.system_ex_path)) {
log_debug(" [LINK] failed to reset duplicate mount stack for %s",
state.system_ex_path);
}
return true;
}
void cleanup_duplicate_title_mounts(void) {
for_each_title_app_dir(" for duplicate cleanup", false,
cleanup_duplicate_title_mounts_entry, NULL);
}
// --- Copy Helpers for Install Action ---
static int copy_param_json_rewrite(const char *src, const char *dst) {
FILE *fs = fopen(src, "rb");
if (!fs)
return -1;
if (fseek(fs, 0, SEEK_END) != 0) {
fclose(fs);
return -1;
}
long file_size = ftell(fs);
if (file_size < 0) {
fclose(fs);
return -1;
}
rewind(fs);
size_t len = (size_t)file_size;
char *buf = (char *)malloc(len + 1);
if (!buf) {
fclose(fs);
return -1;
}
if (len > 0 && fread(buf, 1, len, fs) != len) {
free(buf);
fclose(fs);
return -1;
}
fclose(fs);
buf[len] = '\0';
char *hit = strstr(buf, "upgradable");
if (hit) {
size_t offset = (size_t)(hit - buf);
size_t tail = len - offset - 10u;
memcpy(hit, "standard", 8u);
memmove(hit + 8u, hit + 10u, tail + 1u);
len -= 2u;
}
FILE *fd = fopen(dst, "wb");
if (!fd) {
free(buf);
return -1;
}
int ret = 0;
if (len > 0 && fwrite(buf, 1, len, fd) != len)
ret = -1;
if (fclose(fd) != 0)
ret = -1;
if (ret == 0 && hit)
log_debug(" [COPY] param.json patched: %s", dst);
free(buf);
return ret;
}
int copy_file(const char *src, const char *dst) {
if (strstr(src, "/sce_sys/param.json")) {
return copy_param_json_rewrite(src, dst);
}
char buf[8192];
FILE *fs = fopen(src, "rb");
if (!fs)
return -1;
FILE *fd = fopen(dst, "wb");
if (!fd) {
fclose(fs);
return -1;
}
int ret = 0;
while (true) {
size_t n = fread(buf, 1, sizeof(buf), fs);
if (n > 0 && fwrite(buf, 1, n, fd) != n) {
ret = -1;
break;
}
if (n < sizeof(buf)) {
if (ferror(fs))
ret = -1;
break;
}
}
if (fflush(fd) != 0)
ret = -1;
if (fclose(fd) != 0)
ret = -1;
if (fclose(fs) != 0)
ret = -1;
return ret;
}
int copy_dir(const char *src, const char *dst) {
if (mkdir(dst, 0777) != 0 && errno != EEXIST)
return -1;
DIR *d = opendir(src);
if (!d)
return -1;
int ret = 0;
struct dirent *e;
char ss[MAX_PATH], dd[MAX_PATH];
struct stat st;
struct stat lst;
while ((e = readdir(d))) {
if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, ".."))
continue;
snprintf(ss, sizeof(ss), "%s/%s", src, e->d_name);
snprintf(dd, sizeof(dd), "%s/%s", dst, e->d_name);
if (lstat(ss, &lst) != 0) {
ret = -1;
break;
}
if (S_ISLNK(lst.st_mode)) {
if (stat(ss, &st) != 0) {
ret = -1;
break;
}
if (S_ISDIR(st.st_mode)) {
log_debug(" [COPY] refusing symlink directory: %s", ss);
ret = -1;
break;
}
} else {
st = lst;
}
if (S_ISDIR(st.st_mode)) {
if (copy_dir(ss, dd) != 0) {
ret = -1;
break;
}
} else {
if (copy_file(ss, dd) != 0) {
ret = -1;
break;
}
}
}
if (closedir(d) != 0)
ret = -1;
return ret;
}
int remount_system_ex(void) {
struct iovec iov[] = {
IOVEC_ENTRY("from"), IOVEC_ENTRY("/dev/ssd0.system_ex"),
IOVEC_ENTRY("fspath"), IOVEC_ENTRY("/system_ex"),
IOVEC_ENTRY("fstype"), IOVEC_ENTRY("exfatfs"),
IOVEC_ENTRY("large"), IOVEC_ENTRY("yes"),
IOVEC_ENTRY("timezone"), IOVEC_ENTRY("static"),
IOVEC_ENTRY("async"), IOVEC_ENTRY(NULL),
IOVEC_ENTRY("ignoreacl"), IOVEC_ENTRY(NULL)};
return nmount(iov, IOVEC_SIZE(iov), MNT_UPDATE);
}
bool mount_title_nullfs(const char *title_id, const char *src_path) {
char dst[MAX_PATH];
char src_eboot[MAX_PATH];
char dst_eboot[MAX_PATH];
snprintf(dst, sizeof(dst), "/system_ex/app/%s", title_id);
snprintf(src_eboot, sizeof(src_eboot), "%s/eboot.bin", src_path);
snprintf(dst_eboot, sizeof(dst_eboot), "%s/eboot.bin", dst);
struct stat src_st;
if (stat(src_path, &src_st) != 0) {
log_debug(" [LINK] source path check failed for %s -> %s: %s", title_id,
src_path, strerror(errno));
return false;
}
if (!S_ISDIR(src_st.st_mode)) {
log_debug(" [LINK] source path is not a directory for %s: %s mode=0%o",
title_id, src_path, (unsigned)(src_st.st_mode & 077777));
return false;
}
if (access(src_eboot, F_OK) != 0) {
bool tried_image_recovery = false;
if (source_path_needs_cleanup(src_path, &tried_image_recovery)) {
log_debug(" [LINK] source eboot.bin missing for %s: %s", title_id,
src_eboot);
return false;
}
}
title_mount_state_t state;
if (!inspect_title_stack(title_id, src_path, NULL, &state)) {
log_debug(" [LINK] inspect failed for %s (%s): %s", title_id, dst,
strerror(errno));
return false;
}
if (state.mounted) {
if (state.has_our_nullfs &&
(state.top_is_our_nullfs || state.has_our_backport) &&
access(dst_eboot, F_OK) == 0) {
log_debug(" [LINK] mount stack already active: %s -> %s", src_path, dst);
return true;
}
if (!unmount_controlled_mount_stack(dst)) {
log_debug(" [LINK] failed to reset mount stack for %s (%s)", title_id,
dst);
return false;
}
}
int mkdir_res = mkdir(dst, 0755);
if (mkdir_res != 0) {
if (errno != EEXIST) {
log_debug(" [LINK] Failed to create mount directory for title %s: %s",
title_id, strerror(errno));
return false;
}
struct stat dst_st;
if (stat(dst, &dst_st) != 0) {
log_debug(" [LINK] mount directory exists but stat failed for %s: %s",
dst, strerror(errno));
return false;
}
if (!S_ISDIR(dst_st.st_mode)) {
log_debug(" [LINK] mount target is not a directory: %s mode=0%o", dst,
(unsigned)(dst_st.st_mode & 077777));
return false;
}
}
struct iovec iov[] = {
IOVEC_ENTRY("fstype"), IOVEC_ENTRY("nullfs"),
IOVEC_ENTRY("from"), IOVEC_ENTRY(src_path),
IOVEC_ENTRY("fspath"), IOVEC_ENTRY(dst),
};
if (nmount(iov, IOVEC_SIZE(iov), 0) != 0) {
log_debug(" [LINK] Failed to auto-mount nullfs title=%s src=%s dst=%s: %s",
title_id, src_path, dst, strerror(errno));
return false;
}
if (access(dst_eboot, F_OK) != 0) {
log_debug(" [LINK] mounted nullfs but eboot.bin is missing at target: %s",
dst_eboot);
if (unmount(dst, 0) != 0 && errno != ENOENT && errno != EINVAL) {
if (unmount(dst, MNT_FORCE) != 0 && errno != ENOENT && errno != EINVAL) {
log_debug(" [LINK] failed to rollback empty nullfs mount %s: %s", dst,
strerror(errno));
}
}
return false;
}
log_debug(" [LINK] nullfs mounted: %s -> %s", src_path, dst);
return true;
}
bool path_matches_root_or_child(const char *path, const char *root) {
if (!path || !root || root[0] == '\0')
return false;
size_t root_len = strlen(root);
if (strncmp(path, root, root_len) != 0)
return false;
return (path[root_len] == '\0' || path[root_len] == '/');
}
typedef struct {
const char *removed_source_root;
bool unmount_system_ex_bind;
bool tried_image_recovery;
} cleanup_mount_links_ctx_t;
static bool cleanup_mount_links_entry(const char *title_id,
const title_link_paths_t *paths,
void *ctx_ptr) {
cleanup_mount_links_ctx_t *ctx = (cleanup_mount_links_ctx_t *)ctx_ptr;
struct stat lst;
if (stat(paths->mount_link, &lst) != 0 || !S_ISREG(lst.st_mode))
return true;
char source_path[MAX_PATH];
bool should_remove = false;
bool matches_removed_source = false;
if (!read_mount_link_file(paths->mount_link, source_path, sizeof(source_path))) {
should_remove = true;
}
if (!should_remove) {
if (ctx->removed_source_root && ctx->removed_source_root[0] != '\0') {
matches_removed_source =
path_matches_root_or_child(source_path, ctx->removed_source_root);
should_remove = matches_removed_source;
} else {
should_remove =
source_path_needs_cleanup(source_path, &ctx->tried_image_recovery);
}
}
if (!should_remove)
return true;
bool keep_mount_link = false;
bool mount_link_staged = false;
if (ctx->unmount_system_ex_bind) {
title_mount_state_t state;
bool inspected = inspect_title_stack(title_id, source_path,
ctx->removed_source_root, &state);
bool top_is_ours = state.top_is_our_nullfs || state.has_our_backport;
bool stack_matches_link = false;
if (source_path[0] != '\0') {
stack_matches_link = state.has_our_nullfs;
} else if (matches_removed_source && ctx->removed_source_root &&
ctx->removed_source_root[0] != '\0') {
stack_matches_link = state.has_nullfs_from_root;
}
if (inspected && stack_matches_link && !top_is_ours) {
keep_mount_link = true;
log_debug(" [LINK] keeping mount link for %s: stack still active but "
"top layer is not ours", state.system_ex_path);
} else if (inspected && top_is_ours && stack_matches_link) {
if (rename(paths->mount_link, paths->staged_mount_link) != 0) {
log_debug(" [LINK] stage failed for %s: %s", paths->mount_link,
strerror(errno));
return true;
}
mount_link_staged = true;
if (!unmount_controlled_mount_stack(state.system_ex_path)) {
keep_mount_link = true;
log_debug(" [LINK] failed to unmount mount stack for %s",
state.system_ex_path);
}
}
}
if (!keep_mount_link) {
int unlink_res = mount_link_staged ? unlink(paths->staged_mount_link)
: unlink(paths->mount_link);
if (unlink_res == 0 || errno == ENOENT) {
log_debug(" [LINK] removed stale mount link: %s", paths->mount_link);
if (unlink(paths->mount_image_link) != 0 && errno != ENOENT) {
log_debug(" [LINK] remove failed for %s: %s", paths->mount_image_link,
strerror(errno));
}
} else {
log_debug(" [LINK] remove failed for %s: %s",
mount_link_staged ? paths->staged_mount_link : paths->mount_link,
strerror(errno));
}
return true;
}
if (mount_link_staged && rename(paths->staged_mount_link, paths->mount_link) == 0) {
log_debug(" [LINK] restored mount link after failed cleanup: %s",
paths->mount_link);
} else if (mount_link_staged) {
log_debug(" [LINK] restore failed for %s: %s", paths->mount_link,
strerror(errno));
}
return true;
}
void cleanup_mount_links(const char *removed_source_root,
bool unmount_system_ex_bind) {
cleanup_mount_links_ctx_t ctx = {
.removed_source_root = removed_source_root,
.unmount_system_ex_bind = unmount_system_ex_bind,
.tried_image_recovery = false,
};
for_each_title_app_dir("", !unmount_system_ex_bind, cleanup_mount_links_entry,
&ctx);
}
static bool shutdown_title_mounts_entry(const char *title_id,
const title_link_paths_t *paths,
void *ctx) {
(void)ctx;
char source_path[MAX_PATH];
if (!read_mount_link_file(paths->mount_link, source_path, sizeof(source_path))) {
return true;
}
title_mount_state_t state;
if (!inspect_title_stack(title_id, source_path, NULL, &state) ||
!state.has_our_nullfs ||
(!state.top_is_our_nullfs && !state.has_our_backport)) {
return true;
}
if (!unmount_controlled_mount_stack(state.system_ex_path)) {
log_debug(" [LINK] failed to unmount shutdown stack for %s",
state.system_ex_path);
}
return true;
}
void shutdown_title_mounts(void) {
for_each_title_app_dir(" during shutdown", false,
shutdown_title_mounts_entry, NULL);
}
+94
View File
@@ -0,0 +1,94 @@
#include "sm_platform.h"
#include "sm_game_cache.h"
#include "sm_limits.h"
#include "sm_log.h"
#include "sm_title_state.h"
struct GameCache {
char path[MAX_PATH];
char title_id[MAX_TITLE_ID];
char title_name[MAX_TITLE_NAME];
bool valid;
};
static struct GameCache g_game_cache[MAX_PENDING];
static void clear_game_cache_slot(int index, const char *reason) {
if (index < 0 || index >= MAX_PENDING || !g_game_cache[index].valid)
return;
if (reason && reason[0] != '\0') {
if (g_game_cache[index].title_id[0] != '\0')
log_debug(" [CACHE] %s: %s (%s)", reason, g_game_cache[index].title_id,
g_game_cache[index].path);
else
log_debug(" [CACHE] %s: %s", reason, g_game_cache[index].path);
}
if (g_game_cache[index].title_id[0] != '\0')
clear_duplicate_title_notification(g_game_cache[index].title_id);
memset(&g_game_cache[index], 0, sizeof(g_game_cache[index]));
}
void cache_game_entry(const char *path, const char *title_id,
const char *title_name) {
for (int k = 0; k < MAX_PENDING; k++) {
if (!g_game_cache[k].valid) {
(void)strlcpy(g_game_cache[k].path, path, sizeof(g_game_cache[k].path));
(void)strlcpy(g_game_cache[k].title_id, title_id,
sizeof(g_game_cache[k].title_id));
(void)strlcpy(g_game_cache[k].title_name, title_name,
sizeof(g_game_cache[k].title_name));
g_game_cache[k].valid = true;
return;
}
}
}
void prune_game_cache(void) {
for (int k = 0; k < MAX_PENDING; k++) {
if (!g_game_cache[k].valid)
continue;
if (access(g_game_cache[k].path, F_OK) == 0)
continue;
clear_game_cache_slot(k, "source removed");
}
}
bool find_cached_game(const char *path, const char *title_id,
const char **existing_path_out) {
if (existing_path_out)
*existing_path_out = NULL;
for (int k = 0; k < MAX_PENDING; k++) {
if (!g_game_cache[k].valid)
continue;
if (path && strcmp(g_game_cache[k].path, path) == 0) {
if (existing_path_out)
*existing_path_out = g_game_cache[k].path;
return true;
}
if (title_id && title_id[0] != '\0' &&
strcmp(g_game_cache[k].title_id, title_id) == 0) {
if (existing_path_out)
*existing_path_out = g_game_cache[k].path;
return true;
}
}
return false;
}
void clear_cached_game(const char *path) {
if (!path || path[0] == '\0')
return;
for (int k = 0; k < MAX_PENDING; k++) {
if (!g_game_cache[k].valid)
continue;
if (strcmp(g_game_cache[k].path, path) != 0)
continue;
clear_game_cache_slot(k, "removed from duplicate tracking");
}
}
+523
View File
@@ -0,0 +1,523 @@
#include "sm_platform.h"
#include <fcntl.h>
#include <pthread.h>
#include <sys/event.h>
#include "sm_fakelib.h"
#include "sm_game_lifecycle.h"
#include "sm_kstuff.h"
#include "sm_limits.h"
#include "sm_log.h"
#include "sm_runtime.h"
#include "sm_time.h"
#define MAX_PENDING_GAME_EXEC_CANDIDATES 32
typedef struct {
bool active;
pid_t pid;
uint64_t exec_time_us;
uint64_t deadline_us;
} pending_game_launch_t;
// SysCore can emit multiple child exec events before the actual game title_id
// becomes visible, so keep a small raw exec candidate buffer here.
static pending_game_launch_t
g_pending_game_launches[MAX_PENDING_GAME_EXEC_CANDIDATES];
static pthread_t g_game_lifecycle_thread;
static bool g_game_lifecycle_thread_started = false;
static volatile sig_atomic_t g_game_lifecycle_stop_requested = 0;
static int g_game_lifecycle_wake_pipe[2] = {-1, -1};
static pthread_mutex_t g_game_lifecycle_start_mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t g_game_lifecycle_start_cond = PTHREAD_COND_INITIALIZER;
static bool g_game_lifecycle_start_ready = false;
static bool g_game_lifecycle_start_success = false;
static bool register_game_exit_watch(int kq, pid_t pid);
static bool is_supported_title_id(const char *title_id) {
return strncmp(title_id, "PPSA", 4) == 0 || strncmp(title_id, "CUSA", 4) == 0;
}
static uint64_t min_nonzero_u64(uint64_t a, uint64_t b) {
if (a == 0)
return b;
if (b == 0)
return a;
return a < b ? a : b;
}
/*
static void bytes_to_hex(const void *src, size_t src_len, char *dst,
size_t dst_size) {
static const char hex_chars[] = "0123456789abcdef";
const uint8_t *bytes = (const uint8_t *)src;
size_t out_pos = 0;
if (!dst || dst_size == 0)
return;
for (size_t i = 0; i < src_len && out_pos + 2 < dst_size; ++i) {
dst[out_pos++] = hex_chars[(bytes[i] >> 4) & 0x0f];
dst[out_pos++] = hex_chars[bytes[i] & 0x0f];
}
dst[out_pos] = '\0';
}
static void log_app_info_response(pid_t pid, int rc, const app_info_t *appinfo) {
if (rc != 0) {
log_debug(" [GAME] sceKernelGetAppInfo pid=%ld failed: rc=0x%08X",
(long)pid, (unsigned)rc);
return;
}
if (!appinfo)
return;
char title_id[MAX_TITLE_ID];
size_t title_len = strnlen(appinfo->title_id, sizeof(appinfo->title_id));
if (title_len >= sizeof(title_id))
title_len = sizeof(title_id) - 1u;
memcpy(title_id, appinfo->title_id, title_len);
title_id[title_len] = '\0';
char unknown2_hex[sizeof(appinfo->unknown2) * 2u + 1u];
bytes_to_hex(appinfo->unknown2, sizeof(appinfo->unknown2), unknown2_hex,
sizeof(unknown2_hex));
log_debug(" [GAME] sceKernelGetAppInfo pid=%ld app_id=0x%08X "
"unknown1=0x%016llX title_id=\"%s\"",
(long)pid, appinfo->app_id,
(unsigned long long)appinfo->unknown1, title_id);
log_debug(" [GAME] sceKernelGetAppInfo pid=%ld unknown2=%s", (long)pid,
unknown2_hex);
}
*/
static bool resolve_game_title_id(pid_t pid, char title_id[MAX_TITLE_ID],
uint32_t *app_id_out) {
app_info_t appinfo;
memset(&appinfo, 0, sizeof(appinfo));
int rc = sceKernelGetAppInfo(pid, &appinfo);
/* log_app_info_response(pid, rc, &appinfo); */
if (rc != 0)
return false;
size_t title_len = strnlen(appinfo.title_id, sizeof(appinfo.title_id));
if (title_len == 0 || title_len >= MAX_TITLE_ID)
return false;
memcpy(title_id, appinfo.title_id, title_len);
title_id[title_len] = '\0';
if (app_id_out)
*app_id_out = appinfo.app_id;
return true;
}
static bool is_process_alive(pid_t pid) {
if (kill(pid, 0) == 0)
return true;
return errno != ESRCH;
}
static bool dispatch_game_launch(int kq, pid_t pid, uint64_t exec_time_us,
const char *title_id, uint32_t app_id) {
if (!register_game_exit_watch(kq, pid)) {
log_debug(" [GAME] skipping launch tracking for %s pid=%ld without exit watch",
title_id, (long)pid);
return false;
}
log_debug(" [GAME] started: %s pid=%ld app_id=0x%08X", title_id,
(long)pid, app_id);
sm_kstuff_game_on_exec(pid, title_id, app_id, exec_time_us);
sm_fakelib_game_on_exec(pid, title_id);
return true;
}
static void clear_pending_game_launch(pending_game_launch_t *entry) {
if (!entry)
return;
memset(entry, 0, sizeof(*entry));
}
static void clear_all_pending_game_launches(void) {
memset(g_pending_game_launches, 0, sizeof(g_pending_game_launches));
}
static void close_game_lifecycle_wake_pipe(void) {
if (g_game_lifecycle_wake_pipe[0] >= 0) {
close(g_game_lifecycle_wake_pipe[0]);
g_game_lifecycle_wake_pipe[0] = -1;
}
if (g_game_lifecycle_wake_pipe[1] >= 0) {
close(g_game_lifecycle_wake_pipe[1]);
g_game_lifecycle_wake_pipe[1] = -1;
}
}
static bool set_fd_nonblocking(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
if (flags < 0)
return false;
return fcntl(fd, F_SETFL, flags | O_NONBLOCK) == 0;
}
static void set_game_lifecycle_start_result(bool success) {
pthread_mutex_lock(&g_game_lifecycle_start_mutex);
g_game_lifecycle_start_success = success;
g_game_lifecycle_start_ready = true;
pthread_cond_broadcast(&g_game_lifecycle_start_cond);
pthread_mutex_unlock(&g_game_lifecycle_start_mutex);
}
static pending_game_launch_t *find_pending_game_launch(pid_t pid) {
for (size_t i = 0; i < MAX_PENDING_GAME_EXEC_CANDIDATES; ++i) {
pending_game_launch_t *entry = &g_pending_game_launches[i];
if (entry->active && entry->pid == pid)
return entry;
}
return NULL;
}
static pending_game_launch_t *reserve_pending_game_launch(uint64_t now_us) {
pending_game_launch_t *oldest_entry = NULL;
for (size_t i = 0; i < MAX_PENDING_GAME_EXEC_CANDIDATES; ++i) {
pending_game_launch_t *entry = &g_pending_game_launches[i];
if (!entry->active)
return entry;
if (now_us != 0 && entry->deadline_us != 0 && now_us >= entry->deadline_us)
return entry;
if (!oldest_entry ||
(entry->deadline_us != 0 &&
(oldest_entry->deadline_us == 0 ||
entry->deadline_us < oldest_entry->deadline_us))) {
oldest_entry = entry;
}
}
return oldest_entry;
}
static pending_game_launch_t *queue_pending_game_launch(pid_t pid,
uint64_t exec_time_us,
uint64_t now_us) {
pending_game_launch_t *entry = find_pending_game_launch(pid);
if (!entry)
entry = reserve_pending_game_launch(now_us);
if (!entry)
return NULL;
if (entry->active && entry->pid != pid) {
log_debug(" [GAME] replacing pending exec pid=%ld with pid=%ld",
(long)entry->pid, (long)pid);
}
uint64_t base_time_us = exec_time_us != 0 ? exec_time_us : now_us;
entry->active = true;
entry->pid = pid;
entry->exec_time_us = base_time_us;
entry->deadline_us =
base_time_us == 0 ? 0 : base_time_us + GAME_APPINFO_LOOKUP_TIMEOUT_US;
return entry;
}
static void defer_confirmed_game_launch_retry(pid_t pid, uint64_t exec_time_us,
uint64_t now_us,
const char *title_id) {
clear_all_pending_game_launches();
if (!queue_pending_game_launch(pid, exec_time_us, now_us)) {
log_debug(" [GAME] failed to defer launch retry for %s pid=%ld", title_id,
(long)pid);
return;
}
log_debug(" [GAME] deferring launch retry for %s pid=%ld", title_id,
(long)pid);
}
static uint64_t next_pending_game_wake_us(uint64_t now_us) {
uint64_t next_wake_us = 0;
for (size_t i = 0; i < MAX_PENDING_GAME_EXEC_CANDIDATES; ++i) {
pending_game_launch_t *entry = &g_pending_game_launches[i];
if (!entry->active)
continue;
if (now_us == 0) {
next_wake_us = min_nonzero_u64(next_wake_us, 1);
continue;
}
uint64_t poll_wake_us = now_us + GAME_LIFECYCLE_POLL_INTERVAL_US;
uint64_t entry_wake_us = min_nonzero_u64(entry->deadline_us, poll_wake_us);
next_wake_us = min_nonzero_u64(next_wake_us, entry_wake_us);
}
return next_wake_us;
}
static const struct timespec *compute_game_wait_timeout(
struct timespec *timeout_out) {
uint64_t now_us = monotonic_time_us();
uint64_t next_wake_us = 0;
next_wake_us = min_nonzero_u64(next_wake_us, next_pending_game_wake_us(now_us));
next_wake_us = min_nonzero_u64(next_wake_us, sm_kstuff_game_next_wake_us(now_us));
if (next_wake_us == 0)
return NULL;
uint64_t wait_us = GAME_LIFECYCLE_POLL_INTERVAL_US;
if (now_us != 0 && next_wake_us > now_us)
wait_us = next_wake_us - now_us;
else if (now_us != 0)
wait_us = 0;
timeout_out->tv_sec = (time_t)(wait_us / 1000000ull);
timeout_out->tv_nsec = (long)((wait_us % 1000000ull) * 1000ull);
return timeout_out;
}
static void poll_game_modules(int kq) {
uint64_t now_us = monotonic_time_us();
for (size_t i = 0; i < MAX_PENDING_GAME_EXEC_CANDIDATES; ++i) {
pending_game_launch_t *entry = &g_pending_game_launches[i];
if (!entry->active)
continue;
char title_id[MAX_TITLE_ID];
uint32_t app_id = 0;
if (resolve_game_title_id(entry->pid, title_id, &app_id)) {
pid_t pid = entry->pid;
uint64_t exec_time_us = entry->exec_time_us;
if (is_supported_title_id(title_id)) {
if (dispatch_game_launch(kq, pid, exec_time_us, title_id, app_id)) {
clear_pending_game_launch(entry);
clear_all_pending_game_launches();
break;
}
defer_confirmed_game_launch_retry(pid, exec_time_us, now_us, title_id);
break;
} else {
clear_pending_game_launch(entry);
}
} else if (!is_process_alive(entry->pid)) {
clear_pending_game_launch(entry);
}
if (entry->active && now_us != 0 && entry->deadline_us != 0 &&
now_us >= entry->deadline_us) {
log_debug(" [GAME] title_id was not available within %uus for pid=%ld",
(unsigned)GAME_APPINFO_LOOKUP_TIMEOUT_US, (long)entry->pid);
clear_pending_game_launch(entry);
}
}
sm_kstuff_game_poll();
}
static bool register_game_exit_watch(int kq, pid_t pid) {
struct kevent kev;
EV_SET(&kev, (uintptr_t)pid, EVFILT_PROC, EV_ADD | EV_ENABLE | EV_CLEAR,
NOTE_EXIT, 0, NULL);
if (kevent(kq, &kev, 1, NULL, 0, NULL) != 0) {
log_debug(" [GAME] failed to register exit watch for pid=%ld: %s",
(long)pid, strerror(errno));
return false;
}
return true;
}
static void handle_game_exec(int kq, pid_t pid) {
if (find_pending_game_launch(pid))
return;
uint64_t now_us = monotonic_time_us();
char title_id[MAX_TITLE_ID];
uint32_t app_id = 0;
if (resolve_game_title_id(pid, title_id, &app_id)) {
if (is_supported_title_id(title_id)) {
if (dispatch_game_launch(kq, pid, now_us, title_id, app_id))
clear_all_pending_game_launches();
else
defer_confirmed_game_launch_retry(pid, now_us, now_us, title_id);
}
return;
}
if (!is_process_alive(pid))
return;
pending_game_launch_t *entry = queue_pending_game_launch(pid, now_us, now_us);
if (!entry)
return;
}
static void handle_game_exit(pid_t pid) {
pending_game_launch_t *entry = find_pending_game_launch(pid);
if (entry)
clear_pending_game_launch(entry);
sm_fakelib_game_on_exit(pid);
sm_kstuff_game_on_exit(pid);
}
static void *game_lifecycle_watcher_main(void *arg) {
(void)arg;
pid_t syscore_pid = find_pid_by_name("SceSysCore.elf", false);
if (syscore_pid <= 0) {
log_debug(" [GAME] failed to find SceSysCore.elf");
set_game_lifecycle_start_result(false);
return NULL;
}
int kq = kqueue();
if (kq < 0) {
log_debug(" [GAME] kqueue failed: %s", strerror(errno));
set_game_lifecycle_start_result(false);
return NULL;
}
struct kevent kev;
EV_SET(&kev, (uintptr_t)syscore_pid, EVFILT_PROC, EV_ADD | EV_ENABLE | EV_CLEAR,
NOTE_FORK | NOTE_EXEC | NOTE_TRACK, 0, NULL);
if (kevent(kq, &kev, 1, NULL, 0, NULL) != 0) {
log_debug(" [GAME] proc watch registration failed: %s",
strerror(errno));
close(kq);
set_game_lifecycle_start_result(false);
return NULL;
}
EV_SET(&kev, (uintptr_t)g_game_lifecycle_wake_pipe[0], EVFILT_READ,
EV_ADD | EV_ENABLE | EV_CLEAR, 0, 0, NULL);
if (kevent(kq, &kev, 1, NULL, 0, NULL) != 0) {
log_debug(" [GAME] wake pipe registration failed: %s", strerror(errno));
close(kq);
set_game_lifecycle_start_result(false);
return NULL;
}
set_game_lifecycle_start_result(true);
log_debug(" [GAME] lifecycle watcher started");
while (!g_game_lifecycle_stop_requested && !should_stop_requested()) {
struct kevent event;
struct timespec timeout;
const struct timespec *timeout_ptr = compute_game_wait_timeout(&timeout);
int nev = kevent(kq, NULL, 0, &event, 1, timeout_ptr);
if (nev < 0) {
if (errno == EINTR)
continue;
if (g_game_lifecycle_stop_requested)
break;
log_debug(" [GAME] kevent wait failed: %s", strerror(errno));
break;
}
if (nev > 0) {
if (event.filter == EVFILT_READ &&
event.ident == (uintptr_t)g_game_lifecycle_wake_pipe[0]) {
char drain_buf[32];
(void)read(g_game_lifecycle_wake_pipe[0], drain_buf,
sizeof(drain_buf));
} else if ((event.fflags & NOTE_TRACKERR) != 0) {
log_debug(" [GAME] NOTE_TRACKERR for pid=%ld", (long)event.ident);
} else {
if ((event.fflags & NOTE_EXEC) != 0)
handle_game_exec(kq, (pid_t)event.ident);
if ((event.fflags & NOTE_EXIT) != 0)
handle_game_exit((pid_t)event.ident);
}
}
poll_game_modules(kq);
}
clear_all_pending_game_launches();
sm_fakelib_game_shutdown();
sm_kstuff_game_shutdown();
close(kq);
log_debug(" [GAME] lifecycle watcher stopped");
return NULL;
}
bool start_game_lifecycle_watcher(void) {
if (!sm_fakelib_game_feature_enabled() && !sm_kstuff_game_feature_enabled())
return true;
if (g_game_lifecycle_thread_started)
return true;
if (pipe(g_game_lifecycle_wake_pipe) != 0) {
log_debug(" [GAME] wake pipe creation failed: %s", strerror(errno));
return false;
}
if (!set_fd_nonblocking(g_game_lifecycle_wake_pipe[0]) ||
!set_fd_nonblocking(g_game_lifecycle_wake_pipe[1])) {
log_debug(" [GAME] wake pipe nonblocking setup failed: %s",
strerror(errno));
close_game_lifecycle_wake_pipe();
return false;
}
g_game_lifecycle_stop_requested = 0;
pthread_mutex_lock(&g_game_lifecycle_start_mutex);
g_game_lifecycle_start_ready = false;
g_game_lifecycle_start_success = false;
pthread_mutex_unlock(&g_game_lifecycle_start_mutex);
int rc =
pthread_create(&g_game_lifecycle_thread, NULL, game_lifecycle_watcher_main,
NULL);
if (rc != 0) {
log_debug(" [GAME] watcher start failed: %s", strerror(rc));
close_game_lifecycle_wake_pipe();
return false;
}
pthread_mutex_lock(&g_game_lifecycle_start_mutex);
while (!g_game_lifecycle_start_ready)
pthread_cond_wait(&g_game_lifecycle_start_cond, &g_game_lifecycle_start_mutex);
bool start_success = g_game_lifecycle_start_success;
pthread_mutex_unlock(&g_game_lifecycle_start_mutex);
if (!start_success) {
(void)pthread_join(g_game_lifecycle_thread, NULL);
close_game_lifecycle_wake_pipe();
return false;
}
g_game_lifecycle_thread_started = true;
return true;
}
void wake_game_lifecycle_watcher(void) {
if (g_game_lifecycle_wake_pipe[1] < 0)
return;
char wake = 'x';
ssize_t written = write(g_game_lifecycle_wake_pipe[1], &wake, sizeof(wake));
if (written < 0 && errno != EAGAIN && errno != EWOULDBLOCK) {
log_debug(" [GAME] wake pipe write failed: %s", strerror(errno));
}
}
void stop_game_lifecycle_watcher(void) {
if (!g_game_lifecycle_thread_started)
return;
g_game_lifecycle_stop_requested = 1;
wake_game_lifecycle_watcher();
(void)pthread_join(g_game_lifecycle_thread, NULL);
g_game_lifecycle_thread_started = false;
g_game_lifecycle_stop_requested = 0;
close_game_lifecycle_wake_pipe();
}
+113
View File
@@ -0,0 +1,113 @@
#include "sm_platform.h"
#include "sm_gameinfo.h"
#include "sm_limits.h"
#include "sm_path_state.h"
// --- Game Metadata Parsing (param.json) ---
static int extract_json_string(const char *json, const char *key, char *out,
size_t out_size) {
char search[64];
snprintf(search, sizeof(search), "\"%s\"", key);
const char *p = strstr(json, search);
if (!p)
return -1;
p = strchr(p + strlen(search), ':');
if (!p)
return -2;
while (*++p && isspace(*p)) {
/* skip */
}
if (*p != '"')
return -3;
p++;
size_t i = 0;
while (i < out_size - 1 && p[i] && p[i] != '"') {
out[i] = p[i];
i++;
}
out[i] = '\0';
return 0;
}
bool get_game_info(const char *base_path, const struct stat *param_st,
char *out_id, char *out_name) {
if (!out_id || !out_name)
return false;
out_id[0] = '\0';
out_name[0] = '\0';
if (!base_path || !param_st || !S_ISREG(param_st->st_mode))
return false;
bool cached_valid = false;
if (load_cached_game_info(base_path, param_st, out_id, out_name, &cached_valid))
return cached_valid;
if (param_st->st_size <= 0 || param_st->st_size > 1024 * 1024) {
store_cached_game_info(base_path, param_st, false, "", "");
return false;
}
char path[MAX_PATH];
snprintf(path, sizeof(path), "%s/sce_sys/param.json", base_path);
FILE *f = fopen(path, "rb");
if (!f) {
store_cached_game_info(base_path, param_st, false, "", "");
return false;
}
size_t len = (size_t)param_st->st_size;
char *buf = (char *)malloc(len + 1);
if (!buf) {
fclose(f);
return false;
}
bool read_ok = (fread(buf, 1, len, f) == len);
fclose(f);
if (!read_ok) {
free(buf);
return false;
}
buf[len] = '\0';
bool valid = false;
int res = extract_json_string(buf, "titleId", out_id, MAX_TITLE_ID);
if (res != 0)
res = extract_json_string(buf, "title_id", out_id, MAX_TITLE_ID);
if (res == 0) {
const char *en_ptr = strstr(buf, "\"en-US\"");
const char *search_start = en_ptr ? en_ptr : buf;
if (extract_json_string(search_start, "titleName", out_name,
MAX_TITLE_NAME) != 0)
extract_json_string(buf, "titleName", out_name, MAX_TITLE_NAME);
if (out_name[0] == '\0')
(void)strlcpy(out_name, out_id, MAX_TITLE_NAME);
valid = true;
}
free(buf);
store_cached_game_info(base_path, param_st, valid, out_id, out_name);
return valid;
}
bool directory_has_param_json(const char *dir_path, struct stat *param_st_out) {
if (!dir_path || dir_path[0] == '\0')
return false;
int dir_fd = open(dir_path, O_RDONLY | O_DIRECTORY);
if (dir_fd < 0)
return false;
struct stat st;
if (fstatat(dir_fd, "sce_sys", &st, 0) == 0 && S_ISDIR(st.st_mode) &&
fstatat(dir_fd, "sce_sys/param.json", &st, 0) == 0 &&
S_ISREG(st.st_mode)) {
if (param_st_out)
*param_st_out = st;
close(dir_fd);
return true;
}
close(dir_fd);
return false;
}
+926
View File
@@ -0,0 +1,926 @@
#include "sm_platform.h"
#include "sm_runtime.h"
#include "sm_image.h"
#include "sm_hash.h"
#include "sm_image_cache.h"
#include "sm_game_cache.h"
#include "sm_log.h"
#include "sm_config_mount.h"
#include "sm_limits.h"
#include "sm_mount_defs.h"
#include "sm_mount_device.h"
#include "sm_filesystem.h"
#include "sm_path_state.h"
#include "sm_path_utils.h"
#include "sm_paths.h"
static bool wait_for_dev_node_state(const char *devname, bool should_exist) {
for (int i = 0; i < LVD_NODE_WAIT_RETRIES; i++) {
if ((access(devname, F_OK) == 0) == should_exist)
return true;
sceKernelUsleep(LVD_NODE_WAIT_US);
}
return false;
}
static uint32_t get_lvd_sector_size_fallback(image_fs_type_t fs_type) {
const runtime_config_t *cfg = runtime_config();
switch (fs_type) {
case IMAGE_FS_UFS:
return cfg->lvd_sector_ufs;
case IMAGE_FS_PFS:
return cfg->lvd_sector_pfs;
case IMAGE_FS_EXFAT:
default:
return cfg->lvd_sector_exfat;
}
}
static uint32_t get_lvd_sector_size(const char *path, image_fs_type_t fs_type) {
uint32_t fallback = get_lvd_sector_size_fallback(fs_type);
if (!path)
return fallback;
struct statfs sfs;
if (statfs(path, &sfs) != 0)
return fallback;
uint64_t fs_cluster_size = (uint64_t)sfs.f_bsize;
if (fs_cluster_size == 0)
fs_cluster_size = (uint64_t)sfs.f_iosize;
if (fs_cluster_size == 0 || fs_cluster_size >= (uint64_t)fallback)
return fallback;
return (uint32_t)fs_cluster_size;
}
static uint32_t get_lvd_secondary_unit(const char *path,
image_fs_type_t fs_type) {
if (fs_type == IMAGE_FS_EXFAT)
return LVD_SECONDARY_UNIT_SINGLE_IMAGE;
return get_lvd_sector_size(path, fs_type);
}
static uint32_t get_md_sector_size(image_fs_type_t fs_type) {
const runtime_config_t *cfg = runtime_config();
switch (fs_type) {
case IMAGE_FS_UFS:
return cfg->md_sector_ufs;
case IMAGE_FS_EXFAT:
default:
return cfg->md_sector_exfat;
}
}
static unsigned int get_md_attach_options(bool mount_read_only) {
unsigned int options = MD_AUTOUNIT | MD_ASYNC;
if (mount_read_only)
options |= MD_READONLY;
return options;
}
static uint16_t get_lvd_attach_raw_flags(image_fs_type_t fs_type,
bool mount_read_only) {
if (fs_type == IMAGE_FS_UFS) {
return mount_read_only ? LVD_ATTACH_RAW_FLAGS_DD_RO
: LVD_ATTACH_RAW_FLAGS_DD_RW;
}
return mount_read_only ? LVD_ATTACH_RAW_FLAGS_SINGLE_RO
: LVD_ATTACH_RAW_FLAGS_SINGLE_RW;
}
static unsigned int get_nmount_flags(image_fs_type_t fs_type,
bool mount_read_only,
const char **mount_mode_out) {
if (fs_type == IMAGE_FS_UFS) {
if (mount_mode_out)
*mount_mode_out = mount_read_only ? "dd_ro" : "dd_rw";
return mount_read_only ? UFS_NMOUNT_FLAG_RO : UFS_NMOUNT_FLAG_RW;
}
if (mount_mode_out)
*mount_mode_out = mount_read_only ? "rdonly" : "rw";
return mount_read_only ? MNT_RDONLY : 0;
}
static uint16_t normalize_lvd_raw_flags(uint16_t raw_flags) {
if ((raw_flags & 0x800Eu) != 0u) {
uint32_t raw = (uint32_t)raw_flags;
uint32_t len = (raw & 0xFFFF8000u) + ((raw & 2u) << 6) +
(8u * (raw & 1u)) + (2u * ((raw >> 2) & 1u)) +
(2u * (raw & 8u)) + 4u;
return (uint16_t)len;
}
return (uint16_t)(8u * ((uint32_t)raw_flags & 1u) + 4u);
}
static uint16_t get_lvd_image_type(image_fs_type_t fs_type) {
if (fs_type == IMAGE_FS_UFS)
return LVD_ATTACH_IMAGE_TYPE_UFS_DOWNLOAD_DATA;
if (fs_type == IMAGE_FS_PFS)
return LVD_ATTACH_IMAGE_TYPE_PFS_SAVE_DATA;
return LVD_ATTACH_IMAGE_TYPE_SINGLE;
}
static uint16_t get_lvd_source_type(const char *path) {
if (!path)
return LVD_ENTRY_TYPE_FILE;
if (strncmp(path, "/dev/sbram0", strlen("/dev/sbram0")) == 0)
return LVD_ENTRY_TYPE_SPECIAL;
struct stat st;
if (stat(path, &st) == 0) {
if (S_ISCHR(st.st_mode) || S_ISBLK(st.st_mode))
return LVD_ENTRY_TYPE_SPECIAL;
}
return LVD_ENTRY_TYPE_FILE;
}
// --- Image Path and Naming Helpers ---
static image_fs_type_t detect_image_fs_type(const char *name) {
const char *dot = strrchr(name, '.');
if (!dot)
return IMAGE_FS_UNKNOWN;
if (strcasecmp(dot, ".ffpkg") == 0)
return IMAGE_FS_UFS;
if (strcasecmp(dot, ".exfat") == 0)
return IMAGE_FS_EXFAT;
if (strcasecmp(dot, ".ffpfs") == 0)
return IMAGE_FS_PFS;
return IMAGE_FS_UNKNOWN;
}
static const char *image_fs_name(image_fs_type_t fs_type) {
switch (fs_type) {
case IMAGE_FS_UFS:
return "ufs";
case IMAGE_FS_EXFAT:
return "exfatfs";
case IMAGE_FS_PFS:
return "pfs";
default:
return "unknown";
}
}
void log_fs_stats(const char *tag, const char *path,
const char *type_hint) {
struct statfs sfs;
if (statfs(path, &sfs) != 0) {
log_debug(" [%s] FS stats read failed for %s: %s", tag, path,
strerror(errno));
return;
}
const char *type_name = type_hint;
if (sfs.f_fstypename[0] != '\0')
type_name = sfs.f_fstypename;
if (!type_name)
type_name = "unknown";
uint64_t bsize = (uint64_t)sfs.f_bsize;
uint64_t iosize = (uint64_t)sfs.f_iosize;
uint64_t blocks = (uint64_t)sfs.f_blocks;
uint64_t bfree = (uint64_t)sfs.f_bfree;
uint64_t bavail = (uint64_t)sfs.f_bavail;
uint64_t files = (uint64_t)sfs.f_files;
uint64_t ffree = (uint64_t)sfs.f_ffree;
uint64_t total_bytes = blocks * bsize;
uint64_t free_bytes = bfree * bsize;
uint64_t avail_bytes = bavail * bsize;
log_debug(" [%s] FS stats: path=%s type=%s bsize=%llu iosize=%llu "
"blocks=%llu bfree=%llu bavail=%llu files=%llu ffree=%llu "
"flags=0x%lX total=%lluB free=%lluB avail=%lluB",
tag, path, type_name, (unsigned long long)bsize,
(unsigned long long)iosize, (unsigned long long)blocks,
(unsigned long long)bfree, (unsigned long long)bavail,
(unsigned long long)files, (unsigned long long)ffree,
(unsigned long)sfs.f_flags, (unsigned long long)total_bytes,
(unsigned long long)free_bytes, (unsigned long long)avail_bytes);
}
static void strip_extension(const char *filename, char *out, size_t out_size) {
const char *dot = strrchr(filename, '.');
size_t len = dot ? (size_t)(dot - filename) : strlen(filename);
if (len >= out_size)
len = out_size - 1;
memcpy(out, filename, len);
out[len] = '\0';
}
static void build_image_mount_point(const char *file_path,
char mount_point[MAX_PATH]) {
const char *filename = get_filename_component(file_path);
char base_name[MAX_PATH];
char mount_name[MAX_PATH];
strip_extension(filename, base_name, sizeof(base_name));
size_t base_len = strlen(base_name);
size_t max_base_len = sizeof(mount_name) - 1u - 9u;
if (base_len > max_base_len)
base_len = max_base_len;
memcpy(mount_name, base_name, base_len);
mount_name[base_len] = '\0';
snprintf(mount_name + base_len, sizeof(mount_name) - base_len, "_%08x",
sm_fnv1a32(file_path));
snprintf(mount_point, MAX_PATH, "%s/%s", IMAGE_MOUNT_BASE, mount_name);
}
typedef bool (*image_attach_fn)(const char *file_path, image_fs_type_t fs_type,
bool mount_read_only, off_t file_size,
int *unit_id_out, char *devname_out,
size_t devname_size);
typedef struct {
attach_backend_t id;
image_attach_fn attach;
} image_backend_ops_t;
static bool attach_md_backend(const char *file_path, image_fs_type_t fs_type,
bool mount_read_only, off_t file_size,
int *unit_id_out, char *devname_out,
size_t devname_size) {
int md_fd = open(MD_CTRL_PATH, O_RDWR);
if (md_fd < 0) {
log_debug(" [IMG][%s] open %s failed: %s",
attach_backend_name(ATTACH_BACKEND_MD), MD_CTRL_PATH,
strerror(errno));
return false;
}
struct md_ioctl req;
memset(&req, 0, sizeof(req));
req.md_version = MDIOVERSION;
req.md_type = MD_VNODE;
req.md_file = (char *)file_path;
req.md_mediasize = file_size;
req.md_sectorsize = get_md_sector_size(fs_type);
req.md_options = get_md_attach_options(mount_read_only);
int last_errno = 0;
log_debug(" [IMG][%s] attach try: options=0x%x",
attach_backend_name(ATTACH_BACKEND_MD), req.md_options);
int ret = ioctl(md_fd, MDIOCATTACH, &req);
if (ret != 0)
last_errno = errno;
close(md_fd);
if (ret != 0) {
errno = last_errno;
log_debug(" [IMG][%s] attach failed: %s (ret: 0x%x)",
attach_backend_name(ATTACH_BACKEND_MD), strerror(errno), ret);
return false;
}
int unit_id = (int)req.md_unit;
if (unit_id < 0) {
log_debug(" [IMG][%s] attach returned invalid unit: %d",
attach_backend_name(ATTACH_BACKEND_MD), unit_id);
return false;
}
snprintf(devname_out, devname_size, "/dev/md%d", unit_id);
if (!wait_for_dev_node_state(devname_out, true)) {
log_debug(" [IMG][%s] device node did not appear: %s",
attach_backend_name(ATTACH_BACKEND_MD), devname_out);
(void)detach_attached_unit(ATTACH_BACKEND_MD, unit_id);
return false;
}
log_debug(" [IMG][%s] attach returned unit=%d",
attach_backend_name(ATTACH_BACKEND_MD), unit_id);
*unit_id_out = unit_id;
return true;
}
static bool attach_lvd_backend(const char *file_path, image_fs_type_t fs_type,
bool mount_read_only, off_t file_size,
int *unit_id_out, char *devname_out,
size_t devname_size) {
int lvd_fd = open(LVD_CTRL_PATH, O_RDWR);
if (lvd_fd < 0) {
log_debug(" [IMG][%s] open %s failed: %s",
attach_backend_name(ATTACH_BACKEND_LVD), LVD_CTRL_PATH,
strerror(errno));
return false;
}
lvd_ioctl_layer_v0_t layers[LVD_ATTACH_LAYER_COUNT];
memset(layers, 0, sizeof(layers));
layers[0].source_type = get_lvd_source_type(file_path);
layers[0].flags = LVD_ENTRY_FLAG_NO_BITMAP;
layers[0].path = file_path;
layers[0].offset = 0;
layers[0].size = (uint64_t)file_size;
uint32_t sector_size = get_lvd_sector_size(file_path, fs_type);
uint32_t secondary_unit = get_lvd_secondary_unit(file_path, fs_type);
uint16_t raw_flags = get_lvd_attach_raw_flags(fs_type, mount_read_only);
uint16_t normalized_flags = normalize_lvd_raw_flags(raw_flags);
lvd_ioctl_attach_v0_t req;
memset(&req, 0, sizeof(req));
req.io_version = LVD_ATTACH_IO_VERSION_V0;
req.image_type = get_lvd_image_type(fs_type);
req.layer_count = LVD_ATTACH_LAYER_COUNT;
req.device_size = (uint64_t)file_size;
req.layers_ptr = layers;
req.sector_size = sector_size;
req.secondary_unit = secondary_unit;
req.flags = normalized_flags;
req.device_id = -1;
int last_errno = 0;
log_debug(" [IMG][%s] attach try: ver=%u sec=%u sec2=%u raw=0x%x "
"flags=0x%x img=%u",
attach_backend_name(ATTACH_BACKEND_LVD), req.io_version,
req.sector_size, req.secondary_unit, raw_flags, req.flags,
req.image_type);
int ret = ioctl(lvd_fd, SCE_LVD_IOC_ATTACH_V0, &req);
if (ret != 0)
last_errno = errno;
close(lvd_fd);
int unit_id = req.device_id;
if (ret != 0) {
errno = last_errno;
log_debug(" [IMG][%s] attach failed: %s (ret: 0x%x)",
attach_backend_name(ATTACH_BACKEND_LVD), strerror(errno), ret);
return false;
}
if (unit_id < 0) {
log_debug(" [IMG][%s] attach returned invalid unit: %d",
attach_backend_name(ATTACH_BACKEND_LVD), unit_id);
return false;
}
log_debug(" [IMG][%s] attach returned unit=%d",
attach_backend_name(ATTACH_BACKEND_LVD), unit_id);
snprintf(devname_out, devname_size, "/dev/lvd%d", unit_id);
if (!wait_for_dev_node_state(devname_out, true)) {
log_debug(" [IMG][%s] device node did not appear: %s",
attach_backend_name(ATTACH_BACKEND_LVD), devname_out);
(void)detach_attached_unit(ATTACH_BACKEND_LVD, unit_id);
return false;
}
*unit_id_out = unit_id;
return true;
}
static const image_backend_ops_t *get_image_backend_ops(attach_backend_t backend) {
static const image_backend_ops_t md_ops = {
.id = ATTACH_BACKEND_MD, .attach = attach_md_backend};
static const image_backend_ops_t lvd_ops = {
.id = ATTACH_BACKEND_LVD, .attach = attach_lvd_backend};
if (backend == ATTACH_BACKEND_MD)
return &md_ops;
if (backend == ATTACH_BACKEND_LVD)
return &lvd_ops;
return NULL;
}
static bool is_cached_image_path(const char *file_path) {
for (int k = 0; k < MAX_IMAGE_MOUNTS; k++) {
image_cache_entry_t cached_entry;
if (get_image_cache_entry(k, &cached_entry) &&
strcmp(cached_entry.path, file_path) == 0) {
return true;
}
}
return false;
}
static bool directory_has_visible_entries(const char *path) {
DIR *dir = opendir(path);
if (!dir)
return false;
bool found = false;
struct dirent *entry;
while ((entry = readdir(dir)) != NULL) {
if (entry->d_name[0] == '.')
continue;
found = true;
break;
}
closedir(dir);
return found;
}
static bool reuse_existing_image_mount(const char *file_path,
const char *mount_point,
bool *cache_failed_out) {
if (cache_failed_out)
*cache_failed_out = false;
struct stat mount_st;
if (stat(mount_point, &mount_st) != 0 || !S_ISDIR(mount_st.st_mode))
return false;
if (!directory_has_visible_entries(mount_point))
return false;
attach_backend_t existing_backend = ATTACH_BACKEND_NONE;
int existing_unit = -1;
if (resolve_device_from_mount(mount_point, &existing_backend, &existing_unit)) {
if (!cache_image_mount(file_path, mount_point, existing_unit,
existing_backend)) {
sm_error_set("IMG", ENOSPC, file_path,
"Image cache full (%u entries), cannot track mount %s",
(unsigned)MAX_IMAGE_MOUNTS, mount_point);
log_debug(" [IMG] image cache full, refusing unmanaged mount reuse: %s",
mount_point);
errno = ENOSPC;
if (cache_failed_out)
*cache_failed_out = true;
return false;
}
log_debug(" [IMG][%s] Already mounted: %s",
attach_backend_name(existing_backend), mount_point);
return true;
}
log_debug(" [IMG] Mount point exists and is non-empty but is not an active "
"mount, reattaching: %s", mount_point);
return false;
}
static bool stat_image_file(const char *file_path, struct stat *st_out) {
if (stat(file_path, st_out) != 0) {
log_debug(" [IMG] stat failed for %s: %s", file_path, strerror(errno));
return false;
}
if (st_out->st_size < 0) {
log_debug(" [IMG] invalid file size for %s: %lld", file_path,
(long long)st_out->st_size);
errno = EINVAL;
return false;
}
return true;
}
static void ensure_mount_dirs(const char *mount_point) {
mkdir(IMAGE_MOUNT_BASE, 0777);
mkdir(mount_point, 0777);
}
static attach_backend_t select_image_backend(const runtime_config_t *cfg,
image_fs_type_t fs_type) {
if (fs_type == IMAGE_FS_EXFAT)
return cfg->exfat_backend;
if (fs_type == IMAGE_FS_UFS)
return cfg->ufs_backend;
return ATTACH_BACKEND_LVD;
}
static bool attach_image_device(const char *file_path, image_fs_type_t fs_type,
bool mount_read_only, off_t file_size,
attach_backend_t attach_backend, int *unit_id_out,
char *devname_out, size_t devname_size) {
const image_backend_ops_t *backend_ops = get_image_backend_ops(attach_backend);
if (!backend_ops) {
log_debug(" [IMG] unsupported attach backend for %s", file_path);
errno = EINVAL;
return false;
}
if (!backend_ops->attach(file_path, fs_type, mount_read_only, file_size,
unit_id_out, devname_out, devname_size)) {
return false;
}
log_debug(" [IMG][%s] Attached as %s", attach_backend_name(attach_backend),
devname_out);
return true;
}
static bool perform_image_nmount(const char *file_path, image_fs_type_t fs_type,
attach_backend_t attach_backend, int unit_id,
const char *devname, const char *mount_point,
bool mount_read_only, bool force_mount) {
struct iovec *iov = NULL;
unsigned int iovlen = 0;
char mount_errmsg[256];
memset(mount_errmsg, 0, sizeof(mount_errmsg));
const char *sigverify = PFS_MOUNT_SIGVERIFY ? "1" : "0";
const char *playgo = PFS_MOUNT_PLAYGO ? "1" : "0";
const char *disc = PFS_MOUNT_DISC ? "1" : "0";
const char *ekpfs_key = PFS_ZERO_EKPFS_KEY_HEX;
struct iovec iov_ufs[] = {
IOVEC_ENTRY("fstype"), IOVEC_ENTRY("ufs"), IOVEC_ENTRY("from"),
IOVEC_ENTRY(devname), IOVEC_ENTRY("fspath"),
IOVEC_ENTRY(mount_point), IOVEC_ENTRY("budgetid"),
IOVEC_ENTRY(DEVPFS_BUDGET_GAME),
IOVEC_ENTRY("async"), IOVEC_ENTRY(NULL),
IOVEC_ENTRY("noatime"), IOVEC_ENTRY(NULL),
IOVEC_ENTRY("automounted"), IOVEC_ENTRY(NULL),
IOVEC_ENTRY("errmsg"), {(void *)mount_errmsg, sizeof(mount_errmsg)},
IOVEC_ENTRY("force"), IOVEC_ENTRY(NULL)};
struct iovec iov_exfat[] = {
IOVEC_ENTRY("from"), IOVEC_ENTRY(devname),
IOVEC_ENTRY("fspath"), IOVEC_ENTRY(mount_point),
IOVEC_ENTRY("fstype"), IOVEC_ENTRY("exfatfs"),
IOVEC_ENTRY("budgetid"), IOVEC_ENTRY(DEVPFS_BUDGET_GAME),
IOVEC_ENTRY("large"), IOVEC_ENTRY("yes"),
IOVEC_ENTRY("timezone"), IOVEC_ENTRY("static"),
IOVEC_ENTRY("async"), IOVEC_ENTRY(NULL),
IOVEC_ENTRY("noatime"), IOVEC_ENTRY(NULL),
IOVEC_ENTRY("ignoreacl"), IOVEC_ENTRY(NULL),
IOVEC_ENTRY("automounted"), IOVEC_ENTRY(NULL),
IOVEC_ENTRY("errmsg"), {(void *)mount_errmsg, sizeof(mount_errmsg)},
IOVEC_ENTRY("force"), IOVEC_ENTRY(NULL)};
struct iovec iov_pfs[] = {
IOVEC_ENTRY("from"), IOVEC_ENTRY(devname),
IOVEC_ENTRY("fspath"), IOVEC_ENTRY(mount_point),
IOVEC_ENTRY("fstype"), IOVEC_ENTRY("pfs"),
IOVEC_ENTRY("sigverify"), IOVEC_ENTRY(sigverify),
IOVEC_ENTRY("mkeymode"), IOVEC_ENTRY(PFS_MOUNT_MKEYMODE),
IOVEC_ENTRY("budgetid"), IOVEC_ENTRY(PFS_MOUNT_BUDGET_ID),
IOVEC_ENTRY("playgo"), IOVEC_ENTRY(playgo),
IOVEC_ENTRY("disc"), IOVEC_ENTRY(disc),
IOVEC_ENTRY("ekpfs"), IOVEC_ENTRY(ekpfs_key),
IOVEC_ENTRY("async"), IOVEC_ENTRY(NULL),
IOVEC_ENTRY("noatime"), IOVEC_ENTRY(NULL),
IOVEC_ENTRY("automounted"), IOVEC_ENTRY(NULL),
IOVEC_ENTRY("errmsg"), {(void *)mount_errmsg, sizeof(mount_errmsg)},
IOVEC_ENTRY("force"), IOVEC_ENTRY(NULL)};
if (fs_type == IMAGE_FS_UFS) {
iov = iov_ufs;
iovlen = (unsigned int)IOVEC_SIZE(iov_ufs) - (force_mount ? 0u : 2u);
} else if (fs_type == IMAGE_FS_EXFAT) {
iov = iov_exfat;
iovlen = (unsigned int)IOVEC_SIZE(iov_exfat) - (force_mount ? 0u : 2u);
} else if (fs_type == IMAGE_FS_PFS) {
log_debug(" [IMG][%s] PFS ro=%d budgetid=%s mkeymode=%s "
"sigverify=%s playgo=%s disc=%s ekpfs=zero",
attach_backend_name(attach_backend), mount_read_only ? 1 : 0,
PFS_MOUNT_BUDGET_ID, PFS_MOUNT_MKEYMODE, sigverify, playgo, disc);
iov = iov_pfs;
iovlen = (unsigned int)IOVEC_SIZE(iov_pfs) - (force_mount ? 0u : 2u);
} else {
log_debug(" [IMG][%s] unsupported fstype=%s",
attach_backend_name(attach_backend), image_fs_name(fs_type));
(void)detach_attached_unit(attach_backend, unit_id);
errno = EINVAL;
return false;
}
const char *mount_mode = NULL;
unsigned int mount_flags =
get_nmount_flags(fs_type, mount_read_only, &mount_mode);
if (nmount(iov, iovlen, (int)mount_flags) == 0)
return true;
int mount_errno = errno;
if (mount_errmsg[0] != '\0') {
sm_error_set("IMG", mount_errno, file_path, "%s", mount_errmsg);
log_debug(" [IMG][%s] nmount %s errmsg: %s",
attach_backend_name(attach_backend), mount_mode, mount_errmsg);
}
log_debug(" [IMG][%s] nmount %s failed: %s",
attach_backend_name(attach_backend), mount_mode,
strerror(mount_errno));
(void)detach_attached_unit(attach_backend, unit_id);
errno = mount_errno;
return false;
}
static bool validate_mounted_image(const char *file_path, image_fs_type_t fs_type,
attach_backend_t attach_backend, int unit_id,
const char *devname,
const char *mount_point) {
struct statfs mounted_sfs;
if (statfs(mount_point, &mounted_sfs) != 0) {
log_debug(" [IMG][%s] statfs after mount failed for %s: %s",
attach_backend_name(attach_backend), mount_point, strerror(errno));
return true;
}
uint32_t min_device_sector =
(attach_backend == ATTACH_BACKEND_MD) ? get_md_sector_size(fs_type)
: get_lvd_sector_size(file_path,
fs_type);
uint64_t fs_block_size = (uint64_t)mounted_sfs.f_bsize;
if (fs_block_size >= (uint64_t)min_device_sector)
return true;
sm_error_set("IMG", EINVAL, file_path,
"Filesystem cluster size (%llu) is smaller than device "
"sector size (%u) for %s",
(unsigned long long)fs_block_size, min_device_sector, devname);
log_debug(" [IMG][%s] %s", attach_backend_name(attach_backend),
sm_last_error()->message);
notify_system("Image mount rejected:\n%s\nCluster size is too small "
"(%llu < %u).\nUse a larger cluster size in the image or "
"lower sector size in config.",
file_path, (unsigned long long)fs_block_size,
min_device_sector);
sm_error_mark_notified();
(void)unmount_image(file_path, unit_id, attach_backend);
errno = EINVAL;
return false;
}
// --- Image Attach + nmount Pipeline ---
bool mount_image(const char *file_path, image_fs_type_t fs_type) {
sm_error_clear();
const runtime_config_t *cfg = runtime_config();
bool mount_read_only = cfg->mount_read_only;
bool mount_mode_overridden = false;
bool force_mount = cfg->force_mount;
const char *filename = get_filename_component(file_path);
if (filename[0] != '\0')
mount_mode_overridden =
get_image_mode_override(filename, &mount_read_only);
if (is_cached_image_path(file_path))
return true;
char mount_point[MAX_PATH];
build_image_mount_point(file_path, mount_point);
struct stat st;
bool cache_failed = false;
if (reuse_existing_image_mount(file_path, mount_point, &cache_failed))
return true;
if (cache_failed)
return false;
if (!stat_image_file(file_path, &st))
return false;
log_debug(" [IMG] Mounting image (%s): %s -> %s", image_fs_name(fs_type),
file_path, mount_point);
if (mount_mode_overridden) {
log_debug(" [CFG] Image mode override: %s -> %s", file_path,
mount_read_only ? "ro" : "rw");
}
ensure_mount_dirs(mount_point);
attach_backend_t attach_backend = select_image_backend(cfg, fs_type);
log_debug(" [IMG][%s] attach backend selected for %s",
attach_backend_name(attach_backend), file_path);
int unit_id = -1;
char devname[64];
memset(devname, 0, sizeof(devname));
if (!attach_image_device(file_path, fs_type, mount_read_only, st.st_size,
attach_backend, &unit_id, devname, sizeof(devname))) {
return false;
}
if (!perform_image_nmount(file_path, fs_type, attach_backend, unit_id, devname,
mount_point, mount_read_only, force_mount)) {
return false;
}
log_debug(" [IMG][%s] Mounted (%s) %s -> %s",
attach_backend_name(attach_backend), image_fs_name(fs_type),
devname, mount_point);
log_fs_stats("IMG", mount_point, image_fs_name(fs_type));
if (!validate_mounted_image(file_path, fs_type, attach_backend, unit_id,
devname, mount_point)) {
return false;
}
if (!cache_image_mount(file_path, mount_point, unit_id, attach_backend)) {
sm_error_set("IMG", ENOSPC, file_path,
"Image cache full (%u entries), rolling back mount",
(unsigned)MAX_IMAGE_MOUNTS);
log_debug(" [IMG] image cache full, rolling back mount: %s -> %s",
file_path, mount_point);
(void)unmount_image(file_path, unit_id, attach_backend);
errno = ENOSPC;
return false;
}
return true;
}
bool unmount_image(const char *file_path, int unit_id, attach_backend_t backend) {
char mount_point[MAX_PATH];
build_image_mount_point(file_path, mount_point);
int resolved_unit = unit_id;
attach_backend_t resolved_backend = backend;
if (resolved_unit < 0 || resolved_backend == ATTACH_BACKEND_NONE) {
if (!resolve_device_from_mount(mount_point, &resolved_backend,
&resolved_unit)) {
resolved_backend = ATTACH_BACKEND_NONE;
resolved_unit = -1;
}
}
log_debug(" [IMG][%s] unmount start: source=%s mount=%s unit=%d",
attach_backend_name(resolved_backend), file_path, mount_point,
resolved_unit);
// Remove mount.lnk and unmount /system_ex/app/<titleid> that point to this
// source before unmounting the virtual disk itself.
cleanup_mount_links(mount_point, true);
clear_cached_game(mount_point);
// Unmount stacked layers (unionfs over image fs).
for (int i = 0; i < MAX_LAYERED_UNMOUNT_ATTEMPTS; i++) {
if (!is_active_image_mount_point(mount_point))
break;
if (unmount(mount_point, 0) == 0)
continue;
if (errno == ENOENT || errno == EINVAL)
break;
if (unmount(mount_point, MNT_FORCE) != 0 && errno != ENOENT &&
errno != EINVAL) {
log_debug(" [IMG][%s] unmount failed for %s: %s",
attach_backend_name(resolved_backend), mount_point,
strerror(errno));
return false;
}
}
if (is_active_image_mount_point(mount_point)) {
log_debug(" [IMG][%s] unmount incomplete for %s",
attach_backend_name(resolved_backend), mount_point);
return false;
}
bool detach_ok = true;
if (resolved_backend != ATTACH_BACKEND_NONE && resolved_unit >= 0)
detach_ok = detach_attached_unit(resolved_backend, resolved_unit);
if (rmdir(mount_point) == 0) {
log_debug(" [IMG] Removed mount directory: %s", mount_point);
log_debug(" [IMG][%s] unmount complete: source=%s mount=%s unit=%d",
attach_backend_name(resolved_backend), file_path, mount_point,
resolved_unit);
return detach_ok;
}
int err = errno;
if (err == ENOENT) {
log_debug(" [IMG][%s] unmount complete: source=%s mount=%s unit=%d",
attach_backend_name(resolved_backend), file_path, mount_point,
resolved_unit);
return detach_ok;
}
if (err == ENOTEMPTY || err == EBUSY) {
log_debug(" [IMG] Mount directory not removed (%s): %s",
strerror(err), mount_point);
if (detach_ok) {
log_debug(" [IMG][%s] unmount complete with leftover dir: source=%s "
"mount=%s unit=%d",
attach_backend_name(resolved_backend), file_path, mount_point,
resolved_unit);
}
return detach_ok;
}
log_debug(" [IMG] Failed to remove mount directory %s: %s", mount_point,
strerror(err));
return detach_ok;
}
void cleanup_stale_image_mounts(void) {
if (should_stop_requested())
return;
for (int k = 0; k < MAX_IMAGE_MOUNTS; k++) {
image_cache_entry_t cached_entry;
if (should_stop_requested())
return;
if (!get_image_cache_entry(k, &cached_entry))
continue;
if (access(cached_entry.path, F_OK) != 0) {
log_debug(" [IMG][%s] Source removed, unmounting: %s",
attach_backend_name(cached_entry.backend), cached_entry.path);
if (unmount_image(cached_entry.path, cached_entry.unit_id,
cached_entry.backend))
invalidate_image_cache_entry(k);
continue;
}
image_fs_type_t fs_type = detect_image_fs_type(cached_entry.path);
char mount_point[MAX_PATH];
build_image_mount_point(cached_entry.path, mount_point);
if (is_active_image_mount_point(mount_point))
continue;
char source_path[MAX_PATH];
(void)strlcpy(source_path, cached_entry.path, sizeof(source_path));
log_debug(" [IMG][%s] mount lost, retrying: %s -> %s",
attach_backend_name(cached_entry.backend), source_path,
mount_point);
clear_cached_game(mount_point);
clear_missing_param_entry(mount_point);
invalidate_image_cache_entry(k);
if (mount_image(source_path, fs_type)) {
clear_image_mount_attempts(source_path);
continue;
}
int mount_err = errno;
if (bump_image_mount_attempts(source_path) == 1 && !sm_error_notified()) {
notify_image_mount_failed(source_path, mount_err);
}
}
}
void cleanup_mount_dirs(void) {
DIR *d = opendir(IMAGE_MOUNT_BASE);
if (!d) {
if (errno != ENOENT)
log_debug(" [IMG] open %s failed: %s", IMAGE_MOUNT_BASE, strerror(errno));
return;
}
struct dirent *entry;
while ((entry = readdir(d)) != NULL) {
if (should_stop_requested())
break;
if (entry->d_name[0] == '.')
continue;
char full_path[MAX_PATH];
snprintf(full_path, sizeof(full_path), "%s/%s", IMAGE_MOUNT_BASE,
entry->d_name);
bool is_dir = false;
if (entry->d_type == DT_DIR) {
is_dir = true;
} else if (entry->d_type == DT_UNKNOWN) {
struct stat st;
if (stat(full_path, &st) == 0)
is_dir = S_ISDIR(st.st_mode);
}
if (!is_dir)
continue;
if (rmdir(full_path) == 0) {
log_debug(" [IMG] removed empty mount dir: %s", full_path);
continue;
}
if (errno == ENOTEMPTY || errno == EBUSY || errno == ENOENT)
continue;
log_debug(" [IMG] failed to remove mount dir %s: %s", full_path,
strerror(errno));
}
closedir(d);
}
void maybe_mount_image_file(const char *full_path, const char *display_name) {
image_fs_type_t fs_type = detect_image_fs_type(display_name);
if (fs_type == IMAGE_FS_UNKNOWN)
return;
if (!is_source_stable_for_mount(full_path, display_name, "IMG"))
return;
if (is_image_mount_limited(full_path))
return;
if (mount_image(full_path, fs_type)) {
clear_image_mount_attempts(full_path);
return;
}
int mount_err = errno;
if (bump_image_mount_attempts(full_path) == 1 && !sm_error_notified()) {
notify_image_mount_failed(full_path, mount_err);
}
}
bool shutdown_image_mounts(void) {
for (int pass = 0; pass < MAX_LAYERED_UNMOUNT_ATTEMPTS; pass++) {
bool any_remaining = false;
bool progress = false;
for (int k = 0; k < MAX_IMAGE_MOUNTS; k++) {
image_cache_entry_t cached_entry;
if (!get_image_cache_entry(k, &cached_entry))
continue;
if (unmount_image(cached_entry.path, cached_entry.unit_id,
cached_entry.backend)) {
invalidate_image_cache_entry(k);
progress = true;
continue;
}
any_remaining = true;
log_debug(" [IMG] shutdown unmount pending (%d/%d): %s", pass + 1,
MAX_LAYERED_UNMOUNT_ATTEMPTS, cached_entry.path);
}
if (!any_remaining)
return true;
if (!progress)
break;
}
return false;
}
+120
View File
@@ -0,0 +1,120 @@
#include "sm_platform.h"
#include "sm_image_cache.h"
#include "sm_limits.h"
struct ImageCache {
char path[MAX_PATH];
char mount_point[MAX_PATH];
int unit_id;
attach_backend_t backend;
bool valid;
};
static struct ImageCache g_image_cache[MAX_IMAGE_MOUNTS];
static int find_cache_index(const char *path, const char *mount_point) {
if ((!path || path[0] == '\0') && (!mount_point || mount_point[0] == '\0'))
return -1;
for (int k = 0; k < MAX_IMAGE_MOUNTS; k++) {
if (!g_image_cache[k].valid)
continue;
if (mount_point && mount_point[0] != '\0' &&
strcmp(g_image_cache[k].mount_point, mount_point) == 0) {
return k;
}
if (path && path[0] != '\0' && strcmp(g_image_cache[k].path, path) == 0)
return k;
}
return -1;
}
static int upsert_image_source_mapping(const char *path, const char *mount_point) {
int entry_index = find_cache_index(path, mount_point);
if (entry_index >= 0) {
struct ImageCache *entry = &g_image_cache[entry_index];
(void)strlcpy(entry->path, path, sizeof(entry->path));
(void)strlcpy(entry->mount_point, mount_point, sizeof(entry->mount_point));
return entry_index;
}
for (int k = 0; k < MAX_IMAGE_MOUNTS; k++) {
if (!g_image_cache[k].valid) {
(void)strlcpy(g_image_cache[k].path, path, sizeof(g_image_cache[k].path));
(void)strlcpy(g_image_cache[k].mount_point, mount_point,
sizeof(g_image_cache[k].mount_point));
g_image_cache[k].unit_id = -1;
g_image_cache[k].backend = ATTACH_BACKEND_NONE;
g_image_cache[k].valid = true;
return k;
}
}
return -1;
}
bool cache_image_source_mapping(const char *path, const char *mount_point) {
return upsert_image_source_mapping(path, mount_point) >= 0;
}
bool cache_image_mount(const char *path, const char *mount_point, int unit_id,
attach_backend_t backend) {
int entry_index = upsert_image_source_mapping(path, mount_point);
if (entry_index < 0)
return false;
struct ImageCache *entry = &g_image_cache[entry_index];
entry->unit_id = unit_id;
entry->backend = backend;
return true;
}
bool get_image_cache_entry(int index, image_cache_entry_t *entry_out) {
if (!entry_out || index < 0 || index >= MAX_IMAGE_MOUNTS ||
!g_image_cache[index].valid) {
return false;
}
memset(entry_out, 0, sizeof(*entry_out));
(void)strlcpy(entry_out->path, g_image_cache[index].path,
sizeof(entry_out->path));
(void)strlcpy(entry_out->mount_point, g_image_cache[index].mount_point,
sizeof(entry_out->mount_point));
entry_out->unit_id = g_image_cache[index].unit_id;
entry_out->backend = g_image_cache[index].backend;
return true;
}
void invalidate_image_cache_entry(int index) {
if (index < 0 || index >= MAX_IMAGE_MOUNTS)
return;
memset(&g_image_cache[index], 0, sizeof(g_image_cache[index]));
}
bool resolve_device_from_mount_cache(const char *mount_point,
attach_backend_t *backend_out,
int *unit_out) {
int entry_index = find_cache_index(NULL, mount_point);
if (entry_index < 0)
return false;
const struct ImageCache *entry = &g_image_cache[entry_index];
if (entry->backend == ATTACH_BACKEND_NONE || entry->unit_id < 0)
return false;
*backend_out = entry->backend;
*unit_out = entry->unit_id;
return true;
}
bool resolve_image_source_from_mount_cache(const char *mount_point,
char *path_out,
size_t path_out_size) {
int entry_index = find_cache_index(NULL, mount_point);
if (entry_index < 0 || !path_out || path_out_size == 0)
return false;
const struct ImageCache *entry = &g_image_cache[entry_index];
path_out[0] = '\0';
(void)strlcpy(path_out, entry->path, path_out_size);
return path_out[0] != '\0';
}
+186
View File
@@ -0,0 +1,186 @@
#include "sm_platform.h"
#include "sm_runtime.h"
#include "sm_install.h"
#include "sm_types.h"
#include "sm_game_cache.h"
#include "sm_log.h"
#include "sm_filesystem.h"
#include "sm_limits.h"
#include "sm_path_utils.h"
#include "sm_appdb.h"
#include "sm_title_state.h"
#include "sm_image_cache.h"
static bool write_link_file(const char *path, const char *value) {
FILE *f = fopen(path, "w");
if (!f) {
log_debug(" [LINK] open failed for %s: %s", path, strerror(errno));
return false;
}
int saved_errno = 0;
if (fprintf(f, "%s", value) < 0)
saved_errno = errno;
if (fflush(f) != 0 && saved_errno == 0)
saved_errno = errno;
if (fclose(f) != 0 && saved_errno == 0)
saved_errno = errno;
if (saved_errno != 0) {
errno = saved_errno;
log_debug(" [LINK] write failed for %s: %s", path, strerror(errno));
return false;
}
return true;
}
// --- Install/Remount Action ---
static bool mount_and_install(const char *src_path, const char *title_id,
const char *title_name, bool is_remount,
bool should_register) {
char user_app_dir[MAX_PATH];
char user_sce_sys[MAX_PATH];
char src_sce_sys[MAX_PATH];
char image_source_path[MAX_PATH];
bool has_image_source = false;
snprintf(user_app_dir, sizeof(user_app_dir), "/user/app/%s", title_id);
if (is_under_image_mount_base(src_path)) {
has_image_source = resolve_image_source_from_mount_cache(
src_path, image_source_path, sizeof(image_source_path));
if (!has_image_source) {
log_debug(" [LINK] image source lookup failed for %s: %s", title_id,
src_path);
}
}
// COPY FILES
if (!is_remount) {
snprintf(user_sce_sys, sizeof(user_sce_sys), "%s/sce_sys", user_app_dir);
mkdir(user_app_dir, 0777);
mkdir(user_sce_sys, 0777);
snprintf(src_sce_sys, sizeof(src_sce_sys), "%s/sce_sys", src_path);
if (copy_dir(src_sce_sys, user_sce_sys) != 0) {
log_debug(" [COPY] Failed to copy sce_sys: %s -> %s", src_sce_sys,
user_sce_sys);
return false;
}
char icon_src[MAX_PATH], icon_dst[MAX_PATH];
snprintf(icon_src, sizeof(icon_src), "%s/sce_sys/icon0.png", src_path);
snprintf(icon_dst, sizeof(icon_dst), "/user/app/%s/icon0.png", title_id);
if (copy_file(icon_src, icon_dst) != 0) {
log_debug(" [COPY] Failed to copy icon: %s -> %s", icon_src, icon_dst);
return false;
}
} else {
log_debug(" [SPEED] Skipping file copy (Assets already exist)");
}
// WRITE TRACKER
char lnk_path[MAX_PATH];
snprintf(lnk_path, sizeof(lnk_path), "%s/mount.lnk", user_app_dir);
if (!write_link_file(lnk_path, src_path))
return false;
log_debug(" [LINK] mount.lnk created: %s -> %s", lnk_path, src_path);
char img_lnk_path[MAX_PATH];
snprintf(img_lnk_path, sizeof(img_lnk_path), "%s/mount_img.lnk",
user_app_dir);
if (has_image_source) {
if (!write_link_file(img_lnk_path, image_source_path))
return false;
log_debug(" [LINK] mount_img.lnk created: %s -> %s", img_lnk_path,
image_source_path);
if (!cache_image_source_mapping(image_source_path, src_path)) {
log_debug(" [LINK] image source cache update failed: %s -> %s",
src_path, image_source_path);
}
} else if (unlink(img_lnk_path) != 0 && errno != ENOENT) {
log_debug(" [LINK] remove failed for %s: %s", img_lnk_path,
strerror(errno));
}
if (!mount_title_nullfs(title_id, src_path)) {
log_debug(" [LINK] mount.lnk created but nullfs mount failed: title=%s "
"src=%s",
title_id, src_path);
return false;
}
if (!should_register) {
log_debug(" [REG] Skip (already present in app.db)");
return true;
}
// REGISTER
char src_snd0[MAX_PATH];
snprintf(src_snd0, sizeof(src_snd0), "%s/sce_sys/snd0.at9", src_path);
bool has_src_snd0 = (access(src_snd0, F_OK) == 0);
mark_register_attempted(title_id);
int res = sceAppInstUtilAppInstallTitleDir(title_id, "/user/app/", 0);
sceKernelUsleep(200000);
if (res == 0) {
invalidate_app_db_title_cache();
log_debug(" [REG] Installed NEW!");
notify_game_installed_rich(title_id);
if (has_src_snd0) {
int snd0_updates = update_snd0info(title_id);
if (snd0_updates >= 0)
log_debug(" [DB] snd0info updated rows=%d", snd0_updates);
}
} else if ((uint32_t)res == 0x80990002u) {
invalidate_app_db_title_cache();
log_debug(" [REG] Restored.");
if (has_src_snd0) {
int snd0_updates = update_snd0info(title_id);
if (snd0_updates >= 0)
log_debug(" [DB] snd0info updated rows=%d", snd0_updates);
}
// Silent on restore/remount to avoid spam
} else {
log_debug(" [REG] FAIL: 0x%x", res);
notify_system("Register failed: %s (%s)\ncode=0x%08X", title_name, title_id,
(uint32_t)res);
return false;
}
return true;
}
// --- Execution (per discovered candidate) ---
void process_scan_candidates(const scan_candidate_t *candidates,
int candidate_count) {
for (int i = 0; i < candidate_count; i++) {
if (should_stop_requested())
return;
const scan_candidate_t *c = &candidates[i];
if (c->installed) {
log_debug(" [ACTION] Remounting: %s", c->title_name);
} else {
log_debug(" [ACTION] Installing: %s (%s)", c->title_name, c->title_id);
notify_system_info("Installing: %s (%s)...", c->title_name,
c->title_id);
}
if (mount_and_install(c->path, c->title_id, c->title_name, c->installed,
!c->in_app_db)) {
clear_failed_mount_attempts(c->title_id);
cache_game_entry(c->path, c->title_id, c->title_name);
} else {
uint8_t failed_attempts = bump_failed_mount_attempts(c->title_id);
if (failed_attempts == MAX_FAILED_MOUNT_ATTEMPTS) {
log_debug(" [RETRY] limit reached (%u/%u): %s (%s)",
(unsigned)failed_attempts,
(unsigned)MAX_FAILED_MOUNT_ATTEMPTS, c->title_name,
c->title_id);
}
}
}
}
+549
View File
@@ -0,0 +1,549 @@
#include "sm_platform.h"
#include "sm_config_mount.h"
#include "sm_filesystem.h"
#include "sm_kstuff.h"
#include "sm_limits.h"
#include "sm_log.h"
#include "sm_path_utils.h"
#include "sm_time.h"
#include "sm_types.h"
#include <stdatomic.h>
#define KSTUFF_SYSENTVEC_TOGGLE_OFFSET 14
#define KSTUFF_SYSENTVEC_ENABLED 0xdeb7u
#define KSTUFF_SYSENTVEC_DISABLED 0xffffu
typedef struct {
bool active;
bool image_backed;
bool pause_applied;
bool focus_override_active;
pid_t pid;
uint32_t app_id;
uint64_t pause_deadline_us;
char title_id[MAX_TITLE_ID];
} kstuff_game_entry_t;
typedef struct {
intptr_t sysentvec_ps5;
intptr_t sysentvec_ps4;
bool supported;
bool restore_on_empty;
bool current_app_focus_valid;
uint32_t current_app_focus_id;
uint64_t restore_retry_us;
kstuff_game_entry_t game;
} kstuff_state_t;
static kstuff_state_t g_kstuff;
static _Atomic uint32_t g_pending_app_focus_id;
static _Atomic bool g_pending_app_focus_valid;
static bool resolve_kstuff_sysentvec_addrs(intptr_t *ps5_out, intptr_t *ps4_out) {
if (!ps5_out || !ps4_out)
return false;
*ps5_out = 0;
*ps4_out = 0;
switch (kernel_get_fw_version() & 0xffff0000u) {
case 0x1000000:
case 0x1010000:
case 0x1020000:
case 0x1050000:
case 0x1100000:
case 0x1110000:
case 0x1120000:
case 0x1130000:
case 0x1140000:
case 0x2000000:
case 0x2200000:
case 0x2250000:
case 0x2260000:
case 0x2300000:
case 0x2500000:
case 0x2700000:
// Older jailbreak chains usually rely on Byepervisor instead of kstuff.
return false;
case 0x3000000:
case 0x3100000:
case 0x3200000:
case 0x3210000:
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0xca0cd8;
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0xca0e50;
return true;
case 0x4000000:
case 0x4020000:
case 0x4030000:
case 0x4500000:
case 0x4510000:
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0xd11bb8;
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0xd11d30;
return true;
case 0x5000000:
case 0x5020000:
case 0x5100000:
case 0x5500000:
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0xe00be8;
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0xe00d60;
return true;
case 0x6000000:
case 0x6020000:
case 0x6500000:
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0xe210a8;
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0xe21220;
return true;
case 0x7000000:
case 0x7010000:
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0xe21ab8;
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0xe21c30;
return true;
case 0x7200000:
case 0x7400000:
case 0x7600000:
case 0x7610000:
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0xe21b78;
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0xe21cf0;
return true;
case 0x8000000:
case 0x8200000:
case 0x8400000:
case 0x8600000:
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0xe21ca8;
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0xe21e20;
return true;
case 0x9000000:
case 0x9050000:
case 0x9200000:
case 0x9400000:
case 0x9600000:
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0xdba648;
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0xdba7c0;
return true;
case 0x10000000:
case 0x10010000:
case 0x10200000:
case 0x10400000:
case 0x10600000:
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0xdba6d8;
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0xdba850;
return true;
default:
return false;
}
}
static bool kstuff_sysentvec_is_enabled(intptr_t sysentvec_addr) {
return kernel_getshort(sysentvec_addr + KSTUFF_SYSENTVEC_TOGGLE_OFFSET) !=
KSTUFF_SYSENTVEC_DISABLED;
}
static uint16_t read_kstuff_sysentvec_toggle(intptr_t sysentvec_addr) {
return (uint16_t)kernel_getshort(sysentvec_addr +
KSTUFF_SYSENTVEC_TOGGLE_OFFSET);
}
static bool kstuff_sysentvec_toggle_is_known(uint16_t value) {
return value == KSTUFF_SYSENTVEC_ENABLED ||
value == KSTUFF_SYSENTVEC_DISABLED;
}
static void set_kstuff_sysentvec_enabled(intptr_t sysentvec_addr, bool enabled) {
(void)kernel_setshort(sysentvec_addr + KSTUFF_SYSENTVEC_TOGGLE_OFFSET,
enabled ? KSTUFF_SYSENTVEC_ENABLED
: KSTUFF_SYSENTVEC_DISABLED);
}
static bool read_kstuff_enabled_state(bool *ps5_out, bool *ps4_out) {
if (!g_kstuff.supported)
return false;
bool ps5_enabled = kstuff_sysentvec_is_enabled(g_kstuff.sysentvec_ps5);
bool ps4_enabled = kstuff_sysentvec_is_enabled(g_kstuff.sysentvec_ps4);
if (ps5_out)
*ps5_out = ps5_enabled;
if (ps4_out)
*ps4_out = ps4_enabled;
return ps5_enabled && ps4_enabled;
}
static bool kstuff_state_matches(bool enabled, bool ps5_enabled,
bool ps4_enabled) {
return ps5_enabled == enabled && ps4_enabled == enabled;
}
static bool apply_kstuff_enabled_state(bool enabled, bool notify_user,
bool *fully_applied_out);
static void mark_restore_needed(void) {
g_kstuff.restore_on_empty = true;
g_kstuff.restore_retry_us = 0;
}
static uint32_t get_pause_delay_seconds_for_title(const char *title_id,
bool *image_backed_out) {
char source_path[MAX_PATH];
bool image_backed =
read_mount_link(title_id, source_path, sizeof(source_path)) &&
is_under_image_mount_base(source_path);
if (image_backed_out)
*image_backed_out = image_backed;
uint32_t delay_seconds =
image_backed ? runtime_config()->kstuff_pause_delay_image_seconds
: runtime_config()->kstuff_pause_delay_direct_seconds;
uint32_t override_delay_seconds = 0;
if (get_kstuff_pause_delay_override_for_title(title_id,
&override_delay_seconds)) {
return override_delay_seconds;
}
return delay_seconds;
}
static void restore_kstuff_if_needed(const char *reason) {
if (g_kstuff.game.active && g_kstuff.game.pause_applied)
return;
if (!g_kstuff.restore_on_empty)
return;
uint64_t now_us = monotonic_time_us();
if (now_us != 0 && g_kstuff.restore_retry_us != 0 &&
now_us < g_kstuff.restore_retry_us) {
return;
}
if (g_kstuff.game.active && !g_kstuff.game.pause_applied) {
if (g_kstuff.game.pause_deadline_us == 0 ||
(now_us != 0 && now_us >= g_kstuff.game.pause_deadline_us)) {
return;
}
}
bool enabled = sm_kstuff_is_enabled();
if (!enabled)
enabled = sm_kstuff_set_enabled(true, true);
if (enabled) {
log_debug(" [KSTUFF] auto-resumed (%s)%s",
reason ? reason : "no reason",
g_kstuff.game.active ? ", pending delay for another tracked game"
: "");
} else {
log_debug(" [KSTUFF] failed to auto-resume (%s)",
reason ? reason : "no reason");
g_kstuff.restore_retry_us =
now_us == 0 ? 0 : now_us + KSTUFF_RESTORE_RETRY_US;
return;
}
g_kstuff.restore_on_empty = false;
g_kstuff.restore_retry_us = 0;
}
static void maybe_apply_kstuff_pause_for_slot(kstuff_game_entry_t *entry) {
if (!entry || !entry->active || entry->pause_applied)
return;
uint64_t now_us = monotonic_time_us();
if (now_us != 0 && now_us < entry->pause_deadline_us)
return;
if (g_kstuff.current_app_focus_valid && entry->app_id != 0 &&
g_kstuff.current_app_focus_id != entry->app_id) {
return;
}
bool was_enabled = sm_kstuff_is_enabled();
bool fully_disabled = false;
(void)apply_kstuff_enabled_state(false, was_enabled, &fully_disabled);
if (!fully_disabled) {
log_debug(" [KSTUFF] failed to auto-pause for %s pid=%ld",
entry->title_id, (long)entry->pid);
return;
}
if (was_enabled) {
mark_restore_needed();
log_debug(" [KSTUFF] auto-paused for %s pid=%ld (%s launch)",
entry->title_id, (long)entry->pid,
entry->image_backed ? "image" : "direct");
} else {
log_debug(" [KSTUFF] pause deadline reached for %s pid=%ld but kstuff "
"was already disabled", entry->title_id, (long)entry->pid);
}
entry->pause_applied = true;
}
static void handle_kstuff_app_focus_change(uint32_t focused_app_id) {
g_kstuff.current_app_focus_valid = true;
g_kstuff.current_app_focus_id = focused_app_id;
if (!g_kstuff.game.active || g_kstuff.game.app_id == 0)
return;
if (focused_app_id != g_kstuff.game.app_id) {
if (!g_kstuff.game.pause_applied || g_kstuff.game.focus_override_active)
return;
bool enabled = sm_kstuff_is_enabled();
if (!enabled)
enabled = sm_kstuff_set_enabled(true, false);
if (!enabled) {
log_debug(" [KSTUFF] failed to enable on app focus leave "
"(tracked=0x%08X current=0x%08X)",
g_kstuff.game.app_id, focused_app_id);
return;
}
g_kstuff.game.focus_override_active = true;
log_debug(" [KSTUFF] temporarily enabled on app focus leave "
"(tracked=0x%08X current=0x%08X)",
g_kstuff.game.app_id, focused_app_id);
return;
}
if (!g_kstuff.game.focus_override_active)
return;
bool fully_disabled = false;
(void)apply_kstuff_enabled_state(false, false, &fully_disabled);
if (!fully_disabled) {
log_debug(" [KSTUFF] failed to restore disabled state on app focus return "
"(app_id=0x%08X)",
focused_app_id);
return;
}
g_kstuff.game.focus_override_active = false;
log_debug(" [KSTUFF] restored disabled state on app focus return "
"(app_id=0x%08X)",
focused_app_id);
}
uint64_t sm_kstuff_game_next_wake_us(uint64_t now_us) {
if (!sm_kstuff_game_feature_enabled())
return 0;
uint64_t next_wake_us = 0;
if (g_kstuff.game.active && !g_kstuff.game.pause_applied) {
if (g_kstuff.current_app_focus_valid && g_kstuff.game.app_id != 0 &&
g_kstuff.current_app_focus_id != g_kstuff.game.app_id) {
next_wake_us = 0;
} else if (g_kstuff.game.pause_deadline_us != 0) {
next_wake_us = g_kstuff.game.pause_deadline_us;
} else if (now_us == 0) {
next_wake_us = 1;
} else {
next_wake_us = now_us + GAME_LIFECYCLE_POLL_INTERVAL_US;
}
}
if (g_kstuff.restore_on_empty) {
uint64_t restore_wake_us = g_kstuff.restore_retry_us;
if (restore_wake_us == 0) {
if (g_kstuff.game.active && !g_kstuff.game.pause_applied) {
if (g_kstuff.game.pause_deadline_us != 0) {
restore_wake_us = g_kstuff.game.pause_deadline_us;
} else if (now_us == 0) {
restore_wake_us = 1;
} else {
restore_wake_us = now_us + GAME_LIFECYCLE_POLL_INTERVAL_US;
}
} else {
restore_wake_us = now_us == 0 ? 1 : now_us;
}
}
if (next_wake_us == 0 || restore_wake_us < next_wake_us)
next_wake_us = restore_wake_us;
}
return next_wake_us;
}
bool sm_kstuff_is_supported(void) {
return g_kstuff.supported;
}
bool sm_kstuff_is_enabled(void) {
return read_kstuff_enabled_state(NULL, NULL);
}
static bool apply_kstuff_enabled_state(bool enabled, bool notify_user,
bool *fully_applied_out) {
if (!g_kstuff.supported)
return false;
bool ps5_enabled = false;
bool ps4_enabled = false;
bool is_enabled = read_kstuff_enabled_state(&ps5_enabled, &ps4_enabled);
bool fully_applied = kstuff_state_matches(enabled, ps5_enabled, ps4_enabled);
if (fully_applied) {
if (fully_applied_out)
*fully_applied_out = true;
return is_enabled;
}
if (ps5_enabled != enabled)
set_kstuff_sysentvec_enabled(g_kstuff.sysentvec_ps5, enabled);
if (ps4_enabled != enabled)
set_kstuff_sysentvec_enabled(g_kstuff.sysentvec_ps4, enabled);
is_enabled = read_kstuff_enabled_state(&ps5_enabled, &ps4_enabled);
fully_applied = kstuff_state_matches(enabled, ps5_enabled, ps4_enabled);
if (fully_applied_out)
*fully_applied_out = fully_applied;
if (fully_applied) {
log_debug(" [KSTUFF] %s both sysentvecs", enabled ? "enabled" : "disabled");
if (notify_user) {
notify_system_info(enabled ? "KStuff active again"
: "KStuff paused while the game is running");
}
} else {
log_debug(" [KSTUFF] %s request incomplete (ps5=%s ps4=%s)",
enabled ? "enable" : "disable", ps5_enabled ? "on" : "off",
ps4_enabled ? "on" : "off");
}
return is_enabled;
}
bool sm_kstuff_set_enabled(bool enabled, bool notify_user) {
return apply_kstuff_enabled_state(enabled, notify_user, NULL);
}
bool sm_kstuff_game_feature_enabled(void) {
return runtime_config()->kstuff_game_auto_toggle && sm_kstuff_is_supported();
}
void sm_kstuff_game_on_exec(pid_t pid, const char *title_id, uint32_t app_id,
uint64_t exec_time_us) {
if (!sm_kstuff_game_feature_enabled())
return;
if (g_kstuff.game.active) {
if (g_kstuff.game.pid == pid) {
log_debug(" [KSTUFF] already tracking pid=%ld for %s", (long)pid,
title_id);
return;
}
log_debug(" [KSTUFF] handoff tracked game pid=%ld (%s) -> pid=%ld (%s)",
(long)g_kstuff.game.pid, g_kstuff.game.title_id, (long)pid,
title_id);
memset(&g_kstuff.game, 0, sizeof(g_kstuff.game));
mark_restore_needed();
restore_kstuff_if_needed("tracked game handoff");
}
if (is_kstuff_pause_disabled_for_title(title_id)) {
log_debug(" [KSTUFF] auto-pause disabled by config for %s pid=%ld "
"app_id=0x%08X", title_id, (long)pid, app_id);
return;
}
bool image_backed = false;
uint32_t delay_seconds =
get_pause_delay_seconds_for_title(title_id, &image_backed);
uint64_t now_us = monotonic_time_us();
uint64_t base_time_us = exec_time_us != 0 ? exec_time_us : now_us;
g_kstuff.game.active = true;
g_kstuff.game.image_backed = image_backed;
g_kstuff.game.pause_applied = false;
g_kstuff.game.focus_override_active = false;
g_kstuff.game.pid = pid;
g_kstuff.game.app_id = app_id;
g_kstuff.game.pause_deadline_us =
base_time_us == 0 ? 0 : base_time_us + (uint64_t)delay_seconds * 1000000ull;
(void)strlcpy(g_kstuff.game.title_id, title_id, sizeof(g_kstuff.game.title_id));
log_debug(" [KSTUFF] tracking game launch: %s pid=%ld app_id=0x%08X source=%s "
"pause_delay=%us", title_id, (long)pid, app_id,
image_backed ? "image" : "direct", delay_seconds);
}
void sm_kstuff_note_app_focus(uint32_t app_id) {
atomic_store(&g_pending_app_focus_id, app_id);
atomic_store(&g_pending_app_focus_valid, true);
}
void sm_kstuff_game_on_exit(pid_t pid) {
if (!g_kstuff.game.active || g_kstuff.game.pid != pid)
return;
log_debug(" [KSTUFF] game stopped: %s pid=%ld",
g_kstuff.game.title_id, (long)pid);
memset(&g_kstuff.game, 0, sizeof(g_kstuff.game));
if (!g_kstuff.game.active)
mark_restore_needed();
restore_kstuff_if_needed("tracked game exit");
}
void sm_kstuff_game_poll(void) {
if (!sm_kstuff_game_feature_enabled())
return;
if (atomic_exchange(&g_pending_app_focus_valid, false)) {
handle_kstuff_app_focus_change(atomic_load(&g_pending_app_focus_id));
}
restore_kstuff_if_needed("pending auto-resume");
maybe_apply_kstuff_pause_for_slot(&g_kstuff.game);
}
void sm_kstuff_game_shutdown(void) {
memset(&g_kstuff.game, 0, sizeof(g_kstuff.game));
restore_kstuff_if_needed("watcher shutdown");
}
void sm_kstuff_init(void) {
memset(&g_kstuff, 0, sizeof(g_kstuff));
atomic_store(&g_pending_app_focus_id, 0);
atomic_store(&g_pending_app_focus_valid, false);
if (!resolve_kstuff_sysentvec_addrs(&g_kstuff.sysentvec_ps5,
&g_kstuff.sysentvec_ps4)) {
log_debug(" [KSTUFF] runtime control unavailable on firmware 0x%08X",
kernel_get_fw_version());
return;
}
uint16_t ps5_toggle = read_kstuff_sysentvec_toggle(g_kstuff.sysentvec_ps5);
uint16_t ps4_toggle = read_kstuff_sysentvec_toggle(g_kstuff.sysentvec_ps4);
if (!kstuff_sysentvec_toggle_is_known(ps5_toggle) ||
!kstuff_sysentvec_toggle_is_known(ps4_toggle)) {
log_debug(" [KSTUFF] runtime control disabled: kstuff markers not "
"present (ps5=0x%04X ps4=0x%04X)",
ps5_toggle, ps4_toggle);
return;
}
g_kstuff.supported = true;
log_debug(" [KSTUFF] runtime control ready (image_delay=%u "
"direct_delay=%u)",
runtime_config()->kstuff_pause_delay_image_seconds,
runtime_config()->kstuff_pause_delay_direct_seconds);
}
void sm_kstuff_shutdown(void) {
sm_kstuff_game_shutdown();
atomic_store(&g_pending_app_focus_id, 0);
atomic_store(&g_pending_app_focus_valid, false);
memset(&g_kstuff, 0, sizeof(g_kstuff));
}
+370
View File
@@ -0,0 +1,370 @@
#include "sm_platform.h"
#include "sm_log.h"
#include "sm_config_mount.h"
#include "sm_types.h"
#include "sm_paths.h"
static sm_error_t g_last_error;
static bool g_notifications_initialized = false;
extern unsigned char smp_icon_png[];
extern unsigned int smp_icon_png_len;
#define SCE_NOTIFICATION_LOCAL_USER_ID_SYSTEM 0xFE
int sceNotificationSend(int userId, bool isLogged, const char *payload);
int sceNotificationSendById(int userid, bool logged_in, const char *useCaseId,
const char *message);
static void notify_system_plain_message(const char *message) {
notify_request_t req;
memset(&req, 0, sizeof(req));
(void)strlcpy(req.message, message, sizeof(req.message));
sceKernelSendNotificationRequest(0, &req, sizeof(req), 0);
log_debug("NOTIFY: %s", req.message);
}
static bool ensure_notification_icon_present(void) {
struct stat st;
if (stat(NOTIFY_ICON_FILE, &st) == 0 && st.st_size > 0)
return true;
(void)mkdir("/user/data", 0777);
(void)mkdir(NOTIFY_ICON_DIR, 0777);
FILE *fp = fopen(NOTIFY_ICON_FILE, "wb");
if (!fp) {
log_debug(" [NOTIFY] icon create failed: %s (%s)", NOTIFY_ICON_FILE,
strerror(errno));
return false;
}
size_t written = fwrite(smp_icon_png, 1, smp_icon_png_len, fp);
int saved_errno = 0;
if (written != smp_icon_png_len)
saved_errno = ferror(fp) ? errno : EIO;
if (fflush(fp) != 0 && saved_errno == 0)
saved_errno = errno;
if (fclose(fp) != 0 && saved_errno == 0)
saved_errno = errno;
if (saved_errno != 0) {
errno = saved_errno;
log_debug(" [NOTIFY] icon write failed: %s (%s)", NOTIFY_ICON_FILE,
strerror(errno));
(void)unlink(NOTIFY_ICON_FILE);
return false;
}
log_debug(" [NOTIFY] icon created: %s", NOTIFY_ICON_FILE);
return true;
}
void sm_notifications_init(void) {
if (g_notifications_initialized)
return;
g_notifications_initialized = ensure_notification_icon_present();
}
static void append_json_escaped(char *dst, size_t dst_size, const char *src) {
size_t used = strlen(dst);
if (used >= dst_size)
return;
for (; *src != '\0' && used + 1 < dst_size; ++src) {
const char *escape = NULL;
char single[2] = {0};
switch (*src) {
case '\\':
escape = "\\\\";
break;
case '"':
escape = "\\\"";
break;
case '\n':
escape = "\\n";
break;
case '\r':
escape = "\\r";
break;
case '\t':
escape = "\\t";
break;
default:
single[0] = *src;
escape = single;
break;
}
size_t escape_len = strlen(escape);
if (used + escape_len >= dst_size)
break;
memcpy(dst + used, escape, escape_len);
used += escape_len;
dst[used] = '\0';
}
}
static bool build_notification_timestamp(time_t timestamp, char out[32]) {
struct tm tm_utc;
if (gmtime_r(&timestamp, &tm_utc) == NULL)
return false;
return strftime(out, 32, "%Y-%m-%dT%H:%M:%S.000Z", &tm_utc) != 0;
}
static bool build_notification_metadata(char *created_at,
char *notification_id,
size_t notification_id_size) {
time_t now;
now = time(NULL);
if (!build_notification_timestamp(now, created_at))
return false;
snprintf(notification_id, notification_id_size, "%u",
(unsigned)((uint32_t)now ^ (uint32_t)getpid()));
return true;
}
static bool send_rich_notification(const char *message) {
char escaped_message[4096];
char escaped_version[128];
char payload[8192];
char created_at[32];
char notification_id[32];
if (!message || message[0] == '\0')
return false;
sm_notifications_init();
if (!g_notifications_initialized)
return false;
escaped_message[0] = '\0';
escaped_version[0] = '\0';
append_json_escaped(escaped_message, sizeof(escaped_message), message);
append_json_escaped(escaped_version, sizeof(escaped_version),
SHADOWMOUNT_VERSION);
if (!build_notification_metadata(created_at, notification_id,
sizeof(notification_id))) {
return false;
}
int len = snprintf(
payload, sizeof(payload),
"{\n"
" \"rawData\": {\n"
" \"viewTemplateType\": \"InteractiveToastTemplateB\",\n"
" \"channelType\": \"ServiceFeedback\",\n"
" \"bundleName\": \"ShadowMountPlusWelcome\",\n"
" \"useCaseId\": \"IDC\",\n"
" \"soundEffect\": \"none\",\n"
" \"toastOverwriteType\": \"InQueue\",\n"
" \"isImmediate\": true,\n"
" \"priority\": 100,\n"
" \"viewData\": {\n"
" \"icon\": {\n"
" \"type\": \"Url\",\n"
" \"parameters\": {\n"
" \"url\": \"" NOTIFY_ICON_FILE "\"\n"
" }\n"
" },\n"
" \"message\": {\n"
" \"body\": \"%s\"\n"
" },\n"
" \"subMessage\": {\n"
" \"body\": \"ShadowMountPlus %s\"\n"
" },\n"
" \"actions\": [\n"
" {\n"
" \"actionName\": \"Go to Debug Settings\",\n"
" \"actionType\": \"DeepLink\",\n"
" \"defaultFocus\": true,\n"
" \"parameters\": {\n"
" \"actionUrl\": \"pssettings:play?function=debug_settings\"\n"
" }\n"
" }\n"
" ]\n"
" },\n"
" \"platformViews\": {\n"
" \"previewDisabled\": {\n"
" \"viewData\": {\n"
" \"icon\": {\n"
" \"type\": \"Predefined\",\n"
" \"parameters\": {\n"
" \"icon\": \"community\"\n"
" }\n"
" },\n"
" \"message\": {\n"
" \"body\": \"%s\"\n"
" }\n"
" }\n"
" }\n"
" }\n"
" },\n"
" \"createdDateTime\": \"%s\",\n"
" \"localNotificationId\": \"%s\"\n"
"}",
escaped_message, escaped_version, escaped_message, created_at,
notification_id);
if (len < 0 || (size_t)len >= sizeof(payload))
return false;
int rc = sceNotificationSend(SCE_NOTIFICATION_LOCAL_USER_ID_SYSTEM, true,
payload);
return rc == 0;
}
static bool send_game_installed_rich_notification(const char *title_id) {
char escaped_title_id[128];
char params[256];
if (!title_id || title_id[0] == '\0')
return false;
escaped_title_id[0] = '\0';
append_json_escaped(escaped_title_id, sizeof(escaped_title_id), title_id);
int len = snprintf(params, sizeof(params),
"{\"npTitleId\":\"%s_00\"}", escaped_title_id);
if (len < 0 || (size_t)len >= sizeof(params))
return false;
int rc = sceNotificationSendById(SCE_NOTIFICATION_LOCAL_USER_ID_SYSTEM, true,
"NUC240", params);
return rc == 0;
}
static void notify_system_plain_v(const char *fmt, va_list args) {
char message[3075];
vsnprintf(message, sizeof(message), fmt, args);
notify_system_plain_message(message);
}
static void log_to_file(const char *fmt, va_list args) {
mkdir(LOG_DIR, 0777);
FILE *fp = fopen(LOG_FILE, "a");
if (fp) {
va_list args_file;
time_t rawtime;
struct tm *timeinfo;
char buffer[80];
time(&rawtime);
timeinfo = localtime(&rawtime);
strftime(buffer, sizeof(buffer), "%H:%M:%S", timeinfo);
va_copy(args_file, args);
fprintf(fp, "[%s] ", buffer);
vfprintf(fp, fmt, args_file);
fprintf(fp, "\n");
va_end(args_file);
fclose(fp);
}
}
void log_debug(const char *fmt, ...) {
if (runtime_config()->debug_enabled == false)
return;
va_list args;
va_list args_copy;
va_start(args, fmt);
va_copy(args_copy, args);
vprintf(fmt, args);
printf("\n");
log_to_file(fmt, args_copy);
va_end(args_copy);
va_end(args);
}
void notify_system_rich(bool allow_in_quiet_mode, const char *fmt, ...) {
char message[3075];
va_list args;
if (runtime_config()->quiet_mode && !allow_in_quiet_mode)
return;
va_start(args, fmt);
vsnprintf(message, sizeof(message), fmt, args);
va_end(args);
if (!send_rich_notification(message)) {
notify_system_plain_message(message);
return;
}
log_debug("NOTIFY: %s", message);
}
void notify_game_installed_rich(const char *title_id) {
if (!send_game_installed_rich_notification(title_id)) {
notify_system_info("Installed: %s",
(title_id && title_id[0] != '\0') ? title_id : "Unknown");
return;
}
log_debug("NOTIFY: installed game %s", title_id ? title_id : "unknown");
}
void notify_system_info(const char *fmt, ...) {
if (runtime_config()->quiet_mode)
return;
va_list args;
va_start(args, fmt);
notify_system_plain_v(fmt, args);
va_end(args);
}
void sm_error_clear(void) {
memset(&g_last_error, 0, sizeof(g_last_error));
}
void sm_error_set(const char *subsystem, int code, const char *path,
const char *fmt, ...) {
memset(&g_last_error, 0, sizeof(g_last_error));
g_last_error.valid = true;
g_last_error.code = code;
if (subsystem && subsystem[0] != '\0')
(void)strlcpy(g_last_error.subsystem, subsystem,
sizeof(g_last_error.subsystem));
if (path && path[0] != '\0')
(void)strlcpy(g_last_error.path, path, sizeof(g_last_error.path));
if (fmt && fmt[0] != '\0') {
va_list args;
va_start(args, fmt);
vsnprintf(g_last_error.message, sizeof(g_last_error.message), fmt, args);
va_end(args);
}
}
const sm_error_t *sm_last_error(void) {
return &g_last_error;
}
bool sm_error_notified(void) {
return g_last_error.notified;
}
void sm_error_mark_notified(void) {
g_last_error.notified = true;
}
void notify_system(const char *fmt, ...) {
va_list args;
va_start(args, fmt);
notify_system_plain_v(fmt, args);
va_end(args);
}
void notify_image_mount_failed(const char *path, int mount_err) {
if (g_last_error.valid && g_last_error.message[0] != '\0') {
notify_system("Image mount failed: 0x%08X (%s)\n%s\n%s",
(uint32_t)mount_err, strerror(mount_err),
g_last_error.message,
(g_last_error.path[0] != '\0') ? g_last_error.path : path);
g_last_error.notified = true;
return;
}
notify_system("Image mount failed: 0x%08X (%s)\n%s", (uint32_t)mount_err,
strerror(mount_err), path);
}
+252
View File
@@ -0,0 +1,252 @@
#include "sm_platform.h"
#include "sm_runtime.h"
#include "sm_mount_device.h"
#include "sm_image_cache.h"
#include "sm_limits.h"
#include "sm_log.h"
#include "sm_config_mount.h"
#include "sm_mount_defs.h"
#include "sm_stability.h"
const char *attach_backend_name(attach_backend_t backend) {
if (backend == ATTACH_BACKEND_LVD)
return "LVD";
if (backend == ATTACH_BACKEND_MD)
return "MD";
return "UNKNOWN";
}
// --- Device Node Wait and Source Stability ---
static bool wait_for_dev_node_state(const char *devname, bool should_exist) {
for (int i = 0; i < LVD_NODE_WAIT_RETRIES; i++) {
if ((access(devname, F_OK) == 0) == should_exist)
return true;
sceKernelUsleep(LVD_NODE_WAIT_US);
}
return false;
}
bool is_source_stable_for_mount(const char *path, const char *name,
const char *tag) {
double age = 0.0;
int st_err = 0;
if (is_path_stable_now(path, &age, &st_err))
return true;
if (st_err != 0)
return false;
log_debug(" [%s] %s modified %.0fs ago, waiting...", tag, name, age);
return false;
}
// --- Mounted Device Resolution (/dev/lvdN, /dev/mdN) ---
static bool parse_unit_from_dev_path(const char *dev_path, const char *prefix,
int *unit_out) {
size_t prefix_len = strlen(prefix);
if (!dev_path || strncmp(dev_path, prefix, prefix_len) != 0)
return false;
char *end = NULL;
long unit = strtol(dev_path + prefix_len, &end, 10);
if (end == dev_path + prefix_len || *end != '\0' || unit < 0 ||
unit > INT_MAX)
return false;
*unit_out = (int)unit;
return true;
}
bool resolve_device_from_mount(const char *mount_point,
attach_backend_t *backend_out, int *unit_out) {
if (!mount_point || !backend_out || !unit_out)
return false;
*backend_out = ATTACH_BACKEND_NONE;
*unit_out = -1;
if (resolve_device_from_mount_cache(mount_point, backend_out, unit_out))
return true;
struct statfs sfs;
if (statfs(mount_point, &sfs) != 0)
return false;
if (strcmp(sfs.f_mntonname, mount_point) != 0)
return false;
if (parse_unit_from_dev_path(sfs.f_mntfromname, "/dev/lvd", unit_out)) {
*backend_out = ATTACH_BACKEND_LVD;
return true;
}
if (parse_unit_from_dev_path(sfs.f_mntfromname, "/dev/md", unit_out)) {
*backend_out = ATTACH_BACKEND_MD;
return true;
}
struct statfs *mntbuf = NULL;
int mntcount = getmntinfo(&mntbuf, MNT_NOWAIT);
if (mntcount <= 0 || !mntbuf)
return false;
for (int i = 0; i < mntcount; i++) {
if (strcmp(mntbuf[i].f_mntonname, mount_point) != 0)
continue;
if (parse_unit_from_dev_path(mntbuf[i].f_mntfromname, "/dev/lvd", unit_out)) {
*backend_out = ATTACH_BACKEND_LVD;
return true;
}
if (parse_unit_from_dev_path(mntbuf[i].f_mntfromname, "/dev/md", unit_out)) {
*backend_out = ATTACH_BACKEND_MD;
return true;
}
}
return false;
}
static bool is_path_mountpoint(const char *path) {
if (!path || path[0] == '\0')
return false;
struct statfs sfs;
return (statfs(path, &sfs) == 0 && strcmp(sfs.f_mntonname, path) == 0);
}
bool is_active_image_mount_point(const char *path) {
return is_path_mountpoint(path);
}
bool wait_for_lvd_release(void) {
for (unsigned int waited_us = 0;; waited_us += LVD_RELEASE_WAIT_POLL_US) {
struct statfs *mntbuf = NULL;
int mntcount = getmntinfo(&mntbuf, MNT_NOWAIT);
bool mounted = false;
for (int i = 0; i < mntcount && mntbuf; i++) {
if (strcmp(mntbuf[i].f_mntfromname, "/dev/lvd2") != 0)
continue;
mounted = true;
break;
}
if (!mounted) {
if (waited_us != 0)
log_debug(" [IMG][LVD] /dev/lvd2 released");
return true;
}
if (waited_us == 0) {
log_debug(" [IMG][LVD] waiting for /dev/lvd2 to be released...");
for (int i = 0; i < mntcount && mntbuf; i++) {
if (strncmp(mntbuf[i].f_mntfromname, "/dev/lvd", 8) != 0)
continue;
log_debug(" [IMG][LVD] mounted: from=%s path=%s type=%s "
"bsize=%llu iosize=%llu blocks=%llu bfree=%llu "
"bavail=%llu files=%llu ffree=%llu flags=0x%lX",
mntbuf[i].f_mntfromname, mntbuf[i].f_mntonname,
mntbuf[i].f_fstypename,
(unsigned long long)(uint64_t)mntbuf[i].f_bsize,
(unsigned long long)(uint64_t)mntbuf[i].f_iosize,
(unsigned long long)(uint64_t)mntbuf[i].f_blocks,
(unsigned long long)(uint64_t)mntbuf[i].f_bfree,
(unsigned long long)(uint64_t)mntbuf[i].f_bavail,
(unsigned long long)(uint64_t)mntbuf[i].f_files,
(unsigned long long)(uint64_t)mntbuf[i].f_ffree,
(unsigned long)mntbuf[i].f_flags);
}
}
if (should_stop_requested())
return false;
if (waited_us >= LVD_RELEASE_WAIT_MAX_US) {
log_debug(" [IMG][LVD] /dev/lvd2 wait timeout reached (%u ms), "
"continuing startup", LVD_RELEASE_WAIT_MAX_US / 1000u);
return true;
}
sceKernelUsleep(LVD_RELEASE_WAIT_POLL_US);
}
}
// --- Device Detach Helpers ---
static bool detach_lvd_unit(int unit_id) {
if (unit_id < 0)
return true;
int fd = open(LVD_CTRL_PATH, O_RDWR);
if (fd < 0) {
log_debug(" [IMG][%s] open %s for detach failed: %s",
attach_backend_name(ATTACH_BACKEND_LVD), LVD_CTRL_PATH,
strerror(errno));
return false;
}
lvd_ioctl_detach_t req;
memset(&req, 0, sizeof(req));
req.device_id = unit_id;
bool ok = true;
if (ioctl(fd, SCE_LVD_IOC_DETACH, &req) != 0) {
log_debug(" [IMG][%s] detach %d failed: %s",
attach_backend_name(ATTACH_BACKEND_LVD), unit_id, strerror(errno));
ok = false;
}
close(fd);
char devname[64];
snprintf(devname, sizeof(devname), "/dev/lvd%d", unit_id);
if (!wait_for_dev_node_state(devname, false)) {
log_debug(" [IMG][%s] device node still present after detach: /dev/lvd%d",
attach_backend_name(ATTACH_BACKEND_LVD), unit_id);
ok = false;
}
return ok;
}
static bool detach_md_unit(int unit_id) {
if (unit_id < 0)
return true;
int fd = open(MD_CTRL_PATH, O_RDWR);
if (fd < 0) {
log_debug(" [IMG][%s] open %s for detach failed: %s",
attach_backend_name(ATTACH_BACKEND_MD), MD_CTRL_PATH,
strerror(errno));
return false;
}
struct md_ioctl req;
memset(&req, 0, sizeof(req));
req.md_version = MDIOVERSION;
req.md_unit = (unsigned int)unit_id;
bool ok = true;
if (ioctl(fd, MDIOCDETACH, &req) != 0) {
int err = errno;
req.md_options = MD_FORCE;
if (ioctl(fd, MDIOCDETACH, &req) != 0) {
log_debug(" [IMG][%s] detach %d failed: %s",
attach_backend_name(ATTACH_BACKEND_MD), unit_id,
strerror(errno));
ok = false;
} else {
log_debug(" [IMG][%s] detach %d forced after error: %s",
attach_backend_name(ATTACH_BACKEND_MD), unit_id,
strerror(err));
}
}
close(fd);
char devname[64];
snprintf(devname, sizeof(devname), "/dev/md%d", unit_id);
if (!wait_for_dev_node_state(devname, false)) {
log_debug(" [IMG][%s] device node still present after detach: /dev/md%d",
attach_backend_name(ATTACH_BACKEND_MD), unit_id);
ok = false;
}
return ok;
}
bool detach_attached_unit(attach_backend_t backend, int unit_id) {
if (backend == ATTACH_BACKEND_MD)
return detach_md_unit(unit_id);
else if (backend == ATTACH_BACKEND_LVD)
return detach_lvd_unit(unit_id);
return true;
}
+253
View File
@@ -0,0 +1,253 @@
#include "sm_platform.h"
#include "sm_path_state.h"
#include "sm_hash.h"
#include "sm_limits.h"
#include "sm_log.h"
#include "sm_path_utils.h"
struct PathStateEntry {
char path[MAX_PATH];
uint8_t missing_param_attempts;
uint8_t image_mount_attempts;
bool missing_param_limit_logged;
bool image_mount_limit_logged;
bool game_info_cached;
bool game_info_valid;
time_t game_info_mtime;
off_t game_info_size;
ino_t game_info_ino;
char game_title_id[MAX_TITLE_ID];
char game_title_name[MAX_TITLE_NAME];
bool valid;
};
static struct PathStateEntry g_path_state[PATH_STATE_CAPACITY];
static uint16_t g_path_state_hash[STATE_HASH_SIZE];
static void rebuild_path_state_hash(void) {
memset(g_path_state_hash, 0, sizeof(g_path_state_hash));
for (int k = 0; k < PATH_STATE_CAPACITY; k++) {
if (!g_path_state[k].valid || g_path_state[k].path[0] == '\0')
continue;
uint32_t slot = sm_fnv1a32(g_path_state[k].path) & (STATE_HASH_SIZE - 1u);
for (uint32_t i = 0; i < STATE_HASH_SIZE; i++) {
if (g_path_state_hash[slot] == 0) {
g_path_state_hash[slot] = (uint16_t)(k + 1);
break;
}
slot = (slot + 1u) & (STATE_HASH_SIZE - 1u);
}
}
}
static struct PathStateEntry *find_path_state(const char *path) {
if (!path || path[0] == '\0')
return NULL;
uint32_t slot = sm_fnv1a32(path) & (STATE_HASH_SIZE - 1u);
for (uint32_t i = 0; i < STATE_HASH_SIZE; i++) {
uint16_t idx = g_path_state_hash[slot];
if (idx == 0)
return NULL;
struct PathStateEntry *entry = &g_path_state[idx - 1u];
if (entry->valid && strcmp(entry->path, path) == 0)
return entry;
slot = (slot + 1u) & (STATE_HASH_SIZE - 1u);
}
return NULL;
}
static struct PathStateEntry *create_path_state(const char *path) {
if (!path || path[0] == '\0')
return NULL;
int slot_k = -1;
for (int k = 0; k < PATH_STATE_CAPACITY; k++) {
if (!g_path_state[k].valid) {
slot_k = k;
break;
}
}
if (slot_k < 0) {
int evict_k = -1;
for (int k = 0; k < PATH_STATE_CAPACITY; k++) {
if (!g_path_state[k].valid)
continue;
if (access(g_path_state[k].path, F_OK) != 0) {
evict_k = k;
break;
}
}
if (evict_k < 0) {
for (int k = 0; k < PATH_STATE_CAPACITY; k++) {
if (!g_path_state[k].valid)
continue;
if (g_path_state[k].missing_param_attempts == 0 &&
g_path_state[k].image_mount_attempts == 0 &&
!g_path_state[k].game_info_cached) {
evict_k = k;
break;
}
}
}
if (evict_k < 0)
evict_k = 0;
memset(&g_path_state[evict_k], 0, sizeof(g_path_state[evict_k]));
rebuild_path_state_hash();
slot_k = evict_k;
}
memset(&g_path_state[slot_k], 0, sizeof(g_path_state[slot_k]));
g_path_state[slot_k].valid = true;
(void)strlcpy(g_path_state[slot_k].path, path,
sizeof(g_path_state[slot_k].path));
uint32_t slot = sm_fnv1a32(path) & (STATE_HASH_SIZE - 1u);
for (uint32_t i = 0; i < STATE_HASH_SIZE; i++) {
if (g_path_state_hash[slot] == 0) {
g_path_state_hash[slot] = (uint16_t)(slot_k + 1);
return &g_path_state[slot_k];
}
slot = (slot + 1u) & (STATE_HASH_SIZE - 1u);
}
g_path_state[slot_k].valid = false;
g_path_state[slot_k].path[0] = '\0';
rebuild_path_state_hash();
return NULL;
}
static struct PathStateEntry *get_or_create_path_state(const char *path) {
struct PathStateEntry *entry = find_path_state(path);
return entry ? entry : create_path_state(path);
}
bool load_cached_game_info(const char *path, const struct stat *param_st,
char *out_id, char *out_name, bool *valid_out) {
if (valid_out)
*valid_out = false;
if (!path || !param_st)
return false;
struct PathStateEntry *entry = find_path_state(path);
if (!entry || !entry->game_info_cached ||
entry->game_info_mtime != param_st->st_mtime ||
entry->game_info_size != param_st->st_size ||
entry->game_info_ino != param_st->st_ino) {
return false;
}
if (valid_out)
*valid_out = entry->game_info_valid;
if (!entry->game_info_valid)
return true;
if (out_id)
(void)strlcpy(out_id, entry->game_title_id, MAX_TITLE_ID);
if (out_name)
(void)strlcpy(out_name, entry->game_title_name, MAX_TITLE_NAME);
return true;
}
void store_cached_game_info(const char *path, const struct stat *param_st,
bool valid, const char *title_id,
const char *title_name) {
if (!path || !param_st)
return;
struct PathStateEntry *entry = get_or_create_path_state(path);
if (!entry)
return;
entry->game_info_cached = true;
entry->game_info_valid = valid;
entry->game_info_mtime = param_st->st_mtime;
entry->game_info_size = param_st->st_size;
entry->game_info_ino = param_st->st_ino;
(void)strlcpy(entry->game_title_id, valid ? title_id : "", MAX_TITLE_ID);
(void)strlcpy(entry->game_title_name, valid ? title_name : "", MAX_TITLE_NAME);
}
void prune_path_state(void) {
bool changed = false;
for (int k = 0; k < PATH_STATE_CAPACITY; k++) {
if (!g_path_state[k].valid || g_path_state[k].path[0] == '\0')
continue;
if (access(g_path_state[k].path, F_OK) == 0)
continue;
memset(&g_path_state[k], 0, sizeof(g_path_state[k]));
changed = true;
}
if (changed)
rebuild_path_state_hash();
}
bool is_missing_param_scan_limited(const char *path) {
if (!is_under_image_mount_base(path))
return false;
struct PathStateEntry *entry = find_path_state(path);
if (!entry)
return false;
return entry->missing_param_attempts >= MAX_MISSING_PARAM_SCAN_ATTEMPTS;
}
void record_missing_param_failure(const char *path) {
if (!is_under_image_mount_base(path))
return;
struct PathStateEntry *entry = get_or_create_path_state(path);
if (!entry) {
log_debug(" [SCAN] missing/invalid param.json: %s", path);
notify_system("Missing/invalid param.json:\n%s", path);
return;
}
if (entry->missing_param_attempts < UINT8_MAX)
entry->missing_param_attempts++;
log_debug(" [SCAN] missing/invalid param.json: %s", path);
if (entry->missing_param_attempts == 1)
notify_system("Missing/invalid param.json:\n%s", path);
if (entry->missing_param_attempts >= MAX_MISSING_PARAM_SCAN_ATTEMPTS &&
!entry->missing_param_limit_logged) {
log_debug(" [SCAN] attempt limit reached (%u), skipping path: %s",
(unsigned)MAX_MISSING_PARAM_SCAN_ATTEMPTS, path);
entry->missing_param_limit_logged = true;
}
}
void clear_missing_param_entry(const char *path) {
struct PathStateEntry *entry = find_path_state(path);
if (!entry)
return;
entry->missing_param_attempts = 0;
entry->missing_param_limit_logged = false;
}
bool is_image_mount_limited(const char *path) {
struct PathStateEntry *entry = find_path_state(path);
return entry ? entry->image_mount_attempts >= MAX_IMAGE_MOUNT_ATTEMPTS
: false;
}
uint8_t bump_image_mount_attempts(const char *path) {
struct PathStateEntry *entry = get_or_create_path_state(path);
if (!entry)
return MAX_IMAGE_MOUNT_ATTEMPTS;
if (entry->image_mount_attempts < UINT8_MAX)
entry->image_mount_attempts++;
if (entry->image_mount_attempts >= MAX_IMAGE_MOUNT_ATTEMPTS &&
!entry->image_mount_limit_logged) {
log_debug(" [IMG] retry limit reached (%u/%u), skipping image: %s",
(unsigned)entry->image_mount_attempts,
(unsigned)MAX_IMAGE_MOUNT_ATTEMPTS, path);
entry->image_mount_limit_logged = true;
}
return entry->image_mount_attempts;
}
void clear_image_mount_attempts(const char *path) {
struct PathStateEntry *entry = find_path_state(path);
if (!entry)
return;
entry->image_mount_attempts = 0;
entry->image_mount_limit_logged = false;
}
+30
View File
@@ -0,0 +1,30 @@
#include "sm_platform.h"
#include "sm_path_utils.h"
#include "sm_paths.h"
const char *get_filename_component(const char *path) {
if (!path)
return "";
const char *base = strrchr(path, '/');
if (!base)
base = strrchr(path, '\\');
return base ? base + 1 : path;
}
bool is_under_image_mount_base(const char *path) {
if (!path)
return false;
size_t image_prefix_len = strlen(IMAGE_MOUNT_BASE);
return (strncmp(path, IMAGE_MOUNT_BASE, image_prefix_len) == 0 &&
path[image_prefix_len] == '/');
}
bool build_backports_root_path(const char *scan_path, char out[MAX_PATH]) {
if (!scan_path || scan_path[0] == '\0')
return false;
if (is_under_image_mount_base(scan_path))
return false;
snprintf(out, MAX_PATH, "%s/%s", scan_path, DEFAULT_BACKPORTS_DIR_NAME);
return true;
}
+409
View File
@@ -0,0 +1,409 @@
#include "sm_platform.h"
#include "sm_runtime.h"
#include "sm_scan.h"
#include "sm_types.h"
#include "sm_game_cache.h"
#include "sm_gameinfo.h"
#include "sm_log.h"
#include "sm_config_mount.h"
#include "sm_mount_device.h"
#include "sm_filesystem.h"
#include "sm_appdb.h"
#include "sm_paths.h"
#include "sm_path_state.h"
#include "sm_path_utils.h"
#include "sm_stability.h"
#include "sm_title_state.h"
#include "sm_image_cache.h"
#include "sm_image.h"
static bool is_under_discovered_param_root(
const char *path, char discovered_param_roots[][MAX_PATH],
int discovered_count) {
if (!path)
return false;
for (int i = 0; i < discovered_count; i++) {
const char *root = discovered_param_roots[i];
if (root[0] == '\0')
continue;
size_t root_len = strlen(root);
if (root_len == 0)
continue;
if (strncmp(path, root, root_len) != 0)
continue;
if (path[root_len] == '\0' || path[root_len] == '/')
return true;
}
return false;
}
static void classify_scan_entry(const char *full_path, unsigned char d_type,
bool *is_dir_out, bool *is_regular_out) {
bool is_dir = false;
bool is_regular = false;
if (d_type == DT_DIR) {
is_dir = true;
} else if (d_type == DT_REG) {
is_regular = true;
} else if (d_type == DT_UNKNOWN) {
struct stat st;
if (lstat(full_path, &st) == 0) {
is_dir = S_ISDIR(st.st_mode);
is_regular = S_ISREG(st.st_mode);
}
}
if (is_dir_out)
*is_dir_out = is_dir;
if (is_regular_out)
*is_regular_out = is_regular;
}
// --- Candidate Discovery ---
static bool try_collect_candidate_for_directory(
const char *full_path, scan_candidate_t *candidates, int max_candidates,
int *candidate_count, const struct AppDbTitleList *app_db_titles,
bool app_db_titles_ready, char discovered_param_roots[][MAX_PATH],
int *discovered_param_root_count) {
struct stat param_st;
memset(&param_st, 0, sizeof(param_st));
bool has_param_json = false;
char title_id[MAX_TITLE_ID];
char title_name[MAX_TITLE_NAME];
title_id[0] = '\0';
title_name[0] = '\0';
if (is_under_discovered_param_root(full_path, discovered_param_roots,
*discovered_param_root_count)) {
return true;
}
if (is_under_image_mount_base(full_path) && !is_active_image_mount_point(full_path)) {
log_debug(" [SKIP] inactive mount path: %s", full_path);
return true;
}
has_param_json = directory_has_param_json(full_path, &param_st);
if (!has_param_json) {
if (is_missing_param_scan_limited(full_path)) {
log_debug(" [SKIP] param.json retry limit reached: %s", full_path);
} else {
record_missing_param_failure(full_path);
}
return false;
}
if (!get_game_info(full_path, &param_st, title_id, title_name)) {
record_missing_param_failure(full_path);
log_debug(" [SKIP] game info unavailable: %s", full_path);
return true;
}
if (!is_under_discovered_param_root(full_path, discovered_param_roots,
*discovered_param_root_count) &&
*discovered_param_root_count < MAX_PENDING) {
(void)strlcpy(discovered_param_roots[*discovered_param_root_count], full_path,
MAX_PATH);
(*discovered_param_root_count)++;
}
clear_missing_param_entry(full_path);
for (int i = 0; i < *candidate_count; i++) {
if (strcmp(candidates[i].title_id, title_id) == 0) {
notify_duplicate_title_once(title_id, full_path, candidates[i].path);
return true;
}
}
if (!app_db_titles_ready) {
return true;
}
bool in_app_db = app_db_title_list_contains(app_db_titles, title_id);
bool installed = in_app_db && is_installed(title_id);
char tracked_path[MAX_PATH];
bool link_matches_source =
installed && read_mount_link(title_id, tracked_path, sizeof(tracked_path)) &&
strcmp(tracked_path, full_path) == 0;
bool mounted = false;
if (link_matches_source) {
mounted = is_data_mounted(title_id);
if (!mounted && mount_title_nullfs(title_id, full_path))
mounted = is_data_mounted(title_id);
}
if (in_app_db) {
const char *cached_path = NULL;
if (find_cached_game(full_path, title_id, &cached_path)) {
if (cached_path && strcmp(cached_path, full_path) != 0)
notify_duplicate_title_once(title_id, full_path, cached_path);
return true;
}
}
if (!in_app_db && was_register_attempted(title_id)) {
uint8_t register_attempts = get_register_attempts(title_id);
log_debug(" [SKIP] register/install retry limit reached (%u/%u): %s (%s)",
(unsigned)register_attempts, (unsigned)MAX_REGISTER_ATTEMPTS,
title_name, title_id);
return true;
}
// Installed status requires both app files and app.db presence.
if (link_matches_source) {
return true;
}
uint8_t failed_attempts = get_failed_mount_attempts(title_id);
if (failed_attempts >= MAX_FAILED_MOUNT_ATTEMPTS) {
log_debug(" [SKIP] mount/register retry limit reached (%u/%u): %s (%s)",
(unsigned)failed_attempts, (unsigned)MAX_FAILED_MOUNT_ATTEMPTS,
title_name, title_id);
return true;
}
if (!wait_for_stability_fast(full_path, title_name)) {
log_debug(" [SKIP] source not stable yet: %s (%s)", title_name, full_path);
return true;
}
if (*candidate_count >= max_candidates) {
log_debug(" [SKIP] candidate queue full (%d): %s (%s)", max_candidates,
title_name, title_id);
return true;
}
(void)strlcpy(candidates[*candidate_count].path, full_path,
sizeof(candidates[*candidate_count].path));
(void)strlcpy(candidates[*candidate_count].title_id, title_id,
sizeof(candidates[*candidate_count].title_id));
(void)strlcpy(candidates[*candidate_count].title_name, title_name,
sizeof(candidates[*candidate_count].title_name));
candidates[*candidate_count].installed = installed;
candidates[*candidate_count].in_app_db = in_app_db;
(*candidate_count)++;
return true;
}
static void collect_candidates_recursively(
const char *dir_path, scan_candidate_t *candidates, int max_candidates,
int *candidate_count, const struct AppDbTitleList *app_db_titles,
bool app_db_titles_ready, char discovered_param_roots[][MAX_PATH],
int *discovered_param_root_count) {
if (should_stop_requested() || !dir_path || dir_path[0] == '\0')
return;
// Once a directory has sce_sys/param.json (valid or not),
// it is treated as a terminal game root and descendants are skipped.
if (try_collect_candidate_for_directory(
dir_path, candidates, max_candidates, candidate_count, app_db_titles,
app_db_titles_ready, discovered_param_roots,
discovered_param_root_count)) {
return;
}
DIR *d = opendir(dir_path);
if (!d)
return;
struct dirent *entry;
while ((entry = readdir(d)) != NULL) {
if (should_stop_requested())
break;
if (entry->d_name[0] == '.')
continue;
char full_path[MAX_PATH];
snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, entry->d_name);
bool is_dir = false;
classify_scan_entry(full_path, entry->d_type, &is_dir, NULL);
if (!is_dir)
continue;
collect_candidates_recursively(
full_path, candidates, max_candidates, candidate_count, app_db_titles,
app_db_titles_ready, discovered_param_roots, discovered_param_root_count);
}
closedir(d);
}
static bool resolve_backport_mount_context(const char *title_id,
const char *source_path,
int scan_path_count,
char backport_path[MAX_PATH],
char system_ex_path[MAX_PATH]) {
char backport_source_path[MAX_PATH];
if (is_under_image_mount_base(source_path)) {
if (!resolve_image_source_from_mount_cache(source_path,
backport_source_path,
sizeof(backport_source_path))) {
return false;
}
} else {
(void)strlcpy(backport_source_path, source_path,
sizeof(backport_source_path));
}
const char *owning_scan_path = NULL;
size_t owning_scan_path_len = 0;
for (int i = 0; i < scan_path_count; i++) {
const char *scan_path = get_scan_path(i);
if (!path_matches_root_or_child(backport_source_path, scan_path))
continue;
size_t scan_path_len = strlen(scan_path);
if (owning_scan_path && scan_path_len <= owning_scan_path_len)
continue;
owning_scan_path = scan_path;
owning_scan_path_len = scan_path_len;
}
if (!owning_scan_path)
return false;
char backport_root[MAX_PATH];
if (!build_backports_root_path(owning_scan_path, backport_root))
return false;
snprintf(backport_path, MAX_PATH, "%s/%s", backport_root, title_id);
snprintf(system_ex_path, MAX_PATH, "/system_ex/app/%s", title_id);
return true;
}
void mount_backport_overlays(void) {
int scan_path_count = get_scan_path_count();
if (scan_path_count == 0)
return;
DIR *d = opendir("/user/app");
if (!d) {
if (errno != ENOENT)
log_debug(" [BKP] open /user/app failed: %s", strerror(errno));
return;
}
struct dirent *entry;
while ((entry = readdir(d)) != NULL) {
if (should_stop_requested())
break;
if (!resolve_title_app_dir(entry, NULL, 0))
continue;
char source_path[MAX_PATH];
if (!read_mount_link(entry->d_name, source_path, sizeof(source_path)))
continue;
char backport_path[MAX_PATH];
char system_ex_path[MAX_PATH];
if (!resolve_backport_mount_context(entry->d_name, source_path,
scan_path_count, backport_path,
system_ex_path)) {
continue;
}
bool overlay_active = false;
if (!reconcile_title_backport_mount(entry->d_name, source_path,
backport_path, &overlay_active) ||
overlay_active) {
continue;
}
if (!wait_for_stability_fast(backport_path, "BKP"))
continue;
overlay_active = false;
if (!reconcile_title_backport_mount(entry->d_name, source_path,
backport_path, &overlay_active) ||
overlay_active) {
continue;
}
mount_backport_overlay(system_ex_path, backport_path, entry->d_name);
}
closedir(d);
}
// --- Unified Scan Pass (images + game candidates) ---
void cleanup_lost_sources_before_scan(void) {
// 1) Drop stale game cache entries for deleted sources.
prune_game_cache();
// 2) Drop stale/broken mount links and unmount stale /system_ex stacks.
cleanup_mount_links(NULL, true);
// 3) Unmount stale image mounts for deleted image files.
cleanup_stale_image_mounts();
// 4) Drop stale path-state entries.
prune_path_state();
}
int collect_scan_candidates(scan_candidate_t *candidates,
int max_candidates,
int *total_found_out) {
int candidate_count = 0;
const struct AppDbTitleList *app_db_titles = NULL;
bool app_db_titles_ready = get_app_db_title_list_cached(&app_db_titles);
char discovered_param_roots[MAX_PENDING][MAX_PATH];
int discovered_param_root_count = 0;
memset(discovered_param_roots, 0, sizeof(discovered_param_roots));
if (!app_db_titles_ready) {
log_debug(" [DB] app.db title list unavailable for this scan cycle");
}
for (int i = 0; i < get_scan_path_count(); i++) {
const char *scan_path = get_scan_path(i);
if (should_stop_requested())
goto done;
bool has_backports_root = !is_under_image_mount_base(scan_path);
DIR *d = opendir(scan_path);
if (!d)
continue;
struct dirent *entry;
while ((entry = readdir(d)) != NULL) {
if (should_stop_requested()) {
closedir(d);
goto done;
}
if (entry->d_name[0] == '.')
continue;
if (has_backports_root &&
strcmp(entry->d_name, DEFAULT_BACKPORTS_DIR_NAME) == 0) {
continue;
}
char full_path[MAX_PATH];
snprintf(full_path, sizeof(full_path), "%s/%s", scan_path, entry->d_name);
bool is_dir = false;
bool is_regular = false;
classify_scan_entry(full_path, entry->d_type, &is_dir, &is_regular);
if (!path_matches_root_or_child(scan_path, IMAGE_MOUNT_BASE) && is_regular)
maybe_mount_image_file(full_path, entry->d_name);
if (!is_dir)
continue;
if (runtime_config()->recursive_scan) {
collect_candidates_recursively(
full_path, candidates, max_candidates, &candidate_count,
app_db_titles, app_db_titles_ready, discovered_param_roots,
&discovered_param_root_count);
} else {
(void)try_collect_candidate_for_directory(
full_path, candidates, max_candidates, &candidate_count,
app_db_titles, app_db_titles_ready, discovered_param_roots,
&discovered_param_root_count);
}
}
closedir(d);
}
done:
if (total_found_out)
*total_found_out = discovered_param_root_count;
return candidate_count;
}
+739
View File
@@ -0,0 +1,739 @@
#include "sm_platform.h"
#include <pthread.h>
#include "sm_game_lifecycle.h"
#include "sm_kstuff.h"
#include "sm_limits.h"
#include "sm_log.h"
#include "sm_runtime.h"
#include "sm_shellcore_flags.h"
#define SHELLCORE_FLAG_WAITMODE_OR 2u
#define SHELLCORE_FLAG_ALL_BITS UINT64_MAX
typedef intptr_t sm_kernel_event_flag_t;
int sceKernelOpenEventFlag(sm_kernel_event_flag_t *ef, const char *name);
int sceKernelPollEventFlag(sm_kernel_event_flag_t ef, uint64_t bit_pattern,
unsigned int wait_mode, uint64_t *result_pattern);
int sceKernelCloseEventFlag(sm_kernel_event_flag_t ef);
typedef struct {
const char *name;
sm_kernel_event_flag_t handle;
uint64_t last_pattern;
int last_rc;
bool is_open;
bool has_last_pattern;
bool has_last_rc;
} shellcore_flag_monitor_t;
typedef struct {
uint64_t mask;
const char *name;
} shellcore_flag_bit_desc_t;
static shellcore_flag_monitor_t g_shellcore_flags[] = {
{.name = "SceShellCoreUtilAppFocus", .handle = -1},
{.name = "SceSystemStateMgrInfo", .handle = -1},
/* {.name = "SceSystemStateMgrStatus", .handle = -1},
{.name = "SceSysCoreSuspend", .handle = -1},
{.name = "SceLncUtilSystemStatus", .handle = -1},
{.name = "SceShellCoreUtilCtrlFocus", .handle = -1},
{.name = "SceShellCoreUtilPowerControl", .handle = -1},
{.name = "SceShellCoreUtilRunLevel", .handle = -1},
{.name = "SceShellCoreUtilUIStatus", .handle = -1},
{.name = "SceAutoMountUsbMass", .handle = -1},
{.name = "SceVshctlInternalUserFlag", .handle = -1},
{.name = "SceBootStatusFlags", .handle = -1},
{.name = "SceSysCoreReboot", .handle = -1},
*/
};
static pthread_t g_shellcore_flag_thread;
static pthread_mutex_t g_shellcore_flag_mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t g_shellcore_flag_cond = PTHREAD_COND_INITIALIZER;
static bool g_shellcore_flag_thread_started = false;
static bool g_shellcore_flag_start_ready = false;
static bool g_shellcore_flag_start_success = false;
static volatile sig_atomic_t g_shellcore_flag_stop_requested = 0;
#if 0
static const shellcore_flag_bit_desc_t g_lnc_util_system_status_bits[] = {
{0x0000000000000001ULL, "EXTRA_AUDIO_CPU_BUDGET_AVAILABLE"},
{0x0000000000000002ULL, "SHELLUI_FG_GAME_BG_CPU_MODE"},
};
static const shellcore_flag_bit_desc_t g_power_control_bits[] = {
{0x0000000000020000ULL, "SCREEN_SAVER_ON"},
{0x0000000000040000ULL, "POWER_TRANSITION"},
{0x0000000000080000ULL, "POWER_LOCK"},
{0x0000000000100000ULL, "SOCIAL_SCREEN_STATUS"},
{0x0000000000200000ULL, "REQUEST_CAMERA_CALIBRATION"},
{0x0000000002000000ULL, "USER_INPUT_PULSE"},
{0x0000000004000000ULL, "IMPOSE_OVERLAY"},
{0x0000000008000000ULL, "AUTO_POWER_DOWN_TIMER_RESET"},
{0x0000000010000000ULL, "REMOTE_PLAY_STATUS"},
{0x0000000020000000ULL, "SHARE_PLAY_STATUS"},
{0x0000000040000000ULL, "GAME_LIVE_STREAMING"},
{0x0000000080000000ULL, "BGM_CPU_BUDGET"},
{0x0000000100000000ULL, "PRENOTIFY_REMOTE_PLAY"},
{0x0000000200000000ULL, "PRENOTIFY_SHARE_PLAY"},
{0x0000000400000000ULL, "PRENOTIFY_GAME_LIVE_STREAMING"},
{0x0000000800000000ULL, "REMOTE_PLAY_CPU_BUDGET"},
{0x0000001000000000ULL, "SHARE_PLAY_CPU_BUDGET"},
{0x0000100000000000ULL, "EXT_STORAGE_APP_MOVE_IN_PROGRESS"},
{0x0000200000000000ULL, "STATE_0x2000000000000"},
};
static const shellcore_flag_bit_desc_t g_shellcore_ui_status_bits[] = {
{0x0000000000000001ULL, "LOW_BIT_0x1"},
{0x0000000000000010ULL, "LOW_BIT_0x10"},
{0x0000000000018000ULL, "BASIC_PRODUCT_SHAPE_FIELD"},
{0x0000000000020000ULL, "EXT_HDD_MOUNT_UNBLOCK"},
{0x0000000400000000ULL, "HIGH32_BIT2"},
};
static const shellcore_flag_bit_desc_t g_vshctl_internal_user_bits[] = {
{0x0000000000000001ULL, "INTERNAL_CRONOS_USER"},
};
static const shellcore_flag_bit_desc_t g_system_state_mgr_status_bits[] = {
{0x0000000000000001ULL, "CEC_ONE_TOUCH_PLAY_COMMAND"},
{0x0000000000000002ULL, "MEDIA_PLAYBACK_MODE"},
{0x0000000000000004ULL, "BD_DRIVE_READY"},
{0x0000000000000008ULL, "TICK_MUSIC_PLAYBACK"},
{0x0000000000000010ULL, "TICK_VIDEO_PLAYBACK"},
{0x0000000000000020ULL, "TICK_0x20"},
{0x0000000000000040ULL, "TICK_PARTY_CHAT"},
{0x0000000000000100ULL, "TURN_OFF_REQUEST"},
{0x0000000000000200ULL, "REBOOT_REQUEST"},
{0x0000000000000400ULL, "ENTER_STANDBY_REQUEST"},
{0x0000000000010000ULL, "START_SHUTDOWN_TIMER"},
{0x0000000000020000ULL, "START_REBOOT_TIMER"},
{0x0000000000040000ULL, "START_STANDBY_TIMER"},
{0x0000000000080000ULL, "EXTEND_SHUTDOWN_TIMER"},
{0x0000000000100000ULL, "CANCEL_SHUTDOWN_TIMER"},
{0x0000000000200000ULL, "SHELLUI_SHUTDOWN_IN_PROGRESS"},
{0x0000000000400000ULL, "EXTEND_SHUTDOWN_TIMER_POST_AUTOUPDATE"},
{0x0000000001000000ULL, "BIGAPP_LAUNCH_READY"},
{0x0000000004000000ULL, "VSH_AUTO_UPDATE_VERIFY_DONE"},
{0x0000000008000000ULL, "START_REBOOT_TIMER_FAST"},
};
static const shellcore_flag_bit_desc_t g_boot_status_bits[] = {
{0x0000000000000002ULL, "BIT_0x2"},
{0x0000000000000040ULL, "BIT_0x40"},
{0x0000000000000080ULL, "BIT_0x80"},
{0x0000000000000200ULL, "BIT_0x200"},
{0x0000000000004000ULL, "BIT_0x4000"},
{0x0000000001000000ULL, "BIGAPP_LAUNCH_READY"},
};
static const shellcore_flag_bit_desc_t g_syscore_reboot_bits[] = {
{0x0000000000000040ULL, "BIT_0x40"},
{0x0000000000004000ULL, "BIT_0x4000"},
{0x0000000100000000ULL, "REBOOT_FLAG"},
};
static const shellcore_flag_bit_desc_t g_syscore_suspend_bits[] = {
{0x0000000000000001ULL, "SUSPEND_ACK"},
{0x0000000000000002ULL, "SUSPEND_REQ_BIT1"},
{0x0000000000000004ULL, "SUSPEND_REQ_BIT2"},
{0x0000000000000008ULL, "SUSPEND_REQ_BIT3"},
};
#endif
enum {
SYSTEM_STATE_MGR_STATE_INVALID = 0u,
SYSTEM_STATE_MGR_STATE_INITIALIZING = 10u,
SYSTEM_STATE_MGR_STATE_SHUTDOWN_ON_GOING = 100u,
SYSTEM_STATE_MGR_STATE_POWER_SAVING = 200u,
SYSTEM_STATE_MGR_STATE_SUSPEND_ON_GOING = 300u,
SYSTEM_STATE_MGR_STATE_MAIN_ON_STANDBY = 500u,
SYSTEM_STATE_MGR_STATE_WORKING = 1000u,
};
enum {
SYSTEM_STATE_MGR_REBOOT_CAUSE_NONE = 0u,
SYSTEM_STATE_MGR_REBOOT_CAUSE_CFF = 1u,
};
static const shellcore_flag_bit_desc_t *get_shellcore_flag_bits(
const shellcore_flag_monitor_t *flag, size_t *count_out) {
if (count_out)
*count_out = 0;
if (!flag || !flag->name)
return NULL;
#if 0
if (strcmp(flag->name, "SceLncUtilSystemStatus") == 0) {
if (count_out) {
*count_out = sizeof(g_lnc_util_system_status_bits) /
sizeof(g_lnc_util_system_status_bits[0]);
}
return g_lnc_util_system_status_bits;
}
if (strcmp(flag->name, "SceShellCoreUtilPowerControl") == 0) {
if (count_out)
*count_out = sizeof(g_power_control_bits) / sizeof(g_power_control_bits[0]);
return g_power_control_bits;
}
if (strcmp(flag->name, "SceShellCoreUtilUIStatus") == 0) {
if (count_out) {
*count_out = sizeof(g_shellcore_ui_status_bits) /
sizeof(g_shellcore_ui_status_bits[0]);
}
return g_shellcore_ui_status_bits;
}
if (strcmp(flag->name, "SceVshctlInternalUserFlag") == 0) {
if (count_out) {
*count_out = sizeof(g_vshctl_internal_user_bits) /
sizeof(g_vshctl_internal_user_bits[0]);
}
return g_vshctl_internal_user_bits;
}
if (strcmp(flag->name, "SceSystemStateMgrStatus") == 0) {
if (count_out) {
*count_out = sizeof(g_system_state_mgr_status_bits) /
sizeof(g_system_state_mgr_status_bits[0]);
}
return g_system_state_mgr_status_bits;
}
if (strcmp(flag->name, "SceBootStatusFlags") == 0) {
if (count_out)
*count_out = sizeof(g_boot_status_bits) / sizeof(g_boot_status_bits[0]);
return g_boot_status_bits;
}
if (strcmp(flag->name, "SceSysCoreReboot") == 0) {
if (count_out)
*count_out = sizeof(g_syscore_reboot_bits) / sizeof(g_syscore_reboot_bits[0]);
return g_syscore_reboot_bits;
}
if (strcmp(flag->name, "SceSysCoreSuspend") == 0) {
if (count_out)
*count_out = sizeof(g_syscore_suspend_bits) /
sizeof(g_syscore_suspend_bits[0]);
return g_syscore_suspend_bits;
}
#endif
return NULL;
}
static const char *get_system_state_mgr_state_name(unsigned state) {
switch (state) {
case SYSTEM_STATE_MGR_STATE_INVALID:
return "INVALID";
case SYSTEM_STATE_MGR_STATE_INITIALIZING:
return "INITIALIZING";
case SYSTEM_STATE_MGR_STATE_SHUTDOWN_ON_GOING:
return "SHUTDOWN_ON_GOING";
case SYSTEM_STATE_MGR_STATE_POWER_SAVING:
return "POWER_SAVING";
case SYSTEM_STATE_MGR_STATE_SUSPEND_ON_GOING:
return "SUSPEND_ON_GOING";
case SYSTEM_STATE_MGR_STATE_MAIN_ON_STANDBY:
return "MAIN_ON_STANDBY";
case SYSTEM_STATE_MGR_STATE_WORKING:
return "WORKING";
default:
return "UNKNOWN";
}
}
static const char *get_system_state_mgr_reboot_cause_name(unsigned cause) {
switch (cause) {
case SYSTEM_STATE_MGR_REBOOT_CAUSE_NONE:
return "NONE";
case SYSTEM_STATE_MGR_REBOOT_CAUSE_CFF:
return "CFF";
default:
return "UNKNOWN";
}
}
static void append_shellcore_flag_token(char *dst, size_t dst_size,
const char *token) {
size_t used;
if (!dst || dst_size == 0 || !token || token[0] == '\0')
return;
used = strnlen(dst, dst_size);
if (used >= dst_size - 1)
return;
(void)snprintf(dst + used, dst_size - used, "%s%s", used == 0 ? "" : "|",
token);
}
static bool format_shellcore_flag_special_known_masks(
const shellcore_flag_monitor_t *flag, char *dst, size_t dst_size) {
if (!flag || !flag->name || !dst || dst_size == 0)
return false;
#if 0
if (strcmp(flag->name, "SceShellCoreUtilRunLevel") == 0) {
append_shellcore_flag_token(dst, dst_size, "RUN_LEVEL_NUMERIC");
append_shellcore_flag_token(dst, dst_size, "POLL_MASK=0xFFFFFFF");
append_shellcore_flag_token(dst, dst_size, "INITIAL=10");
append_shellcore_flag_token(dst, dst_size, "READY=100");
return true;
}
if (strcmp(flag->name, "SceAutoMountUsbMass") == 0) {
append_shellcore_flag_token(dst, dst_size, "USB_MOUNT_BITS=0xFFFFFFF");
return true;
}
#endif
if (strcmp(flag->name, "SceSystemStateMgrInfo") == 0) {
append_shellcore_flag_token(dst, dst_size,
"CURRENT_STATE=bits[0..15]");
append_shellcore_flag_token(dst, dst_size,
"TRIGGER_CODE=bits[32..47]");
append_shellcore_flag_token(dst, dst_size,
"REBOOT_CAUSE=bits[48..63]");
append_shellcore_flag_token(dst, dst_size, "STATE_INVALID=0");
append_shellcore_flag_token(dst, dst_size, "STATE_INITIALIZING=10");
append_shellcore_flag_token(dst, dst_size,
"STATE_SHUTDOWN_ON_GOING=100");
append_shellcore_flag_token(dst, dst_size, "STATE_POWER_SAVING=200");
append_shellcore_flag_token(dst, dst_size,
"STATE_SUSPEND_ON_GOING=300");
append_shellcore_flag_token(dst, dst_size,
"STATE_MAIN_ON_STANDBY=500");
append_shellcore_flag_token(dst, dst_size, "STATE_WORKING=1000");
append_shellcore_flag_token(dst, dst_size, "REBOOT_CAUSE_NONE=0");
append_shellcore_flag_token(dst, dst_size, "REBOOT_CAUSE_CFF=1");
append_shellcore_flag_token(dst, dst_size, "INITIAL=0x10000000A");
return true;
}
return false;
}
static void format_shellcore_flag_known_masks(const shellcore_flag_monitor_t *flag,
char *dst, size_t dst_size) {
size_t bit_count = 0;
const shellcore_flag_bit_desc_t *bits = get_shellcore_flag_bits(flag, &bit_count);
if (!dst || dst_size == 0)
return;
dst[0] = '\0';
(void)format_shellcore_flag_special_known_masks(flag, dst, dst_size);
for (size_t i = 0; i < bit_count; ++i) {
char token[96];
(void)snprintf(token, sizeof(token), "%s=0x%llX", bits[i].name,
(unsigned long long)bits[i].mask);
append_shellcore_flag_token(dst, dst_size, token);
}
if (strcmp(flag->name, "SceShellCoreUtilAppFocus") == 0) {
append_shellcore_flag_token(dst, dst_size, "APP_ID");
}
}
static bool format_shellcore_flag_special_value(const shellcore_flag_monitor_t *flag,
uint64_t pattern, char *dst,
size_t dst_size) {
if (!flag || !flag->name || !dst || dst_size == 0)
return false;
#if 0
if (strcmp(flag->name, "SceShellCoreUtilRunLevel") == 0) {
char token[96];
(void)snprintf(token, sizeof(token), "RUN_LEVEL=%u",
(unsigned)(pattern & 0x0FFFFFFFULL));
append_shellcore_flag_token(dst, dst_size, token);
if ((pattern & 0x0FFFFFFFULL) == 10u) {
append_shellcore_flag_token(dst, dst_size, "INITIAL");
} else if ((pattern & 0x0FFFFFFFULL) == 100u) {
append_shellcore_flag_token(dst, dst_size, "READY");
}
if ((pattern & ~0x0FFFFFFFULL) != 0) {
(void)snprintf(token, sizeof(token), "UNKNOWN=0x%llX",
(unsigned long long)(pattern & ~0x0FFFFFFFULL));
append_shellcore_flag_token(dst, dst_size, token);
}
return true;
}
if (strcmp(flag->name, "SceAutoMountUsbMass") == 0) {
uint64_t usb_bits = pattern & 0x0FFFFFFFULL;
for (unsigned i = 0; i < 28; ++i) {
if ((usb_bits & (1ULL << i)) == 0)
continue;
char token[32];
(void)snprintf(token, sizeof(token), "USB%u", i);
append_shellcore_flag_token(dst, dst_size, token);
}
if ((pattern & ~0x0FFFFFFFULL) != 0) {
char token[64];
(void)snprintf(token, sizeof(token), "UNKNOWN=0x%llX",
(unsigned long long)(pattern & ~0x0FFFFFFFULL));
append_shellcore_flag_token(dst, dst_size, token);
}
if (dst[0] == '\0')
(void)snprintf(dst, dst_size, "0");
return true;
}
#endif
if (strcmp(flag->name, "SceSystemStateMgrInfo") == 0) {
unsigned current_state = (unsigned)(pattern & 0xFFFFu);
unsigned mid16 = (unsigned)((pattern >> 16) & 0xFFFFu);
unsigned trigger_code = (unsigned)((pattern >> 32) & 0xFFFFu);
unsigned reboot_cause = (unsigned)((pattern >> 48) & 0xFFFFu);
char token[128];
(void)snprintf(token, sizeof(token), "STATE=%s(0x%04X)",
get_system_state_mgr_state_name(current_state), current_state);
append_shellcore_flag_token(dst, dst_size, token);
(void)snprintf(token, sizeof(token), "TRIGGER=0x%04X", trigger_code);
append_shellcore_flag_token(dst, dst_size, token);
(void)snprintf(token, sizeof(token), "REBOOT_CAUSE=%s(0x%04X)",
get_system_state_mgr_reboot_cause_name(reboot_cause),
reboot_cause);
append_shellcore_flag_token(dst, dst_size, token);
if (mid16 != 0) {
(void)snprintf(token, sizeof(token), "MID16=0x%04X", mid16);
append_shellcore_flag_token(dst, dst_size, token);
}
return true;
}
if (strcmp(flag->name, "SceShellCoreUtilAppFocus") == 0) {
unsigned low32 = (unsigned)(pattern & 0xFFFFFFFFu);
unsigned high32 = (unsigned)(pattern >> 32);
char token[96];
if (low32 != 0) {
(void)snprintf(token, sizeof(token), "APP_ID=0x%08X", low32);
append_shellcore_flag_token(dst, dst_size, token);
}
if (high32 != 0) {
(void)snprintf(token, sizeof(token), "HIGH32=0x%08X", high32);
append_shellcore_flag_token(dst, dst_size, token);
}
if (dst[0] == '\0')
(void)snprintf(dst, dst_size, "0");
return true;
}
#if 0
if (strcmp(flag->name, "SceShellCoreUtilUIStatus") == 0) {
unsigned shape_field = (unsigned)((pattern >> 15) & 0x3u);
if (shape_field != 0) {
char token[96];
(void)snprintf(token, sizeof(token), "BASIC_PRODUCT_SHAPE=%u",
shape_field - 1u);
append_shellcore_flag_token(dst, dst_size, token);
}
return false;
}
#endif
return false;
}
static void format_shellcore_flag_set_bits(const shellcore_flag_monitor_t *flag,
uint64_t pattern, char *dst,
size_t dst_size) {
size_t bit_count = 0;
uint64_t known_mask = 0;
const shellcore_flag_bit_desc_t *bits = get_shellcore_flag_bits(flag, &bit_count);
if (!dst || dst_size == 0)
return;
dst[0] = '\0';
if (format_shellcore_flag_special_value(flag, pattern, dst, dst_size) &&
dst[0] != '\0') {
return;
}
for (size_t i = 0; i < bit_count; ++i) {
known_mask |= bits[i].mask;
if ((pattern & bits[i].mask) == 0)
continue;
char token[96];
(void)snprintf(token, sizeof(token), "%s", bits[i].name);
append_shellcore_flag_token(dst, dst_size, token);
}
if (strcmp(flag->name, "SceShellCoreUtilAppFocus") == 0 &&
(pattern >> 32) == 0 && pattern != 0) {
char token[64];
(void)snprintf(token, sizeof(token), "APP_ID=0x%08X", (unsigned)pattern);
append_shellcore_flag_token(dst, dst_size, token);
}
if ((pattern & ~known_mask) != 0) {
char token[64];
(void)snprintf(token, sizeof(token), "UNKNOWN=0x%llX",
(unsigned long long)(pattern & ~known_mask));
append_shellcore_flag_token(dst, dst_size, token);
}
if (dst[0] == '\0')
(void)snprintf(dst, dst_size, "0");
}
static void set_shellcore_flag_start_result(bool success) {
pthread_mutex_lock(&g_shellcore_flag_mutex);
g_shellcore_flag_start_success = success;
g_shellcore_flag_start_ready = true;
pthread_cond_broadcast(&g_shellcore_flag_cond);
pthread_mutex_unlock(&g_shellcore_flag_mutex);
}
static void reset_shellcore_flag_state(shellcore_flag_monitor_t *flag) {
if (!flag)
return;
flag->handle = -1;
flag->is_open = false;
flag->has_last_pattern = false;
flag->has_last_rc = false;
flag->last_pattern = 0;
flag->last_rc = 0;
}
static void log_shellcore_flag_pattern(const shellcore_flag_monitor_t *flag,
uint64_t pattern) {
char decoded[512];
if (!flag)
return;
format_shellcore_flag_set_bits(flag, pattern, decoded, sizeof(decoded));
if (!flag->has_last_pattern) {
log_debug(" [SHELLFLAG] %s initial=0x%016llX low32=0x%08X high32=0x%08X "
"decoded=%s",
flag->name, (unsigned long long)pattern, (unsigned)(pattern),
(unsigned)(pattern >> 32), decoded);
return;
}
log_debug(" [SHELLFLAG] %s value=0x%016llX delta=0x%016llX low32=0x%08X "
"high32=0x%08X decoded=%s",
flag->name, (unsigned long long)pattern,
(unsigned long long)(flag->last_pattern ^ pattern),
(unsigned)(pattern), (unsigned)(pattern >> 32), decoded);
}
static void close_shellcore_flag(shellcore_flag_monitor_t *flag) {
if (!flag || !flag->is_open)
return;
int rc = sceKernelCloseEventFlag(flag->handle);
if (rc < 0) {
log_debug(" [SHELLFLAG] close failed: %s handle=0x%016llX rc=0x%08X",
flag->name, (unsigned long long)flag->handle, (unsigned)rc);
}
reset_shellcore_flag_state(flag);
}
static void close_shellcore_flags(void) {
for (size_t i = 0; i < sizeof(g_shellcore_flags) / sizeof(g_shellcore_flags[0]);
++i) {
close_shellcore_flag(&g_shellcore_flags[i]);
}
}
static size_t open_shellcore_flags(void) {
size_t opened_count = 0;
for (size_t i = 0; i < sizeof(g_shellcore_flags) / sizeof(g_shellcore_flags[0]);
++i) {
shellcore_flag_monitor_t *flag = &g_shellcore_flags[i];
sm_kernel_event_flag_t handle = -1;
int rc = sceKernelOpenEventFlag(&handle, flag->name);
if (rc < 0) {
log_debug(" [SHELLFLAG] open failed: %s rc=0x%08X", flag->name,
(unsigned)rc);
reset_shellcore_flag_state(flag);
continue;
}
flag->handle = handle;
flag->is_open = true;
log_debug(" [SHELLFLAG] opened %s handle=0x%016llX", flag->name,
(unsigned long long)flag->handle);
{
char known_masks[1024];
format_shellcore_flag_known_masks(flag, known_masks, sizeof(known_masks));
if (known_masks[0] != '\0') {
log_debug(" [SHELLFLAG] known %s masks: %s", flag->name, known_masks);
}
}
opened_count++;
}
return opened_count;
}
static void poll_shellcore_flag(shellcore_flag_monitor_t *flag) {
uint64_t result_pattern = 0;
int rc;
bool changed = false;
bool entered_shutdown_on_going = false;
bool entered_resume_working = false;
bool is_system_state_mgr_info = false;
unsigned current_state = 0;
unsigned previous_state = 0;
if (!flag || !flag->is_open)
return;
rc = sceKernelPollEventFlag(flag->handle, SHELLCORE_FLAG_ALL_BITS,
SHELLCORE_FLAG_WAITMODE_OR, &result_pattern);
if (rc < 0) {
if (!flag->has_last_rc || flag->last_rc != rc) {
log_debug(" [SHELLFLAG] %s poll rc=0x%08X", flag->name,
(unsigned)rc);
}
flag->last_rc = rc;
flag->has_last_rc = true;
return;
}
if (!flag->has_last_rc || flag->last_rc != 0 || !flag->has_last_pattern ||
flag->last_pattern != result_pattern) {
log_shellcore_flag_pattern(flag, result_pattern);
changed = flag->has_last_pattern && flag->last_pattern != result_pattern;
}
is_system_state_mgr_info = strcmp(flag->name, "SceSystemStateMgrInfo") == 0;
if (is_system_state_mgr_info) {
current_state = (unsigned)(result_pattern & 0xFFFFu);
previous_state =
flag->has_last_pattern ? (unsigned)(flag->last_pattern & 0xFFFFu) : 0;
entered_shutdown_on_going =
current_state == SYSTEM_STATE_MGR_STATE_SHUTDOWN_ON_GOING &&
(!flag->has_last_pattern ||
previous_state != SYSTEM_STATE_MGR_STATE_SHUTDOWN_ON_GOING);
entered_resume_working =
flag->has_last_pattern &&
current_state == SYSTEM_STATE_MGR_STATE_WORKING &&
previous_state != SYSTEM_STATE_MGR_STATE_WORKING;
}
flag->last_pattern = result_pattern;
flag->has_last_pattern = true;
flag->last_rc = 0;
flag->has_last_rc = true;
if (changed && strcmp(flag->name, "SceShellCoreUtilAppFocus") == 0) {
sm_kstuff_note_app_focus((uint32_t)result_pattern);
wake_game_lifecycle_watcher();
}
if (entered_shutdown_on_going) {
request_shutdown_stop("SceSystemStateMgrInfo=SHUTDOWN_ON_GOING");
}
if (entered_resume_working) {
request_scan_now("SceSystemStateMgrInfo=WORKING");
}
}
static void *shellcore_flag_thread_main(void *arg) {
(void)arg;
size_t opened_count = open_shellcore_flags();
// The active monitor currently depends on both configured flags opening:
// AppFocus plus SceSystemStateMgrInfo.
if (opened_count < 2) {
if (opened_count != 0)
close_shellcore_flags();
set_shellcore_flag_start_result(false);
return NULL;
}
for (size_t i = 0; i < sizeof(g_shellcore_flags) / sizeof(g_shellcore_flags[0]);
++i) {
poll_shellcore_flag(&g_shellcore_flags[i]);
}
log_debug(" [SHELLFLAG] monitor started (%zu/%zu flags opened)", opened_count,
sizeof(g_shellcore_flags) / sizeof(g_shellcore_flags[0]));
set_shellcore_flag_start_result(true);
while (!g_shellcore_flag_stop_requested && !should_stop_requested()) {
for (size_t i = 0; i < sizeof(g_shellcore_flags) / sizeof(g_shellcore_flags[0]);
++i) {
poll_shellcore_flag(&g_shellcore_flags[i]);
}
sceKernelUsleep(SHELLCORE_FLAG_POLL_INTERVAL_US);
}
close_shellcore_flags();
return NULL;
}
bool sm_shellcore_flags_start(void) {
int rc;
if (g_shellcore_flag_thread_started)
return g_shellcore_flag_start_success;
g_shellcore_flag_stop_requested = 0;
g_shellcore_flag_start_ready = false;
g_shellcore_flag_start_success = false;
close_shellcore_flags();
rc = pthread_create(&g_shellcore_flag_thread, NULL, shellcore_flag_thread_main,
NULL);
if (rc != 0) {
log_debug(" [SHELLFLAG] pthread_create failed: rc=%d", rc);
return false;
}
pthread_mutex_lock(&g_shellcore_flag_mutex);
while (!g_shellcore_flag_start_ready)
pthread_cond_wait(&g_shellcore_flag_cond, &g_shellcore_flag_mutex);
pthread_mutex_unlock(&g_shellcore_flag_mutex);
if (!g_shellcore_flag_start_success) {
pthread_join(g_shellcore_flag_thread, NULL);
close_shellcore_flags();
return false;
}
g_shellcore_flag_thread_started = true;
return true;
}
void sm_shellcore_flags_stop(void) {
if (!g_shellcore_flag_thread_started)
return;
g_shellcore_flag_stop_requested = 1;
pthread_join(g_shellcore_flag_thread, NULL);
g_shellcore_flag_thread_started = false;
g_shellcore_flag_start_ready = false;
g_shellcore_flag_start_success = false;
close_shellcore_flags();
}
+43
View File
@@ -0,0 +1,43 @@
#include "sm_platform.h"
#include "sm_stability.h"
#include "sm_config_mount.h"
#include "sm_log.h"
#include "sm_types.h"
bool is_path_stable_now(const char *path, double *root_diff_out,
int *stat_errno_out) {
struct stat st;
time_t now = time(NULL);
ensure_runtime_config_ready();
if (stat_errno_out)
*stat_errno_out = 0;
if (stat(path, &st) != 0) {
if (root_diff_out)
*root_diff_out = -1.0;
if (stat_errno_out)
*stat_errno_out = errno;
return false;
}
double root_diff = difftime(now, st.st_mtime);
if (root_diff_out)
*root_diff_out = root_diff;
if (root_diff < 0.0)
return true;
return root_diff > (double)runtime_config()->stability_wait_seconds;
}
bool wait_for_stability_fast(const char *path, const char *name) {
double diff = 0.0;
int st_err = 0;
if (is_path_stable_now(path, &diff, &st_err))
return true;
if (st_err != 0)
log_debug(" [WAIT] %s stat failed for %s: %s", name, path,
strerror(st_err));
else
log_debug(" [WAIT] %s modified %.0fs ago. Waiting...", name, diff);
return false;
}
+12
View File
@@ -0,0 +1,12 @@
#include "sm_platform.h"
#include "sm_time.h"
uint64_t monotonic_time_us(void) {
struct timespec ts;
memset(&ts, 0, sizeof(ts));
if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0)
return 0;
return (uint64_t)ts.tv_sec * 1000000ull + (uint64_t)ts.tv_nsec / 1000ull;
}
+164
View File
@@ -0,0 +1,164 @@
#include "sm_platform.h"
#include "sm_title_state.h"
#include "sm_hash.h"
#include "sm_limits.h"
#include "sm_log.h"
struct TitleStateEntry {
char title_id[MAX_TITLE_ID];
uint8_t mount_reg_attempts;
uint8_t register_attempts;
bool duplicate_notified_once;
bool valid;
};
static struct TitleStateEntry g_title_state[TITLE_STATE_CAPACITY];
static uint16_t g_title_state_hash[STATE_HASH_SIZE];
static void rebuild_title_state_hash(void) {
memset(g_title_state_hash, 0, sizeof(g_title_state_hash));
for (int k = 0; k < TITLE_STATE_CAPACITY; k++) {
if (!g_title_state[k].valid || g_title_state[k].title_id[0] == '\0')
continue;
uint32_t slot =
sm_fnv1a32(g_title_state[k].title_id) & (STATE_HASH_SIZE - 1u);
for (uint32_t i = 0; i < STATE_HASH_SIZE; i++) {
if (g_title_state_hash[slot] == 0) {
g_title_state_hash[slot] = (uint16_t)(k + 1);
break;
}
slot = (slot + 1u) & (STATE_HASH_SIZE - 1u);
}
}
}
static struct TitleStateEntry *find_title_state(const char *title_id) {
if (!title_id || title_id[0] == '\0')
return NULL;
uint32_t slot = sm_fnv1a32(title_id) & (STATE_HASH_SIZE - 1u);
for (uint32_t i = 0; i < STATE_HASH_SIZE; i++) {
uint16_t idx = g_title_state_hash[slot];
if (idx == 0)
return NULL;
struct TitleStateEntry *entry = &g_title_state[idx - 1u];
if (entry->valid && strcmp(entry->title_id, title_id) == 0)
return entry;
slot = (slot + 1u) & (STATE_HASH_SIZE - 1u);
}
return NULL;
}
static struct TitleStateEntry *create_title_state(const char *title_id) {
if (!title_id || title_id[0] == '\0')
return NULL;
int slot_k = -1;
for (int k = 0; k < TITLE_STATE_CAPACITY; k++) {
if (!g_title_state[k].valid) {
slot_k = k;
break;
}
}
if (slot_k < 0) {
int evict_k = -1;
for (int k = 0; k < TITLE_STATE_CAPACITY; k++) {
if (!g_title_state[k].valid)
continue;
if (g_title_state[k].mount_reg_attempts == 0 &&
g_title_state[k].register_attempts == 0) {
evict_k = k;
break;
}
}
if (evict_k < 0) {
for (int k = 0; k < TITLE_STATE_CAPACITY; k++) {
if (!g_title_state[k].valid)
continue;
if (g_title_state[k].mount_reg_attempts == 0) {
evict_k = k;
break;
}
}
}
if (evict_k < 0)
evict_k = 0;
memset(&g_title_state[evict_k], 0, sizeof(g_title_state[evict_k]));
rebuild_title_state_hash();
slot_k = evict_k;
}
memset(&g_title_state[slot_k], 0, sizeof(g_title_state[slot_k]));
g_title_state[slot_k].valid = true;
(void)strlcpy(g_title_state[slot_k].title_id, title_id,
sizeof(g_title_state[slot_k].title_id));
uint32_t slot = sm_fnv1a32(title_id) & (STATE_HASH_SIZE - 1u);
for (uint32_t i = 0; i < STATE_HASH_SIZE; i++) {
if (g_title_state_hash[slot] == 0) {
g_title_state_hash[slot] = (uint16_t)(slot_k + 1);
return &g_title_state[slot_k];
}
slot = (slot + 1u) & (STATE_HASH_SIZE - 1u);
}
g_title_state[slot_k].valid = false;
g_title_state[slot_k].title_id[0] = '\0';
rebuild_title_state_hash();
return NULL;
}
static struct TitleStateEntry *get_or_create_title_state(const char *title_id) {
struct TitleStateEntry *entry = find_title_state(title_id);
return entry ? entry : create_title_state(title_id);
}
bool was_register_attempted(const char *title_id) {
struct TitleStateEntry *entry = find_title_state(title_id);
return entry ? (entry->register_attempts >= MAX_REGISTER_ATTEMPTS) : false;
}
uint8_t get_register_attempts(const char *title_id) {
struct TitleStateEntry *entry = find_title_state(title_id);
return entry ? entry->register_attempts : 0;
}
void mark_register_attempted(const char *title_id) {
struct TitleStateEntry *entry = get_or_create_title_state(title_id);
if (entry && entry->register_attempts < UINT8_MAX)
entry->register_attempts++;
}
void notify_duplicate_title_once(const char *title_id, const char *path_a,
const char *path_b) {
struct TitleStateEntry *entry = get_or_create_title_state(title_id);
if (!entry || entry->duplicate_notified_once)
return;
entry->duplicate_notified_once = true;
notify_system("Duplicate %s detected:\n%s\n%s", title_id, path_a, path_b);
}
void clear_duplicate_title_notification(const char *title_id) {
struct TitleStateEntry *entry = find_title_state(title_id);
if (entry)
entry->duplicate_notified_once = false;
}
uint8_t get_failed_mount_attempts(const char *title_id) {
struct TitleStateEntry *entry = find_title_state(title_id);
return entry ? entry->mount_reg_attempts : 0;
}
void clear_failed_mount_attempts(const char *title_id) {
struct TitleStateEntry *entry = find_title_state(title_id);
if (entry)
entry->mount_reg_attempts = 0;
}
uint8_t bump_failed_mount_attempts(const char *title_id) {
struct TitleStateEntry *entry = get_or_create_title_state(title_id);
if (!entry)
return MAX_FAILED_MOUNT_ATTEMPTS;
if (entry->mount_reg_attempts < UINT8_MAX)
entry->mount_reg_attempts++;
return entry->mount_reg_attempts;
}