Compare commits

...
3 Commits
3 changed files with 209 additions and 18 deletions

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
# ShadowMountPlus (PS5)
**Version:** `1.6beta1`
**Version:** `1.6beta3`
**Repository:** https://github.com/drakmor/shadowMountPlus
+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"
+71 -17
View File
@@ -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);
}
}
}