mirror of
https://github.com/drakmor/ShadowMountPlus.git
synced 2026-10-20 05:40:07 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
851c269647 | ||
|
|
c9b3415048 | ||
|
|
11b32a97cf | ||
|
|
a43401391d | ||
|
|
0f7bc24928 | ||
|
|
1db534cf82 | ||
|
|
3f81ae589f | ||
|
|
32ae0c9c50 | ||
|
|
5564b4396d | ||
|
|
abe6bbb92b | ||
|
|
896e5af0fd | ||
|
|
15776edb8b | ||
|
|
1bc6477ea7 | ||
|
|
eb2a4c1036 | ||
|
|
8f34cabb20 | ||
|
|
ce74866768 | ||
|
|
f5421c9c5f | ||
|
|
1260396ffd | ||
|
|
7643b3f7f7 | ||
|
|
c00b162a01 |
No files matched your search
@@ -0,0 +1,53 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
name: Bug Report
|
||||
about: Create a report to help us improve ShadowMountPlus
|
||||
title: "[BUG] Short description of the issue"
|
||||
labels: bug
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
**Description**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
Please answer the following questions to help us diagnose the issue:
|
||||
|
||||
**Software Version**
|
||||
- **ShadowMountPlus Version:**
|
||||
|
||||
- **Path to the game that isn't working:**
|
||||
- **Image Type:**
|
||||
- [ ] .fpkg
|
||||
- [ ] .exfat image
|
||||
|
||||
- **How was this image created?**
|
||||
- **Does it work with any other software?**
|
||||
- **Firmware Version & Backports:**
|
||||
**Itemzflow History**
|
||||
- **Has Itemzflow been used for these games before?**
|
||||
- [ ] Yes
|
||||
- [ ] No
|
||||
|
||||
- **If YES: Did you delete the game shortcuts using Itemzflow BEFORE installing ShadowMountPlus?**
|
||||
- [ ] Yes, I deleted them
|
||||
- [ ] No, I did not delete them
|
||||
- [ ] N/A
|
||||
|
||||
**Debug Log**
|
||||
Please attach the `debug.log` file. You can drag and drop the file here, or copy and paste the content inside the spoiler block below.
|
||||
|
||||
<details>
|
||||
<summary>Click to expand debug.log content</summary>
|
||||
|
||||
```text
|
||||
PASTE YOUR LOG HERE
|
||||
```
|
||||
</details>
|
||||
@@ -1,21 +1,22 @@
|
||||
PS5_PAYLOAD_SDK ?= /opt/ps5-payload-sdk
|
||||
include $(PS5_PAYLOAD_SDK)/toolchain/prospero.mk
|
||||
|
||||
# Standard Flags (No extra libraries)
|
||||
PS5_PAYLOAD_SDK ?= /opt/ps5-payload-sdk
|
||||
include $(PS5_PAYLOAD_SDK)/toolchain/prospero.mk
|
||||
|
||||
# Standard Flags (No extra libraries)
|
||||
CFLAGS := -O2 -Wall -D_BSD_SOURCE -std=gnu11 -Isrc
|
||||
|
||||
# Linker
|
||||
LDFLAGS :=
|
||||
|
||||
# Standard Libraries Only
|
||||
|
||||
# Standard Libraries Only
|
||||
LIBS := -lkernel_sys -lSceSystemService -lSceUserService -lSceAppInstUtil -lsqlite3
|
||||
|
||||
# Targets
|
||||
all: shadowmountplus.elf
|
||||
|
||||
# Build Daemon
|
||||
shadowmountplus.elf: src/main.c
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $< $(LIBS)
|
||||
|
||||
clean:
|
||||
|
||||
# Targets
|
||||
all: shadowmountplus.elf
|
||||
|
||||
# Build Daemon
|
||||
shadowmountplus.elf: src/main.c
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $< $(LIBS)
|
||||
$(PS5_PAYLOAD_SDK)/bin/prospero-strip --strip-debug $@
|
||||
|
||||
clean:
|
||||
rm -f shadowmountplus.elf kill.elf src/*.o
|
||||
+115
-64
@@ -3,7 +3,9 @@
|
||||
PURPOSE
|
||||
-------
|
||||
Create a RAW image file, mount it via OSFMount as a logical volume,
|
||||
auto-select FAT32/exFAT by image size, format it, copy a folder into it, then dismount it.
|
||||
format it as exFAT, and either:
|
||||
- format + copy + dismount (default), or
|
||||
- create + mount only for manual steps.
|
||||
|
||||
USAGE (run PowerShell as Administrator)
|
||||
--------------------------------------
|
||||
@@ -22,6 +24,13 @@
|
||||
-Label "DATA" `
|
||||
-ForceOverwrite
|
||||
|
||||
3) Create empty image and keep mounted (manual format/copy):
|
||||
powershell.exe -ExecutionPolicy Bypass -File .\New-OsfExfatImage.ps1 `
|
||||
-ImagePath "C:\images\data.exfat" `
|
||||
-SourceDir "C:\payload\APPXXXX" `
|
||||
-CreateEmptyAndMount `
|
||||
-ForceOverwrite
|
||||
|
||||
PARAMETERS
|
||||
----------
|
||||
-ImagePath Output image file path.
|
||||
@@ -30,13 +39,16 @@
|
||||
Suffixes: K/M/G/T (1024), k/m/g/t (1000), b (512-byte blocks), or bytes.
|
||||
-Label Volume label.
|
||||
-ForceOverwrite Recreate image if it already exists.
|
||||
-CreateEmptyAndMount
|
||||
Create and mount image only. Skip format/copy and leave mounted.
|
||||
|
||||
NOTES
|
||||
-----
|
||||
- This script does NOT auto-elevate. Start PowerShell as Administrator.
|
||||
- FS selection is automatic by image size:
|
||||
* < 4 GB -> FAT32, cluster 4096
|
||||
* >= 4 GB -> exFAT, cluster 32768
|
||||
- Filesystem is always exFAT.
|
||||
- Cluster size is auto-selected:
|
||||
- large-file sets: 65536
|
||||
- small/mixed-file sets: 32768
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
@@ -52,7 +64,9 @@ param(
|
||||
|
||||
[string]$Label = "OSFIMG",
|
||||
|
||||
[switch]$ForceOverwrite
|
||||
[switch]$ForceOverwrite,
|
||||
|
||||
[switch]$CreateEmptyAndMount
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
@@ -74,9 +88,9 @@ function Find-OSFMountCom {
|
||||
if ($cmd) { return $cmd.Source }
|
||||
|
||||
$candidates = @(
|
||||
"$env:ProgramFiles\OSFMount\osfmount.com",
|
||||
"${env:ProgramFiles}\OSFMount\osfmount.com",
|
||||
"${env:ProgramFiles(x86)}\OSFMount\osfmount.com",
|
||||
"$env:ProgramFiles\PassMark\OSFMount\osfmount.com",
|
||||
"${env:ProgramFiles}\PassMark\OSFMount\osfmount.com",
|
||||
"${env:ProgramFiles(x86)}\PassMark\OSFMount\osfmount.com"
|
||||
) | Where-Object { $_ -and (Test-Path $_) }
|
||||
|
||||
@@ -128,39 +142,40 @@ function Get-FreeDriveLetter {
|
||||
|
||||
function Get-OptimalImageSizeBytes([string]$dir, [int]$clusterBytes) {
|
||||
$cluster = [Int64]$clusterBytes
|
||||
[Int64]$metaFixed = 32MB
|
||||
[Int64]$minSlack = 64MB
|
||||
[Int64]$spareMin = 64MB
|
||||
[Int64]$spareMax = 512MB
|
||||
[Int64]$entryMetaBytes = 256
|
||||
|
||||
$files = @(Get-ChildItem -LiteralPath $dir -Recurse -File -Force)
|
||||
$dirs = @(Get-ChildItem -LiteralPath $dir -Recurse -Directory -Force)
|
||||
$dirs = @(Get-ChildItem -LiteralPath $dir -Recurse -Directory -Force)
|
||||
|
||||
[Int64]$sumAllocated = 0
|
||||
[Int64]$rawFileBytes = 0
|
||||
[Int64]$dataBytes = 0
|
||||
foreach ($f in $files) {
|
||||
$len = [Int64]$f.Length
|
||||
$alloc = (($len + $cluster - 1) / $cluster) * $cluster
|
||||
$sumAllocated += $alloc
|
||||
$rawFileBytes += $len
|
||||
$dataBytes += [Int64]([Math]::Ceiling($len / [double]$cluster) * $cluster)
|
||||
}
|
||||
|
||||
# Metadata estimate (moderate, not bloated)
|
||||
[Int64]$metaEntries = ([Int64]$files.Count * 512) + ([Int64]$dirs.Count * 256)
|
||||
[Int64]$dataClusters = [Int64]([Math]::Ceiling($dataBytes / [double]$cluster))
|
||||
[Int64]$fatBytes = $dataClusters * 4
|
||||
[Int64]$bitmapBytes = [Int64]([Math]::Ceiling($dataClusters / 8.0))
|
||||
[Int64]$entryBytes =
|
||||
(([Int64]$files.Count + [Int64]$dirs.Count) * $entryMetaBytes)
|
||||
|
||||
[Int64]$clustersData = [Int64]([Math]::Ceiling($sumAllocated / [double]$cluster))
|
||||
[Int64]$bitmap = [Int64]([Math]::Ceiling($clustersData / 8.0)) # 1 bit per cluster
|
||||
[Int64]$fat = $clustersData * 4 # rough 4 bytes per cluster
|
||||
[Int64]$baseTotal =
|
||||
$dataBytes + $fatBytes + $bitmapBytes + $entryBytes + $metaFixed
|
||||
[Int64]$spareBytes = [Int64]([Math]::Ceiling($baseTotal / 200.0))
|
||||
if ($spareBytes -lt $spareMin) { $spareBytes = $spareMin }
|
||||
if ($spareBytes -gt $spareMax) { $spareBytes = $spareMax }
|
||||
[Int64]$total = $baseTotal + $spareBytes
|
||||
[Int64]$minTotal = $rawFileBytes + $minSlack
|
||||
if ($total -lt $minTotal) { $total = $minTotal }
|
||||
|
||||
[Int64]$baseline = 16MB
|
||||
[Int64]$raw = $sumAllocated + $metaEntries + $bitmap + $fat + $baseline
|
||||
|
||||
# Safety margin: 2% capped to 512 MiB
|
||||
[Int64]$margin = [Int64]([Math]::Ceiling($raw * 0.02))
|
||||
if ($margin -gt 512MB) { $margin = 512MB }
|
||||
if ($margin -lt 8MB) { $margin = 8MB }
|
||||
|
||||
[Int64]$total = $raw + $margin
|
||||
if ($total -lt 64MB) { $total = 64MB }
|
||||
|
||||
# Align to 1 MiB
|
||||
[Int64]$align = 1MB
|
||||
$total = (($total + $align - 1) / $align) * $align
|
||||
|
||||
$total = [Int64]([Math]::Ceiling($total / [double]$align) * $align)
|
||||
return $total
|
||||
}
|
||||
|
||||
@@ -182,6 +197,32 @@ function Get-LogicalDriveFileSystem([string]$driveLetter) {
|
||||
return ""
|
||||
}
|
||||
|
||||
function Get-OptimalExfatClusterSize([string]$dir) {
|
||||
[Int64]$largeFileThreshold = 1MB
|
||||
|
||||
$files = @(Get-ChildItem -LiteralPath $dir -Recurse -File -Force)
|
||||
if ($files.Count -eq 0) { return 32768 }
|
||||
|
||||
[Int64]$rawFileBytes = 0
|
||||
foreach ($f in $files) {
|
||||
$rawFileBytes += [Int64]$f.Length
|
||||
}
|
||||
|
||||
[Int64]$avgFileBytes = [Int64]($rawFileBytes / [Int64]$files.Count)
|
||||
if ($avgFileBytes -ge $largeFileThreshold) { return 65536 }
|
||||
return 32768
|
||||
}
|
||||
|
||||
function Format-AllocationUnitArg([int]$clusterSize) {
|
||||
if ($clusterSize -ge 1MB -and ($clusterSize % 1MB) -eq 0) {
|
||||
return "{0}M" -f ($clusterSize / 1MB)
|
||||
}
|
||||
if ($clusterSize -ge 1KB -and ($clusterSize % 1KB) -eq 0) {
|
||||
return "{0}K" -f ($clusterSize / 1KB)
|
||||
}
|
||||
return "$clusterSize"
|
||||
}
|
||||
|
||||
function Dismount-OsfVolume([string]$osfPath, [string]$mountPoint, [int]$maxAttempts = 6) {
|
||||
if ([string]::IsNullOrWhiteSpace($mountPoint)) { return $false }
|
||||
|
||||
@@ -203,14 +244,12 @@ function Dismount-OsfVolume([string]$osfPath, [string]$mountPoint, [int]$maxAtte
|
||||
return $false
|
||||
}
|
||||
|
||||
function Invoke-FormatVolume([string]$driveLetter, [string]$fileSystem, [int]$clusterSize, [string]$label) {
|
||||
function Invoke-FormatVolume([string]$driveLetter, [int]$clusterSize, [string]$label) {
|
||||
$target = "${driveLetter}:"
|
||||
if ($fileSystem -notin @("FAT32", "exFAT")) {
|
||||
throw "Unsupported file system '$fileSystem'. Expected FAT32 or exFAT."
|
||||
}
|
||||
|
||||
$fileSystem = "exFAT"
|
||||
$clusterArg = Format-AllocationUnitArg -clusterSize $clusterSize
|
||||
$attempts = @(
|
||||
@{ Name = "$fileSystem quick with requested allocation unit"; Args = @($target, "/FS:$fileSystem", "/A:$clusterSize", "/Q", "/V:$label", "/X", "/Y") }
|
||||
@{ Name = "$fileSystem quick with requested allocation unit"; Args = @($target, "/FS:$fileSystem", "/A:$clusterArg", "/Q", "/V:$label", "/X", "/Y") }
|
||||
)
|
||||
|
||||
$lastFormatExitCode = -1
|
||||
@@ -222,6 +261,14 @@ function Invoke-FormatVolume([string]$driveLetter, [string]$fileSystem, [int]$cl
|
||||
$proc = Start-Process -FilePath "format.com" -ArgumentList $attempt.Args -Wait -PassThru -NoNewWindow `
|
||||
-RedirectStandardOutput $stdoutPath -RedirectStandardError $stderrPath
|
||||
$lastFormatExitCode = [int]$proc.ExitCode
|
||||
|
||||
# Show native format.com output in current console.
|
||||
if (Test-Path -LiteralPath $stdoutPath) {
|
||||
Get-Content -LiteralPath $stdoutPath -ErrorAction SilentlyContinue | ForEach-Object { Write-Host $_ }
|
||||
}
|
||||
if (Test-Path -LiteralPath $stderrPath) {
|
||||
Get-Content -LiteralPath $stderrPath -ErrorAction SilentlyContinue | ForEach-Object { Write-Host $_ }
|
||||
}
|
||||
} finally {
|
||||
Remove-Item -LiteralPath $stdoutPath, $stderrPath -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
@@ -246,6 +293,7 @@ function Invoke-FormatVolume([string]$driveLetter, [string]$fileSystem, [int]$cl
|
||||
|
||||
if (-not (Test-Admin)) { throw "Please run PowerShell as Administrator." }
|
||||
if (-not (Test-Path $SourceDir -PathType Container)) { throw "Source directory not found: $SourceDir" }
|
||||
if (-not (Test-Path (Join-Path $SourceDir "eboot.bin") -PathType Leaf)) { throw "eboot.bin not found in source directory: $SourceDir" }
|
||||
|
||||
# Ensure output directory exists
|
||||
$outDir = Split-Path -Parent $ImagePath
|
||||
@@ -260,48 +308,41 @@ if (Test-Path $ImagePath) {
|
||||
|
||||
[Int64]$expectedBytes = 0
|
||||
[string]$osfSizeArg = $null
|
||||
[Int64]$FsSwitchThresholdBytes = 4GB
|
||||
[int]$Fat32ClusterSize = 4096
|
||||
[int]$ExfatClusterSize = 32768
|
||||
[string]$TargetFs = ""
|
||||
[int]$TargetClusterSize = 0
|
||||
[int]$ExfatClusterSize = Get-OptimalExfatClusterSize -dir $SourceDir
|
||||
[bool]$sizeProvided = -not [string]::IsNullOrWhiteSpace($Size)
|
||||
[int]$TargetClusterSize = $ExfatClusterSize
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($Size)) {
|
||||
if (-not $sizeProvided) {
|
||||
Write-Host "[Info] Size not provided. Computing an optimal image size from '$SourceDir'..."
|
||||
$fat32Candidate = Get-OptimalImageSizeBytes -dir $SourceDir -clusterBytes $Fat32ClusterSize
|
||||
if ($fat32Candidate -lt $FsSwitchThresholdBytes) {
|
||||
$expectedBytes = $fat32Candidate
|
||||
} else {
|
||||
$expectedBytes = Get-OptimalImageSizeBytes -dir $SourceDir -clusterBytes $ExfatClusterSize
|
||||
}
|
||||
$expectedBytes = Get-OptimalImageSizeBytes -dir $SourceDir -clusterBytes $TargetClusterSize
|
||||
$osfSizeArg = "$expectedBytes"
|
||||
Write-Host "[Info] Computed image size: $(Format-Bytes $expectedBytes) ($expectedBytes bytes)."
|
||||
} else {
|
||||
$expectedBytes = Parse-SizeToBytes $Size
|
||||
$osfSizeArg = $Size
|
||||
}
|
||||
|
||||
if ($expectedBytes -lt $FsSwitchThresholdBytes) {
|
||||
$TargetFs = "FAT32"
|
||||
$TargetClusterSize = $Fat32ClusterSize
|
||||
} else {
|
||||
$TargetFs = "exFAT"
|
||||
$TargetClusterSize = $ExfatClusterSize
|
||||
if (-not $sizeProvided) {
|
||||
Write-Host "[Info] Computed image size: $(Format-Bytes $expectedBytes) ($expectedBytes bytes)."
|
||||
}
|
||||
Write-Host "[Info] Selected filesystem: $TargetFs (cluster=$TargetClusterSize) for image size $(Format-Bytes $expectedBytes)."
|
||||
Write-Host "[Info] Selected filesystem: exFAT (cluster=$TargetClusterSize) for image size $(Format-Bytes $expectedBytes)."
|
||||
|
||||
$osf = Find-OSFMountCom
|
||||
|
||||
[string]$DriveLetter = ""
|
||||
[string]$MountPoint = ""
|
||||
[bool]$Mounted = $false
|
||||
[bool]$LeaveMounted = $CreateEmptyAndMount.IsPresent
|
||||
|
||||
try {
|
||||
$DriveLetter = Get-FreeDriveLetter
|
||||
$MountPoint = "${DriveLetter}:"
|
||||
|
||||
Write-Host "[1/4] Creating & mounting the image via OSFMount as a logical volume on $MountPoint ..."
|
||||
$out = & $osf -a -t file -f $ImagePath -s $osfSizeArg -m $MountPoint -o rw 2>&1
|
||||
if ($CreateEmptyAndMount) {
|
||||
Write-Host "[1/2] Creating & mounting the image via OSFMount as a logical volume on $MountPoint ..."
|
||||
} else {
|
||||
Write-Host "[1/4] Creating & mounting the image via OSFMount as a logical volume on $MountPoint ..."
|
||||
}
|
||||
$out = & $osf -a -t file -f $ImagePath -s $osfSizeArg -m $MountPoint -o rw,rem 2>&1
|
||||
Write-Host ($out | Out-String).Trim()
|
||||
if ($LASTEXITCODE -ne 0) { throw "osfmount.com failed with exit code $LASTEXITCODE." }
|
||||
$Mounted = $true
|
||||
@@ -310,26 +351,36 @@ try {
|
||||
}
|
||||
|
||||
$dest = "${DriveLetter}:\"
|
||||
Write-Host "[2/4] Formatting $dest as $TargetFs (cluster=$TargetClusterSize, label='$Label') via format.com ..."
|
||||
Invoke-FormatVolume -driveLetter $DriveLetter -fileSystem $TargetFs -clusterSize $TargetClusterSize -label $Label
|
||||
if ($CreateEmptyAndMount) {
|
||||
Write-Host "[2/2] Done. Empty image is mounted at $dest."
|
||||
Write-Host "Manual steps:"
|
||||
Write-Host " 1) Format $dest as exFAT (recommended cluster: 64KB for large-file sets, 32KB for small/mixed sets)."
|
||||
Write-Host " 2) Copy contents of '$SourceDir' to $dest."
|
||||
Write-Host " 3) Dismount: `"$osf`" -d -m $MountPoint"
|
||||
return
|
||||
}
|
||||
|
||||
Write-Host "[2/4] Formatting $dest as exFAT (cluster=$TargetClusterSize, label='$Label') via format.com ..."
|
||||
Invoke-FormatVolume -driveLetter $DriveLetter -clusterSize $TargetClusterSize -label $Label
|
||||
|
||||
if (-not (Test-Path $dest)) { throw "Drive $dest is not accessible after formatting." }
|
||||
|
||||
Write-Host "[3/4] Copying '$SourceDir' -> '$dest' ..."
|
||||
Write-Host "[3/4] Copying contents of '$SourceDir' -> '$dest' ..."
|
||||
$roboArgs = @(
|
||||
$SourceDir, $dest,
|
||||
"/E", "/COPY:DAT", "/DCOPY:DAT",
|
||||
"/R:1", "/W:1",
|
||||
"/NFL", "/NDL", "/NP", "/NJH", "/NJS"
|
||||
"/NFL", "/NDL",
|
||||
"/ETA"
|
||||
)
|
||||
& robocopy.exe @roboArgs | Out-Null
|
||||
& robocopy.exe @roboArgs
|
||||
$robocopyExitCode = $LASTEXITCODE
|
||||
if ($robocopyExitCode -gt 7) { throw "robocopy failed. Exit code: $robocopyExitCode" }
|
||||
|
||||
Write-Host "[4/4] Done. Dismounting OSFMount volume..."
|
||||
}
|
||||
finally {
|
||||
if ($Mounted -and -not [string]::IsNullOrWhiteSpace($MountPoint)) {
|
||||
if ($Mounted -and -not $LeaveMounted -and -not [string]::IsNullOrWhiteSpace($MountPoint)) {
|
||||
try {
|
||||
$currentPath = (Get-Location).Path
|
||||
if ($currentPath -like "$MountPoint*") {
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
# ShadowMountPlus (PS5)
|
||||
|
||||
**Version:** `1.5beta2`
|
||||
**Version:** `1.6beta1`
|
||||
|
||||
**Repository:** https://github.com/drakmor/shadowMountPlus
|
||||
|
||||
**Warning! Mounting images can cause shutdown problems and data corruption on internal drives! This depends on many factors, but is more common with older firmware versions. Please take this into account when testing.**
|
||||
|
||||
|
||||
**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**.
|
||||
@@ -15,42 +19,67 @@
|
||||
|
||||
| Extension | Mounted FS | Attach backend | Status |
|
||||
| --- | --- | --- | --- |
|
||||
| `.exfat` | `exfatfs` | `LVD` or `MD` (configurable) | Stable |
|
||||
| `.ffpkg` | `ufs` | `LVD` or `MD` (configurable) | High performance |
|
||||
| `.exfat` | `exfatfs` | `LVD` or `MD` (configurable) | Optimal |
|
||||
| `.ffpkg` | `ufs` | `LVD` or `MD` (configurable) | Legacy |
|
||||
| `.ffpfs` | `pfs` | `LVD` | Experimental |
|
||||
|
||||
Notes:
|
||||
- Backend, read-only mode, and sector size can be configured via `/data/shadowmount/config.ini`.
|
||||
- Optional debug logging can be enabled via `debug=1` (writes to console and `/data/shadowmount/debug.log`).
|
||||
- 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.**
|
||||
|
||||
## Recommended FS choice
|
||||
|
||||
- Prefer **UFS (`.ffpkg`)** for games that work correctly on UFS: it is generally more performant (~1.5-2.5 x exFAT).
|
||||
- Use **exFAT (`.exfat`)** for games that only work from external-disk style layouts, because exFAT is case-insensitive.
|
||||
- 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.
|
||||
|
||||
## Runtime config (`/data/shadowmount/config.ini`)
|
||||
|
||||
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 `0`)
|
||||
- `mount_read_only=1|0`
|
||||
- `exfat_backend=lvd|md`
|
||||
- `ufs_backend=lvd|md`
|
||||
- `scanpath=<absolute_path>` (can be repeated on multiple lines)
|
||||
- `exfat_sector_size=<value>`
|
||||
- `ufs_sector_size=<value>`
|
||||
- `pfs_sector_size=<value>`
|
||||
- `lvd_exfat_sector_size=<value>`
|
||||
- `lvd_ufs_sector_size=<value>`
|
||||
- `lvd_pfs_sector_size=<value>`
|
||||
- `md_exfat_sector_size=<value>`
|
||||
- `md_ufs_sector_size=<value>`
|
||||
- `debug=1|0` (`1` enables `log_debug` output to console + `/data/shadowmount/debug.log`; default is `1`)
|
||||
- `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`)
|
||||
- `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`)
|
||||
- `lvd_pfs_sector_size=<value>` (default: `32768`)
|
||||
- `md_exfat_sector_size=<value>` (default: `512`)
|
||||
- `md_ufs_sector_size=<value>` (default: `512`)
|
||||
|
||||
Per-image mode override behavior:
|
||||
- Match is done by image file name (without path).
|
||||
- File names with spaces are supported.
|
||||
- If multiple rules target the same file name, the last one in config wins.
|
||||
- If no rule matches, global `mount_read_only` is used.
|
||||
- Example:
|
||||
```ini
|
||||
mount_read_only=1
|
||||
image_rw=PPSA1234-my-image.ffpfs
|
||||
image_rw=MYGame 123.exfat
|
||||
image_ro=legacy_dump.ffpkg
|
||||
```
|
||||
|
||||
Scan path behavior:
|
||||
- If at least one `scanpath=...` is present, only those custom paths are used.
|
||||
- `/data/ufsmnt` is always added automatically, even with custom paths.
|
||||
- Full scan loop runs every 10 seconds.
|
||||
- `/data/imgmnt/{ufsmnt,exfatmnt,pfsmnt}` are 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.
|
||||
|
||||
Validation:
|
||||
- See `config.ini.example` for a ready-to-use template.
|
||||
@@ -59,7 +88,15 @@ Validation:
|
||||
|
||||
Image mountpoints are created under:
|
||||
|
||||
`/data/ufsmnt/<image_name>-<fs_suffix>`
|
||||
`/data/imgmnt/<fs>/<image_name>`
|
||||
|
||||
Where `<fs>` is one of: `ufsmnt`, `exfatmnt`, `pfsmnt`.
|
||||
|
||||
Image layout requirement (`.ffpkg`, `.exfat`, `.ffpfs`):
|
||||
- Game files must be placed at the image root.
|
||||
- Do not add an extra top-level folder inside the image.
|
||||
- Valid example: `/sce_sys/param.json` exists directly from image root.
|
||||
- Invalid example: `/GAME_FOLDER/sce_sys/param.json` (extra nesting level).
|
||||
|
||||
## Scan paths
|
||||
|
||||
@@ -75,21 +112,36 @@ Default scan locations:
|
||||
- `/mnt/usb0` .. `/mnt/usb7`
|
||||
- `/mnt/ext0`
|
||||
- `/mnt/ext1`
|
||||
- `/data/ufsmnt` (mounted image content scan)
|
||||
- `/data/imgmnt/ufsmnt`, `/data/imgmnt/exfatmnt`, `/data/imgmnt/pfsmnt` (mounted image content scan)
|
||||
|
||||
You can override scan roots with `scanpath=...` entries in `/data/shadowmount/config.ini`.
|
||||
|
||||
Recommended folder structure:
|
||||
- Default mode (`recursive_scan=0`):
|
||||
- `/data/homebrew/<TITLE_ID>/`
|
||||
- `/data/etaHEN/games/<TITLE_ID>/`
|
||||
|
||||
- Recursive mode (`recursive_scan=1`):
|
||||
- `/data/homebrew/PS5/<AnyFolder>/<TITLE_ID>/`
|
||||
- `/mnt/ext0/etaHEN/games/<Collection>/<TITLE_ID>/`
|
||||
|
||||
|
||||
## Creating an exFAT image
|
||||
|
||||
Linux (Ubuntu/Debian):
|
||||
- `sudo apt-get install -y exfatprogs exfat-fuse fuse3 rsync`
|
||||
- `truncate -s <image_size> test.exfat`
|
||||
- `mkfs.exfat -c 32768 test.exfat`
|
||||
- `mkdir -p /mnt/exfat`
|
||||
- `mount -t exfat-fuse -o loop test.exfat /mnt/exfat`
|
||||
- `rsync -r --info=progress2 APPXXXX/ /mnt/exfat/`
|
||||
- `umount /mnt/exfat`
|
||||
- Required components installation:
|
||||
- `sudo apt-get update && sudo apt-get install -y exfatprogs exfat-fuse fuse3 rsync`
|
||||
- Script: `mkexfat.sh`
|
||||
- Usage: `./mkexfat.sh <game_root_dir> [output_file]`
|
||||
- Example:
|
||||
- `chmod +x mkexfat.sh`
|
||||
- `./mkexfat.sh ./APPXXXX ./PPSA12345.exfat`
|
||||
- Notes:
|
||||
- Source folder must be the game root and contain `eboot.bin`.
|
||||
- Auto-calculates image size using rounded file allocation + metadata + safety margin.
|
||||
- Automatically selects exFAT cluster profile:
|
||||
- Large-file profile: `64K`
|
||||
- Small/mixed-file profile: `32K`
|
||||
|
||||
Windows:
|
||||
- Recommended: use `make_image.bat` (wrapper for `New-OsfExfatImage.ps1` + OSFMount).
|
||||
@@ -101,8 +153,8 @@ Windows:
|
||||
- `make_image.bat "C:\images\game.exfat" "C:\payload\APPXXXX"`
|
||||
- Behavior:
|
||||
- Auto-sizes the image to fit source content.
|
||||
- Formats and copies the source folder into the image.
|
||||
- Overwrites existing image file (uses `-ForceOverwrite`).
|
||||
- Source folder must be the game root and contain `eboot.bin`.
|
||||
- Formats and copies source folder contents into image root.
|
||||
- Optional (fixed size): run PowerShell script directly:
|
||||
- `powershell.exe -ExecutionPolicy Bypass -File .\New-OsfExfatImage.ps1 -ImagePath "C:\images\game.exfat" -SourceDir "C:\payload\APPXXXX" -Size 8G -ForceOverwrite`
|
||||
|
||||
@@ -110,18 +162,23 @@ Windows:
|
||||
|
||||
FreeBSD:
|
||||
- Script: `mkufs2.sh`
|
||||
- Usage: `./mkufs2.sh <input_dir> [output_file]`
|
||||
- Usage: `./mkufs2.sh <game_root_dir> [output_file]`
|
||||
- Example:
|
||||
- `chmod +x mkufs2.sh`
|
||||
- `./mkufs2.sh ./APPXXXX ./PPSA12345.ffpkg`
|
||||
- Notes:
|
||||
- The script auto-calculates image size (`source + 20% + 10MB` slack).
|
||||
- Uses UFS2 format (`newfs -O 2`) with `-b 32768 -f 4096`.
|
||||
- Requires BSD tools like `mdconfig`, `newfs`, `mount`, `umount`.
|
||||
- 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).
|
||||
|
||||
Windows:
|
||||
- You can create UFS2 images with **UFS2Tool** https://github.com/SvenGDK/UFS2Tool.
|
||||
- Recommended output format for this project is `.ffpkg`.
|
||||
- Example:
|
||||
- `UFS2Tool.exe newfs -O 2 -b 32768 -f 4096 -m 0 -i 262144 -D ./APPXXXX ./PPSA12345.ffpkg`
|
||||
|
||||
|
||||
## Installation and usage
|
||||
|
||||
@@ -140,15 +197,41 @@ Add ShadowMountPlus to your `autoload.txt` for **plk-autoloader** to ensure it s
|
||||
**Sample Configuration:**
|
||||
```ini
|
||||
!1000
|
||||
kstuff.elf
|
||||
!1000
|
||||
notify.elf ; Optional - Remove this line if you do not want Rich Toasts
|
||||
!1000
|
||||
shadowmountplus.elf
|
||||
!3000
|
||||
ps5-backpork.elf
|
||||
!1000
|
||||
kstuff.elf
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If a game is not mounted:
|
||||
- Debug log is enabled by default; if disabled, set `debug=1` in `/data/shadowmount/config.ini`.
|
||||
- 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.
|
||||
- Verify scan depth:
|
||||
- `recursive_scan=0` scans only first-level subfolders;
|
||||
- `recursive_scan=1` scans recursively.
|
||||
- If logs show `source not stable yet`, adjust `stability_wait_seconds` (or wait for source copy/write to finish).
|
||||
- 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 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>`.
|
||||
|
||||
If a game is mounted but does not start:
|
||||
- Check registration notifications (`Register failed ...`).
|
||||
- If the game is not registered, try removing its launcher icon and removing it from Itemzflow.
|
||||
- If this does not help, remove the game data from system settings and retry (this will delete game saves).
|
||||
|
||||
## ⚠️ Notes
|
||||
* **First Run:** If you have a large library, the initial scan may take a few seconds to register all titles.
|
||||
* **Large Games:** For massive games (100GB+), allow a few extra seconds for the system to verify file integrity before the "Installed" notification appears.
|
||||
|
||||
+37
-9
@@ -4,26 +4,49 @@
|
||||
# All keys are optional.
|
||||
# If a key is missing, built-in defaults are used.
|
||||
|
||||
# Debug logging:
|
||||
# 1/true/yes/on -> enable log_debug output to console and /data/shadowmount/debug.log
|
||||
# 0/false/no/off -> disable log_debug output
|
||||
debug=1
|
||||
|
||||
# Global mount mode:
|
||||
# 1/true/yes/on/ro -> read-only mounts
|
||||
# 0/false/no/off/rw -> read-write mounts
|
||||
mount_read_only=1
|
||||
|
||||
# Mounting even damaged file systems
|
||||
# 1/true/yes/on -> include force option
|
||||
# 0/false/no/off -> default mount options (default)
|
||||
force_mount=0
|
||||
|
||||
# Per-image mount mode override by file name (can be repeated many times):
|
||||
# image_ro=<image_filename>
|
||||
# image_rw=<image_filename>
|
||||
# image_ro=PPSA12345.ffpfs
|
||||
# image_rw=My Game Dump.exfat
|
||||
|
||||
# Recursive scan mode:
|
||||
# 1/true/yes/on -> scan subdirectories recursively (no depth limit)
|
||||
# 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
|
||||
|
||||
# Source stabilization wait window (seconds), range: 0..3600
|
||||
# If source mtime is newer than this window, it will be deferred to next scan.
|
||||
stability_wait_seconds=10
|
||||
|
||||
# Backend selection per filesystem:
|
||||
# lvd -> /dev/lvdctl -> /dev/lvdN
|
||||
# md -> /dev/mdctl -> /dev/mdN
|
||||
exfat_backend=lvd
|
||||
ufs_backend=lvd
|
||||
|
||||
# Common sector-size shortcuts (apply to both LVD and MD where applicable):
|
||||
# exfat_sector_size -> lvd_exfat_sector_size + md_exfat_sector_size
|
||||
# ufs_sector_size -> lvd_ufs_sector_size + md_ufs_sector_size
|
||||
# pfs_sector_size -> lvd_pfs_sector_size
|
||||
#
|
||||
exfat_sector_size=512
|
||||
ufs_sector_size=4096
|
||||
pfs_sector_size=32768
|
||||
|
||||
# Backend-specific overrides (optional):
|
||||
# lvd_exfat_sector_size=512
|
||||
# lvd_ufs_sector_size=4096
|
||||
@@ -31,3 +54,8 @@ 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.
|
||||
# "/data/imgmnt/{ufsmnt,exfatmnt,pfsmnt}" are always added automatically.
|
||||
# scanpath=/data/homebrew
|
||||
# scanpath=/data/etaHEN/games
|
||||
+6
-4
@@ -24,13 +24,15 @@ if not exist "%SRCDIR%" (
|
||||
exit /b 3
|
||||
)
|
||||
|
||||
if not exist "%SRCDIR%\eboot.bin" (
|
||||
echo [ERROR] eboot.bin not found in source directory: "%SRCDIR%"
|
||||
exit /b 4
|
||||
)
|
||||
|
||||
REM Run elevated? This BAT does not auto-elevate.
|
||||
REM Right-click -> Run as administrator, or start cmd as admin.
|
||||
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT%" ^
|
||||
-ImagePath "%IMAGE%" ^
|
||||
-SourceDir "%SRCDIR%" ^
|
||||
-ForceOverwrite
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT%" -ImagePath "%IMAGE%" -SourceDir "%SRCDIR%" -ForceOverwrite
|
||||
|
||||
set "RC=%ERRORLEVEL%"
|
||||
if not "%RC%"=="0" (
|
||||
|
||||
+71
-11
@@ -1,6 +1,6 @@
|
||||
#!/bin/sh
|
||||
# For Ubuntu/Debian: sudo apt-get install -y exfatprogs exfat-fuse fuse3 rsync
|
||||
# Create a exFAT image from a directory
|
||||
# For WSL2/Ubuntu/Debian: sudo apt-get install -y exfatprogs exfat-fuse fuse3 rsync
|
||||
# Create an exFAT image from a directory
|
||||
# Usage: mkexfat.sh <input_dir> [output_file]
|
||||
|
||||
set -e
|
||||
@@ -11,25 +11,85 @@ if [ -z "$1" ]; then
|
||||
fi
|
||||
|
||||
INPUT_DIR="$1"
|
||||
OUTPUT="${2:-test.ffpkg}"
|
||||
OUTPUT="${2:-test.exfat}"
|
||||
|
||||
# Calculate required size (dir size + 20% overhead + 10MB slack)
|
||||
BYTES=$(du -s -k "$INPUT_DIR" | awk '{print $1 * 1024}')
|
||||
OVERHEAD=$((BYTES / 5)) # 20%
|
||||
SLACK=$((10 * 1024 * 1024)) # 10MB
|
||||
TOTAL=$((BYTES + OVERHEAD + SLACK))
|
||||
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
|
||||
|
||||
# More accurate sizing for exFAT:
|
||||
# - file payload rounded to cluster size
|
||||
# - FAT + allocation bitmap estimates from cluster count
|
||||
# - directory/entry metadata estimate
|
||||
# - fixed metadata and runtime headroom
|
||||
CLUSTER_SIZE=32768
|
||||
MKFS_CLUSTER_ARG="32K"
|
||||
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
|
||||
|
||||
FILE_COUNT=$(find "$INPUT_DIR" -type f | wc -l | tr -d ' ')
|
||||
DIR_COUNT=$(find "$INPUT_DIR" -type d | wc -l | tr -d ' ')
|
||||
RAW_FILE_BYTES=$(find "$INPUT_DIR" -type f -printf '%s\n' | \
|
||||
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
|
||||
|
||||
# exFAT profile selection (same idea as UFS2 profile):
|
||||
# - large-file sets: 64K cluster
|
||||
# - small/mixed-file sets: 32K cluster
|
||||
if [ "$AVG_FILE_BYTES" -ge "$LARGE_FILE_THRESHOLD" ]; then
|
||||
CLUSTER_SIZE=65536
|
||||
MKFS_CLUSTER_ARG="64K"
|
||||
fi
|
||||
|
||||
DATA_BYTES=$(find "$INPUT_DIR" -type f -printf '%s\n' | \
|
||||
awk -v cls="$CLUSTER_SIZE" '{s += int(($1 + cls - 1) / cls) * cls} END {print s + 0}')
|
||||
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: $BYTES bytes"
|
||||
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: -c $MKFS_CLUSTER_ARG (avg file=$AVG_FILE_BYTES bytes)"
|
||||
echo "Image size: ${MB}MB"
|
||||
|
||||
truncate -s "${MB}M" "$OUTPUT"
|
||||
mkfs.exfat -c 4K "$OUTPUT"
|
||||
mkfs.exfat -c "$MKFS_CLUSTER_ARG" "$OUTPUT"
|
||||
mkdir -p /mnt/exfat
|
||||
mount -t exfat-fuse -o loop "$OUTPUT" /mnt/exfat
|
||||
rsync -r --info=progress2 "$INPUT_DIR" /mnt/exfat/
|
||||
rsync -r --info=progress2 "$INPUT_DIR"/ /mnt/exfat/
|
||||
|
||||
umount /mnt/exfat
|
||||
|
||||
|
||||
@@ -12,22 +12,82 @@ fi
|
||||
INPUT_DIR="$1"
|
||||
OUTPUT="${2:-download0.dat}"
|
||||
|
||||
# Calculate required size (dir size + 20% overhead + 10MB slack)
|
||||
BYTES=$(du -s -k "$INPUT_DIR" | awk '{print $1 * 1024}')
|
||||
OVERHEAD=$((BYTES / 5)) # 20%
|
||||
SLACK=$((10 * 1024 * 1024)) # 10MB
|
||||
TOTAL=$((BYTES + OVERHEAD + SLACK))
|
||||
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
|
||||
|
||||
# More accurate sizing for UFS2:
|
||||
# - file payload rounded to fragment size (-f)
|
||||
# - minimal per-directory allocation
|
||||
# - inode table estimate
|
||||
# - fixed filesystem metadata headroom
|
||||
BLOCK_SIZE=32768
|
||||
FRAG_SIZE=4096
|
||||
MINFREE_PERCENT=0
|
||||
BYTES_PER_INODE=262144
|
||||
INODE_SIZE=256
|
||||
INODE_SPARE=2048
|
||||
META_FIXED=$((64 * 1024 * 1024)) # superblocks, cg metadata, allocator slack
|
||||
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
|
||||
|
||||
FILE_COUNT=$(find "$INPUT_DIR" -type f | wc -l | tr -d ' ')
|
||||
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))
|
||||
INODE_COUNT=$((FILE_COUNT + DIR_COUNT + INODE_SPARE))
|
||||
INODE_BYTES=$((INODE_COUNT * INODE_SIZE))
|
||||
|
||||
BASE_TOTAL=$((DATA_BYTES + DIR_BYTES + INODE_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: $BYTES bytes"
|
||||
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 "Image size: ${MB}MB"
|
||||
|
||||
truncate -s "${MB}M" "$OUTPUT"
|
||||
|
||||
MD=$(mdconfig -a -t vnode -f "$(realpath "$OUTPUT")")
|
||||
newfs -O 2 -b 32768 -f 4096 /dev/${MD}
|
||||
newfs -O 2 -b "$BLOCK_SIZE" -f "$FRAG_SIZE" -m "$MINFREE_PERCENT" -i "$BYTES_PER_INODE" /dev/${MD}
|
||||
|
||||
mkdir -p /mnt
|
||||
mount /dev/${MD} /mnt
|
||||
|
||||
+1405
-560
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user