Compare commits

..
6 Commits
Author SHA1 Message Date
Roman Tarasov 24fba2be21 Reduce scanner directory watchers by default 2026-06-03 21:13:07 +03:00
Roman Tarasov f20b97db65 funding 2026-06-03 17:53:44 +03:00
Roman Tarasov 9d6ab61369 Added description of compressed PFS format 2026-06-03 17:49:43 +03:00
Roman Tarasov 25182da557 Fix PFS mount 2026-06-03 16:09:11 +03:00
Drakmor 72cf4460a9 Merge pull request #55 from drakmor/alert-autofix-1
Potential fix for code scanning alert no. 1: Workflow does not contain permissions
2026-06-03 02:26:06 +03:00
DrakmorandCopilot Autofix powered by AI cc0f4e0684 Potential fix for code scanning alert no. 1: Workflow does not contain permissions
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-06-03 01:49:51 +03:00
8 changed files with 170 additions and 21 deletions

No files matched your search

+2
View File
@@ -0,0 +1,2 @@
custom:
- https://github.com/drakmor/shadowMountPlus#support-development
+3
View File
@@ -13,6 +13,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}
cancel-in-progress: false
permissions:
contents: read
jobs:
build:
outputs:
+84 -1
View File
@@ -11,7 +11,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-lite v1.05+](https://github.com/EchoStretch/kstuff-lite)**.
**Compatibility:** Supports all Jailbroken PS5 firmwares running **[Kstuff-lite v1.07+](https://github.com/EchoStretch/kstuff-lite)**.
## 💜 Support Development
@@ -61,6 +61,8 @@ Supported keys (all optional):
- `image_rw=<image_filename>` (repeatable; force read-write mode for this image filename)
- `image_sector=<image_filename>:<sector_size>` (repeatable; force sector size for this image filename)
- `scan_depth=<1..2>` (`1` = scan only first-level subfolders, `2` = also scan one additional nested level; default: `1`)
- `watch_subdirs=1|0` (`1` keeps vnode watchers on scan root subdirectories; default: `0`)
- `watch_image_files=1|0` (`1` keeps vnode watchers on discovered image files; default: `1`)
- `recursive_scan=1|0` (deprecated compatibility key; `1` forces `scan_depth=2`)
- `scan_interval_seconds=<1..3600>` (full scan loop interval; default: `15`)
- `stability_wait_seconds=<0..3600>` (minimum source age before processing; default: `10`)
@@ -177,6 +179,86 @@ PFSC container layout requirement (`.ffpfsc`):
- A nested `pfs_image.dat` file inside a PFSC container is treated as a PFS image.
- `.ffpfsc` uses the nested outer PFS profile (`img_type=0x02`); `.ffpfs` and `pfs_image.dat` files mounted from inside it use the nested inner profile (`img_type=0x82`). Signature verification and GDDR5 cache setup are kept in code but currently disabled.
## Compressed PFS containers (`.ffpfsc`)
Compressed PFS mode is intended only for nested images. During packing, data is
zero-padded to a `64 KB` sector boundary, so the compressed PFS should be used
as an outer container for another image rather than as a direct game-file
layout.
Recommended layouts:
- exFAT image inside compressed PFS.
- Uncompressed PFS image inside compressed PFS.
MkPFS uses zLib compression. Decompression is hardware-assisted, but throughput
is limited to roughly `150-250 MB/s`. This is about one third of the speed of an
external USB drive, FFPKG/exFAT images, or about one tenth of the internal drive
speed. Keep this in mind when choosing which games to pack. Games that read large
amounts of data or stream textures continuously may stutter.
Use the official [PSBrew/MkPFS](https://github.com/PSBrew/MkPFS) tool to pack
PFS images.
### Packing an uncompressed PFS image into compressed PFS
First create an uncompressed nested PFS image:
```bash
mkpfs pack folder --verify --no-compress --no-adjust-output-file-extension --version PS5 --inode-bits 32 \
'./PPSA07923/PPSA07923-app' \
'./pfs_image.dat'
```
Then pack the nested image into a compressed PFS container:
```bash
mkpfs pack file --verify --version PS5 --inode-bits 32 \
'./pfs_image.dat' \
'./PPSA12345.ffpfsc'
```
After successful packing, the temporary nested image can be removed:
```bash
rm './pfs_image.dat'
```
### Packing an exFAT image into compressed PFS
First create a normal exFAT image using one of the methods from
`Creating an exFAT image`. The nested exFAT image name must keep the `.exfat`
extension, for example `PPSA12345.exfat`.
Linux:
```bash
chmod +x mkexfat.sh
./mkexfat.sh ./PPSA12345-app ./PPSA12345.exfat
```
Windows:
```cmd
make_image.bat "C:\images\PPSA12345.exfat" "C:\payload\PPSA12345-app"
```
The exFAT image must contain the game files at the image root, without an extra
top-level folder.
Then pack the exFAT image into a compressed PFS container:
```bash
mkpfs pack file --verify --version PS5 --inode-bits 32 \
'./PPSA12345.exfat' \
'./PPSA12345.ffpfsc'
```
After successful packing, the temporary exFAT image can be removed:
```bash
rm './PPSA12345.exfat'
```
## Scan paths
Default scan locations:
@@ -372,5 +454,6 @@ If a game is mounted but does not start:
* Gezine
* earthonion
* LightningMods
* RenanGBarreto for his excellent https://github.com/PSBrew/MkPFS
* john-tornblom for SDK
* PS5 R&D Community
+12
View File
@@ -45,6 +45,18 @@
# Default: 1
# scan_depth=1
# Directory watcher granularity:
# 1 -> keep vnode watchers on scan root subdirectories
# 0 -> watch only scan roots/parents and rely on targeted/full scans for children
# Default: 0
# watch_subdirs=0
# Image file watcher granularity:
# 1 -> keep vnode watchers on discovered image files
# 0 -> detect image file changes through targeted/full scans
# Default: 1
# watch_image_files=1
# Legacy compatibility:
# recursive_scan=1 forces scan_depth=2
# Default: disabled
+2
View File
@@ -116,6 +116,8 @@ typedef struct runtime_config {
bool kstuff_game_auto_toggle;
bool kstuff_crash_detection_enabled;
bool legacy_recursive_scan_forced;
bool watch_subdirs;
bool watch_image_files;
char global_fakelib_path[MAX_PATH];
uint32_t global_fakelib_exclude_title_count;
char global_fakelib_exclude_title_ids[MAX_FAKELIB_EXCLUDE_RULES][MAX_TITLE_ID];
+23
View File
@@ -257,6 +257,8 @@ static void init_runtime_config_defaults(runtime_config_state_t *state) {
state->cfg.kstuff_game_auto_toggle = true;
state->cfg.kstuff_crash_detection_enabled = true;
state->cfg.legacy_recursive_scan_forced = false;
state->cfg.watch_subdirs = false;
state->cfg.watch_image_files = true;
(void)strlcpy(state->cfg.global_fakelib_path, DEFAULT_GLOBAL_FAKELIB_PATH,
sizeof(state->cfg.global_fakelib_path));
state->cfg.scan_depth = DEFAULT_SCAN_DEPTH;
@@ -1241,6 +1243,24 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st
continue;
}
if (strcasecmp(key, "watch_subdirs") == 0) {
if (!parse_bool_ini(value, &bval)) {
log_debug(" [CFG] invalid bool at line %d: %s=%s", line_no, key, value);
continue;
}
state->cfg.watch_subdirs = bval;
continue;
}
if (strcasecmp(key, "watch_image_files") == 0) {
if (!parse_bool_ini(value, &bval)) {
log_debug(" [CFG] invalid bool at line %d: %s=%s", line_no, key, value);
continue;
}
state->cfg.watch_image_files = 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);
@@ -1488,6 +1508,7 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st
"legacy_recursive_scan_forced=%d backport_fakelib=%d "
"global_fakelib=%d global_fakelib_priority=%s "
"global_fakelib_path=%s global_fakelib_exclude=%u "
"watch_subdirs=%d watch_image_files=%d "
"kstuff_game_auto_toggle=%d kstuff_crash_detection=%d "
"kstuff_pause_delay_image_s=%u kstuff_pause_delay_direct_s=%u "
"exfat_backend=%s ufs_backend=%s "
@@ -1505,6 +1526,8 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st
state->cfg.global_fakelib_mount_first ? "game" : "global",
state->cfg.global_fakelib_path,
state->cfg.global_fakelib_exclude_title_count,
state->cfg.watch_subdirs ? 1 : 0,
state->cfg.watch_image_files ? 1 : 0,
state->cfg.kstuff_game_auto_toggle ? 1 : 0,
state->cfg.kstuff_crash_detection_enabled ? 1 : 0,
state->cfg.kstuff_pause_delay_image_seconds,
+1
View File
@@ -804,6 +804,7 @@ static bool perform_image_nmount(const char *file_path, image_fs_type_t fs_type,
// IOVEC_ENTRY("pubkey_ver"), IOVEC_ENTRY(pubkey_ver),
// IOVEC_ENTRY("key_ver"), IOVEC_ENTRY(key_ver),
// IOVEC_ENTRY("finalized"), IOVEC_ENTRY(finalized),
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),
+43 -20
View File
@@ -240,7 +240,12 @@ static bool fakelib_runtime_config_changed(const runtime_config_t *old_cfg,
static uint32_t scanner_config_topology_hash(void) {
uint32_t hash = 2166136261u;
int scan_path_count = get_scan_path_count();
uint32_t values[] = {runtime_config()->scan_depth, (uint32_t)scan_path_count};
uint32_t values[] = {
runtime_config()->scan_depth,
runtime_config()->watch_subdirs ? 1u : 0u,
runtime_config()->watch_image_files ? 1u : 0u,
(uint32_t)scan_path_count,
};
for (size_t i = 0; i < sizeof(values) / sizeof(values[0]); i++) {
for (unsigned shift = 0; shift < 32u; shift += 8u) {
@@ -584,6 +589,7 @@ static bool resolve_watch_tree_rebuild_target(const scanner_watch_entry_t *entry
typedef struct {
int kq;
int scan_root_index;
bool watch_subdirs;
} register_watch_tree_ctx_t;
static sm_scan_tree_dir_visit_t register_watch_directory_visit(
@@ -592,9 +598,11 @@ static sm_scan_tree_dir_visit_t register_watch_directory_visit(
scanner_watch_kind_t kind =
(depth_from_root == 0u) ? SCANNER_WATCH_SCAN_ROOT
: SCANNER_WATCH_SCAN_SUBDIR;
if (!register_scanner_watch_entry(ctx->kq, ctx->scan_root_index, dir_path, kind,
(uint8_t)depth_from_root)) {
return SM_SCAN_TREE_DIR_ABORT;
if (depth_from_root == 0u || ctx->watch_subdirs) {
if (!register_scanner_watch_entry(ctx->kq, ctx->scan_root_index, dir_path,
kind, (uint8_t)depth_from_root)) {
return SM_SCAN_TREE_DIR_ABORT;
}
}
return SM_SCAN_TREE_DIR_DESCEND;
@@ -637,19 +645,28 @@ static bool rebuild_scan_root_watch_tree(int kq, int scan_root_index) {
return true;
}
unsigned int scan_depth = get_scan_depth_for_root(scan_root);
register_watch_tree_ctx_t walk_ctx = {
.kq = kq,
.scan_root_index = scan_root_index,
};
sm_scan_tree_callbacks_t callbacks = {
.on_directory = register_watch_directory_visit,
.on_image_file = register_watch_image_visit,
};
if (!sm_scan_tree_walk(scan_root, scan_root, 0u, scan_depth, &callbacks,
&walk_ctx)) {
return false;
const runtime_config_t *cfg = runtime_config();
if (!cfg->watch_subdirs && !cfg->watch_image_files) {
if (!register_scanner_watch_entry(kq, scan_root_index, scan_root,
SCANNER_WATCH_SCAN_ROOT, 0u)) {
return false;
}
} else {
unsigned int scan_depth = get_scan_depth_for_root(scan_root);
register_watch_tree_ctx_t walk_ctx = {
.kq = kq,
.scan_root_index = scan_root_index,
.watch_subdirs = cfg->watch_subdirs,
};
sm_scan_tree_callbacks_t callbacks = {
.on_directory = register_watch_directory_visit,
.on_image_file =
cfg->watch_image_files ? register_watch_image_visit : NULL,
};
if (!sm_scan_tree_walk(scan_root, scan_root, 0u, scan_depth, &callbacks,
&walk_ctx)) {
return false;
}
}
char backport_root[MAX_PATH];
@@ -690,10 +707,15 @@ static bool rebuild_scan_root_watch_subtree(int kq, int scan_root_index,
return true;
}
unsigned int scan_depth = get_scan_depth_for_root(scan_root);
if (!remove_scan_root_watch_entries_for_path(scan_root_index, rebuild_path))
return false;
const runtime_config_t *cfg = runtime_config();
if (!cfg->watch_subdirs && !cfg->watch_image_files) {
clear_scan_root_watch_tree_state(scan_root_index);
return true;
}
unsigned int scan_depth = get_scan_depth_for_root(scan_root);
if (rebuild_depth > scan_depth) {
clear_scan_root_watch_tree_state(scan_root_index);
return true;
@@ -702,10 +724,11 @@ static bool rebuild_scan_root_watch_subtree(int kq, int scan_root_index,
register_watch_tree_ctx_t walk_ctx = {
.kq = kq,
.scan_root_index = scan_root_index,
.watch_subdirs = cfg->watch_subdirs,
};
sm_scan_tree_callbacks_t callbacks = {
.on_directory = register_watch_directory_visit,
.on_image_file = register_watch_image_visit,
.on_image_file = cfg->watch_image_files ? register_watch_image_visit : NULL,
};
if (!sm_scan_tree_walk(scan_root, rebuild_path, rebuild_depth,
scan_depth - rebuild_depth, &callbacks, &walk_ctx)) {