feat: switch to c

This commit is contained in:
7a72 2025-11-21 20:25:54 +08:00
commit 0e8a692177
21 changed files with 1806 additions and 1280 deletions

139
build.sh
View file

@ -13,20 +13,11 @@ BASE_URL="https://github.com/7a72/meta-magic_mount/releases/download"
UPDATE_JSON_URL="https://raw.githubusercontent.com/7a72/meta-magic_mount/public/update.json"
CHANGELOG_URL="https://raw.githubusercontent.com/7a72/meta-magic_mount/public/changelog.md"
# Rust targets
RUST_TARGETS=(
"x86_64-unknown-linux-musl"
"armv7-unknown-linux-musleabihf"
"aarch64-unknown-linux-musl"
)
# Show usage
usage() {
echo "Usage: $0 [--release] [--debug] [--url <base_url>] [--update-json-url <url>]"
echo "Usage: $0 [--release] [--debug]"
echo " --release Build release version"
echo " --debug Build debug version"
echo " --url <url> Set base URL for downloads (default: $BASE_URL)"
echo " --update-json-url Set update JSON URL (default: $UPDATE_JSON_URL)"
echo " (no option) Build both release and debug versions"
echo ""
echo "Version number will be automatically obtained from git tag"
@ -44,14 +35,6 @@ while [[ $# -gt 0 ]]; do
BUILD_TYPES+=("debug")
shift
;;
--url)
BASE_URL="$2"
shift 2
;;
--update-json-url)
UPDATE_JSON_URL="$2"
shift 2
;;
-h|--help)
usage
;;
@ -74,16 +57,9 @@ if ! git rev-parse --git-dir > /dev/null 2>&1; then
exit 1
fi
# Check if cargo is installed
if ! command -v cargo &> /dev/null; then
echo -e "${RED}Error: cargo is not installed${NC}"
exit 1
fi
# Check if cargo-zigbuild is installed
if ! cargo zigbuild --help &> /dev/null 2>&1; then
echo -e "${RED}Error: cargo-zigbuild is not installed${NC}"
echo -e "${YELLOW}Install it with: cargo install cargo-zigbuild${NC}"
# Check if zig is installed
if ! command -v zig &> /dev/null; then
echo -e "${RED}Error: zig is not installed${NC}"
exit 1
fi
@ -175,101 +151,21 @@ EOF
}
# Function to build Rust project for all targets
build_rust_binaries() {
build_binaries() {
local BUILD_TYPE=$1
local BIN_DIR="src/bin"
echo -e "\n${YELLOW}Building Rust binaries for all targets...${NC}"
# Create bin directory if it doesn't exist
if [ -d "$BIN_DIR" ]; then
rm -rf "$BIN_DIR"
fi
mkdir -p "$BIN_DIR"
# Change to src directory
if [ ! -d "src" ]; then
echo -e "${RED}Error: src directory does not exist${NC}"
return 1
fi
echo -e "\n${YELLOW}Building binaries for all targets...${NC}"
cd src || return 1
# Clean previous builds
cargo clean
make clean
# Build for each target
for target in "${RUST_TARGETS[@]}"; do
echo -e "${YELLOW}Building for target: $target${NC}"
# Determine build command based on build type
if [ "$BUILD_TYPE" = "release" ]; then
cargo zigbuild --release --target "$target"
else
cargo zigbuild --target "$target"
fi
if [ $? -ne 0 ]; then
echo -e "${RED}Error: Failed to build for target $target${NC}"
cd ..
return 1
fi
# Determine build output directory
if [ "$BUILD_TYPE" = "release" ]; then
BUILD_OUTPUT_DIR="target/$target/release"
else
BUILD_OUTPUT_DIR="target/$target/debug"
fi
# Get the binary name from Cargo.toml (assuming single binary project)
BINARY_NAME=$(cargo metadata --format-version 1 --no-deps 2>/dev/null | grep -o '"name":"[^"]*"' | head -1 | cut -d'"' -f4)
if [ -z "$BINARY_NAME" ]; then
echo -e "${RED}Error: Could not determine binary name${NC}"
cd ..
return 1
fi
# Copy binary to bin directory with target-specific name
# Map target to Android ABI
case "$target" in
"x86_64-unknown-linux-musl")
ABI_DIR="x86_64"
;;
"armv7-unknown-linux-musleabihf")
ABI_DIR="armeabi-v7a"
;;
"aarch64-unknown-linux-musl")
ABI_DIR="arm64-v8a"
;;
*)
echo -e "${RED}Error: Unknown target $target${NC}"
cd ..
return 1
;;
esac
# Create ABI directory
mkdir -p "bin/$ABI_DIR"
# Copy binary
if [ -f "$BUILD_OUTPUT_DIR/$BINARY_NAME" ]; then
cp "$BUILD_OUTPUT_DIR/$BINARY_NAME" "bin/$ABI_DIR/$BINARY_NAME"
# Strip binary to reduce size (for release builds)
if [ "$BUILD_TYPE" = "release" ]; then
strip "bin/$ABI_DIR/$BINARY_NAME" 2>/dev/null || true
fi
echo -e "${GREEN}Binary copied to bin/$ABI_DIR/$BINARY_NAME${NC}"
else
echo -e "${RED}Error: Binary not found at $BUILD_OUTPUT_DIR/$BINARY_NAME${NC}"
cd ..
return 1
fi
done
make $BUILD_TYPE VERSION="$VERSION"
cd ..
echo -e "${GREEN}All Rust binaries built successfully${NC}"
echo -e "${GREEN}All binaries built successfully${NC}"
return 0
}
@ -310,13 +206,13 @@ build_for_type() {
echo -e "${GREEN}Template copied${NC}"
# Step 2: Build Rust binaries
echo -e "\n${YELLOW}[2/7] Building Rust binaries...${NC}"
build_rust_binaries "$BUILD_TYPE"
echo -e "\n${YELLOW}[2/7] Building binaries...${NC}"
build_binaries "$BUILD_TYPE"
if [ $? -ne 0 ]; then
echo -e "${RED}Error: Rust build failed${NC}"
echo -e "${RED}Error: Build failed${NC}"
return 1
fi
echo -e "${GREEN}Rust build completed${NC}"
echo -e "${GREEN}Build completed${NC}"
# Step 3: Modify log level in metamount.sh (in build directory)
echo -e "\n${YELLOW}[3/7] Modifying metamount.sh configuration...${NC}"
@ -395,10 +291,6 @@ build_for_type() {
echo -e "\n${GREEN}========================================${NC}"
echo -e "${GREEN}Build completed!${NC}"
echo -e "${GREEN}Output file: build/$OUTPUT_NAME${NC}"
echo -e "${GREEN}Update JSON: build/update_${BUILD_TYPE}.json${NC}"
if [ "$BUILD_TYPE" = "release" ]; then
echo -e "${GREEN}Main update JSON: build/update.json${NC}"
fi
echo -e "${GREEN}========================================${NC}"
# Export for summary
@ -445,9 +337,6 @@ fi
echo -e "${GREEN}----------------------------------------${NC}"
echo -e "${GREEN}Generated files:${NC}"
echo -e "${GREEN} - Changelog: build/changelog.md${NC}"
for BUILD_TYPE in "${BUILD_TYPES[@]}"; do
echo -e "${GREEN} - Update JSON (${BUILD_TYPE}): build/update_${BUILD_TYPE}.json${NC}"
done
# Check if release was built
for BUILD_TYPE in "${BUILD_TYPES[@]}"; do
if [ "$BUILD_TYPE" = "release" ]; then

4
src/.gitignore vendored
View file

@ -1,4 +1,2 @@
/target
/out
Cargo.lock
/bin
*.log

View file

@ -1,21 +0,0 @@
[package]
name = "magic_mount_rs"
version = "0.1.2"
edition = "2024"
[dependencies]
anyhow = "1"
clap = { version = "4", features = ["derive"] }
extattr = "1"
log = "0.4"
simplelog = "0.12"
rustix = { version = "0.38", features = ["fs", "mount"] }
libc = "0.2"
serde = { version = "1.0", features = ["derive"] }
toml = "0.8"
[profile.release]
strip = true
opt-level = "z"
lto = true
codegen-units = 1

87
src/Makefile Normal file
View file

@ -0,0 +1,87 @@
PROJECT = magic_mount
VERSION ?= 0.1.0
# use zig cc
ZIG ?= zig
CC := $(ZIG) cc
STRIPPER := strip
# source files
SRCS := utils.c magic_mount.c main.c
# output directory
OUTDIR := bin
# common flags
CFLAGS_COMMON := -D_POSIX_C_SOURCE=200809L -std=c23 -Wall -Wextra -pedantic
CFLAGS_COMMON += -DVERSION=\"$(VERSION)\"
LDFLAGS_COMMON := -static
# build mode specific flags
CFLAGS_RELEASE := -O2 -DNDEBUG
CFLAGS_DEBUG := -O0 -g -DDEBUG
# zig target triples
TARGET_AMD64 ?= x86_64-linux
TARGET_ARM64 ?= aarch64-linux
TARGET_ARMV7 ?= arm-linux
# output binary names
BIN_AMD64 := $(OUTDIR)/mm_amd64
BIN_ARM64 := $(OUTDIR)/mm_arm64
BIN_ARMV7 := $(OUTDIR)/mm_armv7
BINS := $(BIN_AMD64) $(BIN_ARM64) $(BIN_ARMV7)
.PHONY: all clean release debug amd64 arm64 armv7 dirs help strip-bins
# default target
all: release
help:
@echo "Usage:"
@echo " make release [version=X.Y.Z] - Build release version (optimized, stripped)"
@echo " make debug [version=X.Y.Z] - Build debug version (with symbols)"
@echo " make clean - Clean build artifacts"
@echo ""
@echo "Individual targets:"
@echo " make amd64 - Build for x86_64"
@echo " make arm64 - Build for aarch64"
@echo " make armv7 - Build for armv7"
dirs:
mkdir -p $(OUTDIR)
# Release build (default)
release: export CFLAGS := $(CFLAGS_COMMON) $(CFLAGS_RELEASE)
release: dirs $(BINS) strip-bins
@echo "Release build completed: $(VERSION)"
# Debug build
debug: export CFLAGS := $(CFLAGS_COMMON) $(CFLAGS_DEBUG)
debug: dirs $(BINS)
@echo "Debug build completed: $(VERSION)"
strip-bins:
@echo "Stripping binaries..."
@for bin in $(BINS); do \
echo " Stripping $$bin"; \
$(STRIPPER) $$bin; \
done
amd64: $(BIN_AMD64)
arm64: $(BIN_ARM64)
armv7: $(BIN_ARMV7)
$(BIN_AMD64): $(SRCS)
$(CC) -target $(TARGET_AMD64) $(CFLAGS) $^ -o $@ $(LDFLAGS_COMMON)
$(BIN_ARM64): $(SRCS)
$(CC) -target $(TARGET_ARM64) $(CFLAGS) $^ -o $@ $(LDFLAGS_COMMON)
$(BIN_ARMV7): $(SRCS)
$(CC) -target $(TARGET_ARMV7) $(CFLAGS) $^ -o $@ $(LDFLAGS_COMMON)
clean:
rm -rf $(OUTDIR)

826
src/magic_mount.c Normal file
View file

@ -0,0 +1,826 @@
#include "magic_mount.h"
#include "utils.h"
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mount.h>
#include <sys/xattr.h>
#include <sys/statfs.h>
#include <dirent.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <limits.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
MountStats g_stats = { 0 };
const char *g_module_dir = DEFAULT_MODULE_DIR;
const char *g_mount_source = DEFAULT_MOUNT_SOURCE;
char **g_failed_modules = NULL;
int g_failed_modules_count = 0;
char **g_extra_parts = NULL;
int g_extra_parts_count = 0;
static void register_module_failure(const char *module_name)
{
if (!module_name)
return;
for (int i = 0; i < g_failed_modules_count; ++i) {
if (strcmp(g_failed_modules[i], module_name) == 0)
return;
}
char **arr =
realloc(g_failed_modules,
(size_t)(g_failed_modules_count + 1) * sizeof(char *));
if (!arr)
return;
g_failed_modules = arr;
g_failed_modules[g_failed_modules_count] = strdup(module_name);
if (!g_failed_modules[g_failed_modules_count])
return;
g_failed_modules_count++;
}
void add_extra_part_token(const char *start, size_t len)
{
while (len > 0 && (start[0] == ' ' || start[0] == '\t')) {
start++;
len--;
}
while (len > 0 && (start[len - 1] == ' ' || start[len - 1] == '\t'))
len--;
if (len == 0)
return;
char *name = malloc(len + 1);
if (!name)
return;
memcpy(name, start, len);
name[len] = '\0';
char **arr = realloc(g_extra_parts,
(size_t)(g_extra_parts_count + 1) * sizeof(char *));
if (!arr) {
free(name);
return;
}
g_extra_parts = arr;
g_extra_parts[g_extra_parts_count++] = name;
}
typedef enum {
NFT_REGULAR,
NFT_DIRECTORY,
NFT_SYMLINK,
NFT_WHITEOUT
} NodeFileType;
typedef struct Node {
char *name;
NodeFileType type;
struct Node **children;
size_t child_count;
char *module_path;
char *module_name;
bool replace;
bool skip;
bool done;
} Node;
static Node *node_new(const char *name, NodeFileType t)
{
Node *n = calloc(1, sizeof(Node));
if (!n)
return NULL;
n->name = strdup(name ? name : "");
n->type = t;
return n;
}
static void node_free(Node *n)
{
if (!n)
return;
for (size_t i = 0; i < n->child_count; ++i)
node_free(n->children[i]);
free(n->children);
free(n->name);
free(n->module_path);
free(n->module_name);
free(n);
}
static NodeFileType node_type_from_stat(const struct stat *st)
{
if (S_ISCHR(st->st_mode) && st->st_rdev == 0)
return NFT_WHITEOUT;
if (S_ISREG(st->st_mode))
return NFT_REGULAR;
if (S_ISDIR(st->st_mode))
return NFT_DIRECTORY;
if (S_ISLNK(st->st_mode))
return NFT_SYMLINK;
return NFT_WHITEOUT;
}
static bool dir_is_replace(const char *path)
{
char buf[8];
ssize_t len = lgetxattr(path, REPLACE_DIR_XATTR, buf, sizeof(buf) - 1);
if (len <= 0)
return false;
buf[len] = '\0';
return (strcmp(buf, "y") == 0);
}
static Node *node_new_module(const char *name, const char *path,
const char *module_name)
{
struct stat st;
if (lstat(path, &st) < 0)
return NULL;
if (!(S_ISCHR(st.st_mode) || S_ISREG(st.st_mode) ||
S_ISDIR(st.st_mode) || S_ISLNK(st.st_mode)))
return NULL;
NodeFileType t = node_type_from_stat(&st);
Node *n = node_new(name, t);
if (!n)
return NULL;
n->module_path = strdup(path);
if (module_name)
n->module_name = strdup(module_name);
n->replace = (t == NFT_DIRECTORY) && dir_is_replace(path);
g_stats.nodes_total++;
return n;
}
static int node_add_child(Node *parent, Node *child)
{
Node **arr = realloc(parent->children,
(parent->child_count + 1) * sizeof(Node *));
if (!arr) {
errno = ENOMEM;
return -1;
}
parent->children = arr;
parent->children[parent->child_count++] = child;
return 0;
}
static Node *node_find_child(Node *parent, const char *name)
{
for (size_t i = 0; i < parent->child_count; ++i) {
if (strcmp(parent->children[i]->name, name) == 0)
return parent->children[i];
}
return NULL;
}
static Node *node_take_child(Node *parent, const char *name)
{
for (size_t i = 0; i < parent->child_count; ++i) {
if (strcmp(parent->children[i]->name, name) == 0) {
Node *n = parent->children[i];
memmove(&parent->children[i], &parent->children[i + 1],
(parent->child_count - i - 1) * sizeof(Node *));
parent->child_count--;
return n;
}
}
return NULL;
}
static bool module_disabled(const char *mod_dir)
{
char buf[PATH_MAX];
path_join(mod_dir, DISABLE_FILE_NAME, buf, sizeof(buf));
if (path_exists(buf))
return true;
path_join(mod_dir, REMOVE_FILE_NAME, buf, sizeof(buf));
if (path_exists(buf))
return true;
path_join(mod_dir, SKIP_MOUNT_FILE_NAME, buf, sizeof(buf));
if (path_exists(buf))
return true;
return false;
}
static int node_collect(Node *self, const char *dir, const char *module_name,
bool *has_any)
{
DIR *d = opendir(dir);
if (!d) {
LOGE("opendir %s: %s", dir, strerror(errno));
return -1;
}
struct dirent *de;
bool any = false;
char path[PATH_MAX];
while ((de = readdir(d))) {
if (!strcmp(de->d_name, ".") || !strcmp(de->d_name, ".."))
continue;
if (path_join(dir, de->d_name, path, sizeof(path)) != 0) {
closedir(d);
return -1;
}
Node *child = node_find_child(self, de->d_name);
if (!child) {
Node *n = node_new_module(de->d_name, path, module_name);
if (n) {
if (node_add_child(self, n) != 0) {
node_free(n);
closedir(d);
return -1;
}
child = n;
}
}
if (child) {
if (child->type == NFT_DIRECTORY) {
bool sub = false;
if (node_collect(child, path, module_name, &sub) != 0) {
closedir(d);
return -1;
}
if (sub || child->replace)
any = true;
} else {
any = true;
}
}
}
closedir(d);
*has_any = any;
return 0;
}
static Node *collect_root(void)
{
const char *mdir = g_module_dir;
Node *root = node_new("", NFT_DIRECTORY);
Node *system = node_new("system", NFT_DIRECTORY);
if (!root || !system) {
node_free(root);
node_free(system);
return NULL;
}
DIR *d = opendir(mdir);
if (!d) {
LOGE("opendir %s: %s", mdir, strerror(errno));
node_free(root);
node_free(system);
return NULL;
}
struct dirent *de;
bool has_any = false;
char mod[PATH_MAX];
char mod_sys[PATH_MAX];
while ((de = readdir(d))) {
if (!strcmp(de->d_name, ".") || !strcmp(de->d_name, ".."))
continue;
if (path_join(mdir, de->d_name, mod, sizeof(mod)) != 0) {
closedir(d);
node_free(root);
node_free(system);
return NULL;
}
struct stat st;
if (stat(mod, &st) < 0 || !S_ISDIR(st.st_mode))
continue;
if (module_disabled(mod))
continue;
if (path_join(mod, "system", mod_sys, sizeof(mod_sys)) != 0) {
closedir(d);
node_free(root);
node_free(system);
return NULL;
}
if (!path_is_dir(mod_sys))
continue;
LOGD("collecting module %s", de->d_name);
g_stats.modules_total++;
bool sub = false;
if (node_collect(system, mod_sys, de->d_name, &sub) != 0) {
closedir(d);
node_free(root);
node_free(system);
return NULL;
}
if (sub)
has_any = true;
}
closedir(d);
if (!has_any) {
node_free(root);
node_free(system);
return NULL;
}
g_stats.nodes_total += 2;
struct {
const char *name;
bool need_symlink;
} builtin_parts[] = {
{ "vendor", true },
{ "system_ext", true },
{ "product", true },
{ "odm", false },
};
char rp[PATH_MAX];
char sp[PATH_MAX];
for (size_t i = 0; i < sizeof(builtin_parts) / sizeof(builtin_parts[0]);
++i) {
if (path_join("/", builtin_parts[i].name, rp, sizeof(rp)) != 0 ||
path_join("/system", builtin_parts[i].name, sp, sizeof(sp)) != 0) {
node_free(root);
node_free(system);
return NULL;
}
if (!path_is_dir(rp))
continue;
if (builtin_parts[i].need_symlink && !path_is_symlink(sp))
continue;
Node *child = node_take_child(system, builtin_parts[i].name);
if (child && node_add_child(root, child) != 0) {
node_free(child);
node_free(root);
node_free(system);
return NULL;
}
}
for (int i = 0; i < g_extra_parts_count; ++i) {
const char *name = g_extra_parts[i];
if (path_join("/", name, rp, sizeof(rp)) != 0) {
node_free(root);
node_free(system);
return NULL;
}
if (!path_is_dir(rp))
continue;
Node *child = node_take_child(system, name);
if (child && node_add_child(root, child) != 0) {
node_free(child);
node_free(root);
node_free(system);
return NULL;
}
}
if (node_add_child(root, system) != 0) {
node_free(root);
node_free(system);
return NULL;
}
return root;
}
static int clone_symlink(const char *src, const char *dst)
{
char target[PATH_MAX];
ssize_t len = readlink(src, target, sizeof(target) - 1);
if (len < 0) {
LOGE("readlink %s: %s", src, strerror(errno));
return -1;
}
target[len] = '\0';
if (symlink(target, dst) < 0) {
LOGE("symlink %s->%s: %s", dst, target, strerror(errno));
return -1;
}
char *con = NULL;
if (get_selinux(src, &con) == 0 && con) {
set_selinux(dst, con);
free(con);
}
LOGD("clone symlink %s -> %s (%s)", src, dst, target);
return 0;
}
static int mirror(const char *path, const char *work, const char *name);
static int do_magic(const char *base, const char *wbase, Node *node,
bool has_tmpfs)
{
char path[PATH_MAX];
char wpath[PATH_MAX];
if (path_join(base, node->name, path, sizeof(path)) != 0 ||
path_join(wbase, node->name, wpath, sizeof(wpath)) != 0)
return -1;
switch (node->type) {
case NFT_REGULAR: {
const char *target = has_tmpfs ? wpath : path;
if (has_tmpfs) {
if (mkdir_p(wbase) != 0)
return -1;
int fd = open(wpath, O_CREAT | O_WRONLY, 0644);
if (fd < 0) {
LOGE("create %s: %s", wpath, strerror(errno));
return -1;
}
close(fd);
}
if (!node->module_path) {
LOGE("no module file for %s", path);
errno = EINVAL;
return -1;
}
LOGD("bind %s -> %s", node->module_path, target);
if (mount(node->module_path, target, NULL, MS_BIND, NULL) < 0) {
LOGE("bind %s->%s: %s", node->module_path, target,
strerror(errno));
return -1;
}
(void)mount(NULL, target, NULL,
MS_REMOUNT | MS_BIND | MS_RDONLY, NULL);
g_stats.nodes_mounted++;
return 0;
}
case NFT_SYMLINK: {
if (!node->module_path) {
LOGE("no module symlink for %s", path);
errno = EINVAL;
return -1;
}
if (clone_symlink(node->module_path, wpath) != 0)
return -1;
g_stats.nodes_mounted++;
return 0;
}
case NFT_WHITEOUT:
LOGD("whiteout %s", path);
g_stats.nodes_whiteout++;
return 0;
case NFT_DIRECTORY: {
bool create_tmp =
(!has_tmpfs && node->replace && node->module_path);
if (!has_tmpfs && !create_tmp) {
for (size_t i = 0; i < node->child_count; ++i) {
Node *c = node->children[i];
char rp[PATH_MAX];
if (path_join(path, c->name, rp, sizeof(rp)) != 0)
return -1;
bool need = false;
if (c->type == NFT_SYMLINK) {
need = true;
} else if (c->type == NFT_WHITEOUT) {
need = path_exists(rp);
} else {
struct stat st;
if (lstat(rp, &st) == 0) {
NodeFileType rt = node_type_from_stat(&st);
if (rt != c->type || rt == NFT_SYMLINK)
need = true;
} else {
need = true;
}
}
if (need) {
if (!node->module_path) {
LOGE("cannot create tmpfs on %s (%s)",
path, c->name);
c->skip = true;
g_stats.nodes_skipped++;
continue;
}
create_tmp = true;
break;
}
}
}
bool now_tmp = has_tmpfs || create_tmp;
if (now_tmp) {
if (mkdir_p(wpath) != 0)
return -1;
struct stat st;
const char *meta_path = NULL;
if (stat(path, &st) == 0) {
meta_path = path;
} else if (node->module_path &&
stat(node->module_path, &st) == 0) {
meta_path = node->module_path;
} else {
LOGE("no dir meta for %s", path);
errno = ENOENT;
return -1;
}
chmod(wpath, st.st_mode & 07777);
chown(wpath, st.st_uid, st.st_gid);
char *con = NULL;
if (get_selinux(meta_path, &con) == 0 && con) {
set_selinux(wpath, con);
free(con);
}
}
if (create_tmp) {
if (mount(wpath, wpath, NULL, MS_BIND, NULL) < 0) {
LOGE("bind self %s: %s", wpath, strerror(errno));
return -1;
}
}
if (path_exists(path) && !node->replace) {
DIR *d = opendir(path);
if (!d) {
LOGE("opendir %s: %s", path, strerror(errno));
if (now_tmp)
return -1;
} else {
struct dirent *de;
while ((de = readdir(d))) {
if (!strcmp(de->d_name, ".") ||
!strcmp(de->d_name, "..")) {
continue;
}
Node *c = node_find_child(node, de->d_name);
int r = 0;
if (c) {
if (c->skip) {
c->done = true;
continue;
}
c->done = true;
r = do_magic(path, wpath, c, now_tmp);
} else if (now_tmp) {
r = mirror(path, wpath, de->d_name);
}
if (r != 0) {
const char *mn = NULL;
if (c && c->module_name)
mn = c->module_name;
else if (node->module_name)
mn = node->module_name;
if (mn) {
LOGE("child %s/%s failed (module: %s)",
path,
c ? c->name : de->d_name,
mn);
register_module_failure(mn);
} else {
LOGE("child %s/%s failed (no module_name)",
path,
c ? c->name : de->d_name);
}
g_stats.nodes_fail++;
if (now_tmp) {
closedir(d);
return -1;
}
}
}
closedir(d);
}
}
for (size_t i = 0; i < node->child_count; ++i) {
Node *c = node->children[i];
if (c->skip || c->done)
continue;
int r = do_magic(path, wpath, c, now_tmp);
if (r != 0) {
const char *mn = c->module_name ?
c->module_name :
node->module_name;
if (mn) {
LOGE("child %s/%s failed (module: %s)",
path, c->name, mn);
register_module_failure(mn);
} else {
LOGE("child %s/%s failed (no module_name)",
path, c->name);
}
g_stats.nodes_fail++;
if (now_tmp)
return -1;
}
}
if (create_tmp) {
(void)mount(NULL, wpath, NULL,
MS_REMOUNT | MS_BIND | MS_RDONLY, NULL);
if (mount(wpath, path, NULL, MS_MOVE, NULL) < 0) {
LOGE("move %s->%s failed: %s", wpath, path,
strerror(errno));
if (node->module_name)
register_module_failure(node->module_name);
return -1;
}
LOGI("move mountpoint success: %s -> %s", wpath, path);
(void)mount(NULL, path, NULL, MS_REC | MS_PRIVATE, NULL);
}
g_stats.nodes_mounted++;
return 0;
}
}
return 0;
}
static int mirror(const char *path, const char *work, const char *name)
{
char src[PATH_MAX];
char dst[PATH_MAX];
if (path_join(path, name, src, sizeof(src)) != 0 ||
path_join(work, name, dst, sizeof(dst)) != 0)
return -1;
struct stat st;
if (lstat(src, &st) < 0) {
LOGW("lstat %s: %s", src, strerror(errno));
return 0;
}
if (S_ISREG(st.st_mode)) {
int fd = open(dst, O_CREAT | O_WRONLY, st.st_mode & 07777);
if (fd < 0) {
LOGE("create %s: %s", dst, strerror(errno));
return -1;
}
close(fd);
if (mount(src, dst, NULL, MS_BIND, NULL) < 0) {
LOGE("bind %s->%s: %s", src, dst, strerror(errno));
return -1;
}
} else if (S_ISDIR(st.st_mode)) {
if (mkdir(dst, st.st_mode & 07777) < 0 && errno != EEXIST) {
LOGE("mkdir %s: %s", dst, strerror(errno));
return -1;
}
chmod(dst, st.st_mode & 07777);
chown(dst, st.st_uid, st.st_gid);
char *con = NULL;
if (get_selinux(src, &con) == 0 && con) {
set_selinux(dst, con);
free(con);
}
DIR *d = opendir(src);
if (!d) {
LOGE("opendir %s: %s", src, strerror(errno));
return -1;
}
struct dirent *de;
while ((de = readdir(d))) {
if (!strcmp(de->d_name, ".") ||
!strcmp(de->d_name, "..")) {
continue;
}
if (mirror(src, dst, de->d_name) != 0) {
closedir(d);
return -1;
}
}
closedir(d);
} else if (S_ISLNK(st.st_mode)) {
if (clone_symlink(src, dst) != 0)
return -1;
}
return 0;
}
int magic_mount(const char *tmp_root)
{
Node *root = collect_root();
if (!root) {
LOGI("no modules, magic_mount skipped");
return 0;
}
char tmp_dir[PATH_MAX];
if (path_join(tmp_root, "workdir", tmp_dir, sizeof(tmp_dir)) != 0) {
node_free(root);
return -1;
}
if (mkdir_p(tmp_dir) != 0) {
node_free(root);
return -1;
}
LOGI("starting magic_mount core logic: tmpfs_source=%s tmp_dir=%s",
g_mount_source, tmp_dir);
if (mount(g_mount_source, tmp_dir, "tmpfs", 0, "") < 0) {
LOGE("mount tmpfs %s: %s", tmp_dir, strerror(errno));
node_free(root);
return -1;
}
(void)mount(NULL, tmp_dir, NULL, MS_REC | MS_PRIVATE, NULL);
int rc = do_magic("/", tmp_dir, root, false);
if (rc != 0)
g_stats.nodes_fail++;
if (umount2(tmp_dir, MNT_DETACH) < 0)
LOGE("umount %s: %s", tmp_dir, strerror(errno));
(void)rmdir(tmp_dir);
node_free(root);
return rc;
}

39
src/magic_mount.h Normal file
View file

@ -0,0 +1,39 @@
#ifndef MAGIC_MOUNT_H
#define MAGIC_MOUNT_H
#include <stdio.h>
#define DISABLE_FILE_NAME "disable"
#define REMOVE_FILE_NAME "remove"
#define SKIP_MOUNT_FILE_NAME "skip_mount"
#define REPLACE_DIR_XATTR "trusted.overlay.opaque"
#define DEFAULT_MOUNT_SOURCE "KSU"
#define DEFAULT_MODULE_DIR "/data/adb/modules"
typedef struct {
int modules_total;
int nodes_total;
int nodes_mounted;
int nodes_skipped;
int nodes_whiteout;
int nodes_fail;
} MountStats;
extern MountStats g_stats;
extern const char *g_module_dir;
extern const char *g_mount_source;
extern char **g_failed_modules;
extern int g_failed_modules_count;
extern char **g_extra_parts;
extern int g_extra_parts_count;
int magic_mount(const char *tmp_root);
void add_extra_part_token(const char *start, size_t len);
#endif /* MAGIC_MOUNT_H */

311
src/main.c Normal file
View file

@ -0,0 +1,311 @@
#include "magic_mount.h"
#include "utils.h"
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <limits.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <ctype.h>
#define DEFAULT_CONFIG_PATH "/data/adb/magic_mount/mm.conf"
static void usage(const char *prog)
{
fprintf(stderr,
"Magic Mount Version: %s\n"
"Usage: %s [options]\n"
" -m DIR module dir (default: %s)\n"
" -t DIR temp dir (default: auto rw tmpfs + \".magic_mount\")\n"
" -s SRC mount src (default: %s)\n"
" -p LIST extra partitions, comma-separated (e.g. mi_ext,my_stock)\n"
" -l FILE log file (default: stderr, '-'=stdout)\n"
" -c FILE config file (default: " DEFAULT_CONFIG_PATH ")\n"
" -v enable debug logging (default: info)\n"
" -h help\n",
VERSION, prog, DEFAULT_MODULE_DIR,
DEFAULT_MOUNT_SOURCE);
}
static int enter_pid1_ns(void)
{
int fd = open("/proc/1/ns/mnt", O_RDONLY);
if (fd < 0) {
LOGE("open /proc/1/ns/mnt: %s", strerror(errno));
return -1;
}
if (setns(fd, 0) < 0) {
LOGE("setns: %s", strerror(errno));
close(fd);
return -1;
}
close(fd);
LOGI("entered pid1 mnt ns");
return 0;
}
static char *str_trim(char *s)
{
if (!s)
return s;
char *start = s;
while (*start && isspace((unsigned char)*start))
start++;
char *end = start + strlen(start);
while (end > start && isspace((unsigned char)end[-1]))
*--end = '\0';
return start;
}
static bool str_is_true(const char *v)
{
if (!v)
return false;
if (!strcasecmp(v, "1") ||
!strcasecmp(v, "true") ||
!strcasecmp(v, "yes") ||
!strcasecmp(v, "on"))
return true;
return false;
}
static void load_config(const char *config_path,
const char **tmp_dir_out,
const char **log_path_out,
bool *debug_out)
{
FILE *fp = fopen(config_path, "r");
if (!fp) {
LOGD("config file %s not used: %s",
config_path, strerror(errno));
return;
}
LOGI("loading config file: %s", config_path);
char line[1024];
while (fgets(line, sizeof(line), fp)) {
char *p = line;
while (*p && isspace((unsigned char)*p))
p++;
if (*p == '\0' || *p == '\n' || *p == '#')
continue;
char *eq = strchr(p, '=');
if (!eq) {
continue;
}
*eq = '\0';
char *key = str_trim(p);
char *val = str_trim(eq + 1);
size_t vlen = strlen(val);
if (vlen > 0 && (val[vlen - 1] == '\n' || val[vlen - 1] == '\r'))
val[vlen - 1] = '\0';
val = str_trim(val);
if (*key == '\0' || *val == '\0')
continue;
if (!strcasecmp(key, "module_dir")) {
char *dup = strdup(val);
if (dup)
g_module_dir = dup;
} else if (!strcasecmp(key, "temp_dir")) {
char *dup = strdup(val);
if (dup && tmp_dir_out)
*tmp_dir_out = dup;
} else if (!strcasecmp(key, "mount_source")) {
char *dup = strdup(val);
if (dup)
g_mount_source = dup;
} else if (!strcasecmp(key, "partitions")) {
const char *list = val;
const char *q = list;
while (*q) {
const char *start = q;
while (*q && *q != ',')
q++;
size_t len = (size_t)(q - start);
add_extra_part_token(start, len);
if (*q == ',')
q++;
}
} else if (!strcasecmp(key, "log_file")) {
char *dup = strdup(val);
if (dup && log_path_out)
*log_path_out = dup;
} else if (!strcasecmp(key, "debug")) {
if (debug_out && str_is_true(val))
*debug_out = true;
} else {
LOGW("unknown config key: %s", key);
}
}
fclose(fp);
}
int main(int argc, char **argv)
{
const char *tmp_dir = NULL;
const char *log_path = NULL;
const char *config_path = DEFAULT_CONFIG_PATH;
char auto_tmp[PATH_MAX] = { 0 };
bool debug_from_cfg = false;
for (int i = 1; i < argc; ++i) {
const char *arg = argv[i];
if ((strcmp(arg, "-c") == 0 || strcmp(arg, "--config") == 0) &&
i + 1 < argc) {
config_path = argv[++i];
}
}
load_config(config_path, &tmp_dir, &log_path, &debug_from_cfg);
if (debug_from_cfg) {
g_log_level = LV_DEBUG;
}
for (int i = 1; i < argc; ++i) {
const char *arg = argv[i];
if ((strcmp(arg, "-c") == 0 || strcmp(arg, "--config") == 0) &&
i + 1 < argc) {
++i;
continue;
}
if ((strcmp(arg, "-m") == 0 ||
strcmp(arg, "--module-dir") == 0) &&
i + 1 < argc) {
g_module_dir = argv[++i];
} else if ((strcmp(arg, "-t") == 0 ||
strcmp(arg, "--temp-dir") == 0) &&
i + 1 < argc) {
tmp_dir = argv[++i];
} else if ((strcmp(arg, "-s") == 0 ||
strcmp(arg, "--mount-source") == 0) &&
i + 1 < argc) {
g_mount_source = argv[++i];
} else if ((strcmp(arg, "-l") == 0 ||
strcmp(arg, "--log-file") == 0) &&
i + 1 < argc) {
log_path = argv[++i];
} else if (!strcmp(arg, "-v") || !strcmp(arg, "--verbose")) {
g_log_level = LV_DEBUG;
} else if ((strcmp(arg, "-p") == 0 ||
strcmp(arg, "--partitions") == 0) &&
i + 1 < argc) {
const char *list = argv[++i];
const char *p = list;
while (*p) {
const char *start = p;
while (*p && *p != ',')
p++;
size_t len = (size_t)(p - start);
add_extra_part_token(start, len);
if (*p == ',')
p++;
}
} else if (!strcmp(arg, "-h") || !strcmp(arg, "--help")) {
usage(argv[0]);
return 0;
} else {
fprintf(stderr, "Unknown arg: %s\n", arg);
usage(argv[0]);
return 1;
}
}
if (log_path) {
if (!strcmp(log_path, "-")) {
g_log_file = stdout;
} else {
g_log_file = fopen(log_path, "a");
if (!g_log_file) {
fprintf(stderr, "open log %s: %s\n", log_path,
strerror(errno));
return 1;
}
}
}
if (!tmp_dir) {
tmp_dir = select_auto_tempdir(auto_tmp);
}
if (geteuid() != 0) {
LOGE("must run as root (euid=%d)", (int)geteuid());
if (g_log_file && g_log_file != stdout && g_log_file != stderr)
fclose(g_log_file);
free_string_array(&g_extra_parts, &g_extra_parts_count);
return 1;
}
if (enter_pid1_ns() != 0) {
if (g_log_file && g_log_file != stdout && g_log_file != stderr)
fclose(g_log_file);
free_string_array(&g_extra_parts, &g_extra_parts_count);
return 1;
}
int rc = 0;
LOGI("starting magic_mount process: module_dir=%s temp_dir=%s "
"mount_source=%s log_level=%d",
g_module_dir, tmp_dir, g_mount_source, g_log_level);
rc = magic_mount(tmp_dir);
if (rc == 0) {
LOGI("magic_mount completed successfully");
} else {
LOGE("magic_mount failed with rc=%d "
"(check previous logs for details)",
rc);
}
LOGI("magic_mount summary: modules=%d nodes_total=%d mounted=%d "
"skipped=%d whiteouts=%d failures=%d",
g_stats.modules_total, g_stats.nodes_total, g_stats.nodes_mounted,
g_stats.nodes_skipped, g_stats.nodes_whiteout, g_stats.nodes_fail);
if (g_failed_modules_count > 0) {
LOGW("modules with failures (%d):", g_failed_modules_count);
for (int i = 0; i < g_failed_modules_count; ++i) {
LOGW(" failed module: %s", g_failed_modules[i]);
}
}
if (g_log_file && g_log_file != stdout && g_log_file != stderr)
fclose(g_log_file);
free_string_array(&g_failed_modules, &g_failed_modules_count);
free_string_array(&g_extra_parts, &g_extra_parts_count);
return rc == 0 ? 0 : 1;
}

View file

@ -1,116 +0,0 @@
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::{Path, PathBuf};
const CONFIG_FILE_DEFAULT: &str = "/data/adb/magic_mount/config.toml";
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Config {
#[serde(default = "default_moduledir")]
pub moduledir: PathBuf,
#[serde(default)]
pub tempdir: Option<PathBuf>,
#[serde(default = "default_mountsource")]
pub mountsource: String,
#[serde(default = "default_logfile")]
pub logfile: PathBuf,
#[serde(default)]
pub verbose: bool,
#[serde(default)]
pub partitions: Vec<String>,
}
fn default_moduledir() -> PathBuf {
PathBuf::from("/data/adb/modules/")
}
fn default_mountsource() -> String {
"MaGIcMounT".to_string()
}
fn default_logfile() -> PathBuf {
PathBuf::from("/data/adb/magic_mount/mm.log")
}
impl Default for Config {
fn default() -> Self {
Self {
moduledir: default_moduledir(),
tempdir: None,
mountsource: default_mountsource(),
logfile: default_logfile(),
verbose: false,
partitions: Vec::new(),
}
}
}
impl Config {
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
let content = fs::read_to_string(path.as_ref())
.context("failed to read config file")?;
let config: Config = toml::from_str(&content)
.context("failed to parse config file")?;
Ok(config)
}
pub fn load_default() -> Option<Self> {
Self::from_file(CONFIG_FILE_DEFAULT).ok()
}
pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
let content = toml::to_string_pretty(self)
.context("failed to serialize config")?;
if let Some(parent) = path.as_ref().parent() {
fs::create_dir_all(parent)
.context("failed to create config directory")?;
}
fs::write(path.as_ref(), content)
.context("failed to write config file")?;
Ok(())
}
pub fn example() -> String {
let example = Self::default();
toml::to_string_pretty(&example).unwrap_or_default()
}
pub fn merge_with_cli(&mut self,
moduledir: Option<PathBuf>,
tempdir: Option<PathBuf>,
mountsource: Option<String>,
logfile: Option<PathBuf>,
verbose: bool,
partitions: Vec<String>,
) {
if let Some(dir) = moduledir {
self.moduledir = dir;
}
if tempdir.is_some() {
self.tempdir = tempdir;
}
if let Some(source) = mountsource {
self.mountsource = source;
}
if let Some(log) = logfile {
self.logfile = log;
}
if verbose {
self.verbose = true;
}
if !partitions.is_empty() {
self.partitions = partitions;
}
}
}

View file

@ -1,489 +0,0 @@
use crate::magic_mount::NodeFileType::{Directory, RegularFile, Symlink, Whiteout};
use crate::utils::{lgetfilecon, lsetfilecon, ensure_dir_exists};
use anyhow::{Context, Result, bail};
use extattr::lgetxattr;
use rustix::fs::{
Gid, MetadataExt, Mode, MountFlags, MountPropagationFlags, Uid, UnmountFlags, bind_mount,
chmod, chown, mount, move_mount, remount, unmount,
};
use rustix::mount::mount_change;
use rustix::path::Arg;
use std::cmp::PartialEq;
use std::collections::HashMap;
use std::collections::hash_map::Entry;
use std::fs;
use std::fs::{DirEntry, FileType, create_dir, create_dir_all, read_dir, read_link};
use std::os::unix::fs::{FileTypeExt, symlink};
use std::path::{Path, PathBuf};
const DISABLE_FILE_NAME: &str = "disable";
const REMOVE_FILE_NAME: &str = "remove";
const SKIP_MOUNT_FILE_NAME: &str = "skip_mount";
const REPLACE_DIR_XATTR: &str = "trusted.overlay.opaque";
#[derive(PartialEq, Eq, Hash, Clone, Debug)]
enum NodeFileType {
RegularFile,
Directory,
Symlink,
Whiteout,
}
impl NodeFileType {
fn from_file_type(file_type: FileType) -> Option<Self> {
if file_type.is_file() {
Some(RegularFile)
} else if file_type.is_dir() {
Some(Directory)
} else if file_type.is_symlink() {
Some(Symlink)
} else {
None
}
}
}
#[derive(Debug)]
struct Node {
name: String,
file_type: NodeFileType,
children: HashMap<String, Node>,
// the module that owned this node
module_path: Option<PathBuf>,
replace: bool,
skip: bool,
}
impl Node {
fn collect_module_files<T: AsRef<Path>>(&mut self, module_dir: T) -> Result<bool> {
let dir = module_dir.as_ref();
let mut has_file = false;
for entry in dir.read_dir()?.flatten() {
let name = entry.file_name().to_string_lossy().to_string();
let node = match self.children.entry(name.clone()) {
Entry::Occupied(o) => Some(o.into_mut()),
Entry::Vacant(v) => Self::new_module(&name, &entry).map(|it| v.insert(it)),
};
if let Some(node) = node {
has_file |= if node.file_type == Directory {
node.collect_module_files(dir.join(&node.name))? || node.replace
} else {
true
}
}
}
Ok(has_file)
}
fn new_root<T: ToString>(name: T) -> Self {
Node {
name: name.to_string(),
file_type: Directory,
children: Default::default(),
module_path: None,
replace: false,
skip: false,
}
}
fn new_module<T: ToString>(name: T, entry: &DirEntry) -> Option<Self> {
if let Ok(metadata) = entry.metadata() {
let path = entry.path();
let file_type = if metadata.file_type().is_char_device() && metadata.rdev() == 0 {
Some(Whiteout)
} else {
NodeFileType::from_file_type(metadata.file_type())
};
if let Some(file_type) = file_type {
let mut replace = false;
if file_type == Directory
&& let Ok(v) = lgetxattr(&path, REPLACE_DIR_XATTR)
&& String::from_utf8_lossy(&v) == "y"
{
replace = true;
}
return Some(Node {
name: name.to_string(),
file_type,
children: Default::default(),
module_path: Some(path),
replace,
skip: false,
});
}
}
None
}
}
fn collect_module_files(module_dir: &Path, extra_partitions: &[String]) -> Result<Option<Node>> {
let mut root = Node::new_root("");
let mut system = Node::new_root("system");
let module_root = module_dir;
let mut has_file = false;
for entry in module_root.read_dir()?.flatten() {
if !entry.file_type()?.is_dir() {
continue;
}
if entry.path().join(DISABLE_FILE_NAME).exists()
|| entry.path().join(REMOVE_FILE_NAME).exists()
|| entry.path().join(SKIP_MOUNT_FILE_NAME).exists()
{
continue;
}
let mod_system = entry.path().join("system");
if !mod_system.is_dir() {
continue;
}
log::debug!("collecting {}", entry.path().display());
has_file |= system.collect_module_files(&mod_system)?;
}
if has_file {
const BUILTIN_PARTITIONS: [(&str, bool); 4] = [
("vendor", true),
("system_ext", true),
("product", true),
("odm", false),
];
for (partition, require_symlink) in BUILTIN_PARTITIONS {
let path_of_root = Path::new("/").join(partition);
let path_of_system = Path::new("/system").join(partition);
if path_of_root.is_dir() && (!require_symlink || path_of_system.is_symlink()) {
let name = partition.to_string();
if let Some(node) = system.children.remove(&name) {
root.children.insert(name, node);
}
}
}
for partition in extra_partitions {
if BUILTIN_PARTITIONS.iter().any(|(p, _)| p == partition) {
continue;
}
if partition == "system" {
continue;
}
let path_of_root = Path::new("/").join(partition);
let path_of_system = Path::new("/system").join(partition);
let require_symlink = false;
if path_of_root.is_dir() && (!require_symlink || path_of_system.is_symlink()) {
let name = partition.to_string();
if let Some(node) = system.children.remove(&name) {
log::debug!("attach extra partition '{}' to root", name);
root.children.insert(name, node);
}
}
}
root.children.insert("system".to_string(), system);
Ok(Some(root))
} else {
Ok(None)
}
}
fn clone_symlink<Src: AsRef<Path>, Dst: AsRef<Path>>(src: Src, dst: Dst) -> Result<()> {
let src_symlink = read_link(src.as_ref())?;
symlink(&src_symlink, dst.as_ref())?;
lsetfilecon(dst.as_ref(), lgetfilecon(src.as_ref())?.as_str())?;
log::debug!(
"clone symlink {} -> {}({})",
dst.as_ref().display(),
dst.as_ref().display(),
src_symlink.display()
);
Ok(())
}
fn mount_mirror<P: AsRef<Path>, WP: AsRef<Path>>(
path: P,
work_dir_path: WP,
entry: &DirEntry,
) -> Result<()> {
let path = path.as_ref().join(entry.file_name());
let work_dir_path = work_dir_path.as_ref().join(entry.file_name());
let file_type = entry.file_type()?;
if file_type.is_file() {
log::debug!(
"mount mirror file {} -> {}",
path.display(),
work_dir_path.display()
);
fs::File::create(&work_dir_path)?;
bind_mount(&path, &work_dir_path)?;
} else if file_type.is_dir() {
log::debug!(
"mount mirror dir {} -> {}",
path.display(),
work_dir_path.display()
);
create_dir(&work_dir_path)?;
let metadata = entry.metadata()?;
chmod(&work_dir_path, Mode::from_raw_mode(metadata.mode()))?;
unsafe {
chown(
&work_dir_path,
Some(Uid::from_raw(metadata.uid())),
Some(Gid::from_raw(metadata.gid())),
)?;
}
lsetfilecon(&work_dir_path, lgetfilecon(&path)?.as_str())?;
for entry in read_dir(&path)?.flatten() {
mount_mirror(&path, &work_dir_path, &entry)?;
}
} else if file_type.is_symlink() {
log::debug!(
"create mirror symlink {} -> {}",
path.display(),
work_dir_path.display()
);
clone_symlink(&path, &work_dir_path)?;
}
Ok(())
}
fn do_magic_mount<P: AsRef<Path>, WP: AsRef<Path>>(
path: P,
work_dir_path: WP,
current: Node,
has_tmpfs: bool,
) -> Result<()> {
let mut current = current;
let path = path.as_ref().join(&current.name);
let work_dir_path = work_dir_path.as_ref().join(&current.name);
match current.file_type {
RegularFile => {
let target_path = if has_tmpfs {
fs::File::create(&work_dir_path)?;
&work_dir_path
} else {
&path
};
if let Some(module_path) = &current.module_path {
log::debug!(
"mount module file {} -> {}",
module_path.display(),
work_dir_path.display()
);
bind_mount(module_path, target_path).with_context(|| {
format!("mount module file {module_path:?} -> {work_dir_path:?}")
})?;
// we should use MS_REMOUNT | MS_BIND | MS_xxx to change mount flags
if let Err(e) = remount(target_path, MountFlags::RDONLY | MountFlags::BIND, "") {
log::warn!("make file {target_path:?} ro: {e:#?}");
}
} else {
bail!("cannot mount root file {}!", path.display());
}
}
Symlink => {
if let Some(module_path) = &current.module_path {
log::debug!(
"create module symlink {} -> {}",
module_path.display(),
work_dir_path.display()
);
clone_symlink(module_path, &work_dir_path).with_context(|| {
format!("create module symlink {module_path:?} -> {work_dir_path:?}")
})?;
} else {
bail!("cannot mount root symlink {}!", path.display());
}
}
Directory => {
let mut create_tmpfs = !has_tmpfs && current.replace && current.module_path.is_some();
if !has_tmpfs && !create_tmpfs {
for it in &mut current.children {
let (name, node) = it;
let real_path = path.join(name);
let need = match node.file_type {
Symlink => true,
Whiteout => real_path.exists(),
_ => {
if let Ok(metadata) = real_path.symlink_metadata() {
let file_type = NodeFileType::from_file_type(metadata.file_type())
.unwrap_or(Whiteout);
file_type != node.file_type || file_type == Symlink
} else {
// real path not exists
true
}
}
};
if need {
if current.module_path.is_none() {
log::error!(
"cannot create tmpfs on {}, ignore: {name}",
path.display()
);
node.skip = true;
continue;
}
create_tmpfs = true;
break;
}
}
}
let has_tmpfs = has_tmpfs || create_tmpfs;
if has_tmpfs {
log::debug!(
"creating tmpfs skeleton for {} at {}",
path.display(),
work_dir_path.display()
);
create_dir_all(&work_dir_path)?;
let (metadata, path) = if path.exists() {
(path.metadata()?, &path)
} else if let Some(module_path) = &current.module_path {
(module_path.metadata()?, module_path)
} else {
bail!("cannot mount root dir {}!", path.display());
};
chmod(&work_dir_path, Mode::from_raw_mode(metadata.mode()))?;
unsafe {
chown(
&work_dir_path,
Some(Uid::from_raw(metadata.uid())),
Some(Gid::from_raw(metadata.gid())),
)?;
}
lsetfilecon(&work_dir_path, lgetfilecon(path)?.as_str())?;
}
if create_tmpfs {
log::debug!(
"creating tmpfs for {} at {}",
path.display(),
work_dir_path.display()
);
bind_mount(&work_dir_path, &work_dir_path)
.context("bind self")
.with_context(|| format!("creating tmpfs for {path:?} at {work_dir_path:?}"))?;
}
if path.exists() && !current.replace {
for entry in path.read_dir()?.flatten() {
let name = entry.file_name().to_string_lossy().to_string();
let result = if let Some(node) = current.children.remove(&name) {
if node.skip {
continue;
}
do_magic_mount(&path, &work_dir_path, node, has_tmpfs)
.with_context(|| format!("magic mount {}/{name}", path.display()))
} else if has_tmpfs {
mount_mirror(&path, &work_dir_path, &entry)
.with_context(|| format!("mount mirror {}/{name}", path.display()))
} else {
Ok(())
};
if let Err(e) = result {
if has_tmpfs {
return Err(e);
} else {
log::error!("mount child {}/{name} failed: {e:#?}", path.display());
}
}
}
}
if current.replace {
if current.module_path.is_none() {
bail!(
"dir {} is declared as replaced but it is root!",
path.display()
);
} else {
log::debug!("dir {} is replaced", path.display());
}
}
for (name, node) in current.children.into_iter() {
if node.skip {
continue;
}
if let Err(e) = do_magic_mount(&path, &work_dir_path, node, has_tmpfs)
.with_context(|| format!("magic mount {}/{name}", path.display()))
{
if has_tmpfs {
return Err(e);
} else {
log::error!("mount child {}/{name} failed: {e:#?}", path.display());
}
}
}
if create_tmpfs {
log::debug!(
"moving tmpfs {} -> {}",
work_dir_path.display(),
path.display()
);
if let Err(e) = remount(&work_dir_path, MountFlags::RDONLY | MountFlags::BIND, "") {
log::warn!("make dir {path:?} ro: {e:#?}");
}
move_mount(&work_dir_path, &path)
.context("move self")
.with_context(|| format!("moving tmpfs {work_dir_path:?} -> {path:?}"))?;
// make private to reduce peer group count
if let Err(e) = mount_change(&path, MountPropagationFlags::PRIVATE) {
log::warn!("make dir {path:?} private: {e:#?}");
}
}
}
Whiteout => {
log::debug!("file {} is removed", path.display());
}
}
Ok(())
}
pub fn magic_mount<T: AsRef<Path>>(
tmp_path: T,
module_dir: &Path,
mount_source: &str,
extra_partitions: &[String],
) -> Result<()> {
if let Some(root) = collect_module_files(module_dir, extra_partitions)? {
log::debug!("collected: {:#?}", root);
let tmp_root = tmp_path.as_ref();
let tmp_dir = tmp_root.join("workdir");
ensure_dir_exists(&tmp_dir)?;
mount(mount_source, &tmp_dir, "tmpfs", MountFlags::empty(), "")
.context("mount tmp")?;
mount_change(&tmp_dir, MountPropagationFlags::PRIVATE).context("make tmp private")?;
let result = do_magic_mount("/", &tmp_dir, root, false);
if let Err(e) = unmount(&tmp_dir, UnmountFlags::DETACH) {
log::error!("failed to unmount tmp {}", e);
}
fs::remove_dir(tmp_dir).ok();
result
} else {
log::info!("no modules to mount, skipping!");
Ok(())
}
}

View file

@ -1,179 +0,0 @@
mod config;
mod magic_mount;
mod utils;
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use std::path::PathBuf;
use config::Config;
const CONFIG_FILE_DEFAULT: &str = "/data/adb/magic_mount/config.toml";
#[derive(Parser, Debug)]
#[command(name = "magic_mount", version, about = "Magic Mount Metamodule")]
struct Cli {
/// Config file path
#[arg(short = 'c', long = "config")]
config: Option<PathBuf>,
/// Module directory path
#[arg(short = 'm', long = "moduledir")]
moduledir: Option<PathBuf>,
/// Temporary directory path (auto-selected if not specified)
#[arg(short = 't', long = "tempdir")]
tempdir: Option<PathBuf>,
/// Mount source name
#[arg(short = 's', long = "mountsource")]
mountsource: Option<String>,
/// Log file path
#[arg(short = 'l', long = "logfile")]
logfile: Option<PathBuf>,
/// Enable verbose (debug) logging
#[arg(short = 'v', long = "verbose")]
verbose: bool,
/// Extra partitions, comma-separated, eg: -p mi_ext,my_stock
#[arg(short = 'p', long = "partitions", value_delimiter = ',')]
partitions: Vec<String>,
#[command(subcommand)]
command: Option<Commands>,
}
#[derive(Subcommand, Debug)]
enum Commands {
/// Generate example config file
GenConfig {
/// Output path for config file
#[arg(short = 'o', long = "output", default_value = CONFIG_FILE_DEFAULT)]
output: PathBuf,
},
/// Show current effective configuration
ShowConfig,
}
fn load_config(cli: &Cli) -> Result<Config> {
// 1. 尝试从指定的配置文件加载
if let Some(config_path) = &cli.config {
log::info!("Loading config from: {}", config_path.display());
return Config::from_file(config_path)
.context("failed to load specified config file");
}
// 2. 尝试从默认位置加载
if let Some(config) = Config::load_default() {
log::info!("Loaded config from default location: {}", CONFIG_FILE_DEFAULT);
return Ok(config);
}
// 3. 使用默认配置
log::info!("Using default configuration (no config file found)");
Ok(Config::default())
}
fn main() -> Result<()> {
let cli = Cli::parse();
// 处理子命令
if let Some(command) = &cli.command {
match command {
Commands::GenConfig { output } => {
return generate_config(output);
}
Commands::ShowConfig => {
let config = load_config(&cli)?;
return show_config(&config);
}
}
}
// 加载配置
let mut config = load_config(&cli)?;
// 命令行参数覆盖配置文件
config.merge_with_cli(
cli.moduledir,
cli.tempdir,
cli.mountsource,
cli.logfile.clone(),
cli.verbose,
cli.partitions,
);
// 初始化日志
utils::init_logger(&config.logfile, config.verbose)?;
log::info!("Magic Mount Starting");
log::info!("module dir : {}", config.moduledir.display());
let tempdir = if let Some(temp) = config.tempdir {
log::info!("temp dir (cfg) : {}", temp.display());
temp
} else {
let temp = utils::select_temp_dir()
.context("failed to select temp dir automatically")?;
log::info!("temp dir (auto) : {}", temp.display());
temp
};
log::info!("mount source : {}", config.mountsource);
log::info!("log file : {}", config.logfile.display());
log::info!("verbose mode : {}", config.verbose);
if !config.partitions.is_empty() {
log::info!("extra partitions: {:?}", config.partitions);
}
utils::ensure_temp_dir(&tempdir)?;
let result = magic_mount::magic_mount(
&tempdir,
&config.moduledir,
&config.mountsource,
&config.partitions,
);
utils::cleanup_temp_dir(&tempdir);
match result {
Ok(_) => {
log::info!("Magic Mount Completed Successfully");
Ok(())
}
Err(e) => {
log::error!("Magic Mount Failed");
log::error!("error: {:#}", e);
Err(e)
}
}
}
fn generate_config(output: &PathBuf) -> Result<()> {
let config = Config::default();
config.save_to_file(output)
.context("failed to generate config file")?;
println!("✓ Config file generated at: {}", output.display());
println!("\nExample content:");
println!("{}", Config::example());
Ok(())
}
fn show_config(config: &Config) -> Result<()> {
println!("Current Configuration:");
println!("=====================");
println!("Module Dir : {}", config.moduledir.display());
println!("Temp Dir : {}",
config.tempdir.as_ref()
.map(|p| p.display().to_string())
.unwrap_or_else(|| "(auto)".to_string())
);
println!("Mount Source : {}", config.mountsource);
println!("Log File : {}", config.logfile.display());
println!("Verbose : {}", config.verbose);
println!("Partitions : {:?}", config.partitions);
Ok(())
}

View file

@ -1,159 +0,0 @@
use anyhow::{bail, Context, Result};
use log::LevelFilter;
use simplelog::{Config, WriteLogger};
use std::fs::{OpenOptions, write, remove_file, create_dir_all, remove_dir_all, read_to_string};
use std::path::{Path, PathBuf};
#[cfg(any(target_os = "linux", target_os = "android"))]
use extattr::{lsetxattr, Flags as XattrFlags};
const SELINUX_XATTR: &str = "security.selinux";
const TEMP_DIR_SUFFIX: &str = ".magic_mount";
const TMPFS_CANDIDATES: &[&str] = &["/mnt/vendor", "/mnt", "/debug_ramdisk"];
pub fn lsetfilecon<P: AsRef<Path>>(path: P, con: &str) -> Result<()> {
#[cfg(any(target_os = "linux", target_os = "android"))]
lsetxattr(&path, SELINUX_XATTR, con, XattrFlags::empty()).with_context(|| {
format!(
"Failed to change SELinux context for {}",
path.as_ref().display()
)
})?;
Ok(())
}
#[cfg(any(target_os = "linux", target_os = "android"))]
pub fn lgetfilecon<P: AsRef<Path>>(path: P) -> Result<String> {
let con = extattr::lgetxattr(&path, SELINUX_XATTR).with_context(|| {
format!(
"Failed to get SELinux context for {}",
path.as_ref().display()
)
})?;
let con = String::from_utf8_lossy(&con);
Ok(con.to_string())
}
#[cfg(not(any(target_os = "linux", target_os = "android")))]
pub fn lgetfilecon<P: AsRef<Path>>(path: P) -> Result<String> {
unimplemented!()
}
pub fn ensure_dir_exists<T: AsRef<Path>>(dir: T) -> Result<()> {
let result = create_dir_all(&dir);
if dir.as_ref().is_dir() && result.is_ok() {
Ok(())
} else {
bail!("{} is not a regular directory", dir.as_ref().display())
}
}
pub fn init_logger(logfile: &Path, verbose: bool) -> Result<()> {
let level = if verbose {
LevelFilter::Debug
} else if cfg!(debug_assertions) {
LevelFilter::Debug
} else {
LevelFilter::Info
};
let file = OpenOptions::new()
.create(true)
.append(true)
.open(logfile)
.with_context(|| format!("failed to open log file {logfile:?}"))?;
WriteLogger::init(level, Config::default(), file)
.context("failed to init file logger")?;
log::info!("log level: {:?}", level);
Ok(())
}
fn is_writable_tmpfs(path: &Path) -> bool {
if !path.is_dir() {
log::debug!(" {} is not a directory", path.display());
return false;
}
if let Ok(mounts) = read_to_string("/proc/mounts") {
let path_str = path.to_string_lossy();
let is_tmpfs = mounts.lines().any(|line| {
let parts: Vec<&str> = line.split_whitespace().collect();
parts.len() >= 3 && parts[1] == path_str && parts[2] == "tmpfs"
});
if !is_tmpfs {
log::debug!(" {} is not a tmpfs", path.display());
return false;
}
log::debug!(" {} is a tmpfs", path.display());
} else {
log::debug!(" failed to read /proc/mounts");
}
let test_file = path.join(format!(".mm_test_{}", std::process::id()));
let writable = write(&test_file, b"test").is_ok();
if writable {
let _ = remove_file(&test_file);
log::debug!(" {} is writable", path.display());
} else {
log::debug!(" {} is not writable", path.display());
}
writable
}
pub fn select_temp_dir() -> Result<PathBuf> {
log::debug!("searching for suitable tmpfs mount point...");
for candidate in TMPFS_CANDIDATES {
let path = Path::new(candidate);
log::debug!("checking tmpfs candidate: {}", path.display());
if is_writable_tmpfs(path) {
let temp_dir = path.join(TEMP_DIR_SUFFIX);
log::info!(
"selected tmpfs: {} -> {}",
path.display(),
temp_dir.display()
);
return Ok(temp_dir);
}
}
bail!(
"no writable tmpfs found in candidates: {}",
TMPFS_CANDIDATES.join(", ")
)
}
pub fn ensure_temp_dir(temp_dir: &Path) -> Result<()> {
if temp_dir.exists() {
log::debug!("cleaning existing temp dir: {}", temp_dir.display());
remove_dir_all(temp_dir)
.with_context(|| format!("failed to clean temp dir {temp_dir:?}"))?;
}
create_dir_all(temp_dir)
.with_context(|| format!("failed to create temp dir {temp_dir:?}"))?;
log::debug!("temp dir ready: {}", temp_dir.display());
Ok(())
}
pub fn cleanup_temp_dir(temp_dir: &Path) {
if let Err(e) = remove_dir_all(temp_dir) {
log::warn!(
"failed to clean up temp dir {}: {:#}",
temp_dir.display(),
e
);
} else {
log::debug!("cleaned up temp dir: {}", temp_dir.display());
}
}

234
src/utils.c Normal file
View file

@ -0,0 +1,234 @@
#include "utils.h"
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/xattr.h>
#include <sys/statfs.h>
#include <dirent.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
FILE *g_log_file = NULL; /* stderr */
int g_log_level = 2; /* INFO */
static void vlog(const char *lv, const char *fmt, va_list ap)
{
FILE *out = g_log_file ? g_log_file : stderr;
fprintf(out, "[%s] ", lv);
vfprintf(out, fmt, ap);
fputc('\n', out);
fflush(out);
}
void log_msg(const char *lv, const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
vlog(lv, fmt, ap);
va_end(ap);
}
int path_join(const char *base, const char *name, char *buf, size_t n)
{
if (!base) {
errno = EINVAL;
return -1;
}
if (!name || !name[0]) {
if (snprintf(buf, n, "%s", base) >= (int)n) {
errno = ENAMETOOLONG;
return -1;
}
return 0;
}
if (!base[0] || (base[0] == '/' && !base[1])) {
if (snprintf(buf, n, "/%s", name) >= (int)n) {
errno = ENAMETOOLONG;
return -1;
}
} else {
size_t len = strlen(base);
if (base[len - 1] == '/') {
if (snprintf(buf, n, "%s%s", base, name) >= (int)n) {
errno = ENAMETOOLONG;
return -1;
}
} else {
if (snprintf(buf, n, "%s/%s", base, name) >= (int)n) {
errno = ENAMETOOLONG;
return -1;
}
}
}
return 0;
}
bool path_exists(const char *p)
{
struct stat st;
return (stat(p, &st) == 0);
}
bool path_is_dir(const char *p)
{
struct stat st;
return (stat(p, &st) == 0) && S_ISDIR(st.st_mode);
}
bool path_is_symlink(const char *p)
{
struct stat st;
return (lstat(p, &st) == 0) && S_ISLNK(st.st_mode);
}
int mkdir_p(const char *dir)
{
if (!dir || !dir[0]) {
errno = EINVAL;
return -1;
}
struct stat st;
if (stat(dir, &st) == 0) {
if (S_ISDIR(st.st_mode))
return 0;
LOGE("%s is not directory", dir);
errno = ENOTDIR;
return -1;
}
char tmp[PATH_MAX];
if (strlen(dir) >= sizeof(tmp)) {
errno = ENAMETOOLONG;
return -1;
}
strcpy(tmp, dir);
char *s = strrchr(tmp, '/');
if (s && s != tmp) {
*s = 0;
if (mkdir_p(tmp) != 0)
return -1;
}
if (mkdir(dir, 0755) == 0 || errno == EEXIST)
return 0;
LOGE("mkdir %s: %s", dir, strerror(errno));
return -1;
}
static bool is_rw_tmpfs(const char *path)
{
if (!path_is_dir(path))
return false;
struct statfs s;
if (statfs(path, &s) < 0)
return false;
if ((unsigned long)s.f_type != (unsigned long)TMPFS_MAGIC)
return false;
char tmpl[PATH_MAX];
if (path_join(path, ".magic_mount_testXXXXXX", tmpl, sizeof(tmpl)) != 0)
return false;
int fd = mkstemp(tmpl);
if (fd < 0)
return false;
close(fd);
unlink(tmpl);
return true;
}
void free_string_array(char ***arr, int *count)
{
if (!arr || !*arr || !count)
return;
for (int i = 0; i < *count; ++i)
free((*arr)[i]);
free(*arr);
*arr = NULL;
*count = 0;
}
const char *select_auto_tempdir(char buf[PATH_MAX])
{
const char *tmp_list[] = { "/mnt/vendor", "/mnt", "/debug_ramdisk" };
size_t n = sizeof(tmp_list) / sizeof(tmp_list[0]);
for (size_t i = 0; i < n; ++i) {
const char *base = tmp_list[i];
if (!is_rw_tmpfs(base))
continue;
if (path_join(base, ".magic_mount", buf, PATH_MAX) != 0)
continue;
LOGI("auto tempdir selected: %s (from %s)", buf, base);
return buf;
}
LOGW("no rw tmpfs found in candidates, fallback to %s",
DEFAULT_TEMP_DIR);
return DEFAULT_TEMP_DIR;
}
int set_selinux(const char *path, const char *con)
{
if (!path || !con) {
LOGD("set_selinux called with path=%p, con=%p (skip)",
(void *)path, (void *)con);
return 0;
}
LOGD("set_selinux(%s, \"%s\")", path, con);
if (lsetxattr(path, SELINUX_XATTR, con, strlen(con), 0) < 0) {
LOGW("setcon %s: %s", path, strerror(errno));
return -1;
}
LOGD("set_selinux(%s) ok", path);
return 0;
}
int get_selinux(const char *path, char **out)
{
*out = NULL;
if (!path) {
LOGD("get_selinux called with NULL path");
errno = EINVAL;
return -1;
}
LOGD("get_selinux(%s)", path);
ssize_t sz = lgetxattr(path, SELINUX_XATTR, NULL, 0);
if (sz < 0) {
LOGW("getcon %s: %s", path, strerror(errno));
return -1;
}
char *buf = malloc((size_t)sz + 1);
if (!buf) {
errno = ENOMEM;
return -1;
}
ssize_t r = lgetxattr(path, SELINUX_XATTR, buf, (size_t)sz);
if (r < 0) {
LOGW("getcon %s (read): %s", path, strerror(errno));
free(buf);
return -1;
}
buf[r] = '\0';
*out = buf;
LOGD("get_selinux(%s) -> \"%s\"", path, buf);
return 0;
}

61
src/utils.h Normal file
View file

@ -0,0 +1,61 @@
#ifndef UTILS_H
#define UTILS_H
#include <stdio.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdarg.h>
#define SELINUX_XATTR "security.selinux"
#define DEFAULT_TEMP_DIR "/dev/.magic_mount"
#ifndef TMPFS_MAGIC
#define TMPFS_MAGIC 0x01021994
#endif
#ifndef PATH_MAX
#define PATH_MAX 4096
#endif
/* from linux/sched.h normally */
int setns(int fd, int nstype);
extern FILE *g_log_file;
extern int g_log_level; /* 0=ERROR,1=WARN,2=INFO,3=DEBUG */
void log_msg(const char *lv, const char *fmt, ...);
#define LV_ERROR 0
#define LV_WARN 1
#define LV_INFO 2
#define LV_DEBUG 3
#define LOG(lv, ...) \
do { \
if ((lv) <= g_log_level) \
log_msg((lv) == LV_ERROR ? "ERROR" : \
(lv) == LV_WARN ? "WARN" : \
(lv) == LV_INFO ? "INFO" : \
"DEBUG", \
__VA_ARGS__); \
} while (0)
#define LOGE(...) LOG(LV_ERROR, __VA_ARGS__)
#define LOGW(...) LOG(LV_WARN, __VA_ARGS__)
#define LOGI(...) LOG(LV_INFO, __VA_ARGS__)
#define LOGD(...) LOG(LV_DEBUG, __VA_ARGS__)
int path_join(const char *base, const char *name, char *buf, size_t n);
bool path_exists(const char *p);
bool path_is_dir(const char *p);
bool path_is_symlink(const char *p);
int mkdir_p(const char *dir);
const char *select_auto_tempdir(char buf[PATH_MAX]);
void free_string_array(char ***arr, int *count);
int set_selinux(const char *path, const char *con);
int get_selinux(const char *path, char **out);
#endif /* UTILS_H */

View file

@ -1,5 +0,0 @@
moduledir = "/data/adb/modules/"
mountsource = "KSU"
logfile = "/data/adb/magic_mount/mm.log"
verbose = false
partitions = []

View file

@ -6,19 +6,21 @@ ui_print "- Detecting device architecture..."
ABI=$(grep_get_prop ro.product.cpu.abi)
ui_print "- Detected ABI: $ABI"
# Select the correct binary based on architecture
# Select the correct binary based on architecture
case "$ABI" in
arm64-v8a)
ui_print "- Selected architecture: ARM64"
ARCH_BINARY="arm64-v8a/magic_mount_rs"
ARCH_BINARY="mm_arm64"
;;
armeabi-v7a)
ui_print "- Selected architecture: ARM"
ARCH_BINARY="armeabi-v7a/magic_mount_rs"
ARCH_BINARY="mm_armv7"
;;
x86_64)
ui_print "- Selected architecture: AMD64"
ARCH_BINARY="x86_64/magic_mount_rs"
ARCH_BINARY="mm_amd64"
;;
*)
abort "! Unsupported architecture: $ABI"
@ -45,9 +47,9 @@ ui_print "- Architecture-specific binary installed successfully"
mkdir -p /data/adb/magic_mount
if [ ! -f /data/adb/magic_mount/config.toml ] ; then
if [ ! -f /data/adb/magic_mount/mm.conf ] ; then
ui_print "- Add default config"
cat "$MODPATH/config.toml" > /data/adb/magic_mount/config.toml
cat "$MODPATH/mm.conf" > /data/adb/magic_mount/mm.conf
fi
ui_print "- Installation complete"

View file

@ -12,6 +12,24 @@ if [ ! -f "$BINARY" ]; then
exit 1
fi
MM_LOG_FILE=$(busybox awk -F= '
/^[[:space:]]*log_file[[:space:]]*=/ {
val=$2
sub(/#.*/, "", val)
gsub(/^[ \t"]+|[ \t"]+$/, "", val)
print val
}' /data/adb/magic_mount/mm.conf)
if [ -f "$MM_LOG_FILE" ]; then
mv "$MM_LOG_FILE" "$MM_LOG_FILE".old
fi
# Set environment variables
export MODULE_METADATA_DIR="/data/adb/modules"
log "Metadata directory: $MODULE_METADATA_DIR"
log "Executing $BINARY"
$BINARY
EXIT_CODE=$?

5
template/mm.conf Normal file
View file

@ -0,0 +1,5 @@
# Magic Mount Configuration File
module_dir=/data/adb/modules
mount_source=KSU
log_file=/data/adb/magic_mount/mm.log
debug=true

View file

@ -16,7 +16,7 @@ type ConfigState = {
};
const DEFAULT_CONFIG: ConfigState = {
moduledir: "/data/adb/modules/",
moduledir: "/data/adb/modules",
tempdir: "",
mountsource: "KSU",
logfile: "/data/adb/magic_mount/mm.log",
@ -24,8 +24,7 @@ const DEFAULT_CONFIG: ConfigState = {
partitions: [],
};
const CONFIG_PATH = "/data/adb/magic_mount/config.toml";
const LOG_BASE = "/data/adb/magic_mount";
const CONFIG_PATH = "/data/adb/magic_mount/mm.conf";
const App: React.FC = () => {
// ---------- i18n ----------
@ -54,7 +53,8 @@ const App: React.FC = () => {
const [configMessage, setConfigMessage] = useState<string | null>(null);
// ---------- log state ----------
const [logFile, setLogFile] = useState<"mm.log" | "mm.log.old">("mm.log");
type LogSelection = "current" | "old";
const [logSelection, setLogSelection] = useState<LogSelection>("current");
const [logContent, setLogContent] = useState("");
const [logLoading, setLogLoading] = useState(false);
const [logError, setLogError] = useState<string | null>(null);
@ -63,80 +63,88 @@ const App: React.FC = () => {
const [partitionInput, setPartitionInput] = useState("");
// ---------- helpers ----------
const isTrueValue = (v: string): boolean => {
const s = v.trim().toLowerCase();
return s === "1" || s === "true" || s === "yes" || s === "on";
};
const parseTOMLConfig = (text: string): ConfigState | null => {
const parseKvConfig = (text: string): ConfigState | null => {
try {
const result: ConfigState = { ...DEFAULT_CONFIG };
const lines = text.split("\n");
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) continue;
for (let line of lines) {
line = line.trim();
if (!line || line.startsWith("#")) continue;
const match = trimmed.match(/^(\w+)\s*=\s*(.+)$/);
if (!match) continue;
const eqIndex = line.indexOf("=");
if (eqIndex < 0) {
continue;
}
const [, key, value] = match;
const cleanValue = value.replace(/^["']|["']$/g, "").trim();
let key = line.slice(0, eqIndex).trim();
let value = line.slice(eqIndex + 1).trim();
if (!key || !value) continue;
switch (key) {
case "moduledir":
case "mountsource":
case "logfile":
result[key] = cleanValue;
case "module_dir":
result.moduledir = value;
break;
case "tempdir":
result.tempdir = cleanValue;
case "temp_dir":
result.tempdir = value;
break;
case "verbose":
result.verbose = cleanValue === "true";
case "mount_source":
result.mountsource = value;
break;
case "log_file":
result.logfile = value;
break;
case "debug":
result.verbose = isTrueValue(value);
break;
case "partitions":
// 解析数组格式: ["item1", "item2"] 或 []
const arrayMatch = cleanValue.match(/^\[(.*)\]$/);
if (arrayMatch) {
const items = arrayMatch[1]
.split(",")
.map((s) => s.trim().replace(/^["']|["']$/g, ""))
.filter((s) => s);
result.partitions = items;
}
result.partitions = value
.split(",")
.map((s) => s.trim())
.filter((s) => s.length > 0);
break;
default:
// unknown key: ignore
break;
}
}
return result;
} catch (e) {
console.error("Failed to parse TOML:", e);
console.error("Failed to parse config:", e);
return null;
}
};
const serializeTOMLConfig = (cfg: ConfigState): string => {
const lines: string[] = [
"# Magic Mount Configuration File",
"# Generated by Web UI",
"",
`moduledir = "${cfg.moduledir}"`,
];
const serializeKvConfig = (cfg: ConfigState): string => {
const lines: string[] = ["# Magic Mount Configuration File", "# Generated by Web UI", ""];
const moduleDir = cfg.moduledir || DEFAULT_CONFIG.moduledir;
const mountSource = cfg.mountsource || DEFAULT_CONFIG.mountsource;
const logFile = cfg.logfile || DEFAULT_CONFIG.logfile;
lines.push(`module_dir=${moduleDir}`);
// 只有非空值才写入
if (cfg.tempdir) {
lines.push(`tempdir = "${cfg.tempdir}"`);
lines.push(`temp_dir=${cfg.tempdir}`);
}
lines.push(
`mountsource = "${cfg.mountsource}"`,
`logfile = "${cfg.logfile}"`,
`verbose = ${cfg.verbose}`
);
lines.push(`mount_source=${mountSource}`);
if (logFile) {
lines.push(`log_file=${logFile}`);
}
lines.push(`debug=${cfg.verbose ? "true" : "false"}`);
// 分区数组
if (cfg.partitions.length > 0) {
const partStr = cfg.partitions.map((p) => `"${p}"`).join(", ");
lines.push(`partitions = [${partStr}]`);
} else {
lines.push("partitions = []");
lines.push(`partitions=${cfg.partitions.join(",")}`);
}
return lines.join("\n");
@ -149,39 +157,39 @@ const App: React.FC = () => {
try {
const { errno, stdout, stderr } = await exec(
`[ -f "${CONFIG_PATH}" ] && cat "${CONFIG_PATH}" || echo ""`
`[ -f "${CONFIG_PATH}" ] && cat "${CONFIG_PATH}" || echo ""`,
);
if (errno !== 0) {
console.error(stderr);
setConfig(DEFAULT_CONFIG);
setPartitionInput("");
setConfigMessage(L.configReadError);
setConfigMessage(L.config.loadError);
return;
}
if (!stdout.trim()) {
setConfig(DEFAULT_CONFIG);
setPartitionInput("");
setConfigMessage(L.configDefaultUsed);
setConfigMessage(L.config.loadDefault);
return;
}
const parsed = parseTOMLConfig(stdout);
const parsed = parseKvConfig(stdout);
if (parsed) {
setConfig(parsed);
setPartitionInput(parsed.partitions.join(", "));
setConfigMessage(L.configLoaded);
setConfigMessage(L.config.loadSuccess);
} else {
setConfig(DEFAULT_CONFIG);
setPartitionInput("");
setConfigMessage("Invalid TOML format");
setConfigMessage(L.config.loadError);
}
} catch (e) {
console.error(e);
setConfig(DEFAULT_CONFIG);
setPartitionInput("");
setConfigMessage(L.configReadError);
setConfigMessage(L.config.loadError);
} finally {
setConfigLoading(false);
}
@ -198,9 +206,9 @@ const App: React.FC = () => {
.map((s) => s.trim())
.filter((s) => s);
const configToSave = { ...config, partitions };
const configToSave: ConfigState = { ...config, partitions };
const content = serializeTOMLConfig(configToSave);
const content = serializeKvConfig(configToSave);
const escapedContent = content.replace(/'/g, "'\\''");
const shell = `
@ -212,14 +220,14 @@ const App: React.FC = () => {
if (errno !== 0) {
console.error(stderr);
setConfigMessage(L.configSaveFailed);
setConfigMessage(L.config.saveFailed);
} else {
setConfig(configToSave);
setConfigMessage(L.configSaved);
setConfigMessage(L.config.saveSuccess);
}
} catch (e) {
console.error(e);
setConfigMessage(L.configSaveError);
setConfigMessage(L.config.saveError);
} finally {
setConfigSaving(false);
}
@ -231,20 +239,22 @@ const App: React.FC = () => {
setLogError(null);
setLogContent("");
try {
const fullPath = `${LOG_BASE}/${logFile}`;
const basePath = config.logfile || DEFAULT_CONFIG.logfile;
const fullPath = logSelection === "current" ? basePath : `${basePath}.old`;
const { errno, stdout, stderr } = await exec(
`[ -f "${fullPath}" ] && cat "${fullPath}" || echo "Log file not found: ${fullPath}"`
`[ -f "${fullPath}" ] && cat "${fullPath}" || echo "Log file not found"`,
);
if (errno !== 0) {
console.error(stderr);
setLogError(`${L.logReadFailed} (errno=${errno})`);
setLogError(L.logs.readFailed + ` (errno=${errno})`);
} else {
setLogContent(stdout || "");
}
} catch (e) {
console.error(e);
setLogError(L.logReadException);
setLogError(L.logs.readException);
} finally {
setLogLoading(false);
}
@ -262,24 +272,20 @@ const App: React.FC = () => {
loadLog();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeTab, logFile]);
}, [activeTab, logSelection, config.logfile]);
// ---------- render tabs ----------
const renderConfigTab = () => (
<div className="card">
<h2>{L.config}</h2>
<h2>{L.config.title}</h2>
{configLoading && <p className="hint">Loading...</p>}
{configMessage && (
<p className={configMessage.includes("Failed") || configMessage.includes("Invalid") ? "error" : "hint"}>
{configMessage}
</p>
)}
{configMessage && <p className="hint">{configMessage}</p>}
{/* Verbose switch */}
<div className="field">
<label>Verbose Logging</label>
<label>{L.config.verboseLabel}</label>
<div className="loglevel-switch">
<button
className={!config.verbose ? "lv-btn active" : "lv-btn"}
@ -290,7 +296,7 @@ const App: React.FC = () => {
}))
}
>
Off
{L.config.verboseOff}
</button>
<button
className={config.verbose ? "lv-btn active" : "lv-btn"}
@ -301,14 +307,14 @@ const App: React.FC = () => {
}))
}
>
On
{L.config.verboseOn}
</button>
</div>
</div>
{/* Module Directory */}
<div className="field">
<label>moduledir</label>
<label>{L.config.moduleDir}</label>
<input
type="text"
value={config.moduledir}
@ -324,7 +330,7 @@ const App: React.FC = () => {
{/* Temp Directory */}
<div className="field">
<label>tempdir</label>
<label>{L.config.tempDir}</label>
<input
type="text"
value={config.tempdir}
@ -340,7 +346,7 @@ const App: React.FC = () => {
{/* Mount Source */}
<div className="field">
<label>mountsource</label>
<label>{L.config.mountSource}</label>
<input
type="text"
value={config.mountsource}
@ -356,7 +362,7 @@ const App: React.FC = () => {
{/* Log File */}
<div className="field">
<label>logfile</label>
<label>{L.config.logFile}</label>
<input
type="text"
value={config.logfile}
@ -372,64 +378,60 @@ const App: React.FC = () => {
{/* Partitions */}
<div className="field">
<label>partitions</label>
<label>{L.config.partitions}</label>
<input
type="text"
value={partitionInput}
placeholder="e.g., mi_ext,my_stock"
placeholder="mi_ext,my_stock"
onChange={(e) => setPartitionInput(e.target.value)}
/>
</div>
<div className="actions">
<button onClick={loadConfig} disabled={configLoading || configSaving}>
{L.reload}
{L.config.reload}
</button>
<button
className="primary"
onClick={saveConfig}
disabled={configSaving || configLoading}
>
{configSaving ? "Saving..." : L.saveConfig}
<button className="primary" onClick={saveConfig} disabled={configSaving || configLoading}>
{configSaving ? "Saving..." : L.config.save}
</button>
</div>
<p className="path">
Config Path: {CONFIG_PATH}
{L.config.pathLabel}: {CONFIG_PATH}
</p>
</div>
);
const renderLogTab = () => (
<div className="card">
<h2>{L.logView}</h2>
const renderLogTab = () => {
return (
<div className="card">
<h2>{L.logs.title}</h2>
<div className="field">
<label>{L.selectLog}</label>
<div className="log-select-row">
<select
value={logFile}
onChange={(e) =>
setLogFile(e.target.value as "mm.log" | "mm.log.old")
}
>
<option value="mm.log">mm.log</option>
<option value="mm.log.old">mm.log.old</option>
</select>
<div className="field">
<label>{L.logs.select}</label>
<div className="log-select-row">
<select
value={logSelection}
onChange={(e) => setLogSelection(e.target.value as LogSelection)}
>
<option value="current">{L.logs.current}</option>
<option value="old">{L.logs.old}</option>
</select>
<button className="refresh-btn" onClick={loadLog} disabled={logLoading}>
{logLoading ? "Loading..." : L.refreshLog}
</button>
<button className="refresh-btn" onClick={loadLog} disabled={logLoading}>
{logLoading ? "Loading..." : L.logs.refresh}
</button>
</div>
</div>
{logError && <p className="error">{logError}</p>}
<pre className="log-view">
{logLoading && !logContent ? "Loading..." : logContent || L.logs.empty}
</pre>
</div>
{logError && <p className="error">{logError}</p>}
<pre className="log-view">
{logLoading && !logContent ? "Loading..." : logContent || L.emptyLog}
</pre>
</div>
);
);
};
return (
<div className="app-root">
@ -437,19 +439,13 @@ const App: React.FC = () => {
<div className="top-bar">
<div className="title">Magic Mount</div>
<select
className="lang-select"
value={lang}
onChange={handleLangChange}
>
<select className="lang-select" value={lang} onChange={handleLangChange}>
<option value="en">English</option>
<option value="zh">中文</option>
</select>
</div>
<div className="app-main">
{activeTab === "config" ? renderConfigTab() : renderLogTab()}
</div>
<div className="app-main">{activeTab === "config" ? renderConfigTab() : renderLogTab()}</div>
{/* bottom tabs */}
<div className="bottom-bar">
@ -457,13 +453,13 @@ const App: React.FC = () => {
className={activeTab === "config" ? "tab-btn active" : "tab-btn"}
onClick={() => setActiveTab("config")}
>
{L.configTab}
{L.tabs.config}
</button>
<button
className={activeTab === "log" ? "tab-btn active" : "tab-btn"}
onClick={() => setActiveTab("log")}
>
{L.logTab}
{L.tabs.logs}
</button>
</div>
</div>

View file

@ -1,5 +1,5 @@
/* For KernelSU WebUI */
@import url('https://mui.kernelsu.org/internal/insets.css');
@import url("https://mui.kernelsu.org/internal/insets.css");
/* ------- Nord Base Colors ------- */
:root {
@ -35,8 +35,8 @@ body {
margin: 0;
background: var(--nord0);
color: var(--nord6);
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", Inter,
"Segoe UI", Arial, sans-serif;
font-family:
-apple-system, BlinkMacSystemFont, "SF Pro Text", Inter, "Segoe UI", Arial, sans-serif;
}
.app-root {
@ -279,5 +279,5 @@ button:disabled {
font-size: 16px;
font-weight: 600;
color: var(--nord6);
margin-right: auto;
margin-right: auto;
}

View file

@ -1,48 +1,77 @@
{
"en": {
"config": "Configuration",
"reload": "Reload",
"saveConfig": "Save",
"loglevel": "Log Level",
"debug": "debug",
"info": "info",
"logView": "Log Viewer",
"selectLog": "Log File",
"refreshLog": "Refresh Log",
"emptyLog": "(empty)",
"configTab": "Config",
"logTab": "Logs",
"configLoaded": "Config loaded from file.",
"configDefaultUsed": "No config file found, using defaults.",
"configReadError": "Failed to read config, using defaults.",
"configSaved": "Config saved.",
"configSaveFailed": "Failed to save config. Check logs or permissions.",
"configSaveError": "Error occurred while saving config.",
"logReadFailed": "Failed to read log",
"logReadException": "Exception occurred while reading log.",
"configPathLabel": "Config file path"
"tabs": {
"config": "Config",
"logs": "Logs"
},
"config": {
"title": "Configuration",
"reload": "Reload",
"save": "Save",
"loadSuccess": "Configuration loaded.",
"loadDefault": "No config file found, using defaults.",
"loadError": "Failed to read config, using default values.",
"saveSuccess": "Configuration saved.",
"saveFailed": "Failed to save configuration.",
"saveError": "An error occurred while saving.",
"pathLabel": "Config file path",
"moduleDir": "Module Directory",
"tempDir": "Temporary Directory",
"mountSource": "Mount Source",
"logFile": "Log File",
"partitions": "Partitions",
"verboseLabel": "Log Level",
"verboseOff": "Info",
"verboseOn": "Debug"
},
"logs": {
"title": "Log Viewer",
"select": "Log File",
"refresh": "Refresh",
"empty": "(empty)",
"readFailed": "Failed to read log file.",
"readException": "An exception occurred while reading log.",
"current": "Current Log",
"old": "Old Log"
}
},
"zh": {
"config": "配置",
"reload": "重载",
"saveConfig": "保存",
"loglevel": "日志级别",
"debug": "调试",
"info": "信息",
"logView": "日志查看",
"selectLog": "日志文件",
"refreshLog": "刷新日志",
"emptyLog": "(空日志)",
"configTab": "配置",
"logTab": "日志",
"configLoaded": "已从配置文件加载。",
"configDefaultUsed": "未找到配置文件,已使用默认配置。",
"configReadError": "读取配置失败,已使用默认配置。",
"configSaved": "配置已保存",
"configSaveFailed": "保存失败,请查看日志或检查权限。",
"configSaveError": "保存配置时发生错误。",
"logReadFailed": "读取日志失败",
"logReadException": "读取日志时发生异常。",
"configPathLabel": "配置文件路径"
"tabs": {
"config": "配置",
"logs": "日志"
},
"config": {
"title": "配置",
"reload": "重载",
"save": "保存",
"loadSuccess": "已从配置文件加载。",
"loadDefault": "未找到配置文件,已使用默认配置。",
"loadError": "读取配置失败,已使用默认配置。",
"saveSuccess": "配置已保存。",
"saveFailed": "保存失败,请检查权限或日志。",
"saveError": "保存配置时发生错误。",
"pathLabel": "配置文件路径",
"moduleDir": "模块目录",
"tempDir": "临时目录",
"mountSource": "挂载源",
"logFile": "日志文件",
"partitions": "分区列表",
"verboseLabel": "日志级别",
"verboseOff": "信息",
"verboseOn": "调试"
},
"logs": {
"title": "日志查看",
"select": "日志文件",
"refresh": "刷新",
"empty": "(空日志)",
"readFailed": "读取日志失败。",
"readException": "读取日志时发生异常。",
"current": "当前日志",
"old": "旧日志"
}
}
}

Some files were not shown because too many files have changed in this diff Show more