Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions core/relinker/relinker/src/guest/GuestModuleBuilder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,11 @@ std::vector<GuestArtifact> GuestModuleBuilder::Build(const std::filesystem::path
if (hasPrx) directories.push_back(prx);
std::vector<std::filesystem::path> runtimeDirectories;
for (const auto& name : moduleDirectories) {
const auto relative = Io::NativePath(name);
if (relative.empty() || relative.is_absolute() || relative.lexically_normal().generic_string().starts_with(".."))
const auto relative = Io::NativePath(name).lexically_normal();
if (relative.empty() || relative.has_root_path() || *relative.begin() == "..")
throw Domain::RelinkerException("--module-dir must be a directory inside the guest module parent directory: " + name);
const auto directory = (root / relative).lexically_normal();
auto directory = (root / relative).lexically_normal();
if (!directory.has_filename()) directory = directory.parent_path();
if (!std::filesystem::is_directory(directory)) throw Domain::RelinkerException("Guest module path is not a directory: " + Io::Utf8Path(directory));
if (std::find(directories.begin(), directories.end(), directory) != directories.end()) throw Domain::RelinkerException("Duplicate guest module directory: " + Io::Utf8Path(directory));
directories.push_back(directory);
Expand Down Expand Up @@ -101,6 +102,7 @@ std::vector<GuestArtifact> GuestModuleBuilder::Build(const std::filesystem::path
continue;
}
if (!entry.is_regular_file()) continue;
if (std::filesystem::equivalent(entry.path(), inputPath)) continue;
if (isElf(entry.path())) paths.push_back(entry.path());
}
}
Expand Down Expand Up @@ -287,6 +289,10 @@ std::vector<GuestArtifact> GuestModuleBuilder::Build(const std::filesystem::path
const auto deferred = [&](const std::size_t index) {
return !required[index] && std::find(runtimeDirectories.begin(), runtimeDirectories.end(), images[index].SourcePath.parent_path().lexically_normal()) != runtimeDirectories.end();
};
const auto appDirectory = [&](const GuestImage& image) {
const auto directory = image.SourcePath.parent_path().lexically_relative(root).generic_string();
return directory == "." ? std::string("app0") : "app0/" + directory;
};
if (!windows) {
for (auto& image : images) {
for (std::size_t index = 1; index < image.Symbols.size(); ++index) {
Expand Down Expand Up @@ -323,7 +329,7 @@ std::vector<GuestArtifact> GuestModuleBuilder::Build(const std::filesystem::path
Io::AppendU64(dynamic.DynamicSegmentData, dynamic.DynStrData.size());
Io::AppendString(dynamic.DynStrData, name);
};
for (const auto index : order) if (!windows && !deferred(index)) addNeeded("$ORIGIN/app0/" + images[index].SourcePath.parent_path().lexically_relative(root).generic_string() + "/" + images[index].OutputName);
for (const auto index : order) if (!windows && !deferred(index)) addNeeded("$ORIGIN/" + appDirectory(images[index]) + "/" + images[index].OutputName);
for (const auto& name : hostLibraries) addNeeded(name);
if (!windows && runPath != "$ORIGIN" && !runPath.starts_with("$ORIGIN/") && !std::filesystem::path(runPath).is_absolute()) throw Domain::RelinkerException("Guest Linux run path must be absolute or begin with $ORIGIN");
const auto outputDirectory = std::filesystem::absolute(outputPath).parent_path().lexically_normal();
Expand All @@ -344,7 +350,7 @@ std::vector<GuestArtifact> GuestModuleBuilder::Build(const std::filesystem::path
const std::vector<std::uint8_t> code(image.Bytes.begin() + header.Offset, image.Bytes.begin() + header.Offset + header.FileSize);
syscallScanner.ScanCodeSectionForSyscalls(code, header.MappedAddress, header.FileSize);
}
const auto relativeDirectory = "app0/" + image.SourcePath.parent_path().lexically_relative(root).generic_string();
const auto relativeDirectory = appDirectory(image);
const auto destination = outputDirectory / relativeDirectory;
const auto target = destination / image.OutputName;
if (target.lexically_normal() == std::filesystem::absolute(outputPath).lexically_normal()) throw Domain::RelinkerException("Guest output collides with the executable output");
Expand Down
73 changes: 71 additions & 2 deletions core/relinker/relinker/tests/test_guest_module_dir.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,13 @@ def main():
with tempfile.TemporaryDirectory(prefix="anyps5-module-dir-") as directory:
work = Path(directory)

def convert(case, windows, options, executable=None):
def convert(case, windows, options, executable=None, cwd=None):
(case / "sce_module").mkdir(parents=True, exist_ok=True)
source = case / "input.elf"
source.write_bytes(main_fixture() if executable is None else executable)
output = case / ("output.exe" if windows else "output.elf")
result = subprocess.run([str(relinker), *(["--windows"] if windows else []), *options, str(source), str(output)],
capture_output=True, text=True, timeout=30)
cwd=cwd, capture_output=True, text=True, timeout=30)
return result, output

for windows in (False, True):
Expand Down Expand Up @@ -91,6 +91,35 @@ def convert(case, windows, options, executable=None):
else:
assert "$ORIGIN/app0/Media/Modules/needed.prx.guest.prx" in needed_libraries(output.read_bytes()), needed_libraries(output.read_bytes())

case = work / f"{windows}-root"
case.mkdir()
(case / "root.prx").write_bytes(module_with_symbol(True))
(case / "readme.txt").write_text("not ELF")
result, output = convert(case, windows, ["--module-dir", "."])
assert result.returncode == 0, (result.stdout, result.stderr)
artifact = case / "app0" / "root.prx.guest.prx"
assert list((case / "app0").rglob("*.guest.prx")) == [artifact], list(case.rglob("*.guest.prx"))
assert artifact.read_bytes().startswith(b"MZ" if windows else b"\x7fELF"), artifact
assert " root.prx.guest.prx\n" in result.stdout, result.stdout
if windows:
assert "__aps5_guest_initialize" in pe_export_names(artifact.read_bytes())
else:
assert not needed_libraries(output.read_bytes()), needed_libraries(output.read_bytes())
if windows and os.name == "nt":
run = subprocess.run([str(output)], capture_output=True, text=True, timeout=30)
assert run.returncode == 42, (run.returncode, run.stdout, run.stderr)

case = work / f"{windows}-root-needed"
case.mkdir()
(case / "needed.prx").write_bytes(module_with_symbol(True))
result, output = convert(case, windows, ["--module-dir", "."], executable_with_needed())
assert result.returncode == 0, (result.stdout, result.stderr)
needed = case / "app0" / "needed.prx.guest.prx"
if windows:
assert "__aps5_guest_initialize" not in pe_export_names(needed.read_bytes())
else:
assert "$ORIGIN/app0/needed.prx.guest.prx" in needed_libraries(output.read_bytes()), needed_libraries(output.read_bytes())

case = work / f"{windows}-plugins"
for rejected, message in ((str(plugins), "inside the guest module parent directory"),
("../outside", "inside the guest module parent directory"),
Expand All @@ -101,6 +130,46 @@ def convert(case, windows, options, executable=None):

result, output = convert(case, windows, ["--skip-sce-module", "--module-dir", "Media/Plugins"])
assert result.returncode != 0 and "conflicts with --skip-sce-module" in result.stderr, result.stderr

for index, (name, relative) in enumerate((("..plugins", "..plugins"), ("Media/../..plugins", "..plugins"),
("./..plugins/.", "..plugins"), ("Media/../Media/Plugins", "Media/Plugins"),
("Media/Plugins/", "Media/Plugins"))):
case = work / f"{windows}-relative-{index}"
(case / "Media" / "Plugins").mkdir(parents=True)
(case / "..plugins").mkdir()
source = case / relative / "plugin.prx"
original = bytes(module_with_symbol(True))
source.write_bytes(original)
result, output = convert(case, windows, ["--module-dir", name])
assert result.returncode == 0, (name, result.stdout, result.stderr)
artifact = case / "app0" / relative / "plugin.prx.guest.prx"
assert list(case.rglob("*.guest.prx")) == [artifact]
assert artifact.read_bytes().startswith(b"MZ" if windows else b"\x7fELF")
assert source.read_bytes() == original
if windows:
assert "__aps5_guest_initialize" in pe_export_names(artifact.read_bytes()), name
else:
assert not needed_libraries(output.read_bytes()), name
if windows and os.name == "nt":
run = subprocess.run([str(output)], capture_output=True, text=True, timeout=30)
assert run.returncode == 42, (run.returncode, run.stdout, run.stderr)

case = work / f"{windows}-rooted"
(case / "Media" / "Plugins").mkdir(parents=True)
(case / "..plugins").mkdir()
external = work / f"{windows}-external"
external.mkdir()
original = bytes(module_with_symbol(True))
for directory in (external, case / "..plugins"):
(directory / "plugin.prx").write_bytes(original)
rejected = [str(external), "../" + external.name, "Media/../../" + external.name]
if os.name == "nt":
rejected.extend((str(external)[len(external.drive):], case.drive + "..plugins", case.drive))
for name in rejected:
result, output = convert(case, windows, ["--module-dir", name], cwd=case)
assert result.returncode == 2 and "inside the guest module parent directory" in result.stderr, (name, result.stderr)
assert not output.exists() and not list(case.rglob("*.guest.prx"))
assert (external / "plugin.prx").read_bytes() == original
print("Guest module directory option tests passed")


Expand Down
2 changes: 1 addition & 1 deletion docs/user/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ All switches are disabled by default. `unused-filter` defaults to `0`; `--rpath`
| `--rpath <path>` | Set the system library search path. Quote `$ORIGIN` to prevent shell expansion, for example `--rpath '$ORIGIN/libs'` in Bash or PowerShell. Linux guest modules require an absolute path or a path beginning with `$ORIGIN`. Windows requires a nonempty ASCII path and supports `$ORIGIN` as the executable directory. |
| `--autorun` | Run the output after conversion, print its exit code, and wait for Enter. Adds executable permissions for Linux output. Requires the target OS and prepared runtime layout. |
| `--sce-module-path <path>` | Parent of the module directories, not the module directory itself. Default: input ELF parent; relative input paths resolve from the process working directory. Explicit absolute path: used directly. Explicit relative path (including `.` and `..`): relative to the process working directory. Output path has no effect. Requires one nonempty value; repeats, nonexistent or nondirectory paths, access failures, and combination with `--skip-sce-module` are errors. |
| `--module-dir <dir>` | Relink the ELF modules of a directory under the module parent directory, besides `sce_module`/`sce_modules`/`prx`; outputs keep the relative path. A module the executable or another module needs is initialized at startup, any other when the title loads it with `sceKernelLoadStartModule`, with its start arguments (for example Unity plugins in `Media/Plugins`). Repeat for multiple directories. Conflicts with `--skip-sce-module`. |
| `--module-dir <dir>` | Relink the ELF modules of a directory under the module parent directory, besides `sce_module`/`sce_modules`/`prx`; outputs keep the relative path. A module the executable or another module needs is initialized at startup, any other when the title loads it with `sceKernelLoadStartModule`, with its start arguments (for example Unity plugins in `Media/Plugins`). Repeatable; `.` is the parent itself, without the input ELF. Conflicts with `--skip-sce-module`. |
| `--skip-sce-module` | Deprecated. Disable bundled module processing and module directory checks. Conflicts with explicit `--sce-module-path`. |
| `--exclude-sce-module <file>` | Deprecated. Exclude a bundled module by exact filename, not path. Repeat for multiple files; a missing filename is an error. Conflicts with `--skip-sce-module`. |
| `--skip-syscall-check` | Deprecated. Disable syscall scanning in the executable and bundled modules. |
Expand Down
Loading