mirror of
https://github.com/drakmor/ShadowMountPlus.git
synced 2026-10-20 13:50:00 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47e47477d1 | ||
|
|
31c28e1896 | ||
|
|
c1f34ffa22 |
No files matched your search
@@ -1,6 +1,6 @@
|
||||
# ShadowMountPlus (PS5)
|
||||
|
||||
**Version:** `1.6beta1`
|
||||
**Version:** `1.6beta3`
|
||||
|
||||
**Repository:** https://github.com/drakmor/shadowMountPlus
|
||||
|
||||
|
||||
@@ -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"
|
||||
+71
-17
@@ -41,6 +41,8 @@
|
||||
#define MAX_MISSING_PARAM_SCAN_ATTEMPTS 3
|
||||
#define MAX_IMAGE_MOUNT_ATTEMPTS 3
|
||||
#define MAX_LAYERED_UNMOUNT_ATTEMPTS 4
|
||||
#define LVD1_RELEASE_WAIT_MAX_US 15000000u
|
||||
#define LVD1_RELEASE_WAIT_POLL_US 500000u
|
||||
#define IMAGE_MOUNT_READ_ONLY 1
|
||||
#define MIN_SCAN_INTERVAL_SECONDS 1u
|
||||
#define MAX_SCAN_INTERVAL_SECONDS 3600u
|
||||
@@ -53,7 +55,7 @@
|
||||
#define MAX_PATH 1024
|
||||
#define MAX_TITLE_ID 32
|
||||
#define MAX_TITLE_NAME 256
|
||||
#define SHADOWMOUNT_VERSION "1.6beta1"
|
||||
#define SHADOWMOUNT_VERSION "1.6beta3"
|
||||
#define PAYLOAD_NAME "shadowmountplus.elf"
|
||||
#define IMAGE_MOUNT_BASE "/data/imgmnt"
|
||||
#define IMAGE_MOUNT_SUBDIR_UFS "ufsmnt"
|
||||
@@ -373,6 +375,7 @@ static bool g_runtime_cfg_ready = false;
|
||||
static sqlite3 *g_app_db = NULL;
|
||||
static sqlite3_stmt *g_app_db_stmt_update_snd0 = NULL;
|
||||
static char g_last_image_mount_errmsg[256];
|
||||
static bool g_last_image_mount_notified = false;
|
||||
static struct AppDbTitleList g_app_db_title_cache = {0};
|
||||
static bool g_app_db_title_cache_ready = false;
|
||||
static time_t g_app_db_title_cache_mtime = 0;
|
||||
@@ -2223,31 +2226,51 @@ static bool is_active_image_mount_point(const char *path) {
|
||||
}
|
||||
|
||||
static bool wait_for_lvd1_release(void) {
|
||||
bool was_waiting = false;
|
||||
while (true) {
|
||||
for (unsigned int waited_us = 0;; waited_us += LVD1_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/lvd1") == 0) {
|
||||
mounted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!mounted)
|
||||
if (strcmp(mntbuf[i].f_mntfromname, "/dev/lvd1") != 0)
|
||||
continue;
|
||||
mounted = true;
|
||||
break;
|
||||
}
|
||||
if (!mounted) {
|
||||
if (waited_us != 0)
|
||||
log_debug(" [IMG][LVD] /dev/lvd1 released");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!was_waiting) {
|
||||
if (waited_us == 0) {
|
||||
log_debug(" [IMG][LVD] waiting for /dev/lvd1 to be released...");
|
||||
was_waiting = true;
|
||||
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;
|
||||
sceKernelUsleep(500000);
|
||||
if (waited_us >= LVD1_RELEASE_WAIT_MAX_US) {
|
||||
log_debug(" [IMG][LVD] /dev/lvd1 wait timeout reached (%u ms), "
|
||||
"continuing startup", LVD1_RELEASE_WAIT_MAX_US / 1000u);
|
||||
return true;
|
||||
}
|
||||
sceKernelUsleep(LVD1_RELEASE_WAIT_POLL_US);
|
||||
}
|
||||
if (was_waiting)
|
||||
log_debug(" [IMG][LVD] /dev/lvd1 released");
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- Device Detach Helpers ---
|
||||
@@ -2532,6 +2555,7 @@ static bool unmount_image(const char *file_path, int unit_id,
|
||||
static bool mount_image(const char *file_path, image_fs_type_t fs_type) {
|
||||
ensure_runtime_config_ready();
|
||||
g_last_image_mount_errmsg[0] = '\0';
|
||||
g_last_image_mount_notified = false;
|
||||
bool mount_mode_overridden = false;
|
||||
bool mount_read_only = g_runtime_cfg.mount_read_only;
|
||||
bool force_mount = g_runtime_cfg.force_mount;
|
||||
@@ -2853,6 +2877,34 @@ static bool mount_image(const char *file_path, image_fs_type_t fs_type) {
|
||||
image_fs_name(fs_type), devname, mount_point);
|
||||
log_fs_stats("IMG", mount_point, image_fs_name(fs_type));
|
||||
|
||||
struct statfs mounted_sfs;
|
||||
if (statfs(mount_point, &mounted_sfs) == 0) {
|
||||
uint32_t min_device_sector =
|
||||
(attach_backend == ATTACH_BACKEND_MD) ? get_md_sector_size(fs_type)
|
||||
: get_lvd_sector_size(fs_type);
|
||||
uint64_t fs_block_size = (uint64_t)mounted_sfs.f_bsize;
|
||||
if (fs_block_size < (uint64_t)min_device_sector) {
|
||||
snprintf(g_last_image_mount_errmsg, sizeof(g_last_image_mount_errmsg),
|
||||
"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", backend_name(attach_backend),
|
||||
g_last_image_mount_errmsg);
|
||||
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);
|
||||
g_last_image_mount_notified = true;
|
||||
(void)unmount_image(file_path, unit_id, attach_backend);
|
||||
errno = EINVAL;
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
log_debug(" [IMG][%s] statfs after mount failed for %s: %s",
|
||||
backend_name(attach_backend), mount_point, strerror(errno));
|
||||
}
|
||||
|
||||
struct stat param_st;
|
||||
char title_id[MAX_TITLE_ID];
|
||||
char title_name[MAX_TITLE_NAME];
|
||||
@@ -2930,7 +2982,8 @@ static void cleanup_stale_image_mounts(void) {
|
||||
|
||||
int mount_err = errno;
|
||||
if (bump_image_mount_attempts(source_path) == 1) {
|
||||
notify_image_mount_failed(source_path, mount_err);
|
||||
if (!g_last_image_mount_notified)
|
||||
notify_image_mount_failed(source_path, mount_err);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3035,7 +3088,8 @@ static void maybe_mount_image_file(const char *full_path,
|
||||
else {
|
||||
int mount_err = errno;
|
||||
if (bump_image_mount_attempts(full_path) == 1) {
|
||||
notify_image_mount_failed(full_path, mount_err);
|
||||
if (!g_last_image_mount_notified)
|
||||
notify_image_mount_failed(full_path, mount_err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user