From 0e8a692177ae87fdde68ef48c7841eb6037f155b Mon Sep 17 00:00:00 2001 From: 7a72 Date: Fri, 21 Nov 2025 20:25:54 +0800 Subject: [PATCH] feat: switch to c --- build.sh | 139 +------ src/.gitignore | 4 +- src/Cargo.toml | 21 -- src/Makefile | 87 +++++ src/magic_mount.c | 826 +++++++++++++++++++++++++++++++++++++++++ src/magic_mount.h | 39 ++ src/main.c | 311 ++++++++++++++++ src/src/config.rs | 116 ------ src/src/magic_mount.rs | 489 ------------------------ src/src/main.rs | 179 --------- src/src/utils.rs | 159 -------- src/utils.c | 234 ++++++++++++ src/utils.h | 61 +++ template/config.toml | 5 - template/customize.sh | 12 +- template/metamount.sh | 18 + template/mm.conf | 5 + webui/src/App.tsx | 246 ++++++------ webui/src/index.css | 8 +- webui/src/locate.json | 113 +++--- webui/src/main.tsx | 12 +- 21 files changed, 1805 insertions(+), 1279 deletions(-) delete mode 100644 src/Cargo.toml create mode 100644 src/Makefile create mode 100644 src/magic_mount.c create mode 100644 src/magic_mount.h create mode 100644 src/main.c delete mode 100644 src/src/config.rs delete mode 100644 src/src/magic_mount.rs delete mode 100644 src/src/main.rs delete mode 100644 src/src/utils.rs create mode 100644 src/utils.c create mode 100644 src/utils.h delete mode 100644 template/config.toml create mode 100644 template/mm.conf diff --git a/build.sh b/build.sh index 820affe..46da5e0 100644 --- a/build.sh +++ b/build.sh @@ -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 ] [--update-json-url ]" + echo "Usage: $0 [--release] [--debug]" echo " --release Build release version" echo " --debug Build debug version" - echo " --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 diff --git a/src/.gitignore b/src/.gitignore index a6a11a6..0e9062d 100644 --- a/src/.gitignore +++ b/src/.gitignore @@ -1,4 +1,2 @@ -/target -/out -Cargo.lock +/bin *.log \ No newline at end of file diff --git a/src/Cargo.toml b/src/Cargo.toml deleted file mode 100644 index 6538d75..0000000 --- a/src/Cargo.toml +++ /dev/null @@ -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 diff --git a/src/Makefile b/src/Makefile new file mode 100644 index 0000000..df31864 --- /dev/null +++ b/src/Makefile @@ -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) \ No newline at end of file diff --git a/src/magic_mount.c b/src/magic_mount.c new file mode 100644 index 0000000..1c10207 --- /dev/null +++ b/src/magic_mount.c @@ -0,0 +1,826 @@ +#include "magic_mount.h" +#include "utils.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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; +} diff --git a/src/magic_mount.h b/src/magic_mount.h new file mode 100644 index 0000000..a3e8e69 --- /dev/null +++ b/src/magic_mount.h @@ -0,0 +1,39 @@ +#ifndef MAGIC_MOUNT_H +#define MAGIC_MOUNT_H + +#include + +#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 */ diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..5eaf430 --- /dev/null +++ b/src/main.c @@ -0,0 +1,311 @@ +#include "magic_mount.h" +#include "utils.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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; +} diff --git a/src/src/config.rs b/src/src/config.rs deleted file mode 100644 index 6e98696..0000000 --- a/src/src/config.rs +++ /dev/null @@ -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, - - #[serde(default = "default_mountsource")] - pub mountsource: String, - - #[serde(default = "default_logfile")] - pub logfile: PathBuf, - - #[serde(default)] - pub verbose: bool, - - #[serde(default)] - pub partitions: Vec, -} - -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>(path: P) -> Result { - 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::from_file(CONFIG_FILE_DEFAULT).ok() - } - - pub fn save_to_file>(&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, - tempdir: Option, - mountsource: Option, - logfile: Option, - verbose: bool, - partitions: Vec, - ) { - 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; - } - } -} diff --git a/src/src/magic_mount.rs b/src/src/magic_mount.rs deleted file mode 100644 index 70c0326..0000000 --- a/src/src/magic_mount.rs +++ /dev/null @@ -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 { - 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, - // the module that owned this node - module_path: Option, - replace: bool, - skip: bool, -} - -impl Node { - fn collect_module_files>(&mut self, module_dir: T) -> Result { - 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(name: T) -> Self { - Node { - name: name.to_string(), - file_type: Directory, - children: Default::default(), - module_path: None, - replace: false, - skip: false, - } - } - - fn new_module(name: T, entry: &DirEntry) -> Option { - 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> { - 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, Dst: AsRef>(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, WP: AsRef>( - 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, WP: AsRef>( - path: P, - work_dir_path: WP, - current: Node, - has_tmpfs: bool, -) -> Result<()> { - let mut current = current; - let path = path.as_ref().join(¤t.name); - let work_dir_path = work_dir_path.as_ref().join(¤t.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) = ¤t.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) = ¤t.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) = ¤t.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>( - 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(()) - } -} diff --git a/src/src/main.rs b/src/src/main.rs deleted file mode 100644 index afaada1..0000000 --- a/src/src/main.rs +++ /dev/null @@ -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, - - /// Module directory path - #[arg(short = 'm', long = "moduledir")] - moduledir: Option, - - /// Temporary directory path (auto-selected if not specified) - #[arg(short = 't', long = "tempdir")] - tempdir: Option, - - /// Mount source name - #[arg(short = 's', long = "mountsource")] - mountsource: Option, - - /// Log file path - #[arg(short = 'l', long = "logfile")] - logfile: Option, - - /// 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, - - #[command(subcommand)] - command: Option, -} - -#[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 { - // 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(()) -} diff --git a/src/src/utils.rs b/src/src/utils.rs deleted file mode 100644 index 2403880..0000000 --- a/src/src/utils.rs +++ /dev/null @@ -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>(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>(path: P) -> Result { - 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>(path: P) -> Result { - unimplemented!() -} - -pub fn ensure_dir_exists>(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 { - 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()); - } -} diff --git a/src/utils.c b/src/utils.c new file mode 100644 index 0000000..33511d6 --- /dev/null +++ b/src/utils.c @@ -0,0 +1,234 @@ +#include "utils.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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; +} diff --git a/src/utils.h b/src/utils.h new file mode 100644 index 0000000..2f139fd --- /dev/null +++ b/src/utils.h @@ -0,0 +1,61 @@ +#ifndef UTILS_H +#define UTILS_H + +#include +#include +#include +#include + +#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 */ diff --git a/template/config.toml b/template/config.toml deleted file mode 100644 index 1add33f..0000000 --- a/template/config.toml +++ /dev/null @@ -1,5 +0,0 @@ -moduledir = "/data/adb/modules/" -mountsource = "KSU" -logfile = "/data/adb/magic_mount/mm.log" -verbose = false -partitions = [] diff --git a/template/customize.sh b/template/customize.sh index 3257b0c..a03f0c1 100644 --- a/template/customize.sh +++ b/template/customize.sh @@ -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" diff --git a/template/metamount.sh b/template/metamount.sh index bc94769..c9e1f6b 100644 --- a/template/metamount.sh +++ b/template/metamount.sh @@ -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=$? diff --git a/template/mm.conf b/template/mm.conf new file mode 100644 index 0000000..fa4cac4 --- /dev/null +++ b/template/mm.conf @@ -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 diff --git a/webui/src/App.tsx b/webui/src/App.tsx index 14ec45d..5b555a2 100644 --- a/webui/src/App.tsx +++ b/webui/src/App.tsx @@ -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(null); // ---------- log state ---------- - const [logFile, setLogFile] = useState<"mm.log" | "mm.log.old">("mm.log"); + type LogSelection = "current" | "old"; + const [logSelection, setLogSelection] = useState("current"); const [logContent, setLogContent] = useState(""); const [logLoading, setLogLoading] = useState(false); const [logError, setLogError] = useState(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 = () => (
-

{L.config}

+

{L.config.title}

{configLoading &&

Loading...

} - {configMessage && ( -

- {configMessage} -

- )} + {configMessage &&

{configMessage}

} {/* Verbose switch */}
- +
{/* Module Directory */}
- + { {/* Temp Directory */}
- + { {/* Mount Source */}
- + { {/* Log File */}
- + { {/* Partitions */}
- + setPartitionInput(e.target.value)} />
-

- Config Path: {CONFIG_PATH} + {L.config.pathLabel}: {CONFIG_PATH}

); - const renderLogTab = () => ( -
-

{L.logView}

+ const renderLogTab = () => { + return ( +
+

{L.logs.title}

-
- -
- +
+ +
+ - + +
+ + {logError &&

{logError}

} + +
+          {logLoading && !logContent ? "Loading..." : logContent || L.logs.empty}
+        
- - {logError &&

{logError}

} - -
-        {logLoading && !logContent ? "Loading..." : logContent || L.emptyLog}
-      
-
- ); + ); + }; return (
@@ -437,19 +439,13 @@ const App: React.FC = () => {
Magic Mount
-
-
- {activeTab === "config" ? renderConfigTab() : renderLogTab()} -
+
{activeTab === "config" ? renderConfigTab() : renderLogTab()}
{/* bottom tabs */}
@@ -457,13 +453,13 @@ const App: React.FC = () => { className={activeTab === "config" ? "tab-btn active" : "tab-btn"} onClick={() => setActiveTab("config")} > - {L.configTab} + {L.tabs.config}
diff --git a/webui/src/index.css b/webui/src/index.css index 4bd2bea..06ba83c 100644 --- a/webui/src/index.css +++ b/webui/src/index.css @@ -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; } diff --git a/webui/src/locate.json b/webui/src/locate.json index f1fe9c0..252f8ab 100644 --- a/webui/src/locate.json +++ b/webui/src/locate.json @@ -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": "旧日志" + } } } diff --git a/webui/src/main.tsx b/webui/src/main.tsx index bef5202..eff7ccc 100644 --- a/webui/src/main.tsx +++ b/webui/src/main.tsx @@ -1,10 +1,10 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import './index.css' -import App from './App.tsx' +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import "./index.css"; +import App from "./App.tsx"; -createRoot(document.getElementById('root')!).render( +createRoot(document.getElementById("root")!).render( , -) +);