mirror of
https://github.com/InstanceBrick/nightcap.git
synced 2026-10-18 03:01:03 +02:00
694 lines
29 KiB
C++
694 lines
29 KiB
C++
// Modified by vii from komaruworld/mocktail. See README "About this fork".
|
|
#include "runtime/runtime_paths.h"
|
|
|
|
#include "compat/guest_abi.h"
|
|
|
|
#include <gtest/gtest.h>
|
|
#include <unistd.h>
|
|
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <unordered_map>
|
|
#include <utility>
|
|
|
|
namespace mocktail {
|
|
namespace runtime {
|
|
namespace {
|
|
|
|
class MapEnvironment final : public Environment {
|
|
public:
|
|
explicit MapEnvironment(
|
|
std::unordered_map<std::string, std::string> values = {})
|
|
: values_(std::move(values)) {}
|
|
|
|
std::optional<std::string> Get(std::string_view name) const override {
|
|
const auto found = values_.find(std::string(name));
|
|
return found == values_.end() ? std::nullopt
|
|
: std::optional<std::string>(found->second);
|
|
}
|
|
|
|
private:
|
|
std::unordered_map<std::string, std::string> values_;
|
|
};
|
|
|
|
class TemporaryDirectory {
|
|
public:
|
|
TemporaryDirectory() {
|
|
char pattern[] = "/tmp/mocktail_runtime_paths_XXXXXX";
|
|
char* created = mkdtemp(pattern);
|
|
if (created != nullptr) {
|
|
path_ = created;
|
|
}
|
|
}
|
|
|
|
~TemporaryDirectory() {
|
|
std::error_code error;
|
|
std::filesystem::remove_all(path_, error);
|
|
}
|
|
|
|
const std::filesystem::path& path() const { return path_; }
|
|
|
|
private:
|
|
std::filesystem::path path_;
|
|
};
|
|
|
|
class ScopedProcessEnvironment {
|
|
public:
|
|
explicit ScopedProcessEnvironment(std::string name) : name_(std::move(name)) {
|
|
const char* value = std::getenv(name_.c_str());
|
|
if (value != nullptr) {
|
|
original_ = value;
|
|
}
|
|
}
|
|
|
|
~ScopedProcessEnvironment() {
|
|
if (original_.has_value()) {
|
|
(void)setenv(name_.c_str(), original_->c_str(), 1);
|
|
} else {
|
|
(void)unsetenv(name_.c_str());
|
|
}
|
|
}
|
|
|
|
private:
|
|
std::string name_;
|
|
std::optional<std::string> original_;
|
|
};
|
|
|
|
class ScopedCurrentPath {
|
|
public:
|
|
ScopedCurrentPath() : original_(std::filesystem::current_path()) {}
|
|
~ScopedCurrentPath() {
|
|
std::error_code error;
|
|
std::filesystem::current_path(original_, error);
|
|
}
|
|
|
|
private:
|
|
std::filesystem::path original_;
|
|
};
|
|
|
|
void WriteManagedPayloadFiles(const std::filesystem::path& payload) {
|
|
ASSERT_TRUE(RuntimePaths::EnsureDirectory(payload / "assets/content"));
|
|
ASSERT_TRUE(RuntimePaths::EnsureDirectory(payload / "sober_apk"));
|
|
std::ofstream(payload / "libroblox.so") << "ELF fixture";
|
|
std::ofstream(payload / "sober_apk/base.apk") << "base APK fixture";
|
|
std::ofstream(payload / "sober_apk" / compat::kGuestSplitApkFile)
|
|
<< "split APK fixture";
|
|
}
|
|
|
|
TEST(RuntimePathsTest, DerivesXdgDefaults) {
|
|
const MapEnvironment environment({{"HOME", "/home/mocktail"}});
|
|
const RuntimePaths paths =
|
|
RuntimePaths::FromEnvironment(environment, "/workspace");
|
|
|
|
EXPECT_EQ(paths.sober_data_root(),
|
|
"/home/mocktail/.var/app/org.vinegarhq.Sober/data/sober");
|
|
EXPECT_EQ(paths.sober_cache_root(),
|
|
"/home/mocktail/.var/app/org.vinegarhq.Sober/cache/sober");
|
|
EXPECT_EQ(paths.data_root(), "/home/mocktail/.local/share/mocktail");
|
|
EXPECT_EQ(paths.cache_root(), "/home/mocktail/.cache/mocktail");
|
|
EXPECT_EQ(paths.state_root(), "/home/mocktail/.local/state/mocktail");
|
|
EXPECT_EQ(paths.config_root(), "/home/mocktail/.config/mocktail");
|
|
EXPECT_EQ(paths.config_file(), "/home/mocktail/.config/mocktail/config.yaml");
|
|
EXPECT_EQ(paths.payloads_root(),
|
|
"/home/mocktail/.local/share/mocktail/payloads");
|
|
EXPECT_EQ(paths.active_payload_manifest(),
|
|
"/home/mocktail/.local/share/mocktail/current.json");
|
|
EXPECT_EQ(paths.downloads_root(), "/home/mocktail/.cache/mocktail/downloads");
|
|
EXPECT_EQ(paths.logs_root(), "/home/mocktail/.local/state/mocktail/logs");
|
|
EXPECT_EQ(paths.android_runtime_root(),
|
|
"/home/mocktail/.local/share/mocktail/android");
|
|
EXPECT_EQ(paths.android_data_root(),
|
|
"/home/mocktail/.local/share/mocktail/android/data");
|
|
EXPECT_EQ(paths.android_cache_root(),
|
|
"/home/mocktail/.cache/mocktail/android");
|
|
EXPECT_EQ(paths.vulkan_shader_cache_file(),
|
|
"/home/mocktail/.cache/mocktail/graphics/shadercachevk.bin");
|
|
EXPECT_EQ(paths.auth_root(), "/home/mocktail/.local/share/mocktail/auth");
|
|
EXPECT_EQ(paths.cookie_file(),
|
|
"/home/mocktail/.local/share/mocktail/auth/roblox.cookie");
|
|
}
|
|
|
|
TEST(RuntimePathsTest, IgnoresRelativeXdgHomes) {
|
|
const MapEnvironment environment({
|
|
{"HOME", "/home/mocktail"},
|
|
{"XDG_CONFIG_HOME", "relative-config"},
|
|
{"XDG_DATA_HOME", "relative-data"},
|
|
{"XDG_CACHE_HOME", "relative-cache"},
|
|
{"XDG_STATE_HOME", "relative-state"},
|
|
});
|
|
const RuntimePaths paths =
|
|
RuntimePaths::FromEnvironment(environment, "/workspace");
|
|
|
|
EXPECT_EQ(paths.config_root(), "/home/mocktail/.config/mocktail");
|
|
EXPECT_EQ(paths.data_root(), "/home/mocktail/.local/share/mocktail");
|
|
EXPECT_EQ(paths.cache_root(), "/home/mocktail/.cache/mocktail");
|
|
EXPECT_EQ(paths.state_root(), "/home/mocktail/.local/state/mocktail");
|
|
}
|
|
|
|
TEST(RuntimePathsTest, ResolvesValidatedActivePayload) {
|
|
TemporaryDirectory temporary;
|
|
ASSERT_FALSE(temporary.path().empty());
|
|
const std::filesystem::path data = temporary.path() / "data";
|
|
const std::string payload_id =
|
|
"2546-d0cb1fa0deb3d9161b4cd77530cbcd2e50de3a21";
|
|
const std::filesystem::path payload = data / "payloads" / payload_id;
|
|
WriteManagedPayloadFiles(payload);
|
|
std::ofstream(data / "current.json")
|
|
<< "{\"schema_version\":1,\"payload_id\":\"" << payload_id
|
|
<< "\",\"payload_path\":\"payloads/" << payload_id
|
|
<< "\",\"version_name\":\"2.725.1142\",\"version_code\":2546,"
|
|
"\"elf_build_id\":\"d0cb1fa0deb3d9161b4cd77530cbcd2e50de3a21\"}";
|
|
const MapEnvironment environment({
|
|
{"HOME", temporary.path().string()},
|
|
{"MOCKTAIL_DATA_ROOT", data.string()},
|
|
});
|
|
|
|
const RuntimePaths paths =
|
|
RuntimePaths::FromEnvironment(environment, temporary.path());
|
|
const ActivePayloadPaths active = paths.ResolveActivePayload();
|
|
|
|
ASSERT_TRUE(active) << active.error;
|
|
ASSERT_TRUE(active.active);
|
|
EXPECT_EQ(active.root, std::filesystem::canonical(payload));
|
|
EXPECT_EQ(active.roblox_library, active.root / "libroblox.so");
|
|
EXPECT_EQ(active.base_apk, active.root / "sober_apk/base.apk");
|
|
EXPECT_EQ(active.arch_split_apk,
|
|
active.root / "sober_apk" / compat::kGuestSplitApkFile);
|
|
EXPECT_EQ(active.assets_content, active.root / "assets/content");
|
|
EXPECT_EQ(paths.DefaultAssetPath(), active.assets_content);
|
|
}
|
|
|
|
TEST(RuntimePathsTest, RejectsIncompleteActiveAndroidPackage) {
|
|
TemporaryDirectory temporary;
|
|
ASSERT_FALSE(temporary.path().empty());
|
|
const std::filesystem::path data = temporary.path() / "data";
|
|
const std::string payload_id =
|
|
"2546-d0cb1fa0deb3d9161b4cd77530cbcd2e50de3a21";
|
|
const std::filesystem::path payload = data / "payloads" / payload_id;
|
|
WriteManagedPayloadFiles(payload);
|
|
ASSERT_TRUE(std::filesystem::remove(
|
|
payload / "sober_apk" / compat::kGuestSplitApkFile));
|
|
std::ofstream(data / "current.json")
|
|
<< "{\"schema_version\":1,\"payload_id\":\"" << payload_id
|
|
<< "\",\"payload_path\":\"payloads/" << payload_id
|
|
<< "\",\"version_name\":\"2.725.1142\",\"version_code\":2546,"
|
|
"\"elf_build_id\":\"d0cb1fa0deb3d9161b4cd77530cbcd2e50de3a21\"}";
|
|
const MapEnvironment environment({
|
|
{"HOME", temporary.path().string()},
|
|
{"MOCKTAIL_DATA_ROOT", data.string()},
|
|
});
|
|
|
|
const ActivePayloadPaths active =
|
|
RuntimePaths::FromEnvironment(environment, temporary.path())
|
|
.ResolveActivePayload();
|
|
|
|
EXPECT_FALSE(active);
|
|
EXPECT_NE(active.error.find(std::string(compat::kGuestSplitApkFile)),
|
|
std::string::npos);
|
|
}
|
|
|
|
TEST(RuntimePathsTest, ResolvesCompleteApprovedProfileReferences) {
|
|
TemporaryDirectory temporary;
|
|
ASSERT_FALSE(temporary.path().empty());
|
|
const std::filesystem::path data = temporary.path() / "data";
|
|
const std::string payload_id =
|
|
"2718-48fc8ee1fb36fc39072fd8619154ce90eea4b316";
|
|
const std::string approval_generation =
|
|
"1111111111111111111111111111111111111111";
|
|
const std::string approval_filename =
|
|
payload_id + "-" + approval_generation + ".json";
|
|
const std::filesystem::path payload = data / "payloads" / payload_id;
|
|
WriteManagedPayloadFiles(payload);
|
|
ASSERT_TRUE(RuntimePaths::EnsureDirectory(data / "compatibility_profiles"));
|
|
ASSERT_TRUE(RuntimePaths::EnsureDirectory(data / "host_abi_profiles"));
|
|
ASSERT_TRUE(RuntimePaths::EnsureDirectory(data / "approvals"));
|
|
std::ofstream(data / "compatibility_profiles" / approval_filename) << "{}";
|
|
std::ofstream(data / "host_abi_profiles" / approval_filename) << "{}";
|
|
std::ofstream(data / "approvals" / approval_filename) << "{}";
|
|
std::ofstream(data / "current.json")
|
|
<< "{\"schema_version\":1,\"payload_id\":\"" << payload_id
|
|
<< "\",\"payload_path\":\"payloads/" << payload_id
|
|
<< "\",\"version_name\":\"2.730.790\",\"version_code\":2718,"
|
|
"\"elf_build_id\":\"48fc8ee1fb36fc39072fd8619154ce90eea4b316\","
|
|
"\"compatibility_manifest_path\":\"compatibility_profiles/"
|
|
<< approval_filename
|
|
<< "\",\"host_abi_profile_path\":\"host_abi_profiles/"
|
|
<< approval_filename << "\",\"approval_path\":\"approvals/"
|
|
<< approval_filename << "\"}";
|
|
const MapEnvironment environment({
|
|
{"HOME", temporary.path().string()},
|
|
{"MOCKTAIL_DATA_ROOT", data.string()},
|
|
});
|
|
|
|
const ActivePayloadPaths active =
|
|
RuntimePaths::FromEnvironment(environment, temporary.path())
|
|
.ResolveActivePayload();
|
|
|
|
ASSERT_TRUE(active) << active.error;
|
|
ASSERT_TRUE(active.active);
|
|
EXPECT_EQ(active.compatibility_manifest,
|
|
std::filesystem::canonical(data / "compatibility_profiles" /
|
|
approval_filename));
|
|
EXPECT_EQ(active.host_abi_profile,
|
|
std::filesystem::canonical(data / "host_abi_profiles" /
|
|
approval_filename));
|
|
EXPECT_EQ(active.host_abi_approval_receipt,
|
|
std::filesystem::canonical(data / "approvals" / approval_filename));
|
|
}
|
|
|
|
TEST(RuntimePathsTest, RejectsPartialOrEscapingApprovedProfileReferences) {
|
|
TemporaryDirectory temporary;
|
|
ASSERT_FALSE(temporary.path().empty());
|
|
const std::filesystem::path data = temporary.path() / "data";
|
|
const std::string payload_id =
|
|
"2718-48fc8ee1fb36fc39072fd8619154ce90eea4b316";
|
|
const std::string approval_generation =
|
|
"1111111111111111111111111111111111111111";
|
|
const std::string approval_filename =
|
|
payload_id + "-" + approval_generation + ".json";
|
|
const std::filesystem::path payload = data / "payloads" / payload_id;
|
|
WriteManagedPayloadFiles(payload);
|
|
const MapEnvironment environment({
|
|
{"HOME", temporary.path().string()},
|
|
{"MOCKTAIL_DATA_ROOT", data.string()},
|
|
});
|
|
const RuntimePaths paths =
|
|
RuntimePaths::FromEnvironment(environment, temporary.path());
|
|
|
|
std::ofstream(data / "current.json")
|
|
<< "{\"schema_version\":1,\"payload_id\":\"" << payload_id
|
|
<< "\",\"payload_path\":\"payloads/" << payload_id
|
|
<< "\",\"version_name\":\"2.730.790\",\"version_code\":2718,"
|
|
"\"elf_build_id\":\"48fc8ee1fb36fc39072fd8619154ce90eea4b316\","
|
|
"\"approval_path\":\"approvals/"
|
|
<< approval_filename << "\"}";
|
|
ActivePayloadPaths active = paths.ResolveActivePayload();
|
|
EXPECT_FALSE(active);
|
|
EXPECT_NE(active.error.find("complete string set"), std::string::npos);
|
|
|
|
ASSERT_TRUE(RuntimePaths::EnsureDirectory(data / "compatibility_profiles"));
|
|
ASSERT_TRUE(RuntimePaths::EnsureDirectory(data / "host_abi_profiles"));
|
|
ASSERT_TRUE(RuntimePaths::EnsureDirectory(data / "approvals"));
|
|
std::ofstream(data / "compatibility_profiles" / approval_filename) << "{}";
|
|
std::ofstream(data / "host_abi_profiles" / approval_filename) << "{}";
|
|
std::ofstream(data / "approvals" / approval_filename) << "{}";
|
|
std::ofstream(data / "current.json", std::ios::trunc)
|
|
<< "{\"schema_version\":1,\"payload_id\":\"" << payload_id
|
|
<< "\",\"payload_path\":\"payloads/" << payload_id
|
|
<< "\",\"version_name\":\"2.730.790\",\"version_code\":2718,"
|
|
"\"elf_build_id\":\"48fc8ee1fb36fc39072fd8619154ce90eea4b316\","
|
|
"\"compatibility_manifest_path\":\"compatibility_profiles/../"
|
|
"escape.json\","
|
|
"\"host_abi_profile_path\":\"host_abi_profiles/"
|
|
<< approval_filename << "\",\"approval_path\":\"approvals/"
|
|
<< approval_filename << "\"}";
|
|
active = paths.ResolveActivePayload();
|
|
EXPECT_FALSE(active);
|
|
EXPECT_NE(active.error.find("generated direct compatibility_profiles child"),
|
|
std::string::npos);
|
|
|
|
const std::string other_approval_filename =
|
|
payload_id + "-2222222222222222222222222222222222222222.json";
|
|
std::ofstream(data / "host_abi_profiles" / other_approval_filename) << "{}";
|
|
std::ofstream(data / "current.json", std::ios::trunc)
|
|
<< "{\"schema_version\":1,\"payload_id\":\"" << payload_id
|
|
<< "\",\"payload_path\":\"payloads/" << payload_id
|
|
<< "\",\"version_name\":\"2.730.790\",\"version_code\":2718,"
|
|
"\"elf_build_id\":\"48fc8ee1fb36fc39072fd8619154ce90eea4b316\","
|
|
"\"compatibility_manifest_path\":\"compatibility_profiles/"
|
|
<< approval_filename
|
|
<< "\",\"host_abi_profile_path\":\"host_abi_profiles/"
|
|
<< other_approval_filename << "\",\"approval_path\":\"approvals/"
|
|
<< approval_filename << "\"}";
|
|
active = paths.ResolveActivePayload();
|
|
EXPECT_FALSE(active);
|
|
EXPECT_NE(active.error.find("different generations"), std::string::npos);
|
|
|
|
const std::filesystem::path outside_approvals =
|
|
temporary.path() / "outside-approvals";
|
|
ASSERT_TRUE(RuntimePaths::EnsureDirectory(outside_approvals));
|
|
std::ofstream(outside_approvals / approval_filename) << "{}";
|
|
std::error_code filesystem_error;
|
|
ASSERT_TRUE(
|
|
std::filesystem::remove_all(data / "approvals", filesystem_error) > 0);
|
|
ASSERT_FALSE(filesystem_error);
|
|
std::filesystem::create_directory_symlink(
|
|
outside_approvals, data / "approvals", filesystem_error);
|
|
ASSERT_FALSE(filesystem_error);
|
|
std::ofstream(data / "current.json", std::ios::trunc)
|
|
<< "{\"schema_version\":1,\"payload_id\":\"" << payload_id
|
|
<< "\",\"payload_path\":\"payloads/" << payload_id
|
|
<< "\",\"version_name\":\"2.730.790\",\"version_code\":2718,"
|
|
"\"elf_build_id\":\"48fc8ee1fb36fc39072fd8619154ce90eea4b316\","
|
|
"\"compatibility_manifest_path\":\"compatibility_profiles/"
|
|
<< approval_filename
|
|
<< "\",\"host_abi_profile_path\":\"host_abi_profiles/"
|
|
<< approval_filename << "\",\"approval_path\":\"approvals/"
|
|
<< approval_filename << "\"}";
|
|
active = paths.ResolveActivePayload();
|
|
EXPECT_FALSE(active);
|
|
EXPECT_NE(active.error.find("directory is missing, invalid, or a symlink"),
|
|
std::string::npos);
|
|
}
|
|
|
|
TEST(RuntimePathsTest, RejectsActivePayloadTraversalAndSymlink) {
|
|
TemporaryDirectory temporary;
|
|
ASSERT_FALSE(temporary.path().empty());
|
|
const std::filesystem::path data = temporary.path() / "data";
|
|
ASSERT_TRUE(RuntimePaths::EnsureDirectory(data / "payloads"));
|
|
std::ofstream(data / "current.json")
|
|
<< R"({"schema_version":1,"payload_id":"2546-d0cb1fa0deb3d9161b4cd77530cbcd2e50de3a21","payload_path":"payloads/../escape","version_name":"2.725.1142","version_code":2546,"elf_build_id":"d0cb1fa0deb3d9161b4cd77530cbcd2e50de3a21"})";
|
|
const MapEnvironment environment({
|
|
{"HOME", temporary.path().string()},
|
|
{"MOCKTAIL_DATA_ROOT", data.string()},
|
|
});
|
|
const RuntimePaths paths =
|
|
RuntimePaths::FromEnvironment(environment, temporary.path());
|
|
|
|
ActivePayloadPaths active = paths.ResolveActivePayload();
|
|
EXPECT_FALSE(active);
|
|
EXPECT_NE(active.error.find("direct payloads child"), std::string::npos);
|
|
|
|
const std::filesystem::path external = temporary.path() / "external";
|
|
ASSERT_TRUE(RuntimePaths::EnsureDirectory(external / "assets/content"));
|
|
std::ofstream(external / "libroblox.so") << "ELF fixture";
|
|
std::error_code error;
|
|
std::filesystem::create_directory_symlink(
|
|
external, data / "payloads/2546-d0cb1fa0deb3d9161b4cd77530cbcd2e50de3a21",
|
|
error);
|
|
ASSERT_FALSE(error);
|
|
std::ofstream(data / "current.json", std::ios::trunc)
|
|
<< R"({"schema_version":1,"payload_id":"2546-d0cb1fa0deb3d9161b4cd77530cbcd2e50de3a21","payload_path":"payloads/2546-d0cb1fa0deb3d9161b4cd77530cbcd2e50de3a21","version_name":"2.725.1142","version_code":2546,"elf_build_id":"d0cb1fa0deb3d9161b4cd77530cbcd2e50de3a21"})";
|
|
active = paths.ResolveActivePayload();
|
|
EXPECT_FALSE(active);
|
|
EXPECT_NE(active.error.find("symlink"), std::string::npos);
|
|
}
|
|
|
|
TEST(RuntimePathsTest, HonorsExplicitAndXdgOverrides) {
|
|
const MapEnvironment explicit_environment({
|
|
{"HOME", "/home/mocktail"},
|
|
{"MOCKTAIL_CACHE_ROOT", "/cache"},
|
|
{"MOCKTAIL_CONFIG_ROOT", "/config"},
|
|
{"MOCKTAIL_DATA_ROOT", "/data"},
|
|
{"MOCKTAIL_STATE_ROOT", "/state"},
|
|
{"MOCKTAIL_AUTH_ROOT", "/auth"},
|
|
});
|
|
const RuntimePaths explicit_paths =
|
|
RuntimePaths::FromEnvironment(explicit_environment, "/workspace");
|
|
EXPECT_EQ(explicit_paths.cache_root(), "/cache");
|
|
EXPECT_EQ(explicit_paths.config_root(), "/config");
|
|
EXPECT_EQ(explicit_paths.data_root(), "/data");
|
|
EXPECT_EQ(explicit_paths.state_root(), "/state");
|
|
EXPECT_EQ(explicit_paths.auth_root(), "/auth");
|
|
|
|
const MapEnvironment xdg_environment({
|
|
{"HOME", "/home/mocktail"},
|
|
{"XDG_CONFIG_HOME", "/xdg"},
|
|
{"XDG_DATA_HOME", "/xdg-data"},
|
|
{"XDG_CACHE_HOME", "/xdg-cache"},
|
|
{"XDG_STATE_HOME", "/xdg-state"},
|
|
});
|
|
const RuntimePaths xdg_paths =
|
|
RuntimePaths::FromEnvironment(xdg_environment, "/workspace");
|
|
EXPECT_EQ(xdg_paths.config_root(), "/xdg/mocktail");
|
|
EXPECT_EQ(xdg_paths.data_root(), "/xdg-data/mocktail");
|
|
EXPECT_EQ(xdg_paths.cache_root(), "/xdg-cache/mocktail");
|
|
EXPECT_EQ(xdg_paths.state_root(), "/xdg-state/mocktail");
|
|
}
|
|
|
|
TEST(RuntimePathsTest, SelectsExistingRealSoberAssetPath) {
|
|
TemporaryDirectory temporary;
|
|
ASSERT_FALSE(temporary.path().empty());
|
|
const std::filesystem::path home = temporary.path() / "home";
|
|
const std::filesystem::path sober_assets =
|
|
home / ".var/app/org.vinegarhq.Sober/data/sober/assets/content";
|
|
ASSERT_TRUE(RuntimePaths::EnsureDirectory(sober_assets));
|
|
|
|
const MapEnvironment environment({
|
|
{"HOME", home.string()},
|
|
{"MOCKTAIL_USE_REAL_SOBER_PATHS", "1"},
|
|
});
|
|
const RuntimePaths paths = RuntimePaths::FromEnvironment(
|
|
environment, temporary.path() / "empty-workspace");
|
|
|
|
EXPECT_EQ(paths.DefaultAssetPath(), sober_assets);
|
|
}
|
|
|
|
TEST(RuntimePathsTest, SelectsExistingRealSoberAssetOverlayPath) {
|
|
TemporaryDirectory temporary;
|
|
ASSERT_FALSE(temporary.path().empty());
|
|
const std::filesystem::path home = temporary.path() / "home";
|
|
const std::filesystem::path sober_overlay =
|
|
home / ".var/app/org.vinegarhq.Sober/data/sober/asset_overlay";
|
|
ASSERT_TRUE(RuntimePaths::EnsureDirectory(sober_overlay));
|
|
|
|
const MapEnvironment environment({
|
|
{"HOME", home.string()},
|
|
{"MOCKTAIL_USE_REAL_SOBER_PATHS", "1"},
|
|
});
|
|
const RuntimePaths paths = RuntimePaths::FromEnvironment(
|
|
environment, temporary.path() / "empty-workspace");
|
|
|
|
EXPECT_EQ(paths.asset_overlay_dir(), sober_overlay);
|
|
}
|
|
|
|
TEST(RuntimePathsTest, RespectsExplicitAssetOverlayOverride) {
|
|
TemporaryDirectory temporary;
|
|
ASSERT_FALSE(temporary.path().empty());
|
|
const MapEnvironment environment({
|
|
{"HOME", temporary.path().string()},
|
|
{"MOCKTAIL_ASSET_OVERLAY_DIR", "/custom/asset_overlay"},
|
|
});
|
|
const RuntimePaths paths = RuntimePaths::FromEnvironment(
|
|
environment, temporary.path());
|
|
EXPECT_EQ(paths.asset_overlay_dir(), "/custom/asset_overlay");
|
|
}
|
|
|
|
TEST(RuntimePathsTest, PrefersWorkspaceAssets) {
|
|
TemporaryDirectory temporary;
|
|
ASSERT_FALSE(temporary.path().empty());
|
|
ASSERT_TRUE(RuntimePaths::EnsureDirectory(temporary.path() /
|
|
"rbx_bin/assets/content"));
|
|
const MapEnvironment environment;
|
|
|
|
EXPECT_EQ(RuntimePaths::FromEnvironment(environment, temporary.path())
|
|
.DefaultAssetPath(),
|
|
"rbx_bin/assets/content");
|
|
}
|
|
|
|
TEST(RuntimePathsTest, ResolvesAssetsAdjacentToExplicitRobloxLibrary) {
|
|
EXPECT_EQ(ResolveAdjacentRobloxAssetPath(
|
|
"/data/mocktail/payloads/2814-build/libroblox.so",
|
|
"/ignored"),
|
|
"/data/mocktail/payloads/2814-build/assets/content");
|
|
EXPECT_EQ(ResolveAdjacentRobloxAssetPath("rbx_bin/libroblox.so",
|
|
"/workspace/mocktail"),
|
|
"/workspace/mocktail/rbx_bin/assets/content");
|
|
EXPECT_TRUE(
|
|
ResolveAdjacentRobloxAssetPath({}, "/workspace/mocktail").empty());
|
|
EXPECT_TRUE(
|
|
ResolveAdjacentRobloxAssetPath("libroblox.so", {}).empty());
|
|
}
|
|
|
|
TEST(RuntimePathsTest, CreatesNestedDirectoryAndRejectsFile) {
|
|
TemporaryDirectory temporary;
|
|
ASSERT_FALSE(temporary.path().empty());
|
|
const std::filesystem::path nested = temporary.path() / "one/two/three";
|
|
std::error_code error;
|
|
EXPECT_TRUE(RuntimePaths::EnsureDirectory(nested, &error));
|
|
EXPECT_FALSE(error);
|
|
EXPECT_TRUE(std::filesystem::is_directory(nested));
|
|
|
|
const std::filesystem::path file = temporary.path() / "regular-file";
|
|
FILE* output = std::fopen(file.c_str(), "w");
|
|
ASSERT_NE(output, nullptr);
|
|
std::fclose(output);
|
|
EXPECT_FALSE(RuntimePaths::EnsureDirectory(file, &error));
|
|
EXPECT_TRUE(error);
|
|
}
|
|
|
|
TEST(RuntimePathsTest, PreparesManagedPayloadRelativeAssetRoot) {
|
|
TemporaryDirectory temporary;
|
|
ASSERT_FALSE(temporary.path().empty());
|
|
ScopedCurrentPath restore_current_path;
|
|
const std::filesystem::path data = temporary.path() / "data";
|
|
const std::filesystem::path payload = data / "payloads/fixture";
|
|
WriteManagedPayloadFiles(payload);
|
|
const MapEnvironment environment({
|
|
{"HOME", temporary.path().string()},
|
|
{"MOCKTAIL_DATA_ROOT", data.string()},
|
|
});
|
|
const RuntimePaths paths =
|
|
RuntimePaths::FromEnvironment(environment, temporary.path());
|
|
ActivePayloadPaths active;
|
|
active.active = true;
|
|
active.root = payload;
|
|
active.roblox_library = payload / "libroblox.so";
|
|
active.base_apk = payload / "sober_apk/base.apk";
|
|
active.arch_split_apk =
|
|
payload / "sober_apk" / compat::kGuestSplitApkFile;
|
|
active.assets_content = payload / "assets/content";
|
|
|
|
std::string error;
|
|
ASSERT_TRUE(PrepareManagedPayloadWorkingDirectory(paths, active, &error))
|
|
<< error;
|
|
EXPECT_EQ(std::filesystem::current_path(), data);
|
|
EXPECT_TRUE(std::filesystem::is_symlink(data / "rbx_bin/assets"));
|
|
EXPECT_EQ(std::filesystem::canonical(data / "rbx_bin/assets"),
|
|
std::filesystem::canonical(payload / "assets"));
|
|
EXPECT_TRUE(std::filesystem::is_symlink(data / "rbx_bin/sober_apk"));
|
|
EXPECT_EQ(std::filesystem::canonical(data / "rbx_bin/sober_apk"),
|
|
std::filesystem::canonical(payload / "sober_apk"));
|
|
EXPECT_TRUE(std::filesystem::is_symlink(data / "rbx_bin/libroblox.so"));
|
|
EXPECT_EQ(std::filesystem::canonical(data / "rbx_bin/libroblox.so"),
|
|
std::filesystem::canonical(payload / "libroblox.so"));
|
|
}
|
|
|
|
TEST(RuntimePathsTest, ReplacesAllStaleManagedPayloadLinksTogether) {
|
|
TemporaryDirectory temporary;
|
|
ASSERT_FALSE(temporary.path().empty());
|
|
ScopedCurrentPath restore_current_path;
|
|
const std::filesystem::path data = temporary.path() / "data";
|
|
const std::filesystem::path old_payload = data / "payloads/old";
|
|
const std::filesystem::path new_payload = data / "payloads/new";
|
|
WriteManagedPayloadFiles(old_payload);
|
|
WriteManagedPayloadFiles(new_payload);
|
|
const MapEnvironment environment({
|
|
{"HOME", temporary.path().string()},
|
|
{"MOCKTAIL_DATA_ROOT", data.string()},
|
|
});
|
|
const RuntimePaths paths =
|
|
RuntimePaths::FromEnvironment(environment, temporary.path());
|
|
ActivePayloadPaths active;
|
|
active.active = true;
|
|
active.root = old_payload;
|
|
|
|
std::string error;
|
|
ASSERT_TRUE(PrepareManagedPayloadWorkingDirectory(paths, active, &error))
|
|
<< error;
|
|
active.root = new_payload;
|
|
ASSERT_TRUE(PrepareManagedPayloadWorkingDirectory(paths, active, &error))
|
|
<< error;
|
|
|
|
EXPECT_EQ(std::filesystem::canonical(data / "rbx_bin/assets"),
|
|
std::filesystem::canonical(new_payload / "assets"));
|
|
EXPECT_EQ(std::filesystem::canonical(data / "rbx_bin/sober_apk"),
|
|
std::filesystem::canonical(new_payload / "sober_apk"));
|
|
EXPECT_EQ(std::filesystem::canonical(data / "rbx_bin/libroblox.so"),
|
|
std::filesystem::canonical(new_payload / "libroblox.so"));
|
|
}
|
|
|
|
TEST(RuntimePathsTest, PreservesUnexpectedManagedPayloadFiles) {
|
|
TemporaryDirectory temporary;
|
|
ASSERT_FALSE(temporary.path().empty());
|
|
ScopedCurrentPath restore_current_path;
|
|
const std::filesystem::path data = temporary.path() / "data";
|
|
const std::filesystem::path payload = data / "payloads/fixture";
|
|
WriteManagedPayloadFiles(payload);
|
|
ASSERT_TRUE(RuntimePaths::EnsureDirectory(data / "rbx_bin/sober_apk"));
|
|
std::ofstream(data / "rbx_bin/sober_apk/user-file") << "preserve me";
|
|
const MapEnvironment environment({
|
|
{"HOME", temporary.path().string()},
|
|
{"MOCKTAIL_DATA_ROOT", data.string()},
|
|
});
|
|
const RuntimePaths paths =
|
|
RuntimePaths::FromEnvironment(environment, temporary.path());
|
|
ActivePayloadPaths active;
|
|
active.active = true;
|
|
active.root = payload;
|
|
|
|
std::string error;
|
|
EXPECT_FALSE(PrepareManagedPayloadWorkingDirectory(paths, active, &error));
|
|
EXPECT_NE(error.find("refusing to replace non-symlink"), std::string::npos);
|
|
EXPECT_TRUE(std::filesystem::is_regular_file(
|
|
data / "rbx_bin/sober_apk/user-file"));
|
|
EXPECT_FALSE(std::filesystem::exists(data / "rbx_bin/assets"));
|
|
EXPECT_FALSE(std::filesystem::exists(data / "rbx_bin/libroblox.so"));
|
|
}
|
|
|
|
TEST(RuntimePathsTest, ExportsXdgBackedAndroidPathsAndCreatesParents) {
|
|
TemporaryDirectory temporary;
|
|
ASSERT_FALSE(temporary.path().empty());
|
|
const ScopedProcessEnvironment runtime_root("MOCKTAIL_RUNTIME_ROOT");
|
|
const ScopedProcessEnvironment cache_root(
|
|
"MOCKTAIL_ANDROID_CACHE_HOST_ROOT");
|
|
const ScopedProcessEnvironment shader_cache(
|
|
"MOCKTAIL_VULKAN_SHADER_CACHE_HOST_PATH");
|
|
const ScopedProcessEnvironment texture_overrides(
|
|
"MOCKTAIL_TEXTURE_OVERRIDE_DIR");
|
|
const ScopedProcessEnvironment overlay_dir(
|
|
"MOCKTAIL_ASSET_OVERLAY_DIR");
|
|
ASSERT_EQ(unsetenv("MOCKTAIL_RUNTIME_ROOT"), 0);
|
|
ASSERT_EQ(unsetenv("MOCKTAIL_ANDROID_CACHE_HOST_ROOT"), 0);
|
|
ASSERT_EQ(unsetenv("MOCKTAIL_VULKAN_SHADER_CACHE_HOST_PATH"), 0);
|
|
ASSERT_EQ(unsetenv("MOCKTAIL_TEXTURE_OVERRIDE_DIR"), 0);
|
|
ASSERT_EQ(unsetenv("MOCKTAIL_ASSET_OVERLAY_DIR"), 0);
|
|
|
|
const MapEnvironment environment({
|
|
{"HOME", temporary.path().string()},
|
|
{"XDG_DATA_HOME", (temporary.path() / "data").string()},
|
|
{"XDG_CACHE_HOME", (temporary.path() / "cache").string()},
|
|
});
|
|
const RuntimePaths paths =
|
|
RuntimePaths::FromEnvironment(environment, temporary.path());
|
|
std::string error;
|
|
ASSERT_TRUE(ExportRuntimePathEnvironment(paths, &error)) << error;
|
|
|
|
EXPECT_STREQ(std::getenv("MOCKTAIL_RUNTIME_ROOT"),
|
|
paths.android_runtime_root().c_str());
|
|
EXPECT_STREQ(std::getenv("MOCKTAIL_ANDROID_CACHE_HOST_ROOT"),
|
|
paths.android_cache_root().c_str());
|
|
EXPECT_STREQ(std::getenv("MOCKTAIL_VULKAN_SHADER_CACHE_HOST_PATH"),
|
|
paths.vulkan_shader_cache_file().c_str());
|
|
EXPECT_STREQ(std::getenv("MOCKTAIL_TEXTURE_OVERRIDE_DIR"),
|
|
(paths.config_root() / "textures").c_str());
|
|
EXPECT_STREQ(std::getenv("MOCKTAIL_ASSET_OVERLAY_DIR"),
|
|
paths.asset_overlay_dir().c_str());
|
|
EXPECT_TRUE(std::filesystem::is_directory(paths.android_runtime_root()));
|
|
EXPECT_TRUE(std::filesystem::is_directory(paths.android_cache_root()));
|
|
EXPECT_TRUE(std::filesystem::is_directory(
|
|
paths.vulkan_shader_cache_file().parent_path()));
|
|
EXPECT_TRUE(std::filesystem::is_directory(paths.asset_overlay_dir()));
|
|
}
|
|
|
|
TEST(RuntimePathsTest, PreservesExplicitAndroidPathOverrides) {
|
|
TemporaryDirectory temporary;
|
|
ASSERT_FALSE(temporary.path().empty());
|
|
const ScopedProcessEnvironment runtime_root("MOCKTAIL_RUNTIME_ROOT");
|
|
const ScopedProcessEnvironment cache_root(
|
|
"MOCKTAIL_ANDROID_CACHE_HOST_ROOT");
|
|
const ScopedProcessEnvironment shader_cache(
|
|
"MOCKTAIL_VULKAN_SHADER_CACHE_HOST_PATH");
|
|
const ScopedProcessEnvironment overlay_dir(
|
|
"MOCKTAIL_ASSET_OVERLAY_DIR");
|
|
ASSERT_EQ(setenv("MOCKTAIL_RUNTIME_ROOT", "/explicit/runtime", 1), 0);
|
|
ASSERT_EQ(setenv("MOCKTAIL_ANDROID_CACHE_HOST_ROOT", "/explicit/cache", 1),
|
|
0);
|
|
ASSERT_EQ(setenv("MOCKTAIL_VULKAN_SHADER_CACHE_HOST_PATH",
|
|
"/explicit/shader-cache", 1),
|
|
0);
|
|
ASSERT_EQ(setenv("MOCKTAIL_ASSET_OVERLAY_DIR", "/explicit/overlay", 1), 0);
|
|
|
|
const MapEnvironment environment({{"HOME", temporary.path().string()}});
|
|
const RuntimePaths paths =
|
|
RuntimePaths::FromEnvironment(environment, temporary.path());
|
|
std::string error;
|
|
ASSERT_TRUE(ExportRuntimePathEnvironment(paths, &error)) << error;
|
|
|
|
EXPECT_STREQ(std::getenv("MOCKTAIL_RUNTIME_ROOT"), "/explicit/runtime");
|
|
EXPECT_STREQ(std::getenv("MOCKTAIL_ANDROID_CACHE_HOST_ROOT"),
|
|
"/explicit/cache");
|
|
EXPECT_STREQ(std::getenv("MOCKTAIL_VULKAN_SHADER_CACHE_HOST_PATH"),
|
|
"/explicit/shader-cache");
|
|
EXPECT_STREQ(std::getenv("MOCKTAIL_ASSET_OVERLAY_DIR"), "/explicit/overlay");
|
|
}
|
|
|
|
} // namespace
|
|
} // namespace runtime
|
|
} // namespace mocktail
|