mirror of
https://github.com/drakmor/ShadowMountPlus.git
synced 2026-10-18 20:59:59 -05:00
138 lines
4.7 KiB
Bash
138 lines
4.7 KiB
Bash
#!/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"
|