diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fafe33a..ab3d2ca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: - run: brew install clang-format - run: make - run: make check - - run: make unit-test + - run: make test-unit actionlint: runs-on: ubuntu-latest diff --git a/Makefile b/Makefile index fbbbe8b..fd9de8b 100644 --- a/Makefile +++ b/Makefile @@ -1,38 +1,263 @@ -TERMUX_BASE_DIR ?= /data/data/com.termux/files -CFLAGS += -Wall -Wextra -Werror -Wshadow -O2 -C_SOURCE := src/termux-exec.c src/exec-variants.c +export TERMUX_EXEC_PKG__VERSION ?= 1.0 +export TERMUX_EXEC_PKG__ARCH +export TERMUX_EXEC__INSTALL_PREFIX + +export TERMUX__NAME ?= Termux# Default value: `Termux` +export TERMUX__LNAME ?= termux# Default value: `termux` + +export TERMUX_APP__PACKAGE_NAME ?= com.termux# Default value: `com.termux` +export TERMUX_APP__DATA_DIR ?= /data/data/$(TERMUX_APP__PACKAGE_NAME)# Default value: `/data/data/com.termux` + +export TERMUX__ROOTFS ?= $(TERMUX_APP__DATA_DIR)/files# Default value: `/data/data/com.termux/files` +export TERMUX__PREFIX ?= $(TERMUX__ROOTFS)/usr# Default value: `/data/data/com.termux/files/usr` +export TERMUX__PREFIX__BIN_DIR ?= $(TERMUX__PREFIX)/bin# Default value: `/data/data/com.termux/files/usr/bin` +export TERMUX__PREFIX__TMP_DIR ?= $(TERMUX__PREFIX)/tmp# Default value: `/data/data/com.termux/files/usr/tmp` + +export TERMUX_ENV__S_ROOT ?= TERMUX_# Default value: `TERMUX_` +export TERMUX_ENV__SS_TERMUX ?= _# Default value: `_` +export TERMUX_ENV__S_TERMUX ?= $(TERMUX_ENV__S_ROOT)$(TERMUX_ENV__SS_TERMUX)# Default value: `TERMUX__` +export TERMUX_ENV__SS_TERMUX_APP ?= APP__# Default value: `APP__` +export TERMUX_ENV__S_TERMUX_APP ?= $(TERMUX_ENV__S_ROOT)$(TERMUX_ENV__SS_TERMUX_APP)# Default value: `TERMUX_APP__` +export TERMUX_ENV__SS_TERMUX_API_APP ?= API_APP__# Default value: `API_APP__` +export TERMUX_ENV__S_TERMUX_API_APP ?= $(TERMUX_ENV__S_ROOT)$(TERMUX_ENV__SS_TERMUX_API_APP)# Default value: `TERMUX_API_APP__` +export TERMUX_ENV__SS_TERMUX_ROOTFS ?= ROOTFS__# Default value: `ROOTFS__` +export TERMUX_ENV__S_TERMUX_ROOTFS ?= $(TERMUX_ENV__S_ROOT)$(TERMUX_ENV__SS_TERMUX_ROOTFS)# Default value: `TERMUX_ROOTFS__` +export TERMUX_ENV__SS_TERMUX_CORE ?= CORE__# Default value: `CORE__` +export TERMUX_ENV__S_TERMUX_CORE ?= $(TERMUX_ENV__S_ROOT)$(TERMUX_ENV__SS_TERMUX_CORE)# Default value: `TERMUX_CORE__` +export TERMUX_ENV__SS_TERMUX_CORE__TESTS ?= CORE__TESTS__# Default value: `CORE__TESTS__` +export TERMUX_ENV__S_TERMUX_CORE__TESTS ?= $(TERMUX_ENV__S_ROOT)$(TERMUX_ENV__SS_TERMUX_CORE__TESTS)# Default value: `TERMUX_CORE__TESTS__` +export TERMUX_ENV__SS_TERMUX_EXEC ?= EXEC__# Default value: `EXEC__` +export TERMUX_ENV__S_TERMUX_EXEC ?= $(TERMUX_ENV__S_ROOT)$(TERMUX_ENV__SS_TERMUX_EXEC)# Default value: `TERMUX_EXEC__` +export TERMUX_ENV__SS_TERMUX_EXEC__TESTS ?= EXEC__TESTS__# Default value: `EXEC__TESTS__` +export TERMUX_ENV__S_TERMUX_EXEC__TESTS ?= $(TERMUX_ENV__S_ROOT)$(TERMUX_ENV__SS_TERMUX_EXEC__TESTS)# Default value: `TERMUX_EXEC__TESTS__` + +export TERMUX_APP__NAMESPACE ?= $(TERMUX_APP__PACKAGE_NAME)# Default value: `com.termux` +export TERMUX_APP__SHELL_ACTIVITY__COMPONENT_NAME ?= $(TERMUX_APP__PACKAGE_NAME)/$(TERMUX_APP__NAMESPACE).app.TermuxActivity# Default value: `com.termux/com.termux.app.TermuxActivity` +export TERMUX_APP__SHELL_SERVICE__COMPONENT_NAME ?= $(TERMUX_APP__PACKAGE_NAME)/$(TERMUX_APP__NAMESPACE).app.TermuxService# Default value: `com.termux/com.termux.app.TermuxService` + +TERMUX_EXEC__TESTS__API_LEVEL ?= + + +# If architecture not set, find it for the compiler based on which +# predefined architecture macro is defined. The `shell` function +# replaces newlines with a space and a literal space cannot be entered +# in a makefile as its used as a splitter, hence $(SPACE) variable is +# created and used for matching. +ifeq ($(TERMUX_EXEC_PKG__ARCH),) + export PREDEFINED_MACROS := $(shell $(CC) -x c /dev/null -dM -E) + EMPTY := + SPACE := $(EMPTY) $(EMPTY) + ifneq (,$(findstring $(SPACE)#define __i686__ 1$(SPACE),$(SPACE)$(PREDEFINED_MACROS)$(SPACE))) + TERMUX_EXEC_PKG__ARCH := i686 + else ifneq (,$(findstring $(SPACE)#define __x86_64__ 1$(SPACE),$(SPACE)$(PREDEFINED_MACROS)$(SPACE))) + TERMUX_EXEC_PKG__ARCH := x86_64 + else ifneq (,$(findstring $(SPACE)#define __aarch64__ 1$(SPACE),$(SPACE)$(PREDEFINED_MACROS)$(SPACE))) + TERMUX_EXEC_PKG__ARCH := aarch64 + else ifneq (,$(findstring $(SPACE)#define __arm__ 1$(SPACE),$(SPACE)$(PREDEFINED_MACROS)$(SPACE))) + TERMUX_EXEC_PKG__ARCH := arm + else + $(error Unsupported package arch) + endif +endif + + +ifeq ($(DESTDIR)$(PREFIX),) + TERMUX_EXEC__INSTALL_PREFIX := $(TERMUX__PREFIX) +else + TERMUX_EXEC__INSTALL_PREFIX := $(DESTDIR)$(PREFIX) +endif + + + +BUILD_DIR := build +PREFIX_BUILD_DIR := $(BUILD_DIR)/usr +LIB_BUILD_DIR := $(PREFIX_BUILD_DIR)/lib +TESTS_BUILD_DIR := $(PREFIX_BUILD_DIR)/libexec/installed-tests/termux-exec + + +CFLAGS += -Wall -Wextra -Werror -Wshadow -O2 -D_FORTIFY_SOURCE=2 -fstack-protector-strong +FSANTIZE_FLAGS += -fsanitize=address -fsanitize-recover=address -fno-omit-frame-pointer CLANG_FORMAT := clang-format --sort-includes --style="{ColumnLimit: 120}" $(C_SOURCE) CLANG_TIDY ?= clang-tidy -libtermux-exec.so: $(C_SOURCE) - $(CC) $(CFLAGS) $(LDFLAGS) $(C_SOURCE) -DTERMUX_PREFIX=\"$(TERMUX_PREFIX)\" -DTERMUX_BASE_DIR=\"$(TERMUX_BASE_DIR)\" -shared -fPIC -o libtermux-exec.so +C_SOURCE := \ + src/termux-exec-init.c \ + src/data/assert_utils.c \ + src/data/data_utils.c \ + src/exec/exec.c \ + src/exec/exec_variants.c \ + src/file/file_utils.c \ + src/logger/logger.c \ + src/logger/file_logger_impl.c \ + src/logger/standard_logger_impl.c \ + src/os/safe_strerror.c \ + src/os/selinux_utils.c \ + src/termux/termux_env.c \ + src/termux/termux_files.c + +TERMUX_CONSTANTS_MACRO_FLAGS := \ + -DTERMUX_EXEC_PKG__VERSION=\"$(TERMUX_EXEC_PKG__VERSION)\" \ + -DTERMUX__NAME=\"$(TERMUX__NAME)\" \ + -DTERMUX__LNAME=\"$(TERMUX__LNAME)\" \ + -DTERMUX_APP__DATA_DIR=\"$(TERMUX_APP__DATA_DIR)\" \ + -DTERMUX__ROOTFS=\"$(TERMUX__ROOTFS)\" \ + -DTERMUX__PREFIX=\"$(TERMUX__PREFIX)\" \ + -DTERMUX__PREFIX__BIN_DIR=\"$(TERMUX__PREFIX__BIN_DIR)\" \ + -DTERMUX__PREFIX__TMP_DIR=\"$(TERMUX__PREFIX__TMP_DIR)\" \ + -DTERMUX_ENV__S_TERMUX=\"$(TERMUX_ENV__S_TERMUX)\" \ + -DTERMUX_ENV__S_TERMUX_APP=\"$(TERMUX_ENV__S_TERMUX_APP)\" \ + -DTERMUX_ENV__S_TERMUX_ROOTFS=\"$(TERMUX_ENV__S_TERMUX_ROOTFS)\" \ + -DTERMUX_ENV__S_TERMUX_EXEC=\"$(TERMUX_ENV__S_TERMUX_EXEC)\" \ + -DTERMUX_ENV__S_TERMUX_EXEC__TESTS=\"$(TERMUX_ENV__S_TERMUX_EXEC__TESTS)\" + +TERMUX_CONSTANTS_SED_ARGS := \ + -e "s%[@]TERMUX_EXEC_PKG__VERSION[@]%$(TERMUX_EXEC_PKG__VERSION)%g" \ + -e "s%[@]TERMUX_EXEC_PKG__ARCH[@]%$(TERMUX_EXEC_PKG__ARCH)%g" \ + -e "s%[@]TERMUX__LNAME[@]%$(TERMUX__LNAME)%g" \ + -e "s%[@]TERMUX_APP__PACKAGE_NAME[@]%$(TERMUX_APP__PACKAGE_NAME)%g" \ + -e "s%[@]TERMUX__PREFIX[@]%$(TERMUX__PREFIX)%g" \ + -e "s%[@]TERMUX_ENV__S_TERMUX[@]%$(TERMUX_ENV__S_TERMUX)%g" \ + -e "s%[@]TERMUX_ENV__S_TERMUX_APP[@]%$(TERMUX_ENV__S_TERMUX_APP)%g" \ + -e "s%[@]TERMUX_ENV__S_TERMUX_API_APP[@]%$(TERMUX_ENV__S_TERMUX_API_APP)%g" \ + -e "s%[@]TERMUX_ENV__S_TERMUX_ROOTFS[@]%$(TERMUX_ENV__S_TERMUX_ROOTFS)%g" \ + -e "s%[@]TERMUX_ENV__S_TERMUX_CORE[@]%$(TERMUX_ENV__S_TERMUX_CORE)%g" \ + -e "s%[@]TERMUX_ENV__S_TERMUX_CORE__TESTS[@]%$(TERMUX_ENV__S_TERMUX_CORE__TESTS)%g" \ + -e "s%[@]TERMUX_ENV__S_TERMUX_EXEC__TESTS[@]%$(TERMUX_ENV__S_TERMUX_EXEC__TESTS)%g" \ + -e "s%[@]TERMUX_APP__NAMESPACE[@]%$(TERMUX_APP__NAMESPACE)%g" \ + -e "s%[@]TERMUX_APP__SHELL_ACTIVITY__COMPONENT_NAME[@]%$(TERMUX_APP__SHELL_ACTIVITY__COMPONENT_NAME)%g" \ + -e "s%[@]TERMUX_APP__SHELL_SERVICE__COMPONENT_NAME[@]%$(TERMUX_APP__SHELL_SERVICE__COMPONENT_NAME)%g" + +define replace-termux-constants + sed $(TERMUX_CONSTANTS_SED_ARGS) "$1.in" > "$2/$$(basename "$1" | sed "s/\.in$$//")" +endef + + + +all: | pre-build build-runtime-binary-tests + @echo "Building lib/libtermux-exec.so" + @mkdir -p $(LIB_BUILD_DIR) + $(CC) $(CFLAGS) $(LDFLAGS) src/termux-exec.c $(C_SOURCE) -shared -fPIC \ + -fvisibility=hidden \ + $(TERMUX_CONSTANTS_MACRO_FLAGS) \ + -o $(LIB_BUILD_DIR)/libtermux-exec.so + + + @mkdir -p $(TESTS_BUILD_DIR) + + @echo "Building tests/termux-exec-tests" + $(call replace-termux-constants,tests/termux-exec-tests,$(TESTS_BUILD_DIR)) + chmod u+x $(TESTS_BUILD_DIR)/termux-exec-tests + + @echo "Building tests/runtime-script-tests" + $(call replace-termux-constants,tests/runtime-script-tests,$(TESTS_BUILD_DIR)) + chmod u+x $(TESTS_BUILD_DIR)/runtime-script-tests + + + @echo "Building tests/unit-tests" + @mkdir -p $(TESTS_BUILD_DIR) + $(CC) $(CFLAGS) $(LDFLAGS) -g tests/unit-tests.c $(C_SOURCE) \ + $(FSANTIZE_FLAGS) \ + $(TERMUX_CONSTANTS_MACRO_FLAGS) \ + -DTERMUX_EXEC__RUNNING_TESTS=1 \ + -o $(TESTS_BUILD_DIR)/unit-tests-fsanitize + $(CC) $(CFLAGS) $(LDFLAGS) -g tests/unit-tests.c $(C_SOURCE) \ + $(TERMUX_CONSTANTS_MACRO_FLAGS) \ + -DTERMUX_EXEC__RUNNING_TESTS=1 \ + -o $(TESTS_BUILD_DIR)/unit-tests-nofsanitize + + + @mkdir -p $(TESTS_BUILD_DIR)/files + + + @echo "Building tests/files/exec/*" + @mkdir -p $(TESTS_BUILD_DIR)/files/exec + find tests/files/exec -maxdepth 1 -name '*.c' -exec sh -c '$(CC) $(CFLAGS) $(LDFLAGS) "$$1" -o $(TESTS_BUILD_DIR)/files/exec/"$$(basename "$$1" | sed "s/\.c$$//")"' sh "{}" \; + find tests/files/exec -maxdepth 1 \( -name '*.sh' -o -name '*.sym' \) -exec cp -a "{}" $(TESTS_BUILD_DIR)/files/exec/ \; + find tests/files/exec -maxdepth 1 -type f -name "*.in" -exec sh -c \ + 'sed $(TERMUX_CONSTANTS_SED_ARGS) "$$1" > $(TESTS_BUILD_DIR)/files/exec/"$$(basename "$$1" | sed "s/\.in$$//")"' sh "{}" \; + find $(TESTS_BUILD_DIR)/files/exec -maxdepth 1 -name '*.sh' -exec chmod u+x "{}" \; + + + @echo "Building termux-exec-package.json" + $(call replace-termux-constants,termux-exec-package.json,$(BUILD_DIR)) + + @echo "Build termux-exec-package successful" + +build-runtime-binary-tests: + @echo "Building tests/runtime-binary-tests$(TERMUX_EXEC__TESTS__API_LEVEL)" + @mkdir -p $(TESTS_BUILD_DIR) + $(CC) $(CFLAGS) $(LDFLAGS) -g tests/runtime-binary-tests.c $(C_SOURCE) \ + $(FSANTIZE_FLAGS) \ + $(TERMUX_CONSTANTS_MACRO_FLAGS) \ + -o $(TESTS_BUILD_DIR)/runtime-binary-tests-fsanitize$(TERMUX_EXEC__TESTS__API_LEVEL) + $(CC) $(CFLAGS) $(LDFLAGS) -g tests/runtime-binary-tests.c $(C_SOURCE) \ + $(TERMUX_CONSTANTS_MACRO_FLAGS) \ + -o $(TESTS_BUILD_DIR)/runtime-binary-tests-nofsanitize$(TERMUX_EXEC__TESTS__API_LEVEL) + + + +pre-build: | clean + @echo "Building termux-exec-package" + @mkdir -p $(BUILD_DIR) clean: - rm -f libtermux-exec.so tests/*-actual test-binary + rm -rf $(BUILD_DIR) + +install: + @echo "Installing in $(TERMUX_EXEC__INSTALL_PREFIX)" + + install -d $(TERMUX_EXEC__INSTALL_PREFIX)/lib + install $(LIB_BUILD_DIR)/libtermux-exec.so $(TERMUX_EXEC__INSTALL_PREFIX)/lib/libtermux-exec.so + -install: libtermux-exec.so - install libtermux-exec.so $(DESTDIR)$(PREFIX)/lib/libtermux-exec.so + install -d $(TERMUX_EXEC__INSTALL_PREFIX)/libexec/installed-tests/termux-exec + find $(TESTS_BUILD_DIR) -maxdepth 1 -type f -exec install "{}" -t $(TERMUX_EXEC__INSTALL_PREFIX)/libexec/installed-tests/termux-exec/ \; + + + install -d $(TERMUX_EXEC__INSTALL_PREFIX)/libexec/installed-tests/termux-exec/files + install -d $(TERMUX_EXEC__INSTALL_PREFIX)/libexec/installed-tests/termux-exec/files/exec + find $(TESTS_BUILD_DIR)/files/exec -maxdepth 1 \( -type f -o -type l \) -exec cp -a "{}" $(TERMUX_EXEC__INSTALL_PREFIX)/libexec/installed-tests/termux-exec/files/exec/ \; uninstall: - rm -f $(DESTDIR)$(PREFIX)/lib/libtermux-exec.so + @echo "Uninstalling from $(TERMUX_EXEC__INSTALL_PREFIX)" -on-device-tests: libtermux-exec.so - @LD_PRELOAD=${CURDIR}/libtermux-exec.so ./run-tests.sh + rm -f $(TERMUX_EXEC__INSTALL_PREFIX)/lib/libtermux-exec.so + rm -rf $(TERMUX_EXEC__INSTALL_PREFIX)/libexec/installed-tests/termux-exec -format: - $(CLANG_FORMAT) -i $(C_SOURCE) -check: - $(CLANG_FORMAT) --dry-run $(C_SOURCE) - $(CLANG_TIDY) -warnings-as-errors='*' $(C_SOURCE) -- -DTERMUX_BASE_DIR=\"$(TERMUX_BASE_DIR)\" -test-binary: $(C_SOURCE) - $(CC) $(CFLAGS) $(LDFLAGS) $(C_SOURCE) -g -fsanitize=address -fno-omit-frame-pointer -DUNIT_TEST=1 -DTERMUX_BASE_DIR=\"$(TERMUX_BASE_DIR)\" -o test-binary +deb: all + termux-create-package $(BUILD_DIR)/termux-exec-package.json + + + +test: all + $(MAKE) TERMUX_EXEC__INSTALL_PREFIX=install install + + @echo "Running tests" + install/libexec/installed-tests/termux-exec/termux-exec-tests --ld-preload="install/lib/libtermux-exec.so" --tests-path="install/libexec/installed-tests/termux-exec" -vvv all + +test-unit: all + $(MAKE) TERMUX_EXEC__INSTALL_PREFIX=install install + + @echo "Running unit tests" + bash install/libexec/installed-tests/termux-exec/termux-exec-tests --tests-path="install/libexec/installed-tests/termux-exec" -vvv unit + +test-runtime: all + $(MAKE) TERMUX_EXEC__INSTALL_PREFIX=install install + + @echo "Running runtime tests" + install/libexec/installed-tests/termux-exec/termux-exec-tests --ld-preload="install/lib/libtermux-exec.so" --tests-path="install/libexec/installed-tests/termux-exec" -vvv runtime + + + +format: + $(CLANG_FORMAT) -i src/termux-exec.c $(C_SOURCE) +check: + $(CLANG_FORMAT) --dry-run src/termux-exec.c $(C_SOURCE) + $(CLANG_TIDY) -warnings-as-errors='*' src/termux-exec.c $(C_SOURCE) -- \ + $(TERMUX_CONSTANTS_MACRO_FLAGS) -deb: libtermux-exec.so - termux-create-package termux-exec-debug.json -unit-test: test-binary - ./test-binary -.PHONY: deb clean install uninstall test format check-format test +.PHONY: all pre-build build-runtime-binary-tests clean install uninstall deb test test-unit test-runtime format check diff --git a/run-tests.sh b/run-tests.sh deleted file mode 100755 index 0c831d7..0000000 --- a/run-tests.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/data/data/com.termux/files/usr/bin/bash - -set -u - -for f in tests/*.sh; do - printf "Running $f..." - - EXPECTED_FILE=$f-expected - ACTUAL_FILE=$f-actual - - rm -f $ACTUAL_FILE - $f myarg1 myarg2 &> $ACTUAL_FILE - - if cmp --silent $ACTUAL_FILE $EXPECTED_FILE; then - printf " OK\n" - else - printf " FAILED - compare expected $EXPECTED_FILE with ${ACTUAL_FILE}\n" - fi -done - diff --git a/src/data/assert_utils.c b/src/data/assert_utils.c new file mode 100644 index 0000000..98a51e6 --- /dev/null +++ b/src/data/assert_utils.c @@ -0,0 +1,159 @@ +#define _GNU_SOURCE +#include +#include +#include +#include + +#include "assert_utils.h" + +void assert_bool_with_error(bool expected, bool actual, const char* log_tag, const char *fmt, ...) { + if (actual != expected) { + logError(log_tag, "FAILED: expected: %s, actual: %s", expected ? "true" : "false", actual ? "true" : "false"); + if (fmt != NULL) { + va_list args; + va_start(args, fmt); + logMessage(LOG_PRIORITY__ERROR, log_tag, fmt, args); + va_end(args); + } + exit(1); + } +} + +void assert_bool(bool expected, bool actual, const char* log_tag) { + assert_bool_with_error(expected, actual, log_tag, NULL); +} + + + +void assert_false_with_error(bool actual, const char* log_tag, const char *fmt, ...) { + if (actual != false) { + logError(log_tag, "FAILED: expected: false, actual: true"); + if (fmt != NULL) { + va_list args; + va_start(args, fmt); + logMessage(LOG_PRIORITY__ERROR, log_tag, fmt, args); + va_end(args); + } + exit(1); + } +} + +void assert_false(bool actual, const char* log_tag) { + assert_false_with_error(actual, log_tag, NULL); +} + + + +void assert_true_with_error(bool actual, const char* log_tag, const char *fmt, ...) { + if (actual != true) { + logError(log_tag, "FAILED: expected: true, actual: false"); + if (fmt != NULL) { + va_list args; + va_start(args, fmt); + logMessage(LOG_PRIORITY__ERROR, log_tag, fmt, args); + va_end(args); + } + exit(1); + } +} + +void assert_true(bool actual, const char* log_tag) { + assert_true_with_error(actual, log_tag, NULL); +} + + + + + +void assert_int_with_error(int expected, int actual, const char* log_tag, const char *fmt, ...) { + if (actual != expected) { + logError(log_tag, "FAILED: expected: %d, actual: %d", expected, actual); + if (fmt != NULL) { + va_list args; + va_start(args, fmt); + logMessage(LOG_PRIORITY__ERROR, log_tag, fmt, args); + va_end(args); + } + exit(1); + } +} + +void assert_int(int expected, int actual, const char* log_tag) { + assert_int_with_error(expected, actual, log_tag, NULL); +} + + + + + +bool string_equals_regarding_null(const char *expected, const char *actual) { + if (expected == NULL) { + return actual == NULL; + } + + return actual != NULL && strcmp(actual, expected) == 0; +} + + +void assert_string_equals_with_error(const char *expected, const char *actual, + const char* log_tag, const char *fmt, ...) { + if (!string_equals_regarding_null(expected, actual)) { + if (expected == NULL) { + logError(log_tag, "FAILED: expected: null, actual: '%s'", actual); + } else if (actual == NULL) { + logError(log_tag, "FAILED: expected: '%s', actual: null", expected); + } else { + logError(log_tag, "FAILED: expected: '%s', actual: '%s'", expected, actual); + } + if (fmt != NULL) { + va_list args; + va_start(args, fmt); + logMessage(LOG_PRIORITY__ERROR, log_tag, fmt, args); + va_end(args); + } + exit(1); + } +} + +void assert_string_equals(const char *expected, const char *actual, const char* log_tag) { + assert_string_equals_with_error(expected, actual, log_tag, NULL); +} + + + + +void assert_string_null_with_error(const char *actual, const char* log_tag, const char *fmt, ...) { + if (actual != NULL) { + logError(log_tag, "FAILED: expected: null, actual: '%s'", actual); + if (fmt != NULL) { + va_list args; + va_start(args, fmt); + logMessage(LOG_PRIORITY__ERROR, log_tag, fmt, args); + va_end(args); + } + exit(1); + } +} + +void assert_string_null(const char *actual, const char* log_tag) { + assert_string_null_with_error(actual, log_tag, NULL); +} + + + +void assert_string_not_null_with_error(const char *actual, const char* log_tag, const char *fmt, ...) { + if (actual == NULL) { + logError(log_tag, "FAILED: expected: not_null, actual: '%s'", actual); + if (fmt != NULL) { + va_list args; + va_start(args, fmt); + logMessage(LOG_PRIORITY__ERROR, log_tag, fmt, args); + va_end(args); + } + exit(1); + } +} + +void assert_string_not_null(const char *actual, const char* log_tag) { + assert_string_not_null_with_error(actual, log_tag, NULL); +} diff --git a/src/data/assert_utils.h b/src/data/assert_utils.h new file mode 100644 index 0000000..4bd2655 --- /dev/null +++ b/src/data/assert_utils.h @@ -0,0 +1,70 @@ +#ifndef ASSERT_UTILS_H +#define ASSERT_UTILS_H + +#include + +#include "../logger/logger.h" + +void assert_bool_with_error(bool expected, bool actual, const char* log_tag, const char *fmt, ...); +void assert_bool(bool expected, bool actual, const char* log_tag); + + + +void assert_true_with_error(bool actual, const char* log_tag, const char *error_fmt, ...); +void assert_true(bool actual, const char* log_tag); + + + +void assert_false_with_error(bool actual, const char* log_tag, const char *fmt, ...); +void assert_false(bool actual, const char* log_tag); + + + + + +void assert_int_with_error(int expected, int actual, const char* log_tag, const char *fmt, ...); +void assert_int(int expected, int actual, const char* log_tag); + + + + + +bool string_equals_regarding_null(const char *expected, const char *actual); + +void assert_string_equals_with_error(const char *expected, const char *actual, + const char* log_tag, const char *fmt, ...); +void assert_string_equals(const char *expected, const char *actual, const char* log_tag); + + + +void assert_string_null_with_error(const char *actual, const char* log_tag, const char *fmt, ...); +void assert_string_null(const char *actual, const char* log_tag); + + + +void assert_string_not_null_with_error(const char *actual, const char* log_tag, const char *fmt, ...); +void assert_string_not_null(const char *actual, const char* log_tag); + + + + + +#define state__ATrue(state) \ + if (1) { \ + assert_true_with_error(state, \ + LOG_TAG, "%d: %s() -> (%s)", __LINE__, __FUNCTION__, #state); \ + } else ((void)0) + +#define int__AEqual(expected, actual) \ + if (1) { \ + assert_int_with_error(expected, actual, \ + LOG_TAG, "%d: %s()", __LINE__, __FUNCTION__); \ + } else ((void)0) + +#define string__AEqual(expected, actual) \ + if (1) { \ + assert_string_equals_with_error(expected, actual, \ + LOG_TAG, "%d: %s()", __LINE__, __FUNCTION__); \ + } else ((void)0) + +#endif // ASSERT_UTILS_H diff --git a/src/data/data_utils.c b/src/data/data_utils.c new file mode 100644 index 0000000..6de4002 --- /dev/null +++ b/src/data/data_utils.c @@ -0,0 +1,99 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../logger/logger.h" +#include "data_utils.h" + +static const char* LOG_TAG = "data_utils"; + +bool string_starts_with(const char *string, const char *prefix) { + if (string == NULL || prefix == NULL) { + return false; + } + + return *string != '\0' && *prefix != '\0' && (strncmp(string, prefix, strlen(prefix)) == 0); +} + +bool string_ends_with(const char *string, const char *suffix) { + if (string == NULL || suffix == NULL) { + return false; + } + + int strLength = strlen(string); + int suffixLength = strlen(suffix); + + return *string != '\0' && *suffix != '\0' && (strLength >= suffixLength) && \ + (strcmp(string + (strLength - suffixLength), suffix) == 0); +} + + + +int string_to_int(const char* string, int def, const char* log_tag, const char *fmt, ...) { + (void)log_tag; + (void)fmt; + + // We do not use atoi as it will not set errno or return errors. + // - https://man7.org/linux/man-pages/man3/atoi.3.html + // - https://man7.org/linux/man-pages/man3/strtol.3.html + char *end; + errno = 0; + long value = strtol(string, &end, 10); + if (end == string || *end != '\0' || errno != 0 || value < 0 || value > INT_MAX) { + #ifdef TERMUX_EXEC__RUNNING_TESTS + if (fmt != NULL) { + va_list args; + va_start(args, fmt); + logStrerrorMessage(errno, log_tag, fmt, args); + va_end(args); + } + #endif + errno = 0; + return def; + } + + return (int) value; +} + + + +int regex_match(const char *string, const char *pattern, int cflags) { + if (string == NULL || *string == '\0') { + return 1; + } + + regex_t regex; + int result; + char msgbuf[100]; + + // Compile regular expression. + result = regcomp(®ex, pattern, cflags); + if (result != 0) { + regerror(result, ®ex, msgbuf, sizeof(msgbuf)); + logErrorDebug(LOG_TAG, "Failed to compile regex '%s': %s", pattern, msgbuf); + return -1; + } + + // Match regular expression. + result = regexec(®ex, string, 0, NULL, 0); + int match; + if (result == 0) { + match = 0; + } else if (result == REG_NOMATCH) { + match = 1; + } else { + regerror(result, ®ex, msgbuf, sizeof(msgbuf)); + logErrorDebug(LOG_TAG, "Regex match failed '%s': %s", pattern, msgbuf); + match =-1; + } + + regfree(®ex); + + return match; +} diff --git a/src/data/data_utils.h b/src/data/data_utils.h new file mode 100644 index 0000000..22303f9 --- /dev/null +++ b/src/data/data_utils.h @@ -0,0 +1,31 @@ +#ifndef DATA_UTILS_H +#define DATA_UTILS_H + +#include + +/** Check if `str` starts with `prefix`. */ +bool string_starts_with(const char *string, const char *prefix); + +/** Check if `str` ends with `suffix`. */ +bool string_ends_with(const char *string, const char *suffix); + + + +int string_to_int(const char* string, int def, const char* log_tag, const char *fmt, ...); + + + +/** + * Check if `string` matches the `pattern` regex. + * + * - https://man7.org/linux/man-pages/man0/regex.h.0p.html + * - https://pubs.opengroup.org/onlinepubs/009696899/functions/regcomp.html + * + * @param string The string to match. + * @param pattern The regex pattern to match with. + * @param cflags The flags for `regcomp()` like `REG_EXTENDED`. + * @return Returns `0` on match, `1` on no match and `-1` on other failures. + */ +int regex_match(const char *string, const char *pattern, int cflags); + +#endif // DATA_UTILS_H diff --git a/src/exec-variants.c b/src/exec-variants.c deleted file mode 100644 index 672793d..0000000 --- a/src/exec-variants.c +++ /dev/null @@ -1,154 +0,0 @@ -// These exec variants, which ends up calling execve(), comes from bionic: -// https://android.googlesource.com/platform/bionic/+/refs/heads/main/libc/bionic/exec.cpp -// -// For some reason these are only necessary starting with Android 14 - before -// that intercepting execve() is enough. -// -// See the test-program.c for how to test the different variants. - -#define _GNU_SOURCE -#include -#include -#include -#include -#include -#include -#include -#include - -enum { ExecL, ExecLE, ExecLP }; - -static int __exec_as_script(const char *buf, char *const *argv, char *const *envp) { - size_t arg_count = 1; - while (argv[arg_count] != NULL) - ++arg_count; - - const char *script_argv[arg_count + 2]; - script_argv[0] = "sh"; - script_argv[1] = buf; - memcpy(script_argv + 2, argv + 1, arg_count * sizeof(char *)); - return execve(_PATH_BSHELL, (char **const)script_argv, envp); -} - -int execv(const char *name, char *const *argv) { return execve(name, argv, environ); } - -int execvp(const char *name, char *const *argv) { return execvpe(name, argv, environ); } - -int execvpe(const char *name, char *const *argv, char *const *envp) { - // if (name == NULL || *name == '\0') { errno = ENOENT; return -1; } - - // If it's an absolute or relative path name, it's easy. - if (strchr(name, '/') && execve(name, argv, envp) == -1) { - if (errno == ENOEXEC) - return __exec_as_script(name, argv, envp); - return -1; - } - - // Get the path we're searching. - const char *path = getenv("PATH"); - if (path == NULL) - path = _PATH_DEFPATH; - - // Make a writable copy. - size_t len = strlen(path) + 1; - char writable_path[len]; - memcpy(writable_path, path, len); - - bool saw_EACCES = false; - - // Try each element of $PATH in turn... - char *strsep_buf = writable_path; - const char *dir; - while ((dir = strsep(&strsep_buf, ":"))) { - // It's a shell path: double, leading and trailing colons - // mean the current directory. - if (*dir == '\0') - dir = "."; - - size_t dir_len = strlen(dir); - size_t name_len = strlen(name); - - char buf[dir_len + 1 + name_len + 1]; - mempcpy(mempcpy(mempcpy(buf, dir, dir_len), "/", 1), name, name_len + 1); - - execve(buf, argv, envp); - switch (errno) { - case EISDIR: - case ELOOP: - case ENAMETOOLONG: - case ENOENT: - case ENOTDIR: - break; - case ENOEXEC: - return __exec_as_script(buf, argv, envp); - return -1; - case EACCES: - saw_EACCES = true; - break; - default: - return -1; - } - } - if (saw_EACCES) - errno = EACCES; - return -1; -} - -static int __execl(int variant, const char *name, const char *argv0, va_list ap) { - // Count the arguments. - va_list count_ap; - va_copy(count_ap, ap); - size_t n = 1; - while (va_arg(count_ap, char *) != NULL) { - ++n; - } - va_end(count_ap); - - // Construct the new argv. - char *argv[n + 1]; - argv[0] = (char *)argv0; - n = 1; - while ((argv[n] = va_arg(ap, char *)) != NULL) { - ++n; - } - - // Collect the argp too. - char **argp = (variant == ExecLE) ? va_arg(ap, char **) : environ; - - va_end(ap); - - return (variant == ExecLP) ? execvp(name, argv) : execve(name, argv, argp); -} - -int execl(const char *name, const char *arg, ...) { - va_list ap; - va_start(ap, arg); - int result = __execl(ExecL, name, arg, ap); - va_end(ap); - return result; -} - -int execle(const char *name, const char *arg, ...) { - va_list ap; - va_start(ap, arg); - int result = __execl(ExecLE, name, arg, ap); - va_end(ap); - return result; -} - -int execlp(const char *name, const char *arg, ...) { - va_list ap; - va_start(ap, arg); - int result = __execl(ExecLP, name, arg, ap); - va_end(ap); - return result; -} - -int fexecve(int fd, char *const *argv, char *const *envp) { - char buf[40]; - snprintf(buf, sizeof(buf), "/proc/self/fd/%d", fd); - execve(buf, argv, envp); - if (errno == ENOENT) - errno = EBADF; - return -1; -} diff --git a/src/exec/exec.c b/src/exec/exec.c new file mode 100644 index 0000000..c94ac91 --- /dev/null +++ b/src/exec/exec.c @@ -0,0 +1,720 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../data/data_utils.h" +#include "../file/file_utils.h" +#include "../logger/logger.h" +#include "../os/selinux_utils.h" +#include "../termux/termux_env.h" +#include "../termux/termux_files.h" +#include "exec.h" + +static const char* LOG_TAG = "exec"; + + + +/** + * Call the `execve(2)` system call. + * + * - https://man7.org/linux/man-pages/man2/execve.2.html + */ +int execve_syscall(const char *executable_path, char *const argv[], char *const envp[]); + +/** + * Intercept and make changes required for termux and then call + * `execve_syscall()` to execute the `execve(2)` system call. + */ +int execve_intercept(const char *orig_executable_path, char *const argv[], char *const envp[]); + + + +int execve_hook(bool hook, const char *executable_path, char *const argv[], char *const envp[]) { + bool debugLoggingEnabled = getCurrentLogLevel() >= LOG_LEVEL__NORMAL; + + if (debugLoggingEnabled) { + if (hook) { + logErrorDebug(LOG_TAG, "<----- execve() hooked ----->"); + } + logErrorVerbose(LOG_TAG, "executable = '%s'", executable_path); + int tmp_argv_count = 0; + while (argv[tmp_argv_count] != NULL) { + logErrorVerbose(LOG_TAG, " argv[%d] = '%s'", tmp_argv_count, argv[tmp_argv_count]); + tmp_argv_count++; + } + } + + int result; + if (!is_termux_exec_execve_intercept_enabled()) { + logErrorVerbose(LOG_TAG, "Intercept execve disabled"); + result = execve_syscall(executable_path, argv, envp); + } else { + logErrorVerbose(LOG_TAG, "Intercepting execve"); + result = execve_intercept(executable_path, argv, envp); + } + + if (debugLoggingEnabled) { + int saved_errno = errno; + logErrorDebug(LOG_TAG, "<----- execve() failed ----->"); + errno = saved_errno; + } + + return result; +} + +int execve_syscall(const char *executable_path, char *const argv[], char *const envp[]) { + return syscall(SYS_execve, executable_path, argv, envp); +} + +int execve_intercept(const char *orig_executable_path, char *const argv[], char *const envp[]) { + bool debugLoggingEnabled = getCurrentLogLevel() >= LOG_LEVEL__NORMAL; + bool verboseLoggingEnabled = getCurrentLogLevel() >= LOG_LEVEL__VERBOSE; + + // - We normalize the path to remove `.` and `..` path components, + // and duplicate path separators `//`, but without resolving symlinks. + // For instance, `$TERMUX__PREFIX/bin/ls` is a symlink to `$TERMUX__PREFIX/bin/coreutils`, + // but we need to execute `$TERMUX__PREFIX/bin/ls` `/system/bin/linker $TERMUX__PREFIX/bin/ls` + // so that coreutils knows what to execute. + // - For an absolute path, we need to normalize first so that an + // unnormalized prefix like `/usr/./bin` is replaced with `/usr/bin` + // so that `termux_prefix_path()` can successfully match it to + // replace prefix with termux rootfs prefix. + // - For a relative path, we do not replace prefix with termux rootfs + // prefix, but instead prefix the current working directory (`cwd`). + // If `cwd` is set to `/bin` and `./sh` is executed, then + // `/bin/sh` should be executed instead of `$TERMUX__PREFIX/bin/sh`. + // Moreover, to handle the case where the executable path contains + // double dot `..` path components like `../sh`, we need to + // prefix the `cwd` first and then normalize the path, otherwise + // `normalize_path()` will return `null`, as unknown path + // components cannot be removed from a path. + // If instead on returning `null`, `normalize_path()` just + // removed the extra leading double dot components from the start + // and then we prefixed with `cwd`, then final path will be wrong + // since double dot path components would have been removed before + // they could be used to remove path components of the `cwd`. + // - $TERMUX_EXEC__PROC_SELF_EXE will be later set to the processed path + // (normalized/absolutized/prefixed) that will actually be executed. + char executable_path_buffer[strlen(orig_executable_path) + 1]; + strcpy(executable_path_buffer, orig_executable_path); + const char *executable_path = executable_path_buffer; + + + char processed_executable_path_buffer[PATH_MAX]; + if (executable_path[0] == '/') { + // If path is absolute, then normalize first and then replace termux prefix. + executable_path = normalize_path(executable_path_buffer, false, true); + if (executable_path == NULL) { + logStrerrorDebug(LOG_TAG, "Failed to normalize executable path '%s'", orig_executable_path); + return -1; + } + + if (verboseLoggingEnabled && strcmp(orig_executable_path, executable_path) != 0) { + logErrorVVerbose(LOG_TAG, "normalized_executable: '%s'", executable_path); + } + const char *normalized_executable_path = executable_path; + + + executable_path = termux_prefix_path(LOG_TAG, NULL, executable_path, + processed_executable_path_buffer, sizeof(processed_executable_path_buffer)); + if (executable_path == NULL) { + logStrerrorDebug(LOG_TAG, "Failed to prefix normalized executable path '%s'", normalized_executable_path); + return -1; + } + + if (verboseLoggingEnabled && strcmp(normalized_executable_path, executable_path) != 0) { + logErrorVVerbose(LOG_TAG, "prefixed_executable: '%s'", executable_path); + } + } else { + // If path is relative, then absolutize first and then normalize. + executable_path = absolutize_path(executable_path, + processed_executable_path_buffer, sizeof(processed_executable_path_buffer)); + if (executable_path == NULL) { + logStrerrorDebug(LOG_TAG, "Failed to convert executable path '%s' to an absolute path", orig_executable_path); + return -1; + } + + if (verboseLoggingEnabled && strcmp(orig_executable_path, executable_path) != 0) { + logErrorVVerbose(LOG_TAG, "absolutized_executable: '%s'", executable_path); + } + + + char absolute_executable_path_buffer[strlen(processed_executable_path_buffer) + 1]; + strcpy(absolute_executable_path_buffer, processed_executable_path_buffer); + + executable_path = normalize_path(processed_executable_path_buffer, false, true); + if (executable_path == NULL) { + logStrerrorDebug(LOG_TAG, "Failed to normalize absolutized executable path '%s'", absolute_executable_path_buffer); + return -1; + } + + if (verboseLoggingEnabled && strcmp(absolute_executable_path_buffer, executable_path) != 0) { + logErrorVVerbose(LOG_TAG, "normalized_executable: '%s'", executable_path); + } + } + + const char *processed_executable_path = executable_path; + + // - https://man7.org/linux/man-pages/man2/access.2.html + if (access(executable_path, X_OK) != 0) { + // Error out if the file does not exist or is not executable + // fd paths like to a pipes should not be executable either and + // they cannot be seek-ed back if interpreter were to be read. + // https://github.com/bminor/bash/blob/bash-5.2/shell.c#L1649 + logStrerrorDebug(LOG_TAG, "Failed to access executable path '%s'", processed_executable_path); + return -1; + } + + // - https://man7.org/linux/man-pages/man2/open.2.html + int fd = open(executable_path, O_RDONLY); + if (fd == -1) { + errno = ENOENT; + logStrerrorDebug(LOG_TAG, "Failed to open executable path '%s'", processed_executable_path); + return -1; + } + + + + char header[FILE_HEADER__BUFFER_LEN]; + ssize_t read_bytes = read(fd, header, sizeof(header) - 1); + // Ensure read was successful, path could be a directory and EISDIR will be returned. + // - https://man7.org/linux/man-pages/man2/read.2.html + if (read_bytes < 0) { + logStrerrorDebug(LOG_TAG, "Failed to read executable path '%s'", processed_executable_path); + return -1; + } + close(fd); + + struct file_header_info info = { + .interpreter_path = NULL, + .interpreter_arg = NULL, + }; + + if (inspect_file_header(NULL, header, read_bytes, &info) != 0) { + return -1; + } + + bool interpreter_set = info.interpreter_path != NULL; + if (!info.is_elf && !interpreter_set) { + errno = ENOEXEC; + logStrerrorDebug(LOG_TAG, "Not an ELF or no shebang in executable path '%s'", processed_executable_path); + return -1; + } + + if (interpreter_set) { + executable_path = info.interpreter_path; + } + + + + + // Check if system_linker_exec is required. + int system_linker_exec = should_system_linker_exec(executable_path); + if (system_linker_exec < 0) { + return -1; + } + + + + bool modify_env = false; + bool unset_ld_vars_from_env = should_unset_ld_vars_from_env(info.is_non_native_elf, executable_path); + logErrorVVerbose(LOG_TAG, "unset_ld_vars_from_env: '%d'", unset_ld_vars_from_env); + + if (unset_ld_vars_from_env && are_vars_in_env(envp, LD_VARS_TO_UNSET, LD_VARS_TO_UNSET_SIZE)) { + modify_env = true; + } + + + + // If `system_linker_exec` is going to be used, then set `TERMUX_EXEC__PROC_SELF_EXE` + // environment variable to `processed_executable_path`, otherwise + // unset it if it is already set. + char *env_termux_proc_self_exe = NULL; + if (system_linker_exec) { + modify_env = true; + logErrorVVerbose(LOG_TAG, "set_proc_self_exe_var_in_env: '%d'", true); + + if (asprintf(&env_termux_proc_self_exe, "%s%s", ENV_PREFIX__TERMUX_EXEC__PROC_SELF_EXE, processed_executable_path) == -1) { + errno = ENOMEM; + logStrerrorDebug(LOG_TAG, "asprintf failed for '%s%s'", ENV_PREFIX__TERMUX_EXEC__PROC_SELF_EXE, processed_executable_path); + return -1; + } + } else { + const char *proc_self_exe_var[] = { ENV_PREFIX__TERMUX_EXEC__PROC_SELF_EXE }; + if (are_vars_in_env(envp, proc_self_exe_var, 1)) { + logErrorVVerbose(LOG_TAG, "unset_proc_self_exe_var_from_env: '%d'", true); + modify_env = true; + } + } + + logErrorVVerbose(LOG_TAG, "modify_env: '%d'", modify_env); + + + + char **new_envp = NULL; + if (modify_env) { + if (modify_exec_env(envp, &new_envp, &env_termux_proc_self_exe, unset_ld_vars_from_env) != 0 || + new_envp == NULL) { + logErrorDebug(LOG_TAG, "Failed to create modified exec env"); + free(env_termux_proc_self_exe); + return -1; + } + + envp = new_envp; + } + + + + const bool modify_args = system_linker_exec || interpreter_set; + logErrorVVerbose(LOG_TAG, "modify_args: '%d'", modify_args); + + const char **new_argv = NULL; + if (modify_args) { + if (modify_exec_args(argv, &new_argv, orig_executable_path, executable_path, + interpreter_set, system_linker_exec, &info) != 0 || + new_argv == NULL) { + logErrorDebug(LOG_TAG, "Failed to create modified exec args"); + free(env_termux_proc_self_exe); + free(new_envp); + return -1; + } + + // Replace executable path if wrapping with linker. + if (system_linker_exec) { + executable_path = SYSTEM_LINKER_PATH; + } + + argv = (char **) new_argv; + } + + + + if (debugLoggingEnabled) { + logErrorVerbose(LOG_TAG, "Calling syscall execve"); + logErrorVerbose(LOG_TAG, "executable = '%s'", executable_path); + int tmp_argv_count = 0; + int arg_count = 0; + while (argv[tmp_argv_count] != NULL) { + logErrorVerbose(LOG_TAG, " argv[%d] = '%s'", arg_count++, argv[tmp_argv_count]); + tmp_argv_count++; + } + } + + int syscall_ret = execve_syscall(executable_path, argv, envp); + int saved_errno = errno; + logStrerrorDebug(LOG_TAG, "execve() syscall failed for executable path '%s'", executable_path); + free(env_termux_proc_self_exe); + free(new_envp); + free(new_argv); + errno = saved_errno; + return syscall_ret; +} + + + +int inspect_file_header(const char *termux_prefix_dir, char *header, size_t header_len, + struct file_header_info *info) { + if (header_len >= 20 && !memcmp(header, ELFMAG, SELFMAG)) { + info->is_elf = true; + Elf32_Ehdr *ehdr = (Elf32_Ehdr *)header; + if (ehdr->e_machine != EM_NATIVE) { + info->is_non_native_elf = true; + } + return 0; + } + + if (header_len < 3 || !(header[0] == '#' && header[1] == '!')) { + return 0; + } + + // Check if the header contains a newline to end the shebang line. + char *newline_location = memchr(header, '\n', header_len); + if (newline_location == NULL) { + return 0; + } + + // Strip whitespace at end of shebang. + while (*(newline_location - 1) == ' ') { + newline_location--; + } + + // Null terminate the shebang line. + *newline_location = 0; + + // Skip whitespace to find interpreter start. + char const *interpreter = header + 2; + while (*interpreter == ' ') { + interpreter++; + } + if (interpreter == newline_location) { + // Just a blank line up until the newline. + return 0; + } + + // Check for whitespace following the interpreter. + char *whitespace_pos = strchr(interpreter, ' '); + if (whitespace_pos != NULL) { + // Null-terminate the interpreter string. + *whitespace_pos = 0; + + // Find start of argument. + char *interpreter_arg = whitespace_pos + 1; + while (*interpreter_arg != 0 && *interpreter_arg == ' ') { + interpreter_arg++; + } + if (interpreter_arg != newline_location) { + size_t interpreter_arg_buffer_len = sizeof(info->interpreter_arg_buffer); + + size_t interpreter_arg_len = strlen(interpreter_arg); + if (interpreter_arg_buffer_len <= interpreter_arg_len) { + #ifndef TERMUX_EXEC__RUNNING_TESTS + logErrorDebug(LOG_TAG, "The interpreter argument '%s' with length '%zu' is too long to fit in the buffer with length '%zu'", + interpreter_arg, interpreter_arg_len, interpreter_arg_buffer_len); + #endif + errno = ENAMETOOLONG; + return -1; + } + + strcpy(info->interpreter_arg_buffer, interpreter_arg); + info->interpreter_arg = info->interpreter_arg_buffer; + } + } + + #ifndef TERMUX_EXEC__RUNNING_TESTS + logErrorVVerbose(LOG_TAG, "interpreter_path: '%s'", interpreter); + #endif + + // argv[0] must be set to the original interpreter set in the file even + // if it is a relative path and its absolute path is to be executed. + info->orig_interpreter_path = interpreter; + + + + bool verboseLoggingEnabled = getCurrentLogLevel() >= LOG_LEVEL__VERBOSE; + (void)verboseLoggingEnabled; + size_t interpreter_path_buffer_len = sizeof(info->interpreter_path_buffer); + + char interpreter_path_buffer[strlen(interpreter) + 1]; + strcpy(interpreter_path_buffer, interpreter); + char *interpreter_path = interpreter_path_buffer; + + + if (interpreter_path[0] == '/') { + // If path is absolute, then normalize first and then replace termux prefix. + interpreter_path = normalize_path(interpreter_path, false, true); + if (interpreter_path == NULL) { + logStrerrorDebug(LOG_TAG, "Failed to normalize interpreter path '%s'", info->orig_interpreter_path); + return -1; + } + #ifndef TERMUX_EXEC__RUNNING_TESTS + if (verboseLoggingEnabled && strcmp(info->orig_interpreter_path, interpreter_path) != 0) { + logErrorVVerbose(LOG_TAG, "normalized_interpreter: '%s'", interpreter_path); + } + #endif + + + const char *normalized_interpreter_path = interpreter_path; + + info->interpreter_path = termux_prefix_path(LOG_TAG, termux_prefix_dir, + interpreter_path, info->interpreter_path_buffer, interpreter_path_buffer_len); + if (info->interpreter_path == NULL) { + logStrerrorDebug(LOG_TAG, "Failed to prefix normalized interpreter path '%s'", normalized_interpreter_path); + return -1; + } + + #ifndef TERMUX_EXEC__RUNNING_TESTS + if (verboseLoggingEnabled && strcmp(normalized_interpreter_path, info->interpreter_path) != 0) { + logErrorVVerbose(LOG_TAG, "prefixed_interpreter: '%s'", info->interpreter_path); + } + #endif + } else { + char processed_interpreter_path_buffer[PATH_MAX]; + + // If path is relative, then absolutize first and then normalize. + interpreter_path = absolutize_path(interpreter_path, + processed_interpreter_path_buffer, sizeof(processed_interpreter_path_buffer)); + if (interpreter_path == NULL) { + logStrerrorDebug(LOG_TAG, "Failed to convert interpreter path '%s' to an absolute path", info->orig_interpreter_path); + return -1; + } + + #ifndef TERMUX_EXEC__RUNNING_TESTS + if (verboseLoggingEnabled && strcmp(info->orig_interpreter_path, interpreter_path) != 0) { + logErrorVVerbose(LOG_TAG, "absolute_interpreter: '%s'", interpreter_path); + } + #endif + + + char absolute_interpreter_path_buffer[strlen(processed_interpreter_path_buffer) + 1]; + strcpy(absolute_interpreter_path_buffer, processed_interpreter_path_buffer); + + interpreter_path = normalize_path(processed_interpreter_path_buffer, false, true); + if (interpreter_path == NULL) { + logStrerrorDebug(LOG_TAG, "Failed to normalize absolutized interpreter path '%s'", absolute_interpreter_path_buffer); + return -1; + } + + #ifndef TERMUX_EXEC__RUNNING_TESTS + if (verboseLoggingEnabled && strcmp(absolute_interpreter_path_buffer, interpreter_path) != 0) { + logErrorVVerbose(LOG_TAG, "normalized_interpreter: '%s'", interpreter_path); + } + #endif + + + size_t processed_interpreter_path_len = strlen(interpreter_path); + if (interpreter_path_buffer_len <= processed_interpreter_path_len) { + #ifndef TERMUX_EXEC__RUNNING_TESTS + logErrorDebug(LOG_TAG, "The processed interpreter path '%s' with length '%zu' is too long to fit in the buffer with length '%zu'", + interpreter_path, processed_interpreter_path_len, interpreter_path_buffer_len); + #endif + errno = ENAMETOOLONG; + return -1; + } + + strcpy(info->interpreter_path_buffer, interpreter_path); + info->interpreter_path = info->interpreter_path_buffer; + } + + + #ifndef TERMUX_EXEC__RUNNING_TESTS + if (verboseLoggingEnabled && info->interpreter_arg != NULL) { + logErrorVVerbose(LOG_TAG, "interpreter_arg: '%s'", info->interpreter_arg); + } + #endif + + return 0; +} + + + +int should_system_linker_exec(const char *executable_path) { + int system_linker_exec_config = get_termux_exec_system_linker_exec_config(); + logErrorVVerbose(LOG_TAG, "system_linker_exec_config: '%d'", system_linker_exec_config); + + bool system_linker_exec = false; + if (system_linker_exec_config == 0) { // disable + system_linker_exec = false; + + } else if (system_linker_exec_config == 2) { // force + bool system_linker_exec_available = false; + system_linker_exec_available = get_android_build_version_sdk() >= 29; + logErrorVVerbose(LOG_TAG, "system_linker_exec_available: '%d'", system_linker_exec_available); + + if (system_linker_exec_available) { + int is_executable_under_termux_app_data_dir = is_path_under_termux_app_data_dir(LOG_TAG, + executable_path, NULL, NULL); + if (is_executable_under_termux_app_data_dir < 0) { + return -1; + } + logErrorVVerbose(LOG_TAG, "is_executable_under_termux_app_data_dir: '%d'", + is_executable_under_termux_app_data_dir == 0 ? true : false); + + system_linker_exec = is_executable_under_termux_app_data_dir == 0; + } + + } else { // enable + if (system_linker_exec_config != 1) { + logErrorDebug(LOG_TAG, "Warning: Ignoring invalid system_linker_exec_config value and using '1' instead"); + } + + bool app_data_file_exec_exempted = false; + (void)app_data_file_exec_exempted; + + if (get_android_build_version_sdk() >= 29) { + // If running as root or shell user, then the process will + // be assigned a different process context like + // `PROCESS_CONTEXT__AOSP_SU`, + // `PROCESS_CONTEXT__MAGISK_SU` or + // `PROCESS_CONTEXT__SHELL`, which will not be the same + // as the one that's exported in + // `ENV__TERMUX__SE_PROCESS_CONTEXT`, so we need to check + // effective uid equals `0` or `2000` instead. Moreover, + // other su providers may have different contexts, so we + // cannot just check AOSP or MAGISK contexts. + // - https://man7.org/linux/man-pages/man2/getuid.2.html + uid_t uid = geteuid(); + if (uid == 0 || uid == 2000) { + logErrorVVerbose(LOG_TAG, "uid: '%d'", uid); + app_data_file_exec_exempted = true; + } else { + char se_process_context[80]; + bool get_se_process_context_success = false; + + if (get_se_process_context_from_env(LOG_TAG, ENV__TERMUX__SE_PROCESS_CONTEXT, + se_process_context, sizeof(se_process_context))) { + logErrorVVerbose(LOG_TAG, "se_process_context_from_env: '%s'", se_process_context); + get_se_process_context_success = true; + } else if (get_se_process_context_from_file(LOG_TAG, + se_process_context, sizeof(se_process_context))) { + logErrorVVerbose(LOG_TAG, "se_process_context_from_file: '%s'", se_process_context); + get_se_process_context_success = true; + } + + if (get_se_process_context_success) { + app_data_file_exec_exempted = string_starts_with(se_process_context, PROCESS_CONTEXT_PREFIX__UNTRUSTED_APP_25) || + string_starts_with(se_process_context, PROCESS_CONTEXT_PREFIX__UNTRUSTED_APP_27); + } + } + + logErrorVVerbose(LOG_TAG, "app_data_file_exec_exempted: '%d'", app_data_file_exec_exempted); + + if (!app_data_file_exec_exempted) { + int is_executable_under_termux_app_data_dir = is_path_under_termux_app_data_dir(LOG_TAG, + executable_path, NULL, NULL); + if (is_executable_under_termux_app_data_dir < 0) { + return -1; + } + + system_linker_exec = is_executable_under_termux_app_data_dir == 0; + logErrorVVerbose(LOG_TAG, "is_executable_under_termux_app_data_dir: '%d'", system_linker_exec); + } + } + } + + logErrorVVerbose(LOG_TAG, "system_linker_exec: '%d'", system_linker_exec); + + return 1 ? system_linker_exec : 0; +} + + + +bool should_unset_ld_vars_from_env(bool is_non_native_elf, const char *executable_path) { + return is_non_native_elf || + (string_starts_with(executable_path, "/system/") && + strcmp(executable_path, "/system/bin/sh") != 0 && + strcmp(executable_path, "/system/bin/linker") != 0 && + strcmp(executable_path, "/system/bin/linker64") != 0); +} + +int modify_exec_env(char *const *envp, char ***new_envp_pointer, + char** env_termux_proc_self_exe, bool unset_ld_vars_from_env) { + int env_count = 0; + while (envp[env_count] != NULL) { + env_count++; + } + + // Allocate new environment variable array. Size + 2 since + // we might perhaps append a TERMUX_EXEC__PROC_SELF_EXE variable and + // we will also NULL terminate. + size_t new_envp_size = (sizeof(char *) * (env_count + 2)); + void* result = malloc(new_envp_size); + if (result == NULL) { + logStrerrorDebug(LOG_TAG, "The malloc called failed for new envp with size '%zu'", new_envp_size); + return -1; + } + + char **new_envp = (char **) result; + *new_envp_pointer = new_envp; + + bool already_found_proc_self_exe = false; + int index = 0; + for (int i = 0; i < env_count; i++) { + if (string_starts_with(envp[i], ENV_PREFIX__TERMUX_EXEC__PROC_SELF_EXE)) { + if (env_termux_proc_self_exe != NULL && *env_termux_proc_self_exe != NULL) { + new_envp[index++] = *env_termux_proc_self_exe; + already_found_proc_self_exe = true; + #ifndef TERMUX_EXEC__RUNNING_TESTS + logErrorVVerbose(LOG_TAG, "Overwrite '%s'", *env_termux_proc_self_exe); + #endif + } + #ifndef TERMUX_EXEC__RUNNING_TESTS + else { + logErrorVVerbose(LOG_TAG, "Unset '%s'", envp[i]); + } + #endif + } else { + bool keep = true; + + if (unset_ld_vars_from_env) { + for (int j = 0; j < LD_VARS_TO_UNSET_SIZE; j++) { + if (string_starts_with(envp[i], LD_VARS_TO_UNSET[j])) { + keep = false; + } + } + } + + if (keep) { + new_envp[index++] = envp[i]; + } + #ifndef TERMUX_EXEC__RUNNING_TESTS + else { + logErrorVVerbose(LOG_TAG, "Unset '%s'", envp[i]); + } + #endif + } + } + + if (env_termux_proc_self_exe != NULL && *env_termux_proc_self_exe != NULL && !already_found_proc_self_exe) { + new_envp[index++] = *env_termux_proc_self_exe; + #ifndef TERMUX_EXEC__RUNNING_TESTS + logErrorVVerbose(LOG_TAG, "Set '%s'", *env_termux_proc_self_exe); + #endif + } + + // Null terminate. + new_envp[index] = NULL; + + return 0; +} + + + +int modify_exec_args(char *const *argv, const char ***new_argv_pointer, + const char *orig_executable_path, const char *executable_path, + bool interpreter_set, bool system_linker_exec, struct file_header_info *info) { + int args_count = 0; + while (argv[args_count] != NULL) { + args_count++; + } + + size_t new_argv_size = (sizeof(char *) * (args_count + 2)); + void* result = malloc(new_argv_size); + if (result == NULL) { + logStrerrorDebug(LOG_TAG, "The malloc called failed for new argv with size '%zu'", new_argv_size); + return -1; + } + + const char **new_argv = (const char **) result; + *new_argv_pointer = new_argv; + + int index = 0; + + if (interpreter_set) { + // Use original interpreter path set in executable file as is. + new_argv[index++] = info->orig_interpreter_path; + } else { + // Preserver original `argv[0]` to `execve()`. + new_argv[index++] = argv[0]; + } + + // Add executable path if wrapping with linker. + if (system_linker_exec) { + new_argv[index++] = executable_path; + } + + // Add interpreter argument and script path if executing a script with shebang. + if (interpreter_set) { + if (info->interpreter_arg != NULL) { + new_argv[index++] = info->interpreter_arg; + } + new_argv[index++] = orig_executable_path; + } + + for (int i = 1; i < args_count; i++) { + new_argv[index++] = argv[i]; + } + + // Null terminate. + new_argv[index] = NULL; + + return 0; +} diff --git a/src/exec/exec.h b/src/exec/exec.h new file mode 100644 index 0000000..6f8121c --- /dev/null +++ b/src/exec/exec.h @@ -0,0 +1,306 @@ +#ifndef EXEC_H +#define EXEC_H + +#include + +#include "../termux/termux_env.h" +#include "../termux/termux_files.h" + + +/** The path to android system linker. */ +#if UINTPTR_MAX == 0xffffffff +#define SYSTEM_LINKER_PATH "/system/bin/linker"; +#elif UINTPTR_MAX == 0xffffffffffffffff +#define SYSTEM_LINKER_PATH "/system/bin/linker64"; +#endif + + + + + +/* + * For the `execve()` system call, the kernel imposes a maximum length + * limit on script shebang including the `#!` characters at the start + * of a script. For Linux kernel `< 5.1`, the limit is `128` + * characters and for Linux kernel `>= 5.1`, the limit is `256` + * characters as per `BINPRM_BUF_SIZE` including the null `\0` + * terminator. + * + * If `termux-exec` is set in `LD_PRELOAD` and + * `TERMUX_EXEC__INTERCEPT_EXECVE` is enabled, then shebang limit is + * increased to `340` characters defined by `FILE_HEADER__BUFFER_LEN` + * as shebang is read and script is passed to interpreter as an + * argument by `termux-exec` manually. Increasing limit to `340` also + * fixes issues for older Android kernel versions where limit is + * `128`. The limit is increased to `340`, because `BINPRM_BUF_SIZE` + * would be set based on the assumption that rootfs is at `/`, so we + * add Termux rootfs directory max length to it. + * + * - https://man7.org/linux/man-pages/man2/execve.2.html + * - https://en.wikipedia.org/wiki/Shebang_(Unix)#Character_interpretation + * - https://cs.android.com/android/kernel/superproject/+/0dc2b7de045e6dcfff9e0dfca9c0c8c8b10e1cf3:common/fs/binfmt_script.c;l=34 + * - https://cs.android.com/android/kernel/superproject/+/0dc2b7de045e6dcfff9e0dfca9c0c8c8b10e1cf3:common/include/linux/binfmts.h;l=64 + * - https://cs.android.com/android/kernel/superproject/+/0dc2b7de045e6dcfff9e0dfca9c0c8c8b10e1cf3:common/include/uapi/linux/binfmts.h;l=18 + * + * + * The running a script in `bash`, and the interpreter length is + * `>= 128` (`BINPRM_BUF_SIZE`) and `execve()` system call returns + * `ENOEXEC` (`Exec format error`), then `bash` will read the file + * and run it as `bash` shell commands. + * If interpreter length was `< 128` and `execve()` returned some + * other error than `ENOEXEC`, then `bash` will try to give a + * meaningful error. + * - If script was not executable: `bash: : Permission denied` + * - If script was a directory: `bash: : Is a directory` + * - If `ENOENT` was returned since interpreter file was not found: + * `bash: : cannot execute: required file not found` + * - If some unhandled errno was returned, like interpreter file was a directory: + * `bash: : : bad interpreter` + * + * - https://github.com/bminor/bash/blob/bash-5.2/execute_cmd.c#L5929 + * - https://github.com/bminor/bash/blob/bash-5.2/execute_cmd.c#L5964 + * - https://github.com/bminor/bash/blob/bash-5.2/execute_cmd.c#L5988 + * - https://github.com/bminor/bash/blob/bash-5.2/execute_cmd.c#L6048 + */ + +/** + * The max length for entire shebang line for the Linux kernel `>= 5.1` defined by `BINPRM_BUF_SIZE`. + * Add `FILE_HEADER__` scope to prevent conflicts by header importers. + * + * Default value: `256` + */ +#define FILE_HEADER__BINPRM_BUF_SIZE 256 + +/** + * The max length for interpreter path in the shebang for `termux-exec`. + * + * Default value: `340` + */ +#define FILE_HEADER__INTERPRETER_PATH___MAX_LEN (TERMUX__ROOTFS_DIR___MAX_LEN + FILE_HEADER__BINPRM_BUF_SIZE - 1) // The `- 1` is to only allow one null `\0` terminator. + +/** + * The max length for interpreter arg in the shebang for `termux-exec`. + * + * This is same as `FILE_HEADER__BINPRM_BUF_SIZE`. These is no way to + * divide `BINPRM_BUF_SIZE` between path and arg, so we give it full + * buffer size in case it needs it. + * + * Default value: `256` + */ +#define FILE_HEADER__INTERPRETER_ARG___MAX_LEN FILE_HEADER__BINPRM_BUF_SIZE + +/** + * The max length for entire shebang line for `termux-exec`. + * + * This is same as `FILE_HEADER__INTERPRETER_PATH___MAX_LEN`. + * + * Default value: `340` + */ +#define FILE_HEADER__BUFFER_LEN FILE_HEADER__INTERPRETER_PATH___MAX_LEN + + + +/** + * The info for the file header of an executable file. + * + * - https://refspecs.linuxfoundation.org/elf/gabi4+/ch4.eheader.html + */ +struct file_header_info { + /** Whether executable is an ELF file instead of a script. */ + bool is_elf; + + /** + * Whether the `Elf32_Ehdr.e_machine != EM_NATIVE`, i.e executable + * file is 32-bit binary on a 64-bit host. + */ + bool is_non_native_elf; + + /** + * The original interpreter path set in the executable file that is + * not normalized, absolutized or prefixed. + */ + char const *orig_interpreter_path; + + /** + * The interpreter path set in the executable file that is + * normalized, absolutized and prefixed. + */ + char const *interpreter_path; + /** The underlying buffer for `interpreter_path`. */ + char interpreter_path_buffer[FILE_HEADER__INTERPRETER_PATH___MAX_LEN]; + + /** The arguments to the interpreter set in the executable file. */ + char const *interpreter_arg; + /** The underlying buffer for `interpreter_arg`. */ + char interpreter_arg_buffer[FILE_HEADER__INTERPRETER_ARG___MAX_LEN]; +}; + + + +/** The host native architecture. */ +#ifdef __aarch64__ +#define EM_NATIVE EM_AARCH64 +#elif defined(__arm__) || defined(__thumb__) +#define EM_NATIVE EM_ARM +#elif defined(__x86_64__) +#define EM_NATIVE EM_X86_64 +#elif defined(__i386__) +#define EM_NATIVE EM_386 +#else +#error "unknown arch" +#endif + + +/** + * The list of variables that are unset by `modify_exec_env()` if + * `unset_ld_vars_from_env` is `true`. + */ +static const char *LD_VARS_TO_UNSET[] __attribute__ ((unused)) = { ENV_PREFIX__LD_LIBRARY_PATH, ENV_PREFIX__LD_PRELOAD }; +static int LD_VARS_TO_UNSET_SIZE __attribute__ ((unused)) = 2; + + +/** + * Hook for the `execve()` method in `unistd.h`. + * + * If `is_termux_exec_execve_intercept_enabled()` returns `true`, + * then `execve_intercept()` will be called, otherwise + * `execve_syscall()`. + * + * - https://man7.org/linux/man-pages/man3/exec.3.html + */ +int execve_hook(bool hook, const char *executable_path, char *const argv[], char *const envp[]); + + + +/** + * Inspect file header and set `file_header_info`. + * + * @param termux_prefix_dir The **normalized** path to termux prefix + * directory. If `NULL`, then path returned by + * `get_termux_prefix_dir_from_env_or_default()` + * will be used by calling `get_termux_prefix_dir()`. + * @param header The file header read from the executable file. + * The `FILE_HEADER__BUFFER_LEN` should be used as buffer size + * when reading. + * @param header_len The actual length of the header that was read. + * @param info The `file_header_info` to set. + */ +int inspect_file_header(const char *termux_prefix_dir, char *header, size_t header_len, + struct file_header_info *info); + + +/** + * Whether to use `system_linker_exec`, like to bypass app data file + * execute restrictions. + * + * A call is made to `get_termux_exec_system_linker_exec_config()` to + * get the config for using `system_linker_exec`. + * + * If `disable` is set, then `system_linker_exec` should not be used + * and the default `direct` execution type should be used. + * + * If `enable` is set, then `system_linker_exec` should only be used if: + * - `system_linker_exec` is required to bypass app data file execute + * restrictions, i.e device is running on Android `>= 10`. + * - Effective user does not equal root (`0`) and shell (`2000`) user (used for + * [`adb`](https://developer.android.com/tools/adb)). + * - `TERMUX__SE_PROCESS_CONTEXT` or its fallback `/proc/self/attr/current` + * does not start with `PROCESS_CONTEXT_PREFIX__UNTRUSTED_APP_25` and + * `PROCESS_CONTEXT_PREFIX__UNTRUSTED_APP_27` for which restrictions + * are exempted. + * - `is_path_under_termux_app_data_dir()` returns `true` for the `executable_path`. + * + * If `force` is set, then `system_linker_exec` should only be used if: + * - `system_linker_exec` is supported, i.e device is running on Android `>= 10`. + * - `is_path_under_termux_app_data_dir()` returns `true` for the `executable_path`. + * This can be used if running in an untrusted app with `targetSdkVersion` `<= 28`. + * + * The executable or interpreter paths are checked under + * `TERMUX_APP__DATA_DIR` or `TERMUX_APP__LEGACY_DATA_DIR` instead of + * `TERMUX__ROOTFS` as files could be executed from `TERMUX__APPS_DIR` + * and `TERMUX__CACHE_DIR`, which are not under the Termux rootfs. + * Additionally, Termux rootfs may not exist under app data directory + * at all and could be under another directory under Android rootfs `/`, + * like if compiling packages for `shell` user for the `com.android.shell` + * package with the Termux rootfs under `/data/local/tmp` instead of + * `/data/data/com.android.shell` (and using `force` mode) or + * compiling packages for `/system` directory. + * + * @param executable_path The **normalized** executable or interpreter + * path that will actually be executed. + * @return Returns `0` if `system_linker_exec` should not be used, + * `1` if `system_linker_exec` should be used, otherwise `-1` on failures. + */ +int should_system_linker_exec(const char *executable_path); + + + +/** + * Whether variables in `LD_VARS_TO_UNSET` should be unset before `exec()` + * to prevent issues when executing system binaries that are caused + * if they are set. + * + * @param is_non_native_elf The value for `file_header_info.is_non_native_elf` + * for the executable file. + * @param executable_path The **normalized** executable path to check. + * @return Returns `true` if `is_non_native_elf` equals `true` or + * `executable_path` starts with `/system/`, but does not equal + * `/system/bin/sh`, `system/bin/linker` or `/system/bin/linker64`. + * + */ +bool should_unset_ld_vars_from_env(bool is_non_native_elf, const char *executable_path); + +/** + * Modify the environment for `execve()`. + * + * @param envp The current environment pointer. + * @param new_envp_pointer The new environment pointer to set. + * @param env_termux_proc_self_exe If set, then it will overwrite or + * set the `ENV_PREFIX__TERMUX_EXEC__PROC_SELF_EXE` + * env variable. + * @param unset_ld_vars_from_env If `true`, then variables in + * `LD_VARS_TO_UNSET` will be unset. + * @return Returns `0` if successfully modified the env, otherwise + * `-1` on failures. Its the callers responsibility to call `free()` + * on the `new_envp_pointer` passed. + */ +int modify_exec_env(char *const *envp, char ***new_envp_pointer, + char** env_termux_proc_self_exe, bool unset_ld_vars_from_env); + + + +/** + * Modify the arguments for `execve()`. + * + * If `interpreter_set` is set, then `argv[0]` will be set to + * `file_header_info.orig_interpreter_path`, otherwise the original + * `argv[0]` passed to `execve()` will be preserved. + * + * If `system_linker_exec` is `true`, then `argv[1]` will be set + * to `executable_path` to be executed by the linker. + * + * If `interpreter_set` is set, then `file_header_info.interpreter_arg` + * will be appended if set, followed by the `orig_executable_path` + * passed to `execve()`. + * + * Any additional arguments to `execve()` will be appended after this. + * + * @param argv The current arguments pointer. + * @param new_argv_pointer The new arguments pointer to set. + * @param orig_executable_path The originnal executable path passed to + * `execve()`. + * @param executable_path The **normalized** executable or interpreter + * path that will actually be executed. + * @param interpreter_set Whether a interpreter is set in the executable + * file. + * @param system_linker_exec Whether `system_linker_exec` is enabled. + * @param info The `file_header_info` for the executable file. + * @return Returns `0` if successfully modified the args, otherwise + * `-1` on failures. Its the callers responsibility to call `free()` + * on the `new_argv_pointer` passed. + */ +int modify_exec_args(char *const *argv, const char ***new_argv_pointer, + const char *orig_executable_path, const char *executable_path, + bool interpreter_set, bool system_linker_exec, struct file_header_info *info); + +#endif // EXEC_H diff --git a/src/exec/exec_variants.c b/src/exec/exec_variants.c new file mode 100644 index 0000000..5b861fb --- /dev/null +++ b/src/exec/exec_variants.c @@ -0,0 +1,184 @@ +/*- + * Copyright (c) 1991, 1993 + * The Regents of the University of California. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the University nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../exec/exec.h" +#include "../logger/logger.h" +#include "exec_variants.h" + +static const char* LOG_TAG = "exec"; + +int execl_hook(bool hook, int variant, const char *name, const char *argv0, va_list ap) { + if (hook) { + logErrorDebug(LOG_TAG, "<----- %s() hooked ----->", EXEC_VARIANTS_STR[variant]); + } + + // Count the arguments. + va_list count_ap; + va_copy(count_ap, ap); + size_t n = 1; + while (va_arg(count_ap, char *) != NULL) { + ++n; + } + va_end(count_ap); + + // Construct the new argv. + char *argv[n + 1]; + argv[0] = (char *)argv0; + n = 1; + while ((argv[n] = va_arg(ap, char *)) != NULL) { + ++n; + } + + // Collect the argp too. + char **argp = (variant == ExecLE) ? va_arg(ap, char **) : environ; + + va_end(ap); + + return (variant == ExecLP) ? execvp_hook(false, name, argv) : execve_hook(false, name, argv, argp); +} + + +int execv_hook(bool hook, const char *name, char *const *argv) { + if (hook) { + logErrorDebug(LOG_TAG, "<----- execv() hooked ----->"); + } + + return execve_hook(false, name, argv, environ); +} + +int execvp_hook(bool hook, const char *name, char *const *argv) { + if (hook) { + logErrorDebug(LOG_TAG, "<----- execvp() hooked ----->"); + } + + return execvpe_hook(false, name, argv, environ); +} + +int __exec_as_script(const char *buf, char *const *argv, char *const *envp) { + size_t arg_count = 1; + while (argv[arg_count] != NULL) + ++arg_count; + + const char *script_argv[arg_count + 2]; + script_argv[0] = "sh"; + script_argv[1] = buf; + memcpy(script_argv + 2, argv + 1, arg_count * sizeof(char *)); + return execve_hook(false, _PATH_BSHELL, (char **const)script_argv, envp); +} + +int execvpe_hook(bool hook, const char *name, char *const *argv, char *const *envp) { + if (hook) { + logErrorDebug(LOG_TAG, "<----- execvpe() hooked ----->"); + } + + // Do not allow null name. + if (name == NULL || *name == '\0') { + errno = ENOENT; + return -1; + } + + // If it's an absolute or relative path name, it's easy. + if (strchr(name, '/') && execve_hook(false, name, argv, envp) == -1) { + if (errno == ENOEXEC) + return __exec_as_script(name, argv, envp); + return -1; + } + + // Get the path we're searching. + const char *path = getenv("PATH"); + if (path == NULL) + path = _PATH_DEFPATH; + + // Make a writable copy. + size_t len = strlen(path) + 1; + char writable_path[len]; + memcpy(writable_path, path, len); + + bool saw_EACCES = false; + + // Try each element of $PATH in turn... + char *strsep_buf = writable_path; + const char *dir; + while ((dir = strsep(&strsep_buf, ":"))) { + // It's a shell path: double, leading and trailing colons + // mean the current directory. + if (*dir == '\0') + dir = "."; + + size_t dir_len = strlen(dir); + size_t name_len = strlen(name); + + char buf[dir_len + 1 + name_len + 1]; + mempcpy(mempcpy(mempcpy(buf, dir, dir_len), "/", 1), name, name_len + 1); + + execve_hook(false, buf, argv, envp); + switch (errno) { + case EISDIR: + case ELOOP: + case ENAMETOOLONG: + case ENOENT: + case ENOTDIR: + break; + case ENOEXEC: + return __exec_as_script(buf, argv, envp); + return -1; + case EACCES: + saw_EACCES = true; + break; + default: + return -1; + } + } + if (saw_EACCES) + errno = EACCES; + return -1; +} + +int fexecve_hook(bool hook, int fd, char *const *argv, char *const *envp) { + if (hook) { + logErrorDebug(LOG_TAG, "<----- fexecve() hooked ----->"); + } + + char buf[40]; + snprintf(buf, sizeof(buf), "/proc/self/fd/%d", fd); + execve_hook(false, buf, argv, envp); + if (errno == ENOENT) + errno = EBADF; + return -1; +} diff --git a/src/exec/exec_variants.h b/src/exec/exec_variants.h new file mode 100644 index 0000000..96d56e3 --- /dev/null +++ b/src/exec/exec_variants.h @@ -0,0 +1,69 @@ +#ifndef EXEC_VARIANTS_H +#define EXEC_VARIANTS_H + +#include + +/** + * Hooks for all the `exec()` family of functions in `unistd.h`, other + * than `execve()`, whose hook `execve_hook()` is declared in `exec.h`. + * + * - https://man7.org/linux/man-pages/man3/exec.3.html + * + * These `exec()` variants end up calling `execve()` and are ported + * from `libc/bionic/exec.cpp`. + * + * For Android `< 14` intercepting `execve()` was enough. + * For Android `>= 14` requires intercepting the entire `exec()` family + * of functions. It might be related to the `3031a7e4` commit in `bionic`, + * in which `exec.cpp` added `memtag-stack` for `execve()` and shifted + * to calling `__execve()` internally in it. + * + * Hooking the entire family should also solve some issues on older + * Android versions, check `libc/bionic/exec.cpp` git history. + * + * Tests for each `exec` family is done by `run_all_exec_wrappers_test` + * in `runtime-binary-tests.c`. + * + * - https://cs.android.com/android/platform/superproject/+/android-14.0.0_r1:bionic/libc/bionic/exec.cpp + * - https://cs.android.com/android/platform/superproject/+/android-14.0.0_r1:bionic/libc/SYSCALLS.TXT;l=68 + * - https://cs.android.com/android/_/android/platform/bionic/+/3031a7e45eb992d466610bec3bb589c41b74992b + */ + + +enum EXEC_VARIANTS { ExecVE, ExecL, ExecLP, ExecLE, ExecV, ExecVP, ExecVPE, FExecVE }; + +static const char * const EXEC_VARIANTS_STR[] = { + [ExecVE] = "execve", + [ExecL] = "execl", [ExecLP] = "execlp", [ExecLE] = "execle", + [ExecV] = "execv", [ExecVP] = "execvp", [ExecVPE] = "execvpe", + [FExecVE] = "fexecve" +}; + + +/** + * Hook for the `execl()`, `execle()` and `execlp()` functions in `unistd.h` + * which redirects the call to `execve()`. + */ +int execl_hook(bool hook, int variant, const char *name, const char *argv0, va_list ap); + +/** + * Hook for the `execv()` function in `unistd.h` which redirects the call to `execve()`. + */ +int execv_hook(bool hook, const char *name, char *const *argv); + +/** + * Hook for the `execvp()` function in `unistd.h` which redirects the call to `execve()`. + */ +int execvp_hook(bool hook, const char *name, char *const *argv); + +/** + * Hook for the `execvpe()` function in `unistd.h` which redirects the call to `execve()`. + */ +int execvpe_hook(bool hook, const char *name, char *const *argv, char *const *envp); + +/** + * Hook for the `fexecve()` function in `unistd.h` which redirects the call to `execve()`. + */ +int fexecve_hook(bool hook, int fd, char *const *argv, char *const *envp); + +#endif // EXEC_VARIANTS_H diff --git a/src/file/canonicalize.c b/src/file/canonicalize.c new file mode 100644 index 0000000..c602c50 --- /dev/null +++ b/src/file/canonicalize.c @@ -0,0 +1,287 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../data/data_utils.h" +#include "file_utils.h" + +/* + * Copyright (c) 1994, 2010, Oracle and/or its affiliates. + * Copyright (c) 2023 stargateoss. + * + * All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/** + * Pathname canonicalization for Unix file systems. + * - https://cs.android.com/android/platform/superproject/+/android-14.0.0_r1:libcore/ojluni/src/main/native/canonicalize_md.c + */ + +/** + * Check the given name sequence to see if it can be further collapsed. + * Return zero if not, otherwise return the number of names in the sequence. + */ +int __collapsible(char *names) { + char *p = names; + int dots = 0, n = 0; + + while (*p) { + if ((p[0] == '.') && ((p[1] == '\0') + || (p[1] == '/') + || ((p[1] == '.') && ((p[2] == '\0') + || (p[2] == '/'))))) { + dots = 1; + } + n++; + while (*p) { + if (*p == '/') { + while (*p && *p == '/') { + p++; + } + break; + } + p++; + } + } + return (dots ? n : 0); +} + +/** + * Split the names in the given name sequence, replacing slashes with nulls and filling in the given + * index array. + */ +void __splitNames(char *names, char **ix) { + char *p = names; + int i = 0; + + while (*p) { + ix[i++] = p++; + while (*p) { + if (*p == '/') { + while (*p && *p == '/') { + *p++ = '\0'; + } + break; + } + p++; + } + } +} + +/** + * Join the names in the given name sequence, ignoring names whose index entries have been cleared + * and replacing nulls with slashes as needed. + */ +void __joinNames(char *names, int nc, char **ix) { + int i; + char *p; + + for (i = 0, p = names; i < nc; i++) { + if (!ix[i]) continue; + // Do not write to memory before start of path, like for case `././c/./` + if (i > 0 && p > names) { + p[-1] = '/'; + } + if (p == ix[i]) { + p += strlen(p) + 1; + } else { + char *q = ix[i]; + while ((*p++ = *q++)); + } + } + *p = '\0'; +} + + +char* __normalizePath(char* path, bool keepEndSeparator, bool removeDoubleDot) { + //fprintf(stderr, "path='%s'\n", path); + if (path == NULL) { return NULL; } + + size_t pathLength = strlen(path); + size_t originalPathLength = pathLength; + if (pathLength < 1 || strcmp(path, ".") == 0 || strcmp(path, "..") == 0) { return NULL; } + + char originalPath[pathLength + 1]; + if (removeDoubleDot) { + strcpy(originalPath, path); + } + + bool endsWithSeparator = pathLength > 0 && path[pathLength - 1] == '/'; + + // Replace consecutive duplicate separators `//` with a single separator `/`, + // regardless of if single or double dot components exist. + remove_dup_separator(path, true); + if (strcmp(path, "/") == 0) { return path; } + if (strcmp(path, "./") == 0 || strcmp(path, "../") == 0) { return NULL; } + if (strcmp(path, "/..") == 0 || strcmp(path, "/../") == 0) { + path[0] = '/'; path[1] = '\0'; + return path; + } + + + + // Remove single `.` and double dot `..` path components if at least 2 components exist. + bool isAbsolutePath = path[0] == '/'; + char* names = (isAbsolutePath) ? path + 1 : path; // Preserve first '/' + + int nc = __collapsible(names); + + if (nc >= 2) { + int i, j; + + //char** ix = (char **) alloca(nc * sizeof(char *)); + size_t ix_size = (nc * sizeof(char *)); + void* result = malloc(ix_size); + if (result == NULL) { + logStrerrorDebug(LOG_TAG, "The malloc called failed for ix in 'normalizePath(%s)' with size '%zu'", path, ix_size); + return NULL; + } + + char **ix = (char **) result; + + __splitNames(names, ix); + + for (i = 0; i < nc; i++) { + int dots = 0; + + // Find next occurrence of "." or "..". + do { + char *p = ix[i]; // NOLINT + if (p != NULL && p[0] == '.') { + if (p[1] == '\0') { + dots = 1; + break; + } + if ((p[1] == '.') && (p[2] == '\0')) { + dots = 2; + break; + } + } + i++; + } while (i < nc); + if (i >= nc) break; + + // At this point i is the index of either a `.` or a `..`, so take the appropriate action and + // then continue the outer loop. + if (dots == 1) { + // Remove this instance of `.` + ix[i] = NULL; + } else { + if (removeDoubleDot) { + // Remove this instance of `..` and any preceding component if one exists and is possible. + for (j = i - 1; j >= 0; j--) { + if (ix[j]) break; + } + + ix[i] = NULL; + if (j < 0) { + // If a preceding component is not found. + if (!isAbsolutePath) { + // If path is relative, then exit with error since we cannot remove + // unknown components. + free(ix); + return NULL; + } else { + // If path is absolute, then nothing to do, as paths above rootfs + // are to be reset to `/`. + continue; + } + } else { + // If a preceding component is found. + if (j == 0 && string_starts_with(ix[j], "~")) { + // If the first component equals `~` or `~user`, then exit with error + // since we cannot remove unknown components. + free(ix); + return NULL; + } else { + // Remove the component. + ix[j] = NULL; + } + } + + } + } + // i will be incremented at the top of the loop. + } + + __joinNames(names, nc, ix); + free(ix); + + if (keepEndSeparator && endsWithSeparator) { + // Restore trailing `/`. + pathLength = strlen(path); + if (strcmp(path, "/") != 0 && pathLength > 0 && path[pathLength - 1] != '/' && (pathLength + 1) <= originalPathLength) { + path[pathLength] = '/'; + path[pathLength + 1] = '\0'; + } + } + } + + if (*path == '\0') { return NULL; } + //fprintf(stderr, "path1='%s'\n", path); + + + + // Handle cases where only one component exists and it is for a single dot `.` component as above + // logic wouldn't run for it. + + // Remove ./ from start. + if (string_starts_with(path, "./")) { + if (strlen(path) == 2) return NULL; + path = path + 2; + } + + // Remove /. from end. + if (string_ends_with(path, "/.")) { + pathLength = strlen(path); + if (pathLength == 2) { + path[0] = '/'; path[1] = '\0'; + return path; + } + int trim = keepEndSeparator ? 1 : 2; + path[pathLength - trim] = '\0'; + } + + if (strcmp(path, "/./") == 0) { + path[0] = '/'; path[1] = '\0'; + return path; + } + + // If path originally did not have separator at end or if user wants it removed. + if (!endsWithSeparator || !keepEndSeparator) { + // Remove trailing `/`. + pathLength = strlen(path); + if (strcmp(path, "/") != 0 && pathLength > 0 && path[pathLength - 1] == '/') { + path[pathLength - 1] = '\0'; + } + } + + return *path != '\0' ? path : NULL; +} diff --git a/src/file/file_utils.c b/src/file/file_utils.c new file mode 100644 index 0000000..b81b7e0 --- /dev/null +++ b/src/file/file_utils.c @@ -0,0 +1,352 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include + +#include "../logger/logger.h" +#include "file_utils.h" + +static const char* LOG_TAG = "file_utils"; + +#include "canonicalize.c" + +char* absolutize_path(const char *path, char *absolute_path, int buffer_len) { + if (buffer_len < PATH_MAX) { + errno = EINVAL; + return NULL; + } + + if (path == NULL) { + errno = EINVAL; + return NULL; + } + + size_t path_len = strlen(path); + if (path_len < 1) { + errno = EINVAL; + return NULL; + } + + if (path_len >= PATH_MAX) { + errno = ENAMETOOLONG; + return NULL; + } + + if (path[0] != '/') { + char pwd[PATH_MAX]; + if (getcwd(pwd, sizeof(pwd)) == NULL) { + return NULL; + } + + size_t pwd_len = strlen(pwd); + // Following a change in Linux 2.6.36, the pathname returned by the + // getcwd() system call will be prefixed with the string + // "(unreachable)" if the current directory is not below the root + // directory of the current process (e.g., because the process set a + // new filesystem root using chroot(2) without changing its current + // directory into the new root). Such behavior can also be caused + // by an unprivileged user by changing the current directory into + // another mount namespace. When dealing with pathname from + // untrusted sources, callers of the functions described in this + // page should consider checking whether the returned pathname + // starts with '/' or '(' to avoid misinterpreting an unreachable + // path as a relative pathname. + // - https://man7.org/linux/man-pages/man3/getcwd.3.html + if (pwd_len < 1 || pwd[0] != '/') { + errno = ENOENT; + return NULL; + } + + memcpy(absolute_path, pwd, pwd_len); + if (absolute_path[pwd_len - 1] != '/') { + absolute_path[pwd_len] = '/'; + pwd_len++; + } + + size_t absolutized_path_len = pwd_len + path_len; + if (absolutized_path_len >= PATH_MAX) { + errno = ENAMETOOLONG; + return NULL; + } + + memcpy(absolute_path + pwd_len, path, path_len); + absolute_path[absolutized_path_len] = '\0'; + } else { + strcpy(absolute_path, path); + } + + return absolute_path; +} + + + + + +char* normalize_path(char* path, bool keepEndSeparator, bool removeDoubleDot) { + return __normalizePath(path, keepEndSeparator, removeDoubleDot); +} + + + + +char* remove_dup_separator(char* path, bool keepEndSeparator) { + if (path == NULL || *path == '\0') { + return NULL; + } + + char* in = path; + char* out = path; + char prevChar = 0; + int n = 0; + for (; *in != '\0'; in++) { + // Remove duplicate path separators. + if (!(*in == '/' && prevChar == '/')) { + *(out++) = *in; + n++; + } + prevChar = *in; + } + *out = '\0'; + + // Remove the trailing path separator, except when path equals `/`. + if (!keepEndSeparator && prevChar == '/' && n > 1) { + *(--out) = '\0'; + } + + return path; +} + + + + + +char* get_fd_realpath(const char* log_tag, const char *path, char *real_path, size_t buffer_len) { + char path_string[strlen(path) + 1]; + strcpy(path_string, path); + char* fd_string = basename(path_string); + + int fd = string_to_int(fd_string, -1, log_tag, "Failed to convert fd string '%s' to fd for fd path '%s'", fd_string, path); + if (fd < 0) { + return NULL; + } + + #ifndef TERMUX_EXEC__RUNNING_TESTS + logErrorVVerbose(log_tag, "fd_path: '%s', fd: '%d'", path, fd); + #endif + + struct stat fd_statbuf; + int fd_result = fstat(fd, &fd_statbuf); + if (fd_result < 0) { + logStrerrorDebug(log_tag, "Failed to stat fd '%d' for fd path '%s'", fd, path); + return NULL; + } + + ssize_t length = readlink(path, real_path, buffer_len - 1); + if (length < 0) { + logStrerrorDebug(log_tag, "Failed to get real path for fd path '%s'", path); + return NULL; + } else { + real_path[length] = '\0'; + } + + #ifndef TERMUX_EXEC__RUNNING_TESTS + logErrorVVerbose(log_tag, "real_path: '%s'", real_path); + #endif + + // Check if fd is for a regular file. + // We should log real path first before doing this check. + if (!S_ISREG(fd_statbuf.st_mode)) { + #ifndef TERMUX_EXEC__RUNNING_TESTS + logErrorDebug(log_tag, "The real path '%s' for fd path '%s' is of type '%d' instead of a regular file", + real_path, path, fd_statbuf.st_mode & S_IFMT); + #endif + errno = ENOEXEC; + return NULL; + } + + size_t real_path_length = strlen(real_path); + if (real_path_length < 1 || real_path[0] != '/') { + logErrorDebug(log_tag, "A non absolute real path '%s' returned for fd path '%s'", real_path, path); + errno = ENOEXEC; + return NULL; + } + + struct stat path_statbuf; + int path_result = stat(real_path, &path_statbuf); + if (path_result < 0) { + logStrerrorDebug(log_tag, "Failed to stat real path '%s' returned for fd path '%s'", real_path, path); + return NULL; + } + + // If the original file when fd was opened has now been replaced with a different file. + if (fd_statbuf.st_dev != path_statbuf.st_dev || fd_statbuf.st_ino != path_statbuf.st_ino) { + logErrorDebug(log_tag, "The file at real path '%s' is not for the original fd '%d'", real_path, fd); + errno = ENXIO; + return NULL; + } + + return real_path; +} + + + + + +bool is_path_in_dir_path(const char* label, const char* path, const char* dir_path, bool ensure_under) { + if (path == NULL || *path != '/' || dir_path == NULL || *dir_path != '/' ) { + return 1; + } + + // If root `/`, preserve it as is, otherwise append `/` to dir_path. + char *dir_sub_path; + bool is_rootfs = strcmp(dir_path, "/") == 0; + if (asprintf(&dir_sub_path, is_rootfs ? "%s" : "%s/", dir_path) == -1) { + errno = ENOMEM; + logStrerrorDebug(LOG_TAG, "asprintf failed while checking if the path '%s' is under %s '%s'", path, label, dir_path); + return -1; + } + + int result; + if (ensure_under) { + result = strcmp(dir_sub_path, path) != 0 && string_starts_with(path, dir_sub_path) ? 0 : 1; + } else { + result = strcmp(dir_sub_path, path) == 0 || string_starts_with(path, dir_sub_path) ? 0 : 1; + } + + free(dir_sub_path); + + return result; +} + +int is_path_or_fd_path_in_dir_path(const char* log_tag, const char* label, + const char *path, const char *dir_path, bool ensure_under) { + if (path == NULL || *path == '\0') { + return 1; + } + + if (strstr(path, "/fd/") != NULL && regex_match(path, REGEX__PROC_FD_PATH, REG_EXTENDED) == 0) { + char real_path_buffer[PATH_MAX]; + (void)real_path_buffer; + const char* real_path = get_fd_realpath(log_tag, path, real_path_buffer, sizeof(real_path_buffer)); + if (real_path == NULL) { + return -1; + } + + return is_path_in_dir_path(label, real_path, dir_path, ensure_under); + } else { + return is_path_in_dir_path(label, path, dir_path, ensure_under); + } +} + + + + + +int get_path_from_env(const char* log_tag, const char* label, + const char * env_variable_name, size_t env_path_max_len, + char *buffer, size_t buffer_len) { + (void)label; + (void)LOG_TAG; + (void)log_tag; + + const char* env_path = getenv(env_variable_name); + if (env_path != NULL) { + size_t env_path_len = strlen(env_path); + if (env_path_len > 0) { + if (env_path[0] == '/' && (env_path_max_len < 1 || env_path_len < env_path_max_len)) { + if (buffer_len <= env_path_len) { + #ifndef TERMUX_EXEC__RUNNING_TESTS + logErrorDebug(LOG_TAG, "The %s '%s' with length '%zu' is too long to fit in the buffer with length '%zu'", + label, env_path, env_path_len, buffer_len); + #endif + errno = EINVAL; + return -1; + } + + strcpy(buffer, env_path); + return 0; + } + + #ifndef TERMUX_EXEC__RUNNING_TESTS + logErrorVVerbose(log_tag, "Ignoring invalid %s with length '%zu' set in '%s' env variable: '%s'", + label, env_path_len, env_variable_name, env_path); + if (env_path_max_len >= 1) { + logErrorVVerbose(log_tag, "The %s must be an absolute path starting with a '/' with max length '%d' including the null '\0' terminator", + label, env_path_max_len); + } else { + logErrorVVerbose(log_tag, "The %s must be an absolute path starting with a '/'", + label); + } + #endif + } + } + + return 1; +} + +const char* get_path_from_env_or_default(const char* log_tag, const char* label, + const char * env_variable_name, size_t env_path_max_len, + const char* default_path, int default_path_access_check_type, + char *buffer, size_t buffer_len) { + + const char *path; + int result = get_path_from_env(log_tag, label, + env_variable_name, env_path_max_len, buffer, buffer_len); + if (result < 0) { + return NULL; + } else if (result == 0) { + normalize_path(buffer, false, true); + path = buffer; + logErrorVVerbose(log_tag, "%s: '%s'", label, path); + return path; + } + + // Use default path. + size_t path_len = strlen(default_path); + if (default_path[0] != '/' || buffer_len <= path_len) { + logErrorDebug(log_tag, "The default_%s '%s' with length '%zu' must be an absolute path starting with a '/' with max length '%zu'", + label, default_path, path_len, buffer_len); + errno = EINVAL; + return NULL; + } + + strcpy(buffer, default_path); + normalize_path(buffer, false, true); + path = default_path; + logErrorVVerbose(log_tag, "default_%s: '%s'", label, path); + + // Check default path access if required. + if (default_path_access_check_type >= 0) { + if (default_path_access_check_type == 0) { + if (access(path, F_OK) != 0) { + logStrerrorDebug(log_tag, "The default_%s '%s' does not exist", label, default_path); + return NULL; + } + } else if (default_path_access_check_type == 1) { + if (access(path, R_OK) != 0) { + logStrerrorDebug(log_tag, "The default_%s '%s' is not readable", label, default_path); + return NULL; + } + } else if (default_path_access_check_type == 2) { + if (access(path, (R_OK | W_OK)) != 0) { + logStrerrorDebug(log_tag, "The default_%s '%s' is not readable or writable", label, default_path); + return NULL; + } + } else if (default_path_access_check_type == 3) { + if (access(path, (R_OK | X_OK)) != 0) { + logStrerrorDebug(log_tag, "The default_%s '%s' is not readable or executable", label, default_path); + return NULL; + } + } else if (default_path_access_check_type == 4) { + if (access(path, (R_OK | W_OK | X_OK)) != 0) { + logStrerrorDebug(log_tag, "The default_%s '%s' is not readable, writable or executable", label, default_path); + return NULL; + } + } + } + + return path; +} diff --git a/src/file/file_utils.h b/src/file/file_utils.h new file mode 100644 index 0000000..12fca81 --- /dev/null +++ b/src/file/file_utils.h @@ -0,0 +1,271 @@ +#ifndef FILE_UTILS_H +#define FILE_UTILS_H + +#include + + + +/** + * Regex to match fd paths `/proc/self/fd/` and `/proc//fd/`. + */ +#define REGEX__PROC_FD_PATH "^((/proc/(self|[0-9]+))|(/dev))/fd/[0-9]+$" + + + + + +/** + * Absolutize a unix path with the current working directory (`cwd`) + * as prefix for the path if its not absolute. + */ +char* absolutize_path(const char *path, char *absolute_path, int buffer_len); + + + + + +/** + * Normalize a unix path. This won't normalize a windows path with + * backslashes and other restrictions. + * + * - Replaces consecutive duplicate path separators `//` with a single + * path separator `/`. + * - Removes trailing path separator `/` if `keepEndSeparator` is not + * `true`. + * - Replaces `/./` with `/`. + * - Removes `./` from start. + * - Removes `/.` from end. + * - Optionally removes `/../` and `/..` along with the parent + * directories equal to count of `..`. + * + * - If path is `null`, empty or equals `.`, then `null` will be + * returned. + * - If path is empty after removals, then `null` will be returned. + * - If double dots `..` need to be removed and no parent directory + * path components exist in the path that can be removed, then + * - If path is an absolute path, then path above rootfs will reset + * to `/` and any remaining path will be kept as well, like + * `/a/b/../../../c` will be resolved to `/c`. + * This behaviour is same as {@link #canonicalizePath(char*, char*, size_t)} + * and `realpath`. + * Note that [`org.apache.commons.io.FilenameUtils#normalize(String)`] + * will return `null`, but this implementation will not since it + * makes more sense to reset path to `/` for unix filesystems. + * See also `CVE-2021-29425` for a path traversal vulnerability + * for `//../foo` in `commons.io` that was fixed in `2.7`. + * - https://nvd.nist.gov/vuln/detail/cve-2021-29425 + * - https://issues.apache.org/jira/browse/IO-556 + * - https://issues.apache.org/jira/browse/IO-559 + * - https://github.com/apache/commons-io/commit/2736b6fe + * - If path is relative like `../` or starts with `~/` or `~user/`, + * then `null` will be returned, as unknown path components cannot + * be removed from a path. + * - The path separator `/` will not be added to home `~` and `~user` + * if it doesn't already exist like + * [`org.apache.commons.io.FilenameUtils#normalize(String)`] does. + * See also `getUnixPathPrefixLength(String)` in `FileUtils.java`. + * + * Following examples assume `keepEndSeparator` is `false` and + * `removeDoubleDot` is `true`. Check `runNormalizeTests()` in + * `FileUtilsTestsProvider.java` for more examples. + * + * ``` + * null -> null + * `` -> null + * `/` -> `/` + * + * `:` -> `:` + * `1:/a/b/c.txt` -> `1:/a/b/c.txt` + * `///a//b///c/d///` -> `/a/b/c/d` + * `/foo//` -> `/foo` + * `/foo/./` -> `/foo` + * `//foo//./bar` -> `/foo/bar` + * `~` -> `~` + * `~/` -> `~` + * `~user/` -> `~user` + * `~user` -> `~user` + * `~user/a` -> `~user/a` + * + * `.` -> null + * `./` -> null + * `/.` -> `/` + * `/./` -> `/` + * `././` -> null + * `/./.` -> `/` + * `/././a` -> `/a` + * `/./b/./` -> `/b` + * `././c/./` -> `c` + * + * `..` -> null + * `../` -> null + * `/..` -> `/` + * `/../` -> `/` + * `/../a` -> `/a` + * `../a/.` -> null + * `../a` -> null + * `/a/b/..` -> `/a` + * `/a/b/../..` -> `/` + * `/a/b/../../..` -> `/` + * `/a/b/../../../c` -> `/c` + * `./../a` -> null + * `././../a` -> null + * `././.././a` -> null + * `a/../../b` -> null + * `a/./../b` -> `b` + * `a/./.././b` -> `b` + * `foo/../../bar` -> null + * `/foo/../bar` -> `/bar` + * `/foo/../bar/` -> `/bar` + * `/foo/../bar/../baz` -> `/baz` + * `foo/bar/..` -> `foo` + * `foo/../bar` -> `bar` + * `~/..` -> null + * `~/../` -> null + * `~/../a/.` -> null + * `~/..` -> null + * `~/foo/../bar/` -> `~/bar` + * `C:/foo/../bar` -> `C:/bar` + * `//server/foo/../bar` -> `/server/bar` + * `//server/../bar` -> `/bar` + * ``` + * + * [`org.apache.commons.io.FilenameUtils#normalize(String)`]: https://commons.apache.org/proper/commons-io/apidocs/org/apache/commons/io/FilenameUtils.html#normalize-java.lang.String- + * + * @param path The `path` to normalize. + * @param keepEndSeparator Whether to keep path separator `/` at end if it exists. + * @param removeDoubleDot If set to `true`. then double dots `..` will be removed along with the + * parent directory for each `..` component. + * @return Returns the `path` passed as `normalized path`. + */ +char* normalize_path(char* path, bool keepEndSeparator, bool removeDoubleDot); + + + +/** + * Remove consecutive duplicate path separators `//` and the trailing + * path separator if not rootfs `/`. + * + * @param path The `path` to remove from. + * @param keepEndSeparator Whether to keep path separator `/` at end if it exists. + * @return Returns the path with consecutive duplicate path separators removed. + */ +char* remove_dup_separator(char* path, bool keepEndSeparator); + + + + + +/** + * Get the real path of an fd `path`. + * + * **The `path` passed must match `REGEX__PROC_FD_PATH`, as no checks + * are done by this function for whether path is for a fd path. If + * an integer is set to the path basename, then it is assumed to be the + * fd number, even if it is for a valid fd to be checked or not.** + * + * - https://stackoverflow.com/questions/1188757/retrieve-filename-from-file-descriptor-in-c + */ +char* get_fd_realpath(const char* log_tag, const char *path, char *real_path, + size_t buffer_len); + + + + + +/** + * Check whether the `path` is in `dir_path`. + * + * @param label The label for errors. + * @param path The `path` to check. + * @param dir_path The `directory path` to check in. This must be an + * absolute path. + * @param ensure_under If set to `true`, then it will be ensured that + * `path` is under the directory and does not + * equal it. If set to `false`, it can either + * equal the directory path or be under it. + * @return Returns `0` if `path` is in `dir_path`, `1` if `path` is + * not in `dir_path`, otherwise `-1` on other failures. + */ +bool is_path_in_dir_path(const char* label, const char* path, const char* dir_path, bool ensure_under); + +/** + * Check whether the `path` or a fd path is in `dir_path`. + * + * If path is a fd path matched by `REGEX__PROC_FD_PATH`, then the + * real path of the fd returned by `get_fd_realpath()` will be checked + * instead. + * + * This is a wrapper for `is_path_in_dir_path(const char*, const char*, const char*, bool)` + * + * @param log_tag The log tag to use for logging. + * @param label The label for errors. + * @param path The `path` to check. + * @param dir_path The `directory path` to check in. This must be an + * absolute path. + * @param ensure_under If set to `true`, then it will be ensured that + * `path` is under the directory and does not + * equal it. If set to `false`, it can either + * equal the directory path or be under it. + * @return Returns `0` if `path` is in `dir_path`, `1` if `path` is + * not in `dir_path`, otherwise `-1` on other failures. + */ +int is_path_or_fd_path_in_dir_path(const char* log_tag, const char* label, + const char *path, const char *dir_path, bool ensure_under); + + + + + +/** + * Get a path from an environment variable if its set to a valid + * absolute path, optionally with max length `env_path_max_len`. + * + * If `env_path_max_len` is `<= 0`, then max length check is ignored, + * but path length must still be `<= buffer_len` including the null + * `\0` terminator. + * + * @param log_tag The log tag to use for logging. + * @param label The label for errors. + * @param env_variable_name The environment variable name from which + * to read the path. + * @param env_path_max_len The max length for the path. + * @param buffer The output path buffer. + * @param buffer_len The output path buffer length. + * @return Returns `0` if a valid path is found and copied to the + * buffer, `1` if valid path is not found, otherwise `-1` on other + * failures. + */ +int get_path_from_env(const char* log_tag, const char* label, + const char * env_variable_name, size_t env_path_max_len, + char *buffer, size_t buffer_len); + +/** + * Get a path from an environment variable by calling + * `get_path_from_env()`, otherwise if it fails, then return + * `default_path` passed. + * + * @param log_tag The log tag to use for logging. + * @param label The label for errors. + * @param env_variable_name The environment variable name from which + * to read the path. + * @param env_path_max_len The max length for the path. + * @param default_path The default path to return if reading it from + * the environment variable fails. + * @param default_path_access_check_type The optional `access()` + * system call check to perform if default path + * is to be returned. Valid values are: + * - `0`: `F_OK` + * - `1`: `R_OK` + * - `2`: `R_OK | W_OK` + * - `3`: `R_OK | X_OK` + * - `4`: `R_OK | W_OK | X_OK` + * @param buffer The output path buffer. + * @param buffer_len The output path buffer length. + * @return Returns the `char *` to path on success, otherwise `NULL`. + */ +const char* get_path_from_env_or_default(const char* log_tag, const char* label, + const char * env_variable_name, size_t env_path_max_len, + const char* default_path, int default_path_access_check_type, + char *buffer, size_t buffer_len); + +#endif // FILE_UTILS_H diff --git a/src/logger/file_logger_impl.c b/src/logger/file_logger_impl.c new file mode 100644 index 0000000..1055f7b --- /dev/null +++ b/src/logger/file_logger_impl.c @@ -0,0 +1,99 @@ +#define _GNU_SOURCE +#include +#include +#include +#include + +#include + +#include "logger.h" +#include "file_logger_impl.h" +#include "../os/safe_strerror.h" + +const struct ILogger sFileLoggerImpl = { + .printMessage = printMessageToFile, +}; + + +static FILE *sLogFileStream = NULL; + +static char sLogFilePathBuffer[PATH_MAX]; + +/** The log file path. */ +static const char* sLogFilePath = NULL; + +static bool sWarnNoLogFileSet = true; + + +void printMessageToFile(bool logOnStderr, const char *message) { + (void)logOnStderr; + + if (sLogFileStream == NULL) { + if (sWarnNoLogFileSet) { + fprintf(stderr, "No log file set"); + sWarnNoLogFileSet = false; + } + return; + } + + fprintf(sLogFileStream, "%s", message); + fflush(sLogFileStream); +} + + + +FILE* getLogFileStream() { + return sLogFileStream; +} + +const char* getLogFilePath() { + return sLogFilePath; +} + +int setLogFilePath(const char* logFilePath) { + sWarnNoLogFileSet = true; + closeLogFile(); + + if (logFilePath == NULL) { + return 0; + } + + size_t logFilePathLength = strlen(logFilePath); + if (logFilePathLength < 1) { + return 0; + } + + if (sizeof(sLogFilePathBuffer) <= logFilePathLength) { + fprintf(stderr, "The log file path '%s' with length '%zu' is too long to fit in the buffer with length '%zu'", + logFilePath, logFilePathLength, sizeof(sLogFilePathBuffer)); + closeLogFile(); + return -1; + } + + strcpy(sLogFilePathBuffer, logFilePath); + sLogFilePath = sLogFilePathBuffer; + + sLogFileStream = fopen(sLogFilePath, "w"); + if (sLogFileStream == NULL) { + char strerrorBuffer[STRERROR_BUFFER_SIZE]; + safe_strerror_r(errno, strerrorBuffer, sizeof(strerrorBuffer)); + + fprintf(stderr, "Failed to open log file '%s': %s\n", sLogFilePath, strerrorBuffer); + closeLogFile(); + return -1; + } + + sWarnNoLogFileSet = false; + + return 0; +} + +int closeLogFile() { + if (sLogFileStream != NULL) { + fclose(sLogFileStream); + sLogFileStream = NULL; + } + sLogFilePath = NULL; + sLogFilePathBuffer[0] = '\0'; + return 0; +} diff --git a/src/logger/file_logger_impl.h b/src/logger/file_logger_impl.h new file mode 100644 index 0000000..be91425 --- /dev/null +++ b/src/logger/file_logger_impl.h @@ -0,0 +1,17 @@ +#ifndef FILE_LOGGER_IMPL_H +#define FILE_LOGGER_IMPL_H + +#include + +#include "logger.h" + +extern const struct ILogger sFileLoggerImpl; + +void printMessageToFile(bool logOnStderr, const char* message); + +FILE* getLogFileStream(); +const char* getLogFilePath(); +int setLogFilePath(const char* logFilePath); +int closeLogFile(); + +#endif // FILE_LOGGER_IMPL_H diff --git a/src/logger/logger.c b/src/logger/logger.c new file mode 100644 index 0000000..243c015 --- /dev/null +++ b/src/logger/logger.c @@ -0,0 +1,387 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include + +#include + +#include "../os/safe_strerror.h" +#include "logger.h" +#include "standard_logger_impl.h" + +static char sDefaultLogTagBuffer[LOGGER_TAG_MAX_LENGTH] = "Logger"; + +/** The default log tag used if log methods that do not require a `tag` are called. */ +static const char* sDefaultLogTag = sDefaultLogTagBuffer; + +static char sLogTagPrefixBuffer[LOGGER_TAG_MAX_LENGTH] = ""; + +/** The log tag prefixed before the `tag` passed to log methods that require a `tag` by `getFullLogTag()`. */ +static const char* sLogTagPrefix = sLogTagPrefixBuffer; + +/** The current log level. */ +static int sCurrentLogLevel = DEFAULT_LOG_LEVEL; + + +/** The `ILogger` implementation to use for logging. */ +static const struct ILogger* sLoggerImpl = &sStandardLoggerImpl; + + +/** + * The log format mode for log entries as per `LOG_FORMAT_MODE__*`. + */ +static int sLogFormatMode = LOG_FORMAT_MODE__PID_PRIORITY_TAG_AND_MESSAGE; + + +/** + * The pid of current process. + * + * - https://man7.org/linux/man-pages/man2/getpid.2.html + * - https://cs.android.com/android/platform/superproject/+/android-13.0.0_r54:bionic/libc/bionic/pthread_internal.h + * - https://cs.android.com/android/platform/superproject/+/android-13.0.0_r54:bionic/libc/bionic/getpid.cpp + */ +static pid_t sLogPid = -1; + +/** + * Whether to cache `sLogPid` that is used for + * `LOG_FORMAT_MODE__PID_PRIORITY_TAG_AND_MESSAGE`. + * + * If enabled, and a process forks a new process, the cached pid value + * of the parent will be cloned to the child and log entries will + * contain wrong value in the child. So if enabling caching, then + * call `updateLogPid()` on each fork manually to update the value. + * + * By default caching is disabled. + */ +static bool sCacheLogPid = false; + + + +static const char* getFullLogTag(const char* tag, char *buffer, size_t buffer_len); + + + + + +int getCurrentLogLevel() { + return sCurrentLogLevel; +} + +int setCurrentLogLevel(int currentLogLevel) { + if (!isLogLevelValid(currentLogLevel)) { + return sCurrentLogLevel; + } + + sCurrentLogLevel = getLogLevelIfValidOtherwiseDefault(currentLogLevel); + return sCurrentLogLevel; +} + + +bool isLogLevelValid(int logLevel) { + return (logLevel >= MIN_LOG_LEVEL && logLevel <= MAX_LOG_LEVEL); +} + +int getLogLevelIfValidOtherwiseDefault(int logLevel) { + return isLogLevelValid(logLevel) ? logLevel : DEFAULT_LOG_LEVEL; +} + + + +void setDefaultLogTagAndPrefix(const char* defaultLogTag) { + if (defaultLogTag == NULL || strlen(defaultLogTag) < 1) return; + setDefaultLogTag(defaultLogTag); + + char logTagPrefix[strlen(defaultLogTag) + 2]; + snprintf(logTagPrefix, sizeof(logTagPrefix), "%s.", defaultLogTag); + setLogTagPrefix(logTagPrefix); +} + + +const char* getDefaultLogTag() { + return sDefaultLogTag; +} + +void setDefaultLogTag(const char* defaultLogTag) { + if (defaultLogTag == NULL) return; + + size_t defaultLogTagLength = strlen(defaultLogTag); + if (defaultLogTagLength < 1 || sizeof(sDefaultLogTagBuffer) <= defaultLogTagLength) return; + + strcpy(sDefaultLogTagBuffer, defaultLogTag); +} + + +const char* getLogTagPrefix() { + return sLogTagPrefix; +} + +void setLogTagPrefix(const char* logTagPrefix) { + if (logTagPrefix == NULL) return; + + size_t logTagPrefixLength = strlen(logTagPrefix); + if (sizeof(sLogTagPrefixBuffer) <= logTagPrefixLength) return; + + strcpy(sLogTagPrefixBuffer, logTagPrefix); +} + + +const struct ILogger* getLoggerImpl() { + return sLoggerImpl; +} + +void setLoggerImpl(const struct ILogger* loggerImpl) { + if (loggerImpl != NULL) + sLoggerImpl = loggerImpl; +} + + +int getLogFormatMode() { + return sLogFormatMode; +} + +void setLogFormatMode(int logFormatMode) { + if (logFormatMode == LOG_FORMAT_MODE__MESSAGE || \ + logFormatMode == LOG_FORMAT_MODE__TAG_AND_MESSAGE || \ + logFormatMode == LOG_FORMAT_MODE__PID_PRIORITY_TAG_AND_MESSAGE) { + sLogFormatMode = logFormatMode; + } +} + + +bool getCacheLogPid() { + return sCacheLogPid; +} + +void setCacheLogPid(bool cacheLogPid) { + sCacheLogPid = cacheLogPid; +} + + + +pid_t getLogPid() { + if (sCacheLogPid) { + if (sLogPid < 0) + sLogPid = getpid(); + return sLogPid; + } else { + return getpid(); + } +} + +void updateLogPid() { + sLogPid = getpid(); +} + + + + + +static void printMessage(char logPriorityChar, bool logOnStderr, + const char* tag, const char* fmt, va_list args) { + int errnoCode = errno; + + int logFormatMode = getLogFormatMode(); + + char finalMessage[LOGGER_ENTRY_MAX_PAYLOAD]; + + if (logFormatMode == LOG_FORMAT_MODE__MESSAGE) { + char currentMessage[LOGGER_ENTRY_MAX_PAYLOAD - 1]; + vsnprintf(currentMessage, sizeof(currentMessage), fmt, args); + + snprintf(finalMessage, sizeof(finalMessage), "%s\n", currentMessage); + } else if (logFormatMode == LOG_FORMAT_MODE__TAG_AND_MESSAGE) { + const char* finalTag = tag != NULL ? tag : "null"; + + char currentMessage[LOGGER_ENTRY_MAX_PAYLOAD - LOGGER_TAG_MAX_LENGTH - 2 - 1]; + vsnprintf(currentMessage, sizeof(currentMessage), fmt, args); + + snprintf(finalMessage, sizeof(finalMessage), "%-8s: %s\n", + finalTag, currentMessage); + } else if (logFormatMode == LOG_FORMAT_MODE__PID_PRIORITY_TAG_AND_MESSAGE) { + const char* finalTag = tag != NULL ? tag : "null"; + + char currentMessage[LOGGER_ENTRY_MAX_PAYLOAD - 5 - 1 - 1 -1 - LOGGER_TAG_MAX_LENGTH - 2 - 1]; + vsnprintf(currentMessage, sizeof(currentMessage), fmt, args); + + snprintf(finalMessage, sizeof(finalMessage), "%5d %c %-8s: %s\n", + getLogPid(), logPriorityChar, finalTag, currentMessage); + } + + sLoggerImpl->printMessage(logOnStderr, finalMessage); + + errno = errnoCode; +} + +#define ILOGGER_IMPL(logPriorityName, logPriorityChar, logOnStderr) \ +void logVPrint##logPriorityName(const char* tag, const char* fmt, va_list args) { \ + printMessage(logPriorityChar, logOnStderr, tag, fmt, args); \ +} + +ILOGGER_IMPL(Error, 'E', true) +ILOGGER_IMPL(Warn, 'W', true) +ILOGGER_IMPL(Info, 'I', false) +ILOGGER_IMPL(Debug, 'D', false) +ILOGGER_IMPL(Verbose, 'V', false) + +#undef ILOGGER_IMPL + + + + + +void logMessage(int logPriority, const char* tag, const char* fmt, va_list args) { + char tag_buffer[LOGGER_TAG_MAX_LENGTH]; + + if (logPriority == LOG_PRIORITY__ERROR && sCurrentLogLevel >= LOG_LEVEL__NORMAL) { + logVPrintError(getFullLogTag(tag, tag_buffer, sizeof(tag_buffer)), fmt, args); + } else if (logPriority == LOG_PRIORITY__WARN && sCurrentLogLevel >= LOG_LEVEL__NORMAL) { + logVPrintWarn(getFullLogTag(tag, tag_buffer, sizeof(tag_buffer)), fmt, args); + } else if (logPriority == LOG_PRIORITY__INFO && sCurrentLogLevel >= LOG_LEVEL__NORMAL) { + logVPrintInfo(getFullLogTag(tag, tag_buffer, sizeof(tag_buffer)), fmt, args); + } else if (logPriority == LOG_PRIORITY__DEBUG && sCurrentLogLevel >= LOG_LEVEL__DEBUG) { + logVPrintDebug(getFullLogTag(tag, tag_buffer, sizeof(tag_buffer)), fmt, args); + } else if (logPriority == LOG_PRIORITY__VERBOSE && sCurrentLogLevel >= LOG_LEVEL__VERBOSE) { + logVPrintVerbose(getFullLogTag(tag, tag_buffer, sizeof(tag_buffer)), fmt, args); + } else if (logPriority == LOG_PRIORITY__VVERBOSE && sCurrentLogLevel >= LOG_LEVEL__VVERBOSE) { + logVPrintVerbose(getFullLogTag(tag, tag_buffer, sizeof(tag_buffer)), fmt, args); + } else if (logPriority == LOG_PRIORITY__VVVERBOSE && sCurrentLogLevel >= LOG_LEVEL__VVVERBOSE) { + logVPrintVerbose(getFullLogTag(tag, tag_buffer, sizeof(tag_buffer)), fmt, args); + } +} + + + +/** + * Get full log tag to use for logging for a `tag`. + * + * If `tag` equals `sDefaultLogTag` or `sLogTagPrefix` is empty, then + * `tag` is returned as is, otherwise `sLogTagPrefix` is prefixed + * before `tag`. + * If `tag` is `null` or empty, then `sDefaultLogTag` is returned. + */ +static const char* getFullLogTag(const char* tag, char *buffer, size_t buffer_len) { + if (tag == NULL || strlen(tag) < 1) { + return sDefaultLogTag; + } else if (strcmp(sDefaultLogTag, tag) == 0 || strlen(sLogTagPrefix) < 1) { + return tag; + } else { + snprintf(buffer, buffer_len, "%s%s", sLogTagPrefix, tag); + return buffer; + } +} + + + + + +#define LOG_MESSAGE_IMPL(logPriorityName, logPriorityNum, logLevel) \ +void log##logPriorityName(const char* tag, const char* fmt, ...) { \ + if (sCurrentLogLevel >= logLevel) { \ + va_list args; \ + va_start(args, fmt); \ + logMessage(logPriorityNum, tag, fmt, args); \ + va_end(args); \ + } \ +} + +LOG_MESSAGE_IMPL(Error, LOG_PRIORITY__ERROR, LOG_LEVEL__NORMAL) +LOG_MESSAGE_IMPL(Warn, LOG_PRIORITY__WARN, LOG_LEVEL__NORMAL) +LOG_MESSAGE_IMPL(Info, LOG_PRIORITY__INFO, LOG_LEVEL__NORMAL) +LOG_MESSAGE_IMPL(Debug, LOG_PRIORITY__DEBUG, LOG_LEVEL__DEBUG) +LOG_MESSAGE_IMPL(Verbose, LOG_PRIORITY__VERBOSE, LOG_LEVEL__VERBOSE) +LOG_MESSAGE_IMPL(VVerbose, LOG_PRIORITY__VVERBOSE, LOG_LEVEL__VVERBOSE) +LOG_MESSAGE_IMPL(VVVerbose, LOG_PRIORITY__VVVERBOSE, LOG_LEVEL__VVVERBOSE) + +#undef LOG_MESSAGE_IMPL + + + + + +static bool logErrorForLogLevel(int logLevel, const char* tag, const char* fmt, va_list args) { + if (sCurrentLogLevel >= logLevel) { + logMessage(LOG_PRIORITY__ERROR, tag, fmt, args); + return true; + } + return false; +} + + + +#define LOG_ERROR_MESSAGE_FOR_LOG_LEVEL_IMPL(logPriorityName, logLevel) \ +bool logError##logPriorityName(const char* tag, const char* fmt, ...) { \ + if (sCurrentLogLevel >= logLevel) { \ + va_list args; \ + va_start(args, fmt); \ + bool logged = logErrorForLogLevel(logLevel, tag, fmt, args); \ + va_end(args); \ + return logged; \ + } \ + return false; \ +} + +LOG_ERROR_MESSAGE_FOR_LOG_LEVEL_IMPL(Debug, LOG_LEVEL__DEBUG) +LOG_ERROR_MESSAGE_FOR_LOG_LEVEL_IMPL(Verbose, LOG_LEVEL__VERBOSE) +LOG_ERROR_MESSAGE_FOR_LOG_LEVEL_IMPL(VVerbose, LOG_LEVEL__VVERBOSE) +LOG_ERROR_MESSAGE_FOR_LOG_LEVEL_IMPL(VVVerbose, LOG_LEVEL__VVVERBOSE) +LOG_ERROR_MESSAGE_FOR_LOG_LEVEL_IMPL(Private, LOG_LEVEL__DEBUG) + +#undef LOG_ERROR_MESSAGE_FOR_LOG_LEVEL_IMPL + + + + + +void logStrerrorMessage(int errnoCode, const char* tag, const char* fmt, va_list args) { + if (errnoCode == 0) { + logMessage(LOG_PRIORITY__ERROR, tag, fmt, args); + } else { + char strerrorBuffer[STRERROR_BUFFER_SIZE]; + safe_strerror_r(errnoCode, strerrorBuffer, sizeof(strerrorBuffer)); + + size_t strerror_length = strlen(strerrorBuffer); + + char currentMessage[LOGGER_ENTRY_MAX_PAYLOAD - strerror_length - 1]; + vsnprintf(currentMessage, sizeof(currentMessage), fmt, args); + + char newMessage[strlen(currentMessage) + strerror_length + 2 + 1]; + snprintf(newMessage, sizeof(newMessage), "%s: %s", currentMessage, strerrorBuffer); + + logError(tag, "%s", newMessage); + } +} + +void logStrerror(const char* tag, const char* fmt, ...) { + int errnoCode = errno; + va_list args; + va_start(args, fmt); + logStrerrorMessage(errnoCode, tag, fmt, args); + va_end(args); + errno = errnoCode; +} + + + +#define LOG_STRERROR_MESSAGE_FOR_LOG_LEVEL_IMPL(logPriorityName, logLevel) \ +bool logStrerror##logPriorityName(const char* tag, const char* fmt, ...) { \ + if (sCurrentLogLevel >= logLevel) { \ + int errnoCode = errno; \ + va_list args; \ + va_start(args, fmt); \ + logStrerrorMessage(errnoCode, tag, fmt, args); \ + va_end(args); \ + errno = errnoCode; \ + return true; \ + } \ + return false; \ +} + +LOG_STRERROR_MESSAGE_FOR_LOG_LEVEL_IMPL(Debug, LOG_LEVEL__DEBUG) +LOG_STRERROR_MESSAGE_FOR_LOG_LEVEL_IMPL(Verbose, LOG_LEVEL__VERBOSE) +LOG_STRERROR_MESSAGE_FOR_LOG_LEVEL_IMPL(VVerbose, LOG_LEVEL__VVERBOSE) +LOG_STRERROR_MESSAGE_FOR_LOG_LEVEL_IMPL(VVVerbose, LOG_LEVEL__VVVERBOSE) +LOG_STRERROR_MESSAGE_FOR_LOG_LEVEL_IMPL(Private, LOG_LEVEL__DEBUG) + +#undef LOG_STRERROR_MESSAGE_FOR_LOG_LEVEL_IMPL diff --git a/src/logger/logger.h b/src/logger/logger.h new file mode 100644 index 0000000..2ce0d75 --- /dev/null +++ b/src/logger/logger.h @@ -0,0 +1,326 @@ +#ifndef LOGGER_H +#define LOGGER_H + +#include +#include +#include + +#include + +/* Log Levels */ + +/** + * The `off` log level for `logMessage()` to log nothing. + */ +static const int LOG_LEVEL__OFF = 0; + +/** + * The `normal` log level for `logMessage()` to log error, warn and + * info messages and stacktraces. + */ +static const int LOG_LEVEL__NORMAL = 1; + +/** + * The `debug` log level for `logMessage()` to log debug messages. + */ +static const int LOG_LEVEL__DEBUG = 2; + +/** + * The `verbose` log level for `logMessage()` to log verbose messages. + */ +static const int LOG_LEVEL__VERBOSE = 3; + +/** + * The `vverbose` log level for `logMessage()` to log very verbose + * messages. + */ +static const int LOG_LEVEL__VVERBOSE = 4; + +/** + * The `vvverbose` log level for `logMessage()` to log very verbose + * messages. + */ +static const int LOG_LEVEL__VVVERBOSE = 5; + + +/** + * The minimum log level. + */ +static const int MIN_LOG_LEVEL = LOG_LEVEL__OFF; + +/** + * The maximum log level. + */ +static const int MAX_LOG_LEVEL = LOG_LEVEL__VVVERBOSE; + +/** + * The default log level. + */ +static const int DEFAULT_LOG_LEVEL = LOG_LEVEL__NORMAL; + + + +/* Log Priorities */ + +/** + * The `error` log priority for `logMessage()` to use `logVPrintError()` + * if `sCurrentLogLevel` is greater or equal to `LOG_LEVEL__NORMAL`. + */ +static const int LOG_PRIORITY__ERROR = 0; + +/** + * The `warn` log priority for `logMessage()` to use `logVPrintWarn()` + * if `sCurrentLogLevel` is greater or equal to `LOG_LEVEL__NORMAL`. + */ +static const int LOG_PRIORITY__WARN = 1; + +/** + * The `info` log priority for `logMessage()` to use `logVPrintInfo()` + * if `sCurrentLogLevel` is greater or equal to `LOG_LEVEL__NORMAL`. + */ +static const int LOG_PRIORITY__INFO = 2; + +/** + * The `debug` log priority for `logMessage()` to use `logVPrintDebug()` + * if `sCurrentLogLevel` is greater or equal to `LOG_LEVEL__DEBUG`. + */ +static const int LOG_PRIORITY__DEBUG = 3; + +/** + * The `verbose` log priority for `logMessage()` to use `logVPrintVerbose()` + * if `sCurrentLogLevel` is greater or equal to `LOG_LEVEL__VERBOSE`. + */ +static const int LOG_PRIORITY__VERBOSE = 4; + +/** + * The `vverbose` log priority for `logMessage()` to use `logVPrintVerbose()` + * if `sCurrentLogLevel` is greater or equal to `LOG_LEVEL__VVERBOSE`. + */ +static const int LOG_PRIORITY__VVERBOSE = 5; + +/** + * The `vvverbose` log priority for `logMessage()` to use `logVPrintVerbose()` + * if `sCurrentLogLevel` is greater or equal to `LOG_LEVEL__VVVERBOSE`. + */ +static const int LOG_PRIORITY__VVVERBOSE = 6; + + + +/** + * The `message` log format mode to log entries + * in the format `message`. + */ +static const int LOG_FORMAT_MODE__MESSAGE = 0; + +/** + * The `tag_and_message` log format mode to log entries + * in the format `tag: message`. + */ +static const int LOG_FORMAT_MODE__TAG_AND_MESSAGE = 1; + +/** + * The `pid_priority_tag_and_message` log format mode to log entries + * in the format `pid priority tag: message`. + */ +static const int LOG_FORMAT_MODE__PID_PRIORITY_TAG_AND_MESSAGE = 2; + + + +/* + * The maximum size of the log entry tag. + */ +#define LOGGER_TAG_MAX_LENGTH 23 + 1 + +/* + * The maximum size of the log entry payload that can be written to + * the logger. An attempt to write more than this amount will result + * in a truncated log entry. + * + * The limit is 4068 but this includes log tag and log level + * prefix `D/` before log tag and `: ` suffix after it. + * + * #define LOGGER_ENTRY_MAX_PAYLOAD 4068 + * - https://cs.android.com/android/platform/superproject/+/android-13.0.0_r54:system/logging/liblog/include/log/log.h;l=66 + */ +#define LOGGER_ENTRY_MAX_PAYLOAD 4068 + + + +struct ILogger { + void (*printMessage)(bool logOnStderr, const char *message); +}; + + + +/** Get logger `sCurrentLogLevel`. */ +int getCurrentLogLevel(); + +/** + * Set logger `sCurrentLogLevel` if a valid `logLevel` is passed. + * + * @return Returns the new log level set. + */ +int setCurrentLogLevel(int logLevel); + +/** + * Returns `true` if `logLevel` passed is a valid log level, otherwise `false`. + */ +bool isLogLevelValid(int logLevel); + +/** + * Returns `true` if `logLevel` passed is a valid log level, otherwise `DEFAULT_LOG_LEVEL`. + */ +int getLogLevelIfValidOtherwiseDefault(int logLevel); + + + +/** + * Set `sDefaultLogTag` by calling `setDefaultLogTag()` and set + * `sLogTagPrefix` by calling `setLogTagPrefix()`. + * The `sLogTagPrefix` will be set to `defaultLogTag` followed by a dot `.`. + */ +void setDefaultLogTagAndPrefix(const char* defaultLogTag); + + + +/** Get logger `sDefaultLogTag`. */ +const char* getDefaultLogTag(); +/** + * Set `sDefaultLogTag`. + * + * See also `getFullLogTag()`. + */ +void setDefaultLogTag(const char* defaultLogTag); + + + +/** Get logger `sLogTagPrefix`. */ +const char* getLogTagPrefix(); + +/** + * Set `sLogTagPrefix`. + * + * See also `getFullLogTag()`. + */ +void setLogTagPrefix(const char* logTagPrefix); + + + +/** Get logger `sLoggerImpl`. */ +const struct ILogger* getLoggerImpl(); + +/** Set `sLoggerImpl`. */ +void setLoggerImpl(const struct ILogger* loggerImpl); + + + +/** Get logger `sLogFormatMode`. */ +int getLogFormatMode(); + +/** Set `sLogFormatMode`. */ +void setLogFormatMode(int logFormatMode); + + + +/** Get logger `sLogPid`. */ +pid_t getLogPid(); + +/** Update logger `sLogPid`. */ +void updateLogPid(); + + +/** Get logger `sCacheLogPid`. */ +bool getCacheLogPid(); + +/** Set `sCacheLogPid`. */ +void setCacheLogPid(bool cacheLogPid); + + + + + +/** + * Log a message with a specific priority. + * + * @param logPriority The priority to log the message with. Check `LOG_PRIORITY__*` constants + * to know above which `sCurrentLogLevel` they will start logging. + * @param tag The tag with which to log after passing it to `getFullLogTag()`. + * @param fmt The format string for the message. + * @param args The `va_list` of the arguments for the message. + */ +void logMessage(int logPriority, const char* tag, const char* fmt, va_list args); + + + +/* Log a message with `LOG_PRIORITY__ERROR`. */ +void logError(const char* tag, const char* fmt, ...); + +/* Log a message with `LOG_PRIORITY__WARN`. */ +void logWarn(const char* tag, const char* fmt, ...); + +/* Log a message with `LOG_PRIORITY__INFO`. */ +void logInfo(const char* tag, const char* fmt, ...); + +/* Log a message with `LOG_PRIORITY__DEBUG`. */ +void logDebug(const char* tag, const char* fmt, ...); + +/* Log a message with `LOG_PRIORITY__VERBOSE`. */ +void logVerbose(const char* tag, const char* fmt, ...); + +/* Log a message with `LOG_PRIORITY__VVERBOSE`. */ +void logVVerbose(const char* tag, const char* fmt, ...); + +/* Log a message with `LOG_PRIORITY__VVVERBOSE`. */ +void logVVVerbose(const char* tag, const char* fmt, ...); + + + +/* Log a message with `LOG_PRIORITY__ERROR` if current log level is `>=` `LOG_LEVEL__DEBUG`. */ +bool logErrorDebug(const char* tag, const char* fmt, ...); + +/* Log a message with `LOG_PRIORITY__ERROR` if current log level is `>=` `LOG_LEVEL__VERBOSE`. */ +bool logErrorVerbose(const char* tag, const char* fmt, ...); + +/* Log a message with `LOG_PRIORITY__ERROR` if current log level is `>=` `LOG_LEVEL__VVERBOSE`. */ +bool logErrorVVerbose(const char* tag, const char* fmt, ...); + +/* Log a message with `LOG_PRIORITY__ERROR` if current log level is `>=` `LOG_LEVEL__VVVERBOSE`. */ +bool logErrorVVVerbose(const char* tag, const char* fmt, ...); + +/* Log a private message with `LOG_PRIORITY__ERROR`. This is equivalent to `logErrorDebug*()` methods. */ +bool logErrorPrivate(const char* tag, const char* fmt, ...); + + + + + +/* Log a message with `LOG_PRIORITY__ERROR` with `: strerror(errno)` appended to it. + * + * @param errnoCode The errno to use for `strerror(errno)`. If this + equals `0`, then `logMessage()` will be called + instead to log to message without any `strerror` appended. + * @param tag The tag with which to log after passing it to `getFullLogTag()`. + * @param fmt The format string for the message. + * @param args The `va_list` of the arguments for the message. + */ +void logStrerrorMessage(int errnoCode, const char* tag, const char* fmt, va_list args); + +/* Log a message with `LOG_PRIORITY__ERROR` with `: strerror(errno)` appended to it. */ +void logStrerror(const char* tag, const char* fmt, ...); + +/* Log a message with `LOG_PRIORITY__ERROR` with `: strerror(errno)` appended to it if current log level is `>=` `LOG_LEVEL__DEBUG`. */ +bool logStrerrorDebug(const char* tag, const char* fmt, ...); + +/* Log a message with `LOG_PRIORITY__ERROR` with `: strerror(errno)` appended to it if current log level is `>=` `LOG_LEVEL__VERBOSE`. */ +bool logStrerrorVerbose(const char* tag, const char* fmt, ...); + +/* Log a message with `LOG_PRIORITY__ERROR` with `: strerror(errno)` appended to it if current log level is `>=` `LOG_LEVEL__VVERBOSE`. */ +bool logStrerrorVVerbose(const char* tag, const char* fmt, ...); + +/* Log a message with `LOG_PRIORITY__ERROR` with `: strerror(errno)` appended to it if current log level is `>=` `LOG_LEVEL__VVVERBOSE`. */ +bool logStrerrorVVVerbose(const char* tag, const char* fmt, ...); + +/* Log a private message with `LOG_PRIORITY__ERROR` with `: strerror(errno)` appended to it. This is equivalent to `logStrerrorDebug*()` methods. */ +bool logStrerrorPrivate(const char* tag, const char* fmt, ...); + +#endif // LOGGER_H diff --git a/src/logger/standard_logger_impl.c b/src/logger/standard_logger_impl.c new file mode 100644 index 0000000..d2df0e4 --- /dev/null +++ b/src/logger/standard_logger_impl.c @@ -0,0 +1,21 @@ +#define _GNU_SOURCE +#include +#include +#include + +#include "logger.h" +#include "standard_logger_impl.h" + +const struct ILogger sStandardLoggerImpl = { + .printMessage = printMessageToStdStream, +}; + + + +void printMessageToStdStream(bool logOnStderr, const char *message) { + fprintf(logOnStderr ? stderr : stdout, "%s", message); + + if (!logOnStderr) { + fflush(stdout); + } +} diff --git a/src/logger/standard_logger_impl.h b/src/logger/standard_logger_impl.h new file mode 100644 index 0000000..1a4923d --- /dev/null +++ b/src/logger/standard_logger_impl.h @@ -0,0 +1,12 @@ +#ifndef STANDARD_LOGGER_IMPL_H +#define STANDARD_LOGGER_IMPL_H + +#include + +#include "logger.h" + +extern const struct ILogger sStandardLoggerImpl; + +void printMessageToStdStream(bool logOnStderr, const char* message); + +#endif // STANDARD_LOGGER_IMPL_H diff --git a/src/os/safe_strerror.c b/src/os/safe_strerror.c new file mode 100644 index 0000000..06adc80 --- /dev/null +++ b/src/os/safe_strerror.c @@ -0,0 +1,109 @@ +/* + * Copyright 2006-2009 The Chromium Authors + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + * - https://github.com/chromium/chromium/blob/e4622aaeccea84652488d1822c28c78b7115684f/LICENSE + * - https://github.com/chromium/chromium/blob/e4622aaeccea84652488d1822c28c78b7115684f/base/posix/safe_strerror.cc + */ + +#include +#include +#include + +#include "safe_strerror.h" + +#if defined(__GLIBC__) || defined(OS_NACL) + #define USE_HISTORICAL_STRERROR_R 1 +// Post-L versions of bionic define the GNU-specific strerror_r if _GNU_SOURCE +// is defined, but the symbol is renamed to __gnu_strerror_r which only exists +// on those later versions. For parity, add the same condition as bionic. +#elif defined(__BIONIC__) && defined(_GNU_SOURCE) && __ANDROID_API__ >= 23 +#define USE_HISTORICAL_STRERROR_R 1 +#else +#define USE_HISTORICAL_STRERROR_R 0 +#endif + +/* +#if USE_HISTORICAL_STRERROR_R +// glibc has two strerror_r functions: a historical GNU-specific one that +// returns type char *, and a POSIX.1-2001 compliant one available since 2.3.4 +// that returns int. This wraps the GNU-specific one. +static void wrap_posix_strerror_r( + char* (*strerror_r_ptr)(int, char*, size_t), + int err, + char* buf, + size_t len) { + // GNU version. + char *rc = (*strerror_r_ptr)(err, buf, len); + if (rc != buf) { + // glibc did not use buf and returned a static string instead. Copy it + // into buf. + buf[0] = '\0'; + strncat(buf, rc, len - 1); + } + // The GNU version never fails. Unknown errors get an "unknown error" message. + // The result is always null terminated. +} +#endif // USE_HISTORICAL_STRERROR_R +*/ + +// Wrapper for strerror_r functions that implement the POSIX interface. POSIX +// does not define the behaviour for some of the edge cases, so we wrap it to +// guarantee that they are handled. This is compiled on all POSIX platforms, but +// it will only be used on Linux if the POSIX strerror_r implementation is +// being used (see below). +static void wrap_posix_strerror_r( + int (*strerror_r_ptr)(int, char*, size_t), + int errnoCode, + char* buf, + size_t len) { + int old_errno = errno; + // Have to cast since otherwise we get an error if this is the GNU version + // (but in such a scenario this function is never called). Sadly we can't use + // C++-style casts because the appropriate one is reinterpret_cast but it's + // considered illegal to reinterpret_cast a type to itself, so we get an + // error in the opposite case. + int result = (*strerror_r_ptr)(errnoCode, buf, len); + if (result == 0) { + // POSIX is vague about whether the string will be terminated, although + // it indirectly implies that typically ERANGE will be returned, instead + // of truncating the string. We play it safe by always terminating the + // string explicitly. + buf[len - 1] = '\0'; + } else { + // Error. POSIX is vague about whether the return value is itself a system + // error code or something else. On Linux currently it is -1 and errno is + // set. On BSD-derived systems it is a system error and errno is unchanged. + // We try and detect which case it is so as to put as much useful info as + // we can into our message. + int strerror_error; // The error encountered in strerror + int new_errno = errno; + if (new_errno != old_errno) { + // errno was changed, so probably the return value is just -1 or something + // else that doesn't provide any info, and errno is the error. + strerror_error = new_errno; + } else { + // Either the error from strerror_r was the same as the previous value, or + // errno wasn't used. Assume the latter. + strerror_error = result; + } + // snprintf truncates and always null-terminates. + snprintf(buf, + len, + "Error %d while retrieving error %d", + strerror_error, + errnoCode); + } + errno = old_errno; +} + +void safe_strerror_r(int errnoCode, char* buf, size_t len) { + if (buf == NULL || len <= 0) { + return; + } + // If using glibc (i.e., Linux), the compiler will automatically select the + // appropriate overloaded function based on the function type of strerror_r. + // The other one will be elided from the translation unit since both are + // static. + wrap_posix_strerror_r(&strerror_r, errnoCode, buf, len); +} diff --git a/src/os/safe_strerror.h b/src/os/safe_strerror.h new file mode 100644 index 0000000..c52fa79 --- /dev/null +++ b/src/os/safe_strerror.h @@ -0,0 +1,44 @@ +/* + * Copyright 2011 The Chromium Authors + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + * - https://github.com/chromium/chromium/blob/e4622aaeccea84652488d1822c28c78b7115684f/LICENSE + * - https://github.com/chromium/chromium/blob/e4622aaeccea84652488d1822c28c78b7115684f/base/posix/safe_strerror.h + */ + +#ifndef SAFE_STRERROR_H +#define SAFE_STRERROR_H + +#include + +/** + * Buffer size for a `strerror()` description. + */ +#define STRERROR_BUFFER_SIZE 256 + +/** + * Get the `errno` description with `strerror()` call in the `buf` passed. + * + * This method declares safe, portable alternative to the POSIX `strerror()` function. + * The `strerror()` is inherently unsafe in multi-threaded apps and should never be used. + * Doing so can cause crashes. Additionally, the thread-safe alternative `strerror_r()` varies in + * semantics across platforms. Use these functions instead. + * + * This method is a thread-safe `strerror()` function with dependable semantics that never fails. + * It will write the string form of error `errnoCode` to buffer `buf` of length `len`. + * If there is an error calling the OS's `strerror_r()` function then a message to that effect will + * be printed into `buf`, truncating if necessary. The final result is always null-terminated. + * The value of `errno` is never changed. + * + * Use this instead of `strerror_r()`. + * + * - https://man7.org/linux/man-pages/man3/errno.3.html + * - https://man7.org/linux/man-pages/man3/strerror.3.html + * + * @param errnoCode The `errno` code to get description for. + * @param buf The buffer to get description in. + * @param len The buffer length. + */ +void safe_strerror_r(int errnoCode, char* buf, size_t len); + +#endif // SAFE_STRERROR_H diff --git a/src/os/selinux_utils.c b/src/os/selinux_utils.c new file mode 100644 index 0000000..53acd9d --- /dev/null +++ b/src/os/selinux_utils.c @@ -0,0 +1,61 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include + +#include "../data/data_utils.h" +#include "../logger/logger.h" +#include "selinux_utils.h" + +bool get_se_process_context_from_env(const char* log_tag, const char* var, char buffer[], size_t buffer_len) { + const char* value = getenv(var); + if (value == NULL) { + return false; + } + + size_t value_length = strlen(value); + if (value_length < 1) { + return false; + } + + if (regex_match(value, REGEX__PROCESS_CONTEXT, REG_EXTENDED) != 0) { + logErrorVVerbose(log_tag, "Ignoring invalid se_process_context value set in '%s' env variable: '%s'", + var, value); + return false; + } + + + if (buffer_len <= value_length) { + logErrorDebug(log_tag, "The se_process_context '%s' with length '%zu' is too long to fit in the buffer with length '%zu'", + value, value_length, buffer_len); + errno = EINVAL; + return -1; + } + + strcpy(buffer, value); + + return true; +} + +bool get_se_process_context_from_file(const char* log_tag, char buffer[], size_t buffer_len) { + FILE *fp = fopen("/proc/self/attr/current", "r"); + if (fp == NULL) { + logErrorVVerbose(log_tag, "Failed to open '/proc/self/attr/current' to read se_process_context: '%d'", errno); + return false; + } + + if (fgets(buffer, buffer_len, fp) != NULL) { + if (buffer_len > 0 && buffer[buffer_len-1] == '\n') { + buffer[--buffer_len] = '\0'; + } + } else { + logErrorVVerbose(log_tag, "Failed to read se_process_context from '/proc/self/attr/current': '%d'", errno); + return false; + } + + fclose(fp); + return true; +} diff --git a/src/os/selinux_utils.h b/src/os/selinux_utils.h new file mode 100644 index 0000000..9e6f3e5 --- /dev/null +++ b/src/os/selinux_utils.h @@ -0,0 +1,131 @@ +#ifndef SELINUX_UTILS_H +#define SELINUX_UTILS_H + +#include + +/** + * Regex to match SELinux context of a process on Android. + * + * The security context label is in the format `user:role:type:sensitivity[:categories]`. + * For non system apps, it will be something like `u:r:untrusted_app:s0:c159,c256,c512,c768`. + * + * The `user` is always `u`. + * + * The role is always `r` for subjects (example: processes) and `object_r` + * for objects (example: files). + * + * The `type` depends on the app type. The common ones are the following. + * - `untrusted_app`: untrusted app processes running with `targetSdkVersion` + * equal to android sdk version or with `targetSdkVersion` `>=` than + * the max `targetSdkVersion` of the highest `untrusted_app_*` + * backward compatibility domains. + * - `untrusted_app_25`: untrusted app processes running with `targetSdkVersion` `<= 25`. + * - `untrusted_app_27`: untrusted app processes running with `targetSdkVersion` `26-28`. + * - `untrusted_app_27`: untrusted app processes running with `26 <= targetSdkVersion` `<= 28`. + * - `untrusted_app_29`: untrusted app processes running with `targetSdkVersion` `= 29`. + * - `untrusted_app_30`: untrusted app processes running with `targetSdkVersion` `30-31`. + * - `untrusted_app_32`: untrusted app processes running with `targetSdkVersion` `32-33`. + * - `system_app`: system app processes that are signed with the platform + * key and use `sharedUserId=com.android.system`. + * - `platform_app`: platform app processes that are signed with the platform + * key and do not use `sharedUserId=com.android.system`. + * - `priv_app`: privileged app processes that are not signed with the + * platform key. + * + * The `sensitivity` is always `s0`. + * + * The Multi-Category Security (MCS) `categories` are optional, but if set, + * then either `2` or `4` categories are set where each category starts with + * `c` followed by a number. The `untrusted_app_25` should have 2 categories, + * while `>=` `untrusted_app_27` should have 4 categories if app has + * `targetSdkVersion` `>= 28`. For example for uid `10159` and user `0` + * security context with be set to `u:r:untrusted_app_27:s0:c159,c256,c512,c768` + * and for uid `1010159` and user `10` it will be set to + * `u:r:untrusted_app_27:s0:c159,c256,c522,c768`, note `c512` vs `c522`. + * This is because `untrusted_app_25` selinux domain uses `levelFrom=user` + * selector in SELinux `seapp_contexts`, which adds two category types. + * + * - https://source.android.com/docs/security/features/selinux/concepts#security_contexts + * - https://cs.android.com/android/platform/superproject/+/bea25463132c3f4bb35816d175bbe8551f11fc9d:system/sepolicy/README.apps.md + * - https://github.com/termux/termux-app/issues/3167#issuecomment-1369708339 + * - https://github.com/SELinuxProject/selinux-notebook/blob/main/src/mls_mcs.md#multi-level-and-multi-category-security + * - https://cs.android.com/android/platform/superproject/+/android-14.0.0_r1:system/sepolicy/private/seapp_contexts + * - https://cs.android.com/android/platform/superproject/+/android-14.0.0_r1:system/sepolicy/private/untrusted_app.te;l=13-14 + * - https://cs.android.com/android/platform/superproject/+/android-14.0.0_r9:external/selinux/libselinux/src/context.c;l=17 + * - https://cs.android.com/android/platform/superproject/+/android-14.0.0_r9:external/selinux/libselinux/src/android/android_seapp.c;l=686 + */ +#define REGEX__PROCESS_CONTEXT "^u:r:[^\n\t\r :]+:s0(:c[0-9]+,c[0-9]+(,c[0-9]+,c[0-9]+)?)?$" + +/** + * The process context prefix assigned to untrusted app processes + * running with `targetSdkVersion <= 25`. + * + * - https://cs.android.com/android/platform/superproject/+/android-13.0.0_r18:system/sepolicy/private/seapp_contexts;l=176 + * - https://cs.android.com/android/platform/superproject/+/android-13.0.0_r18:system/sepolicy/public/untrusted_app.te;l=31 + * - https://cs.android.com/android/platform/superproject/+/android-13.0.0_r18:/system/sepolicy/private/untrusted_app_25.te + */ +#define PROCESS_CONTEXT_PREFIX__UNTRUSTED_APP_25 "u:r:untrusted_app_25:" + +/** + * The process context prefix assigned to untrusted app processes + * running with `26 <= targetSdkVersion <= 28`. + * + * - https://cs.android.com/android/platform/superproject/+/android-13.0.0_r18:system/sepolicy/private/seapp_contexts;l=174 + * - https://cs.android.com/android/platform/superproject/+/android-13.0.0_r18:system/sepolicy/public/untrusted_app.te;l=28 + * - https://cs.android.com/android/platform/superproject/+/android-13.0.0_r18:system/sepolicy/private/untrusted_app_27.te + */ +#define PROCESS_CONTEXT_PREFIX__UNTRUSTED_APP_27 "u:r:untrusted_app_27:" + +/** + * The process context assigned to `root` (`0`) user processes by + * AOSP, like for `adb root`. The process context always equals + * `u:r:su:s0` without any categories. + * + * - https://cs.android.com/android/platform/superproject/+/android-14.0.0_r1:system/sepolicy/private/su.te + * - https://cs.android.com/android/platform/superproject/+/android-14.0.0_r1:system/core/rootdir/init.usb.rc;l=15 + * - https://cs.android.com/android/platform/superproject/+/android-14.0.0_r1:system/sepolicy/private/init.te;l=32 + */ +#define PROCESS_CONTEXT__AOSP_SU "u:r:su:s0" + +/** + * The process context type assigned to `root` (`0`) user processes by + * Magisk, like for `su` commands. The process context always equals + * `u:r:magisk:s0` without any categories. + * + * - https://github.com/topjohnwu/Magisk + * - https://github.com/topjohnwu/Magisk/blob/master/native/src/include/consts.hpp#L36 + * - https://github.com/topjohnwu/Magisk/blob/master/docs/tools.md#magiskpolicy + */ +#define PROCESS_CONTEXT__MAGISK_SU "u:r:magisk:s0" + +/** + * The process context assigned to `shell` (`2000`) user, like for + * `adb shell`. The process context always equals `u:r:shell:s0` + * without any categories. + * + * - https://cs.android.com/android/platform/superproject/+/android-14.0.0_r1:system/sepolicy/private/seapp_contexts;l=168 + * - https://cs.android.com/android/platform/superproject/+/android-14.0.0_r1:system/sepolicy/private/shell.te + * - https://cs.android.com/android/platform/superproject/+/android-14.0.0_r1:system/core/rootdir/init.rc + * - https://cs.android.com/android/platform/superproject/+/android-14.0.0_r1:packages/modules/adb/daemon/shell_service.cpp;l=379 + */ +#define PROCESS_CONTEXT__SHELL "u:r:shell:s0" + +/** + * Get selinux context of the current process from the + * `ENV__TERMUX__SE_PROCESS_CONTEXT` env variable. + * + * @return Return `true` if a valid selinux context was set in env + * variable and was copied to the `buffer`, otherwise `false`. + */ +bool get_se_process_context_from_env(const char* log_tag, const char* var, char buffer[], size_t buffer_len); + +/** + * Get selinux context of the current process from the + * `/proc/self/attr/current` file. + * + * @return Return `true` if a valid selinux context was successfully read + * and was copied to the `buffer`, otherwise `false`. + */ +bool get_se_process_context_from_file(const char* log_tag, char buffer[], size_t buffer_len); + +#endif // SELINUX_UTILS_H diff --git a/src/termux-exec-init.c b/src/termux-exec-init.c new file mode 100644 index 0000000..d6d306e --- /dev/null +++ b/src/termux-exec-init.c @@ -0,0 +1,55 @@ +#include + +#include "termux-exec-init.h" +#include "logger/logger.h" +#include "termux/termux_env.h" + +static bool sInitLogger = true; + + + +int initProcess(bool shouldLogVersion) { + // Sometimes when a process starts, errno is set to values like + // EINVAL (22) and (ECHILD 10). Noticed on Android 11 (aarch64) and + // Android 14 (x86_64). + // It is not being caused by `termux-exec` as it happens even + // without `LD_PRELOAD` being set. + // Moreover, errno is 0 before `execve_syscall()` is called by + // `execve_intercept()` to replace the process, but in the `main()` + // of new process, errno is not zero, so something happens during + // the `syscall()` itself or in callers of `main()`. And manually + // setting errno before `execve_syscall()` does not transfer it + // to `main()` of new process. + // Programs should set errno to `0` at init themselves. + // We unset it here since programs should have already handled their + // errno if it was set by something else and `termux-exec` library + // also has checks to error out if errno is set in various places, + // like initially in `string_to_int()` called by `get_termux_exec_log_level()`. + // Saving errno is useless as it will not be transferred anyways. + // - https://wiki.sei.cmu.edu/confluence/display/c/ERR30-C.+Take+care+when+reading+errno + errno = 0; + + return initLogger(shouldLogVersion); +} + +int initLogger(bool shouldLogVersion) { + if (sInitLogger) { + setDefaultLogTagAndPrefix(TERMUX__LNAME); + setCurrentLogLevel(get_termux_exec_log_level()); + setCacheLogPid(true); + setLogFormatMode(LOG_FORMAT_MODE__PID_PRIORITY_TAG_AND_MESSAGE); + sInitLogger = false; + + if (shouldLogVersion) { + logErrorVVerbose("", "TERMUX_EXEC_PKG__VERSION: '%s'", TERMUX_EXEC_PKG__VERSION); + } + } + return 0; +} + + + +int exitProcess() { + sInitLogger = true; + return 0; +} diff --git a/src/termux-exec-init.h b/src/termux-exec-init.h new file mode 100644 index 0000000..8f9895d --- /dev/null +++ b/src/termux-exec-init.h @@ -0,0 +1,10 @@ +#ifndef TERMUX_EXEC_INIT_H +#define TERMUX_EXEC_INIT_H + +#include + +int initProcess(bool shouldLogVersion); +int initLogger(bool shouldLogVersion); +int exitProcess(); + +#endif // TERMUX_EXEC_INIT_H diff --git a/src/termux-exec.c b/src/termux-exec.c index 01edce8..7a34354 100644 --- a/src/termux-exec.c +++ b/src/termux-exec.c @@ -1,644 +1,92 @@ -// Make android_get_device_api_level() use an inline variant, -// as a libc symbol for it exists only in android-29+. -#undef __ANDROID_API__ -#define __ANDROID_API__ 28 - #define _GNU_SOURCE -// #include -// #include -#include -#include -#include -#include -#include +#include #include -#include -#include -#include -#include #include -#ifdef __ANDROID__ -#include -#endif - -#if UINTPTR_MAX == 0xffffffff -#define SYSTEM_LINKER_PATH "/system/bin/linker"; -#elif UINTPTR_MAX == 0xffffffffffffffff -#define SYSTEM_LINKER_PATH "/system/bin/linker64"; -#endif - -#ifdef __aarch64__ -#define EM_NATIVE EM_AARCH64 -#elif defined(__arm__) || defined(__thumb__) -#define EM_NATIVE EM_ARM -#elif defined(__x86_64__) -#define EM_NATIVE EM_X86_64 -#elif defined(__i386__) -#define EM_NATIVE EM_386 -#else -#error "unknown arch" -#endif - -#define TERMUX_BASE_DIR_MAX_LEN 200 - -#define TERMUX_PREFIX_ENV_NAME "TERMUX__PREFIX" - -#define LOG_PREFIX "[termux-exec] " - -// Get the termux base directory, where all termux-installed packages reside under. -// This is normally /data/data/com.termux/files -static char const *get_termux_base_dir(char *buffer) { - char const *prefix = getenv(TERMUX_PREFIX_ENV_NAME); - if (prefix && strlen(prefix) <= TERMUX_BASE_DIR_MAX_LEN) { - char *last_slash_ptr = strrchr(prefix, '/'); - if (last_slash_ptr != NULL) { - size_t len_up_until_last_slash = last_slash_ptr - prefix + 1; - if (len_up_until_last_slash > 10) { // Just a sanity check low limit. - snprintf(buffer, len_up_until_last_slash, "%s", prefix); - buffer[len_up_until_last_slash] = 0; - return buffer; - } - } - } - return TERMUX_BASE_DIR; +#include "termux-exec-init.h" +#include "exec/exec.h" +#include "exec/exec_variants.h" +#include "logger/logger.h" +#include "termux/termux_env.h" + +/** + * This file defines functions intercepted by `libtermux-exec.so` using `LD_PRELOAD`. + * + * All exported functions must explicitly enable `default` visibility + * with `__attribute__((visibility("default")))` as `libtermux-exec.so` + * is compiled with `-fvisibility=hidden` so that no other internal + * functions get exported. + * + * You can check exported symbols for dynamic linking after building with: + * `nm --demangle --dynamic --defined-only --extern-only /home/builder/.termux-build/termux-exec/src/build/usr/lib/libtermux-exec.so`. + */ + + + +__attribute__((visibility("default"))) +int execl(const char *name, const char *arg, ...) { + initProcess(true); + + va_list ap; + va_start(ap, arg); + int result = execl_hook(true, ExecL, name, arg, ap); + va_end(ap); + return result; } -// Check if `string` starts with `prefix`. -static bool starts_with(const char *string, const char *prefix) { return strncmp(string, prefix, strlen(prefix)) == 0; } - -// Rewrite e.g. "/bin/sh" and "/usr/bin/sh" to "${TERMUX_PREFIX}/bin/sh". -// -// The termux_base_dir argument comes from get_termux_base_dir(), so have a -// max length of TERMUX_BASE_DIR_MAX_LEN. -static const char *termux_rewrite_executable(const char *termux_base_dir, const char *executable_path, char *buffer, - int buffer_len) { - char termux_bin_path[TERMUX_BASE_DIR_MAX_LEN + 16]; - int termux_bin_path_len = sprintf(termux_bin_path, "%s/usr/bin/", termux_base_dir); +__attribute__((visibility("default"))) +int execlp(const char *name, const char *arg, ...) { + initProcess(true); - if (executable_path[0] != '/') { - return executable_path; - } - - char *bin_match = strstr(executable_path, "/bin/"); - if (bin_match == executable_path || bin_match == (executable_path + 4)) { - // Found "/bin/" or "/xxx/bin" at the start of executable_path. - strcpy(buffer, termux_bin_path); - char *dest = buffer + termux_bin_path_len; - // Copy what comes after "/bin/": - const char *src = bin_match + 5; - size_t bytes_to_copy = buffer_len - termux_bin_path_len; - strncpy(dest, src, bytes_to_copy); - return buffer; - } else { - return executable_path; - } + va_list ap; + va_start(ap, arg); + int result = execl_hook(true, ExecLP, name, arg, ap); + va_end(ap); + return result; } -// Remove LD_LIBRARY_PATH and LD_LIBRARY_PATH entries from envp. -static void remove_ld_from_env(char *const *envp, char ***allocation) { - bool create_new_env = false; - int env_length = 0; - while (envp[env_length] != NULL) { - if (starts_with(envp[env_length], "LD_LIBRARY_PATH=") || starts_with(envp[env_length], "LD_PRELOAD=")) { - create_new_env = true; - } - env_length++; - } - - if (!create_new_env) { - return; - } +__attribute__((visibility("default"))) +int execle(const char *name, const char *arg, ...) { + initProcess(true); - char **new_envp = malloc(sizeof(char *) * env_length); - *allocation = new_envp; - int new_envp_idx = 0; - int old_envp_idx = 0; - - while (old_envp_idx < env_length) { - if (!starts_with(envp[old_envp_idx], "LD_LIBRARY_PATH=") && !starts_with(envp[old_envp_idx], "LD_PRELOAD=")) { - new_envp[new_envp_idx++] = envp[old_envp_idx]; - } - old_envp_idx++; - } - - new_envp[new_envp_idx] = NULL; + va_list ap; + va_start(ap, arg); + int result = execl_hook(true, ExecLE, name, arg, ap); + va_end(ap); + return result; } -// From https://stackoverflow.com/questions/4774116/realpath-without-resolving-symlinks/34202207#34202207 -static const char *normalize_path(const char *src, char *result_buffer) { - char pwd[PATH_MAX]; - if (getcwd(pwd, sizeof(pwd)) == NULL) { - return src; - } - - size_t res_len; - size_t src_len = strlen(src); - - const char *ptr = src; - const char *end = &src[src_len]; - const char *next; - - if (src_len == 0 || src[0] != '/') { - // relative path - size_t pwd_len = strlen(pwd); - memcpy(result_buffer, pwd, pwd_len); - res_len = pwd_len; - } else { - res_len = 0; - } +__attribute__((visibility("default"))) +int execv(const char *name, char *const *argv) { + initProcess(true); - for (ptr = src; ptr < end; ptr = next + 1) { - next = (char *)memchr(ptr, '/', end - ptr); - if (next == NULL) { - next = end; - } - size_t len = next - ptr; - switch (len) { - case 2: - if (ptr[0] == '.' && ptr[1] == '.') { - const char *slash = (char *)memrchr(result_buffer, '/', res_len); - if (slash != NULL) { - res_len = slash - result_buffer; - } - continue; - } - break; - case 1: - if (ptr[0] == '.') { - continue; - } - break; - case 0: - continue; - } - - if (res_len != 1) { - result_buffer[res_len++] = '/'; - } - - memcpy(&result_buffer[res_len], ptr, len); - res_len += len; - } - - if (res_len == 0) { - result_buffer[res_len++] = '/'; - } - result_buffer[res_len] = '\0'; - return result_buffer; + return execv_hook(true, name, argv); } -struct file_header_info { - bool is_elf; - // If executing a 32-bit binary on a 64-bit host: - bool is_non_native_elf; - char interpreter_buf[256]; - char const *interpreter; - char const *interpreter_arg; -}; - -static void inspect_file_header(char const *termux_base_dir, char *header, size_t header_len, - struct file_header_info *result) { - if (header_len >= 20 && !memcmp(header, ELFMAG, SELFMAG)) { - result->is_elf = true; - Elf32_Ehdr *ehdr = (Elf32_Ehdr *)header; - if (ehdr->e_machine != EM_NATIVE) { - result->is_non_native_elf = true; - } - return; - } - - if (header_len < 5 || !(header[0] == '#' && header[1] == '!')) { - return; - } +__attribute__((visibility("default"))) +int execvp(const char *name, char *const *argv) { + initProcess(true); - // Check if the header contains a newline to end the shebang line: - char *newline_location = memchr(header, '\n', header_len); - if (newline_location == NULL) { - return; - } - - // Strip whitespace at end of shebang: - while (*(newline_location - 1) == ' ') { - newline_location--; - } - - // Null terminate the shebang line: - *newline_location = 0; - - // Skip whitespace to find interpreter start: - char const *interpreter = header + 2; - while (*interpreter == ' ') { - interpreter++; - } - if (interpreter == newline_location) { - // Just a blank line up until the newline. - return; - } - - // Check for whitespace following the interpreter: - char *whitespace_pos = strchr(interpreter, ' '); - if (whitespace_pos != NULL) { - // Null-terminate the interpreter string. - *whitespace_pos = 0; - - // Find start of argument: - char *interpreter_arg = whitespace_pos + 1; - while (*interpreter_arg != 0 && *interpreter_arg == ' ') { - interpreter_arg++; - } - if (interpreter_arg != newline_location) { - result->interpreter_arg = interpreter_arg; - } - } - - result->interpreter = - termux_rewrite_executable(termux_base_dir, interpreter, result->interpreter_buf, sizeof(result->interpreter_buf)); -} - -static int execve_syscall(const char *executable_path, char *const argv[], char *const envp[]) { - return syscall(SYS_execve, executable_path, argv, envp); + return execvp_hook(true, name, argv); } -// Interceptor of the execve(2) system call using LD_PRELOAD. -int execve(const char *executable_path, char *const argv[], char *const envp[]) { - if (getenv("TERMUX_EXEC_OPTOUT") != NULL) { - return execve_syscall(executable_path, argv, envp); - } - - const bool termux_exec_debug = getenv("TERMUX_EXEC_DEBUG") != NULL; - if (termux_exec_debug) { - fprintf(stderr, LOG_PREFIX "Intercepting execve('%s'):\n", executable_path); - int tmp_argv_count = 0; - while (argv[tmp_argv_count] != NULL) { - fprintf(stderr, LOG_PREFIX " argv[%d] = '%s'\n", tmp_argv_count, argv[tmp_argv_count]); - tmp_argv_count++; - } - } - - // Size of TERMUX_BASE_DIR_MAX_LEN + "/usr/bin/sh". - char termux_base_dir_buf[TERMUX_BASE_DIR_MAX_LEN + 16]; - - const char *termux_base_dir = get_termux_base_dir(termux_base_dir_buf); - if (termux_exec_debug) { - fprintf(stderr, LOG_PREFIX "Using base dir: '%s'\n", termux_base_dir); - } - - const char *orig_executable_path = executable_path; - - char executable_path_buffer[PATH_MAX]; - executable_path = termux_rewrite_executable(termux_base_dir, executable_path, executable_path_buffer, - sizeof(executable_path_buffer)); - if (termux_exec_debug && executable_path_buffer == executable_path) { - fprintf(stderr, LOG_PREFIX "Rewritten path: '%s'\n", executable_path); - } - - if (access(executable_path, X_OK) != 0) { - // Error out if the file is not executable: - errno = EACCES; - return -1; - } - - int fd = open(executable_path, O_RDONLY); - if (fd == -1) { - errno = ENOENT; - return -1; - } - - // execve(2): "The kernel imposes a maximum length on the text that follows the "#!" characters - // at the start of a script; characters beyond the limit are ignored. Before Linux 5.1, the - // limit is 127 characters. Since Linux 5.1, the limit is 255 characters." - // We use one more byte since inspect_file_header() will null terminate the buffer. - char header[256]; - ssize_t read_bytes = read(fd, header, sizeof(header) - 1); - close(fd); - - struct file_header_info info = { - .interpreter = NULL, - .interpreter_arg = NULL, - }; - inspect_file_header(termux_base_dir, header, read_bytes, &info); - - if (!info.is_elf && info.interpreter == NULL) { - if (termux_exec_debug) { - fprintf(stderr, LOG_PREFIX "Not ELF or shebang, returning ENOEXEC\n"); - } - errno = ENOEXEC; - return -1; - } - - if (info.interpreter != NULL) { - executable_path = info.interpreter; - if (termux_exec_debug) { - fprintf(stderr, LOG_PREFIX "Path to interpreter from shebang: '%s'\n", info.interpreter); - } - } +__attribute__((visibility("default"))) +int execvpe(const char *name, char *const *argv, char *const *envp) { + initProcess(true); - char normalized_path_buffer[PATH_MAX]; - executable_path = normalize_path(executable_path, normalized_path_buffer); - - char **new_allocated_envp = NULL; - - const char **new_argv = NULL; - - // Only wrap with linker: - // - If running on an Android 10+ where it's necesssary, and - // - If trying to execute a file under the termux base directory - bool wrap_in_linker = -#ifdef __ANDROID__ - android_get_device_api_level() >= 29 && android_get_application_target_sdk_version() >= 29 && -#endif - (strstr(executable_path, termux_base_dir) != NULL); - - bool cleanup_env = info.is_non_native_elf || - // Avoid interfering with Android /system software by removing - // LD_PRELOAD and LD_LIBRARY_PATH from env if executing something - // there. But /system/bin/{sh,linker,linker64} are ok with those - // and should keep them. - (starts_with(executable_path, "/system/") && !starts_with(executable_path, "/system/bin/sh") && - !starts_with(executable_path, "/system/bin/linker")); - if (cleanup_env) { - remove_ld_from_env(envp, &new_allocated_envp); - if (new_allocated_envp) { - envp = new_allocated_envp; - if (termux_exec_debug) { - fprintf(stderr, LOG_PREFIX "Removed LD_PRELOAD and LD_LIBRARY_PATH from env\n"); - } - } - } - - const bool argv_needs_rewriting = wrap_in_linker || info.interpreter != NULL; - if (argv_needs_rewriting) { - int orig_argv_count = 0; - while (argv[orig_argv_count] != NULL) { - orig_argv_count++; - } - - new_argv = malloc(sizeof(char *) * (2 + orig_argv_count)); - int current_argc = 0; - - // Keep program name: - new_argv[current_argc++] = argv[0]; - - // Specify executable path if wrapping with linker: - if (wrap_in_linker) { - // Normalize path without resolving symlink. For instance, $PREFIX/bin/ls is - // a symlink to $PREFIX/bin/coreutils, but we need to execute - // "/system/bin/linker $PREFIX/bin/ls" so that coreutils knows what to execute. - new_argv[current_argc++] = executable_path; - executable_path = SYSTEM_LINKER_PATH; - } - - // Add interpreter argument and script path if exec:ing a script with shebang: - if (info.interpreter != NULL) { - if (info.interpreter_arg) { - new_argv[current_argc++] = info.interpreter_arg; - } - new_argv[current_argc++] = orig_executable_path; - } - - for (int i = 1; i < orig_argv_count; i++) { - new_argv[current_argc++] = argv[i]; - } - new_argv[current_argc] = NULL; - argv = (char **)new_argv; - } - - if (termux_exec_debug) { - fprintf(stderr, LOG_PREFIX "Calling syscall execve('%s'):\n", executable_path); - int tmp_argv_count = 0; - int arg_count = 0; - while (argv[tmp_argv_count] != NULL) { - fprintf(stderr, LOG_PREFIX " argv[%d] = '%s'\n", arg_count++, argv[tmp_argv_count]); - tmp_argv_count++; - } - } - - // Setup TERMUX_EXEC__PROC_SELF_EXE as the absolute path to the executable - // file we are executing. That is, we are executing: - // /system/bin/linker64 ${TERMUX_EXEC__PROC_SELF_EXE} ..arguments.. - // This is to allow patching software that uses /proc/self/exe - // to instead use getenv("TERMUX_EXEC__PROC_SELF_EXE"). - char *termux_self_exe; - if (asprintf(&termux_self_exe, "TERMUX_EXEC__PROC_SELF_EXE=%s", orig_executable_path) == -1) { - if (termux_exec_debug) { - fprintf(stderr, LOG_PREFIX "asprintf failed\n"); - } - free(new_argv); - free(new_allocated_envp); - errno = ENOMEM; - return -1; - } - int num_env = 0; - while (envp[num_env] != NULL) { - num_env++; - } - // Allocate new environment variable array. Size + 2 since - // we might perhaps append a TERMUX_EXEC__PROC_SELF_EXE variable and - // we will also NULL terminate. - char **new_envp = malloc(sizeof(char *) * (num_env + 2)); - bool already_found_self_exe = false; - for (int i = 0; i < num_env; i++) { - if (starts_with(envp[i], "TERMUX_EXEC__PROC_SELF_EXE=")) { - new_envp[i] = termux_self_exe; - already_found_self_exe = true; - if (termux_exec_debug) { - fprintf(stderr, LOG_PREFIX "Overwriting to '%s'\n", termux_self_exe); - } - } else { - new_envp[i] = envp[i]; - } - } - if (!already_found_self_exe) { - new_envp[num_env++] = termux_self_exe; - if (termux_exec_debug) { - fprintf(stderr, LOG_PREFIX "Setting new '%s'\n", termux_self_exe); - } - } - // Null terminate. - new_envp[num_env] = NULL; - - int syscall_ret = execve_syscall(executable_path, argv, new_envp); - int saved_errno = errno; - if (termux_exec_debug) { - perror(LOG_PREFIX "execve() syscall failed"); - } - free(new_argv); - free(new_allocated_envp); - free(new_envp); - free(termux_self_exe); - errno = saved_errno; - return syscall_ret; -} - -#ifdef UNIT_TEST -#include -#define TERMUX_BIN_PATH TERMUX_BASE_DIR "/usr/bin/" - -void assert_string_equals(const char *expected, const char *actual) { - if (strcmp(actual, expected) != 0) { - fprintf(stderr, "Assertion failed - expected '%s', was '%s'\n", expected, actual); - exit(1); - } -} - -void test_starts_with() { - assert(starts_with("/path/to/file", "/path")); - assert(!starts_with("/path", "/path/to/file")); -} - -void test_termux_rewrite_executable() { - char buf[PATH_MAX]; - char const *b = TERMUX_BASE_DIR; - assert_string_equals(TERMUX_BIN_PATH "sh", termux_rewrite_executable(b, "/bin/sh", buf, PATH_MAX)); - assert_string_equals(TERMUX_BIN_PATH "sh", termux_rewrite_executable(b, "/usr/bin/sh", buf, PATH_MAX)); - assert_string_equals("/system/bin/sh", termux_rewrite_executable(b, "/system/bin/sh", buf, PATH_MAX)); - assert_string_equals("/system/bin/tool", termux_rewrite_executable(b, "/system/bin/tool", buf, PATH_MAX)); - assert_string_equals(TERMUX_BIN_PATH "sh", termux_rewrite_executable(b, TERMUX_BIN_PATH "sh", buf, PATH_MAX)); - assert_string_equals(TERMUX_BIN_PATH, termux_rewrite_executable(b, "/bin/", buf, PATH_MAX)); - assert_string_equals("./ab/sh", termux_rewrite_executable(b, "./ab/sh", buf, PATH_MAX)); -} - -void test_remove_ld_from_env() { - { - char *test_env[] = {"MY_ENV=1", NULL}; - char **allocated_envp; - remove_ld_from_env(test_env, &allocated_envp); - assert(allocated_envp == NULL); - assert_string_equals("MY_ENV=1", test_env[0]); - assert(test_env[1] == NULL); - } - { - char *test_env[] = {"MY_ENV=1", "LD_PRELOAD=a", NULL}; - char **allocated_envp; - remove_ld_from_env(test_env, &allocated_envp); - assert(allocated_envp != NULL); - assert_string_equals("MY_ENV=1", allocated_envp[0]); - assert(allocated_envp[1] == NULL); - free(allocated_envp); - } - { - char *test_env[] = {"MY_ENV=1", "LD_PRELOAD=a", "A=B", "LD_LIBRARY_PATH=B", "B=C", NULL}; - char **allocated_envp; - remove_ld_from_env(test_env, &allocated_envp); - assert(allocated_envp != NULL); - assert_string_equals("MY_ENV=1", allocated_envp[0]); - assert_string_equals("A=B", allocated_envp[1]); - assert_string_equals("B=C", allocated_envp[2]); - assert(allocated_envp[3] == NULL); - free(allocated_envp); - } + return execvpe_hook(true, name, argv, envp); } -void test_normalize_path() { - char expected[PATH_MAX * 2]; - char pwd[PATH_MAX]; - char normalized_path_buffer[PATH_MAX]; +__attribute__((visibility("default"))) +int fexecve(int fd, char *const *argv, char *const *envp) { + initProcess(true); - assert(getcwd(pwd, sizeof(pwd)) != NULL); - - sprintf(expected, "%s/path/to/binary", pwd); - assert_string_equals(expected, normalize_path("path/to/binary", normalized_path_buffer)); - assert_string_equals(expected, normalize_path("path/../path/to/binary", normalized_path_buffer)); - assert_string_equals(expected, normalize_path("./path/to/../to/binary", normalized_path_buffer)); - assert_string_equals( - "/usr/bin/sh", normalize_path("../../../../../../../../../../../../usr/./bin/../bin/sh", normalized_path_buffer)); + return fexecve_hook(true, fd, argv, envp); } -void test_inspect_file_header() { - char header[256]; - struct file_header_info info = {.interpreter_arg = NULL}; - - sprintf(header, "#!/bin/sh\n"); - inspect_file_header(TERMUX_BASE_DIR, header, 256, &info); - assert(!info.is_elf); - assert(!info.is_non_native_elf); - assert_string_equals(TERMUX_BIN_PATH "sh", info.interpreter); - assert(info.interpreter_arg == NULL); - - sprintf(header, "#!/bin/sh -x\n"); - inspect_file_header(TERMUX_BASE_DIR, header, 256, &info); - assert(!info.is_elf); - assert(!info.is_non_native_elf); - assert_string_equals(TERMUX_BIN_PATH "sh", info.interpreter); - assert_string_equals("-x", info.interpreter_arg); - - sprintf(header, "#! /bin/sh -x\n"); - inspect_file_header(TERMUX_BASE_DIR, header, 256, &info); - assert(!info.is_elf); - assert(!info.is_non_native_elf); - assert_string_equals(TERMUX_BIN_PATH "sh", info.interpreter); - assert_string_equals("-x", info.interpreter_arg); - - sprintf(header, "#!/bin/sh -x \n"); - inspect_file_header(TERMUX_BASE_DIR, header, 256, &info); - assert(!info.is_elf); - assert(!info.is_non_native_elf); - assert_string_equals(TERMUX_BIN_PATH "sh", info.interpreter); - assert_string_equals("-x", info.interpreter_arg); - - sprintf(header, "#!/bin/sh -x \n"); - inspect_file_header(TERMUX_BASE_DIR, header, 256, &info); - assert(!info.is_elf); - assert(!info.is_non_native_elf); - assert_string_equals(TERMUX_BIN_PATH "sh", info.interpreter); - assert_string_equals("-x", info.interpreter_arg); - - info.interpreter = NULL; - info.interpreter_arg = NULL; - // An ELF header for a 32-bit file. - // See https://en.wikipedia.org/wiki/Executable_and_Linkable_Format#File_header - sprintf(header, "\177ELF"); - // Native instruction set: - header[0x12] = EM_NATIVE; - header[0x13] = 0; - inspect_file_header(TERMUX_BASE_DIR, header, 256, &info); - assert(info.is_elf); - assert(!info.is_non_native_elf); - assert(info.interpreter == NULL); - assert(info.interpreter_arg == NULL); - - info.interpreter = NULL; - info.interpreter_arg = NULL; - // An ELF header for a 64-bit file. - // See https://en.wikipedia.org/wiki/Executable_and_Linkable_Format#File_header - sprintf(header, "\177ELF"); - // 'Fujitsu MMA Multimedia Accelerator' instruction set - likely non-native. - header[0x12] = 0x36; - header[0x13] = 0; - inspect_file_header(TERMUX_BASE_DIR, header, 256, &info); - assert(info.is_elf); - assert(info.is_non_native_elf); - assert(info.interpreter == NULL); - assert(info.interpreter_arg == NULL); -} - -void test_get_termux_base_dir() { - char buffer[TERMUX_BASE_DIR_MAX_LEN + 16]; - - setenv(TERMUX_PREFIX_ENV_NAME, "/data/data/com.termux/files/usr", true); - assert_string_equals("/data/data/com.termux/files", get_termux_base_dir(buffer)); - - setenv(TERMUX_PREFIX_ENV_NAME, "/data/data/com.termux/files/usr/made/up", true); - assert_string_equals("/data/data/com.termux/files/usr/made", get_termux_base_dir(buffer)); - - setenv(TERMUX_PREFIX_ENV_NAME, "/a/path/to/some/prefix/here", true); - assert_string_equals("/a/path/to/some/prefix", get_termux_base_dir(buffer)); - - setenv(TERMUX_PREFIX_ENV_NAME, "/a/path/to/some/prefix/here", true); - assert_string_equals("/a/path/to/some/prefix", get_termux_base_dir(buffer)); - - unsetenv(TERMUX_PREFIX_ENV_NAME); - assert_string_equals(TERMUX_BASE_DIR, get_termux_base_dir(buffer)); -} +__attribute__((visibility("default"))) +int execve(const char *name, char *const argv[], char *const envp[]) { + initProcess(true); -int main() { - test_starts_with(); - test_termux_rewrite_executable(); - test_remove_ld_from_env(); - test_normalize_path(); - test_inspect_file_header(); - test_get_termux_base_dir(); - return 0; + return execve_hook(true, name, argv, envp); } -#endif diff --git a/src/termux/termux_env.c b/src/termux/termux_env.c new file mode 100644 index 0000000..c4510e2 --- /dev/null +++ b/src/termux/termux_env.c @@ -0,0 +1,131 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include + +#ifdef __ANDROID__ +#include +#endif + +#include "../data/data_utils.h" +#include "../logger/logger.h" +#include "termux_env.h" + +static const char* LOG_TAG = "termux_env"; + + + +/** The Android build version sdk. */ +static int sAndroidBuildVersionSdk = 0; + + + +bool get_bool_env_value(const char *name, bool def) { + const char* value = getenv(name); + if (value == NULL || strlen(value) < 1) { + return def; + } else if (strcmp(value, "1") == 0 || strcmp(value, "true") == 0 || + strcmp(value, "on") == 0 || strcmp(value, "yes") == 0 || strcmp(value, "y") == 0) { + return true; + } else if (strcmp(value, "0") == 0 || strcmp(value, "false") == 0 || + strcmp(value, "off") == 0 || strcmp(value, "no") == 0 || strcmp(value, "n") == 0) { + return false; + } + return def; +} + + + +bool are_vars_in_env(char *const *envp, const char *vars[], int n) { + int env_length = 0; + while (envp[env_length] != NULL) { + for (int i = 0; i < n; i++) { + if (string_starts_with(envp[env_length], vars[i])) { + return true; + } + } + env_length++; + } + return false; +} + + + + + +int get_android_build_version_sdk() { + if (sAndroidBuildVersionSdk > 0) { + return sAndroidBuildVersionSdk; + } + + const char* value = getenv(ENV__ANDROID__BUILD_VERSION_SDK); + if (value != NULL && strlen(value) > 0) { + char build_version_sdk_string[strlen(value) + 1]; + strcpy(build_version_sdk_string, value); + int build_version_sdk = string_to_int(build_version_sdk_string, -1, + LOG_TAG, "Failed to convert '%s' env variable value '%s' to an int", ENV__ANDROID__BUILD_VERSION_SDK, build_version_sdk_string); + if (build_version_sdk > 0) { + sAndroidBuildVersionSdk = build_version_sdk; + return sAndroidBuildVersionSdk; + } + } + + #ifdef __ANDROID__ + sAndroidBuildVersionSdk = android_get_device_api_level(); + #endif + + return sAndroidBuildVersionSdk; +} + + + + + +static int get_log_level_value(const char *name) { + const char* value = getenv(name); + if (value == NULL || strlen(value) < 1) { + return DEFAULT_LOG_LEVEL; + } else { + char log_level_string[strlen(value) + 1]; + strcpy(log_level_string, value); + int log_level = string_to_int(log_level_string, -1, + LOG_TAG, "Failed to convert '%s' env variable value '%s' to an int", name, log_level_string); + if (log_level < 0) { + return DEFAULT_LOG_LEVEL; + } + return log_level; + } +} + +int get_termux_exec_log_level() { + return get_log_level_value(ENV__TERMUX_EXEC__LOG_LEVEL); +} + + + +bool is_termux_exec_execve_intercept_enabled() { + return get_bool_env_value(ENV__TERMUX_EXEC__INTERCEPT_EXECVE, E_DEF_VAL__TERMUX_EXEC__INTERCEPT_EXECVE); +} + +int get_termux_exec_system_linker_exec_config() { + int def = E_DEF_VAL__TERMUX_EXEC__SYSTEM_LINKER_EXEC; + const char* value = getenv(ENV__TERMUX_EXEC__SYSTEM_LINKER_EXEC); + if (value == NULL || strlen(value) < 1) { + return def; + } else if (strcmp(value, "disable") == 0) { + return 0; + } else if (strcmp(value, "enable") == 0) { + return 1; + } else if (strcmp(value, "force") == 0) { + return 2; + } + return def; +} + + + +int get_termux_exec__tests_log_level() { + return get_log_level_value(ENV__TERMUX_EXEC__TESTS__LOG_LEVEL); +} diff --git a/src/termux/termux_env.h b/src/termux/termux_env.h new file mode 100644 index 0000000..443892c --- /dev/null +++ b/src/termux/termux_env.h @@ -0,0 +1,380 @@ +#ifndef TERMUX_ENV_H +#define TERMUX_ENV_H + +#include + +/* + * Environment for Unix-like systems. + * + * - https://manpages.debian.org/testing/manpages/environ.7.en.html + * - https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html + */ + +/** + * Environment variable for the list of directory paths separated with + * colons `:` that certain functions and utilities use in searching + * for an executable file known only by a file `basename`. The list + * of directory paths is searched from beginning to end, by checking + * the path formed by concatenating a directory path, a path + * separator `/`, and the executable file `basename`, and the first + * file found, if any, with execute permission is executed. + * + * - https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html + * - https://manpages.debian.org/testing/manpages/environ.7.en.html#PATH + * - https://manpages.debian.org/testing/manpages-dev/basename.3.en.html + * + * Type: `string` + */ +#define ENV__PATH "PATH" +#define ENV_PREFIX__PATH ENV__PATH "=" + +/** + * Environment variable for the list of directory paths separated with + * colons `:` that should be searched for dynamic/shared library + * files that are dependencies of executables or libraries to be + * linked against. The list of directory paths is searched from + * beginning to end, by checking the path formed by concatenating a + * directory path, a path separator `/`, and the library file + * `basename`, and the first file found, if any, with read permission + * is opened. + * + * - https://manpages.debian.org/testing/manpages/ld.so.8.en.html#LD_LIBRARY_PATH + * + * Type: `string` + */ +#define ENV__LD_LIBRARY_PATH "LD_LIBRARY_PATH" +#define ENV_PREFIX__LD_LIBRARY_PATH ENV__LD_LIBRARY_PATH "=" + +/** + * Environment variable for the list of ELF shared object paths + * separated with colons `:` to be loaded before all others. This + * feature can be used to selectively override functions in other + * shared objects. + * + * - https://manpages.debian.org/testing/manpages/ld.so.8.en.html#LD_PRELOAD + * + * Type: `string` + */ +#define ENV__LD_PRELOAD "LD_PRELOAD" +#define ENV_PREFIX__LD_PRELOAD ENV__LD_PRELOAD "=" + +/** + * Environment variable for a path of a directory made available for + * programs that need a place to create temporary files. + * + * - https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html + * - https://manpages.debian.org/testing/manpages/environ.7.en.html + * - https://manpages.debian.org/testing/manpages-dev/tempnam.3.en.html + * + * Type: `string` + */ +#define ENV__TMPDIR "TMPDIR" +#define ENV_PREFIX__TMPDIR ENV__TMPDIR "=" + + + +/* + * Environment for Android. + */ + +#define ANDROID_ENV__S_ANDROID "ANDROID__" + +/** + * Environment variable for the Android build SDK version currently + * running on the device that is defined by `Build.VERSION#SDK_INT` + * and `ro.build.version.sdk` system property. + * + * This is exported by Termux app. + * + * - https://developer.android.com/reference/android/os/Build.VERSION#SDK_INT + * - https://developer.android.com/reference/android/os/Build.VERSION_CODES + * + * Type: `int` + * Default key: `ANDROID__BUILD_VERSION_SDK` + * Values: Unsigned integer values defined by `Build.VERSION_CODES`. + */ +#define ENV__ANDROID__BUILD_VERSION_SDK ANDROID_ENV__S_ANDROID "BUILD_VERSION_SDK" + + + +/* + * Environment for Termux. + */ + +/** + * Environment variable for the Termux app data directory path. + * + * Type: `string` + * Default key: `TERMUX_APP__DATA_DIR` + * Default value: null + * Values: + * - An absolute path with max length `TERMUX_APP__DATA_DIR___MAX_LEN` (`69`) + * including the null `\0` terminator. + */ +#define ENV__TERMUX_APP__DATA_DIR TERMUX_ENV__S_TERMUX_APP "DATA_DIR" + +/** + * Environment variable for the Termux legacy app data directory path. + * + * Type: `string` + * Default key: `TERMUX_APP__LEGACY_DATA_DIR` + * Default value: null + * Values: + * - An absolute path with max length `TERMUX_APP__DATA_DIR___MAX_LEN` (`69`) + * including the null `\0` terminator. + */ +#define ENV__TERMUX_APP__LEGACY_DATA_DIR TERMUX_ENV__S_TERMUX_APP "LEGACY_DATA_DIR" + +/** + * Environment variable for the Termux rootfs directory path. + * + * Type: `string` + * Default key: `TERMUX__ROOTFS` + * Default value: null + * Values: + * - An absolute path with max length `TERMUX__ROOTFS_DIR___MAX_LEN` (`85`) + * including the null `\0` terminator. + */ +#define ENV__TERMUX__ROOTFS TERMUX_ENV__S_TERMUX "ROOTFS" + +/** + * Environment variable for the Termux prefix directory path. + * + * Type: `string` + * Default key: `TERMUX__PREFIX` + * Default value: null + * Values: + * - An absolute path with max length `TERMUX__PREFIX_DIR___MAX_LEN` (`90`) + * including the null `\0` terminator. + */ +#define ENV__TERMUX__PREFIX TERMUX_ENV__S_TERMUX "PREFIX" + +/** + * Environment variable for the SeLinux process context of the Termux + * app process and its child processes. + * + * Type: `string` + * Default key: `TERMUX__SE_PROCESS_CONTEXT` + * Default value: null + * Values: + * - A valid Android SeLinux process context that matches `REGEX__PROCESS_CONTEXT`. + */ +#define ENV__TERMUX__SE_PROCESS_CONTEXT TERMUX_ENV__S_TERMUX "SE_PROCESS_CONTEXT" + +/** + * Environment variable for the Termux rootfs package manager. + * + * Type: `string` + * Default key: `TERMUX_ROOTFS__PACKAGE_MANAGER` + * Default value: null + * Values: `apt` or `pacman` + */ +#define ENV__TERMUX_ROOTFS__PACKAGE_MANAGER TERMUX_ENV__S_TERMUX_ROOTFS "PACKAGE_MANAGER" + + + +/* + * Environment for `termux-exec`. + */ + +/** + * Environment variable for the log level for `termux-exec`. + * + * Type: `int` + * Default key: `TERMUX_EXEC__LOG_LEVEL` + * Default value: DEFAULT_LOG_LEVEL + * Values: + * - `0` (`OFF`) - Log nothing. + * - `1` (`NORMAL`) - Log error, warn and info messages and stacktraces. + * - `2` (`DEBUG`) - Log debug messages. + * - `3` (`VERBOSE`) - Log verbose messages. + * - `4` (`VVERBOSE`) - Log very verbose messages. + */ +#define ENV__TERMUX_EXEC__LOG_LEVEL TERMUX_ENV__S_TERMUX_EXEC "LOG_LEVEL" + + + +/** + * Environment variable for whether `termux-exec` should intercept + * `execve()` wrapper [declared in `unistd.h`. + * + * Type: `bool` + * Default key: `TERMUX_EXEC__INTERCEPT_EXECVE` + * Default value: E_DEF_VAL__TERMUX_EXEC__INTERCEPT_EXECVE + * Values: + * - `true` - Intercept `execve()` enabled. + * - `false` - Intercept `execve()` disabled. + */ +#define ENV__TERMUX_EXEC__INTERCEPT_EXECVE TERMUX_ENV__S_TERMUX_EXEC "INTERCEPT_EXECVE" +static const bool E_DEF_VAL__TERMUX_EXEC__INTERCEPT_EXECVE = true; + +/** + * Environment variable for whether use System Linker Exec Solution, + * like to bypass App Data File Execute Restrictions. + * + * See also `should_system_linker_exec()` + * + * Type: `string` + * Default key: `TERMUX_EXEC__SYSTEM_LINKER_EXEC` + * Default value: E_DEF_VAL__TERMUX_EXEC__SYSTEM_LINKER_EXEC + * Values: + * - `disable` - The `system_linker_exec` will be disabled. + * - `enable` - The `system_linker_exec` will be enabled but only if required. + * - `force` - The `system_linker_exec` will be force enabled even if not required. + */ +#define ENV__TERMUX_EXEC__SYSTEM_LINKER_EXEC TERMUX_ENV__S_TERMUX_EXEC "SYSTEM_LINKER_EXEC" +static const int E_DEF_VAL__TERMUX_EXEC__SYSTEM_LINKER_EXEC = 1; + + + +/** + * Environment variable for the path to the executable file is being + * executed by `execve()` is using `system_linker_exec`. + * + * Type: `string` + * Default key: `TERMUX_EXEC__PROC_SELF_EXE` + * Values: + * - The normalized, absolutized and prefixed path for the executable + * file is being executed by `execve()` if `system_linker_exec` is + * being used. + */ +#define ENV__TERMUX_EXEC__PROC_SELF_EXE TERMUX_ENV__S_TERMUX_EXEC "PROC_SELF_EXE" +#define ENV_PREFIX__TERMUX_EXEC__PROC_SELF_EXE ENV__TERMUX_EXEC__PROC_SELF_EXE "=" + + + + + +/* + * Environment for `termux-exec-tests`. + */ + +/** + * Environment variable for the log level for `termux-exec-tests`. + * + * Type: `int` + * Default key: `TERMUX_EXEC__TESTS__LOG_LEVEL` + * Default value: DEFAULT_LOG_LEVEL + * Values: + * - `0` (`OFF`) - Log nothing. + * - `1` (`NORMAL`) - Log error, warn and info messages and stacktraces. + * - `2` (`DEBUG`) - Log debug messages. + * - `3` (`VERBOSE`) - Log verbose messages. + * - `4` (`VVERBOSE`) - Log very verbose messages. + */ +#define ENV__TERMUX_EXEC__TESTS__LOG_LEVEL TERMUX_ENV__S_TERMUX_EXEC__TESTS "LOG_LEVEL" + + + +/** + * Environment variable for the path to the termux-exec tests. + */ +#define ENV__TERMUX_EXEC__TESTS__TESTS_PATH TERMUX_ENV__S_TERMUX_EXEC__TESTS "TESTS_PATH" + + + + + +/** + * Get an environment variable value as a `bool`. + * + * The following values are parsed as `true`: `1`, `true`, `on`, `yes`, `y` + * The following values are parsed as `false`: `0`, `false`, `off`, `no`, `n` + * For any other value, the `def` value will be returned. + * Comparisons are case-sensitive. + * + * The purpose of this function is to have a single canonical parser for yes-or-no indications + * throughout the system. + * + * - https://cs.android.com/android/platform/superproject/+/android-13.0.0_r18:frameworks/base/core/java/android/os/SystemProperties.java;l=198 + * - https://cs.android.com/android/platform/superproject/+/android-13.0.0_r18:frameworks/base/core/jni/android_os_SystemProperties.cpp;l=123 + * - https://cs.android.com/android/platform/superproject/+/android-13.0.0_r18:system/libbase/include/android-base/parsebool.h + * - https://cs.android.com/android/platform/superproject/+/android-13.0.0_r18:system/libbase/parsebool.cpp + * + * @param value The value to parse as a `bool`. + * @param def The default value. + * @return Return the parsed `bool` value. + */ +bool get_bool_env_value(const char *name, bool def); + + + +/** + * Check if a env variable in `vars` exists in the `envp` where `n` is + * number of elements in `vars` to search starting from the first element. + * + * Each value in vars must be in the format `=`, like `PATH=`. + * + * @return Returns `true` if even a single variable is found, otherwise + * `false`. + */ +bool are_vars_in_env(char *const *envp, const char *vars[], int n); + + + +/** + * Returns the cached Android build version sdk from the + * `ENV__ANDROID__BUILD_VERSION_SDK` env variable, and if its not set, + * then the value from the `android_get_device_api_level()` call + * provided by `` is returned. + * + * The `android_get_device_api_level()` function was added in + * API 29 itself, but bionic provides an inline variant for backward + * compatibility. + * + * Getting value from system properties should be slower than from the + * environment variable. + * + * - https://cs.android.com/android/platform/superproject/+/android-14.0.0_r1:bionic/libc/include/android/api-level.h;l=191-209 + * - https://cs.android.com/android/_/android/platform/bionic/+/c0f46564528c7bec8d490e62633e962f2007b8f4 + * + * @return Return the Android build version sdk. + */ +int get_android_build_version_sdk(); + + + +/** + * Returns the `termux-exec` config for logger log level + * based on the `ENV__TERMUX_EXEC__LOG_LEVEL` env variable. + * + * @return Return the value if `ENV__TERMUX_EXEC__LOG_LEVEL` is + * set, otherwise defaults to `DEFAULT_LOG_LEVEL`. + */ +int get_termux_exec_log_level(); + + + +/** + * Returns the `termux-exec` config for whether `execve` should be + * intercepted based on the `ENV__TERMUX_EXEC__INTERCEPT_EXECVE` env variable. + * + * See also `get_bool_env_value()`. + * + * @return Return `true` if `ENV__TERMUX_EXEC__INTERCEPT_EXECVE` is + * bool enabled, `false` if bool disabled, otherwise defaults to `true`. + */ +bool is_termux_exec_execve_intercept_enabled(); + +/** + * Returns the `termux-exec` config for `system_linker_exec` based on + * the `ENV__TERMUX_EXEC__SYSTEM_LINKER_EXEC` env variable. + * + * @return Return `0` if `ENV__TERMUX_EXEC__SYSTEM_LINKER_EXEC` is set + * to `disable`, `1` if set to `enable`, `2` if set to `force`, + * otherwise defaults to `1` (`enable`). + */ +int get_termux_exec_system_linker_exec_config(); + + + +/** + * Returns the `termux-exec-tests` config for logger log level + * based on the `ENV__TERMUX_EXEC__TESTS__LOG_LEVEL` env variable. + * + * @return Return the value if `ENV__TERMUX_EXEC__TESTS__LOG_LEVEL` is + * set, otherwise defaults to `DEFAULT_LOG_LEVEL`. + */ +int get_termux_exec__tests_log_level(); + +#endif // TERMUX_ENV_H diff --git a/src/termux/termux_files.c b/src/termux/termux_files.c new file mode 100644 index 0000000..c3bc3b8 --- /dev/null +++ b/src/termux/termux_files.c @@ -0,0 +1,437 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "../data/data_utils.h" +#include "../file/file_utils.h" +#include "../logger/logger.h" +#include "termux_env.h" +#include "termux_files.h" + +static const char* LOG_TAG = "termux_files"; + + + +static char sTermuxAppDataDirBuffer[TERMUX_APP__DATA_DIR___MAX_LEN]; + +/** The Termux app data directory path. */ +static const char* sTermuxAppDataDir = NULL; + + +static char sTermuxLegacyAppDataDirBuffer[TERMUX_APP__DATA_DIR___MAX_LEN]; + +/** The Termux legacy app data directory path. */ +static const char* sTermuxLegacyAppDataDir = NULL; + + +static char sTermuxRootfsDirBuffer[TERMUX__ROOTFS_DIR___MAX_LEN]; + +/** The Termux rootfs directory path. */ +static const char* sTermuxRootfsDir = NULL; + + +static char sTermuxPrefixDirBuffer[TERMUX__PREFIX_DIR___MAX_LEN]; + +/** The Termux prefix directory path. */ +static const char* sTermuxPrefixDir = NULL; + + +static char sTermuxTmpDirBuffer[TERMUX__PREFIX__TMP_DIR___MAX_LEN]; + +/** The Termux tmp directory path. */ +static const char* sTermuxTmpDir = NULL; + + + +const char* get_termux_app_data_dir(const char* log_tag) { + if (sTermuxAppDataDir == NULL) { + sTermuxAppDataDir = get_termux_app_data_dir_from_env_or_default(log_tag, + sTermuxAppDataDirBuffer, sizeof(sTermuxAppDataDirBuffer)); + } + + return sTermuxAppDataDir; +} + +int get_termux_app_data_dir_from_env(const char* log_tag, char *buffer, size_t buffer_len) { + return get_path_from_env(log_tag, "app_data_dir", + ENV__TERMUX_APP__DATA_DIR, TERMUX_APP__DATA_DIR___MAX_LEN, + buffer, buffer_len); +} + +const char* get_termux_app_data_dir_from_env_or_default(const char* log_tag, char *buffer, size_t buffer_len) { + return get_path_from_env_or_default(log_tag, "app_data_dir", + ENV__TERMUX_APP__DATA_DIR, TERMUX_APP__DATA_DIR___MAX_LEN, + TERMUX_APP__DATA_DIR, -1, + buffer, buffer_len); +} + + +const char* get_termux_legacy_app_data_dir(const char* log_tag) { + if (sTermuxLegacyAppDataDir == NULL) { + sTermuxLegacyAppDataDir = get_termux_legacy_app_data_dir_from_env_or_default(log_tag, + sTermuxLegacyAppDataDirBuffer, sizeof(sTermuxLegacyAppDataDirBuffer)); + } + + return sTermuxLegacyAppDataDir; +} + +int get_termux_legacy_app_data_dir_from_env(const char* log_tag, char *buffer, size_t buffer_len) { + return get_path_from_env(log_tag, "legacy_app_data_dir", + ENV__TERMUX_APP__LEGACY_DATA_DIR, TERMUX_APP__DATA_DIR___MAX_LEN, + buffer, buffer_len); +} + +const char* get_termux_legacy_app_data_dir_from_env_or_default(const char* log_tag, char *buffer, size_t buffer_len) { + const char* termux_legacy_app_data_dir = get_path_from_env_or_default( + log_tag, "legacy_app_data_dir", + ENV__TERMUX_APP__LEGACY_DATA_DIR, TERMUX_APP__DATA_DIR___MAX_LEN, + TERMUX_APP__DATA_DIR, -1, + buffer, buffer_len); + if (termux_legacy_app_data_dir == NULL) { + return NULL; + } + + // If default path is not for a legacy app data directory path, + // then convert it into a legacy path by extracting the package + // name from the `basename` of path found and appending it to + // `/data/data/`. + if (!string_starts_with(termux_legacy_app_data_dir, "/data/data/")) { + char termux_legacy_app_data_dir_copy[strlen(termux_legacy_app_data_dir) + 1]; + strcpy(termux_legacy_app_data_dir_copy, termux_legacy_app_data_dir); + + termux_legacy_app_data_dir = convert_termux_app_data_dir_to_legacy_path(log_tag, + termux_legacy_app_data_dir_copy, buffer, buffer_len); + if (termux_legacy_app_data_dir == NULL) { + return NULL; + } + + #ifndef TERMUX_EXEC__RUNNING_TESTS + logErrorVVerbose(log_tag, "updated_legacy_app_data_dir: '%s'", termux_legacy_app_data_dir); + #endif + } + + return termux_legacy_app_data_dir; + +} + + +const char* convert_termux_app_data_dir_to_legacy_path(const char* log_tag, + const char *termux_app_data_dir, char *buffer, size_t buffer_len) { + (void)log_tag; + + if (termux_app_data_dir == NULL || *termux_app_data_dir != '/') { + #ifndef TERMUX_EXEC__RUNNING_TESTS + logErrorDebug(log_tag, "The app_data_dir '%s' to be converted to legacy path is not an absolute path", + termux_app_data_dir == NULL ? "" : termux_app_data_dir); + #endif + errno = EINVAL; + return NULL; + } + + char *last_path_separator = strrchr(termux_app_data_dir, '/'); + if (!last_path_separator) { + #ifndef TERMUX_EXEC__RUNNING_TESTS + logErrorDebug(log_tag, "Failed to find last path separator '/' in app_data_dir '%s' to be converted to legacy path", + termux_app_data_dir); + #endif + errno = EINVAL; + return NULL; + } + + size_t last_path_separator_position = last_path_separator - termux_app_data_dir; + size_t termux_app_data_dir_len = strlen(termux_app_data_dir); + + if (last_path_separator_position == 0) { + #ifndef TERMUX_EXEC__RUNNING_TESTS + logErrorDebug(log_tag, "The last path separator '/' is at rootfs in app_data_dir '%s' to be converted to legacy path", + termux_app_data_dir); + #endif + errno = EINVAL; + return NULL; + } + + // If path contains at least one character after last path + // separator `/`, ideally for package name. + if ((last_path_separator_position + 1) >= termux_app_data_dir_len) { + #ifndef TERMUX_EXEC__RUNNING_TESTS + logErrorDebug(log_tag, "No basename found in app_data_dir '%s' to be converted to legacy path", + termux_app_data_dir); + #endif + errno = EINVAL; + return NULL; + } + + // Copy package name from basename. + const char *src = last_path_separator + 1; + size_t termux_legacy_app_data_dir_len = 11 + strlen(src); + if (buffer_len <= termux_legacy_app_data_dir_len) { + #ifndef TERMUX_EXEC__RUNNING_TESTS + logErrorDebug(log_tag, "The legacy_app_data_dir '/data/data/%s' with length '%zu' is too long to fit in the buffer with length '%zu'", + src, termux_legacy_app_data_dir_len, buffer_len); + #endif + errno = ENAMETOOLONG; + return NULL; + } + + strcpy(buffer, "/data/data/"); + strcpy(buffer + 11, src); + + return buffer; +} + + + +const char* get_termux_rootfs_dir(const char* log_tag) { + if (sTermuxRootfsDir == NULL) { + sTermuxRootfsDir = get_termux_rootfs_dir_from_env_or_default(log_tag, + sTermuxRootfsDirBuffer, sizeof(sTermuxRootfsDirBuffer)); + } + + return sTermuxRootfsDir; +} + +int get_termux_rootfs_dir_from_env(const char* log_tag, char *buffer, size_t buffer_len) { + return get_path_from_env(log_tag, "rootfs_dir", + ENV__TERMUX__ROOTFS, TERMUX__ROOTFS_DIR___MAX_LEN, + buffer, buffer_len); +} + +const char* get_termux_rootfs_dir_from_env_or_default(const char* log_tag, char *buffer, size_t buffer_len) { + return get_path_from_env_or_default(log_tag, "rootfs_dir", + ENV__TERMUX__ROOTFS, TERMUX__ROOTFS_DIR___MAX_LEN, + TERMUX__ROOTFS, 3 /* (R_OK | X_OK) */, + buffer, buffer_len); +} + + + +const char* get_termux_prefix_dir(const char* log_tag) { + if (sTermuxPrefixDir == NULL) { + sTermuxPrefixDir = get_termux_prefix_dir_from_env_or_default(log_tag, + sTermuxPrefixDirBuffer, sizeof(sTermuxPrefixDirBuffer)); + } + + return sTermuxPrefixDir; +} + +int get_termux_prefix_dir_from_env(const char* log_tag, char *buffer, size_t buffer_len) { + return get_path_from_env(log_tag, "prefix_dir", + ENV__TERMUX__PREFIX, TERMUX__PREFIX_DIR___MAX_LEN, + buffer, buffer_len); +} + +const char* get_termux_prefix_dir_from_env_or_default(const char* log_tag, char *buffer, size_t buffer_len) { + return get_path_from_env_or_default(log_tag, "prefix_dir", + ENV__TERMUX__PREFIX, TERMUX__PREFIX_DIR___MAX_LEN, + TERMUX__PREFIX, 3 /* (R_OK | X_OK) */, + buffer, buffer_len); +} + + + +const char* get_termux_tmp_dir(const char* log_tag) { + if (sTermuxTmpDir == NULL) { + sTermuxTmpDir = get_termux_tmp_dir_from_env_or_default(log_tag, + sTermuxTmpDirBuffer, sizeof(sTermuxTmpDirBuffer)); + } + + return sTermuxTmpDir; +} + +int get_termux_tmp_dir_from_env(const char* log_tag, char *buffer, size_t buffer_len) { + return get_path_from_env(log_tag, "tmp_dir", + ENV__TMPDIR, TERMUX__PREFIX__TMP_DIR___MAX_LEN, + buffer, buffer_len); +} + +const char* get_termux_tmp_dir_from_env_or_default(const char* log_tag, char *buffer, size_t buffer_len) { + return get_path_from_env_or_default(log_tag, "tmp_dir", + ENV__TMPDIR, TERMUX__PREFIX__TMP_DIR___MAX_LEN, + TERMUX__PREFIX__TMP_DIR, -1, + buffer, buffer_len); +} + + + + + +char *termux_prefix_path(const char* log_tag, const char *termux_prefix_dir, const char *executable_path, + char *buffer, size_t buffer_len) { + (void)log_tag; + (void)LOG_TAG; + + size_t executable_path_len = strlen(executable_path); + if (buffer_len <= executable_path_len) { + #ifndef TERMUX_EXEC__RUNNING_TESTS + logErrorDebug(LOG_TAG, "The original executable_path '%s' with length '%zu' to prefix is too long to fit in the buffer with length '%zu'", + executable_path, executable_path_len, buffer_len); + #endif + errno = ENAMETOOLONG; + return NULL; + } + + // If `executable_path` is not an absolute path. + if (executable_path[0] != '/') { + strcpy(buffer, executable_path); + return buffer; + } + + char termux_bin_path[TERMUX__PREFIX__BIN_DIR___MAX_LEN + 1]; + + // If `executable_path` equals with `/bin` or `/usr/bin` (currently not `/xxx/bin`). + if (strcmp(executable_path, "/bin") == 0 || strcmp(executable_path, "/usr/bin") == 0) { + if (termux_prefix_dir == NULL) { + termux_prefix_dir = get_termux_prefix_dir(log_tag); + if (termux_prefix_dir == NULL) { return NULL; } + } + + // If `termux_prefix_dir` equals `/`. + if (strlen(termux_prefix_dir) == 1 && termux_prefix_dir[0] == '/') { + strcpy(buffer, executable_path); + return buffer; + } + + snprintf(termux_bin_path, sizeof(termux_bin_path), "%s/bin", termux_prefix_dir); + strcpy(buffer, termux_bin_path); + return buffer; + } + + char *bin_match = strstr(executable_path, "/bin/"); + // If `executable_path` starts with `/bin/` or `/xxx/bin`. + if (bin_match == executable_path || bin_match == (executable_path + 4)) { + if (termux_prefix_dir == NULL) { + termux_prefix_dir = get_termux_prefix_dir(log_tag); + if (termux_prefix_dir == NULL) { return NULL; } + } + + // If `termux_prefix_dir` equals `/`. + if (strlen(termux_prefix_dir) == 1 && termux_prefix_dir[0] == '/') { + strcpy(buffer, executable_path); + return buffer; + } + + int termux_bin_path_len = snprintf(termux_bin_path, sizeof(termux_bin_path), + "%s/bin/", termux_prefix_dir); + + strcpy(buffer, termux_bin_path); + char *dest = buffer + termux_bin_path_len; + // Copy what comes after `/bin/`. + const char *src = bin_match + 5; + size_t prefixed_path_len = termux_bin_path_len + strlen(src); + if (buffer_len <= prefixed_path_len) { + #ifndef TERMUX_EXEC__RUNNING_TESTS + logErrorDebug(log_tag, "The prefixed_path '%s%s' with length '%zu' is too long to fit in the buffer with length '%zu'", + termux_bin_path, src, prefixed_path_len, buffer_len); + #endif + errno = ENAMETOOLONG; + return NULL; + } + + strcpy(dest, src); + return buffer; + } else { + strcpy(buffer, executable_path); + return buffer; + } +} + + + + + +int is_path_under_termux_app_data_dir(const char* log_tag, const char *path, + const char *termux_app_data_dir, const char *termux_legacy_app_data_dir) { + if (path == NULL || *path == '\0') { + return 1; + } + + + const char* real_path; + char real_path_buffer[PATH_MAX]; + (void)real_path_buffer; + if (strstr(path, "/fd/") != NULL && regex_match(path, REGEX__PROC_FD_PATH, REG_EXTENDED) == 0) { + real_path = get_fd_realpath(log_tag, path, real_path_buffer, sizeof(real_path_buffer)); + if (real_path == NULL) { + return -1; + } + + path = real_path; + } + + + // Termux app for version `>= 0.119.0` will export + // `TERMUX_APP__DATA_DIR` with the non-legacy app data directory + // paths (`/data/user//` or + // `/mnt/expand//user/0/`) that + // are normally returned by Android `ApplicationInfo.dataDir` call. + // It will also export `TERMUX_APP__LEGACY_DATA_DIR` with the + // legacy app data directory path (`/data/data/`), + // however, it will only be accessible if app is running on + // primary user `0`, or if Android vendor does a bind mount for + // secondary users/profiles as well or if + // bind mount was done manually on rooted devices with + // `MountLegacyAppDataDirPaths.java`. + // + // The checking if `path` passed to this function is under termux + // app data directory is done based on following logic, assuming + // `termux_app_data_dir` and `termux_legacy_app_data_dir` passed + // are `NULL`. The below `FAIL` condition, like where `termux-exec` + // was compiled with the default value of a legacy path + // `/data/data/` and a path starting with + // `/data/user/0/` is executed instead + // of it starting with `/data/data/`, would not + // normally occur as rootfs would be for a legacy path as well, + // and if solving it is needed, then user should used the app + // version (like `>= 0.119.0`) where `TERMUX_APP__DATA_DIR` + // variable is exported automatically. + // - Executing path under legacy app data directory: + // - If `ENV__TERMUX_APP__LEGACY_DATA_DIR` is set and valid: + // - The env path will be used as is. + // - Else: + // - A non-legacy default path will be used after being + // converted to a legacy path. + // - A legacy default path will be used as is. + // - Executing path under non-legacy app data directory: + // - If `ENV__TERMUX_APP__DATA_DIR` is set and valid: + // - The env path will be used as is. + // - Else: + // - A non-legacy default path will be used as is. + // - A legacy default path will be used as is. (FAIL) + if (string_starts_with(path, "/data/data/")) { + if (termux_legacy_app_data_dir == NULL) { + termux_legacy_app_data_dir = get_termux_legacy_app_data_dir(log_tag); + if (termux_legacy_app_data_dir == NULL) { return -1; } + } + + termux_app_data_dir = termux_legacy_app_data_dir; + } else { + if (termux_app_data_dir == NULL) { + termux_app_data_dir = get_termux_app_data_dir(log_tag); + if (termux_app_data_dir == NULL) { return -1; } + } + } + + return is_path_in_dir_path("app_data_dir", path, termux_app_data_dir, true); +} + + + +int is_path_under_termux_rootfs_dir(const char* log_tag, const char *path, + const char *termux_rootfs_dir) { + if (termux_rootfs_dir == NULL) { + termux_rootfs_dir = get_termux_rootfs_dir(log_tag); + if (termux_rootfs_dir == NULL) { return -1; } + } + + return is_path_or_fd_path_in_dir_path(log_tag, "rootfs_dir", path, termux_rootfs_dir, true); +} diff --git a/src/termux/termux_files.h b/src/termux/termux_files.h new file mode 100644 index 0000000..d56850a --- /dev/null +++ b/src/termux/termux_files.h @@ -0,0 +1,334 @@ +#ifndef TERMUX_FILES_H +#define TERMUX_FILES_H + +/** + * The max length for the `TERMUX_APP__DATA_DIR` directory including + * the null '\0' terminator. + * + * Check https://github.com/termux/termux-packages/wiki/Termux-file-system-layout#file-path-limits + * for why the value `69` is chosen. + * + * Constant value: `69` + */ +#define TERMUX_APP__DATA_DIR___MAX_LEN 69 + + + +/** + * The max length for the `TERMUX__ROOTFS` directory including the + * null '\0' terminator. + * + * Check https://github.com/termux/termux-packages/wiki/Termux-file-system-layout#file-path-limits + * for why the value `86` is chosen. + * + * Constant value: `86` + */ +#define TERMUX__ROOTFS_DIR___MAX_LEN 86 + + +/** + * The max length for the `TERMUX__PREFIX` directory including the + * null '\0' terminator. + * + * Constant value: `90` + */ +#define TERMUX__PREFIX_DIR___MAX_LEN (TERMUX__ROOTFS_DIR___MAX_LEN + 1 + 3) // "/usr" (90) + + +/** + * The max length for the `TERMUX__BIN_DIR` including the null '\0' terminator. + * + * Constant value: `94` + */ +#define TERMUX__PREFIX__BIN_DIR___MAX_LEN (TERMUX__PREFIX_DIR___MAX_LEN + 1 + 3) // "/bin" (94) + +/** + * The max safe length for a sub file path under the `TERMUX__BIN_DIR` + * including the null '\0' terminator. + * + * This allows for a filename with max length `33` so that the path + * length is under `128` (`BINPRM_BUF_SIZE`) for Linux kernel `< 5.1`. + * Check `exec.h` docs. + * + * Constant value: `127` + */ +#define TERMUX__PREFIX__BIN_FILE___SAFE_MAX_LEN (TERMUX__PREFIX__BIN_DIR___MAX_LEN + 1 + 33) // "/" (127) + + +/** + * The max length for the `TERMUX__PREFIX__TMP_DIR` directory including + * the null '\0' terminator. + * + * Constant value: `94` + */ +#define TERMUX__PREFIX__TMP_DIR___MAX_LEN (TERMUX__PREFIX_DIR___MAX_LEN + 1 + 3) // "/tmp" (94) + + + +/** + * Get cached `sTermuxAppDataDir` that is set to the path returned by + * `get_termux_app_data_dir_from_env_or_default()` on its first call. + */ +const char* get_termux_app_data_dir(const char* log_tag); + +/** + * Get the Termux app data directory from the `ENV__TERMUX_APP__DATA_DIR` + * env variable value if its set to a valid absolute path with max + * length `TERMUX_APP__DATA_DIR___MAX_LEN`. + * + * + * @return Returns `0` if a valid app data directory is found and + * copied to the buffer, `1` if valid app data directory is not found, + * otherwise `-1` on other failures. + */ +int get_termux_app_data_dir_from_env(const char* log_tag, + char *buffer, size_t buffer_len); + +/** + * Get the Termux app data directory from the `ENV__TERMUX_APP__DATA_DIR` + * env variable if its set to a valid absolute path with max length + * `TERMUX_APP__DATA_DIR___MAX_LEN`, otherwise if it fails, then + * return `TERMUX_APP__DATA_DIR` set by `Makefile` if it is readable + * and executable. + * + * @return Returns the `char *` to app data directory on success, + * otherwise `NULL`. + */ +const char* get_termux_app_data_dir_from_env_or_default(const char* log_tag, + char *buffer, size_t buffer_len); + + +/** + * Get cached `sTermuxLegacyAppDataDir` that is set to the path + * returned by `get_termux_legacy_app_data_dir_from_env_or_default()` + * on its first call. + */ +const char* get_termux_legacy_app_data_dir(const char* log_tag); + +/** + * Get the Termux legacy app data directory from the + * `ENV__TERMUX_APP__LEGACY_DATA_DIR` env variable value if its set to + * a valid absolute path with max length `TERMUX_APP__DATA_DIR___MAX_LEN`. + * + * + * @return Returns `0` if a valid directory is found and + * copied to the buffer, `1` if valid directory is not found, + * otherwise `-1` on other failures. + */ +int get_termux_legacy_app_data_dir_from_env(const char* log_tag, + char *buffer, size_t buffer_len); + +/** + * Get the Termux legacy app data directory from the + * `ENV__TERMUX_APP__LEGACY_DATA_DIR` env variable if its set to a + * valid absolute path with max length `TERMUX_APP__DATA_DIR___MAX_LEN`, + * otherwise if it fails, then return `TERMUX_APP__DATA_DIR` set by + * `Makefile` if it is readable and executable after converting it to + * a legacy path if required by calling + * `convert_termux_app_data_dir_to_legacy_path()`. + * + * @return Returns the `char *` to directory on success, + * otherwise `NULL`. + */ +const char* get_termux_legacy_app_data_dir_from_env_or_default(const char* log_tag, + char *buffer, size_t buffer_len); + + +/** + * Convert a termux app data directory in the format + * `/data/user//` or + * `/mnt/expand//user/0/` to a legacy path + * `/data/data/` by extracting the package name from + * the `basename` and appending it to `/data/data/`. + * + * No validation is currently done to ensure `termux_app_data_dir` + * is in correct format and `basename` is used even if it is not a for + * a package name. So its callers responsibility to pass a valid path. + * + * @return Returns the `char *` to directory on success, + * otherwise `NULL`. + */ +const char* convert_termux_app_data_dir_to_legacy_path(const char* log_tag, + const char *termux_app_data_dir, char *buffer, size_t buffer_len); + + + +/** + * Get cached `sTermuxRootfsDir` that is set to the path returned by + * `get_termux_rootfs_dir_from_env_or_default()` on its first call. + */ +const char* get_termux_rootfs_dir(const char* log_tag); + +/** + * Get the Termux rootfs directory from the `ENV__TERMUX__ROOTFS` env + * variable value if its set to a valid absolute path with max length + * `TERMUX__ROOTFS_DIR___MAX_LEN`. + * + * + * @return Returns `0` if a valid directory is found and copied + * to the buffer, `1` if valid directory is not found, otherwise + * `-1` on other failures. + */ +int get_termux_rootfs_dir_from_env(const char* log_tag, + char *buffer, size_t buffer_len); + +/** + * Get the Termux rootfs directory from the `ENV__TERMUX__ROOTFS` + * env variable if its set to a valid absolute path with max length + * `TERMUX__ROOTFS_DIR___MAX_LEN`, otherwise if it fails, then return + * `TERMUX__ROOTFS` set by `Makefile` if it is readable and executable. + * + * @return Returns the `char *` to directory on success, + * otherwise `NULL`. + */ +const char* get_termux_rootfs_dir_from_env_or_default(const char* log_tag, + char *buffer, size_t buffer_len); + + + +/** + * Get cached `sTermuxPrefixDir` that is set to the path returned by + * `get_termux_prefix_dir_from_env_or_default()` on its first call. + */ +const char* get_termux_prefix_dir(const char* log_tag); + +/** + * Get the Termux prefix directory from the `ENV__TERMUX__PREFIX` env + * variable value if its set to a valid absolute path with max length + * `TERMUX__PREFIX_DIR___MAX_LEN`. + * + * + * @return Returns `0` if a valid directory is found and copied + * to the buffer, `1` if valid directory is not found, otherwise + * `-1` on other failures. + */ +int get_termux_prefix_dir_from_env(const char* log_tag, + char *buffer, size_t buffer_len); + +/** + * Get the Termux prefix directory from the `ENV__TERMUX__PREFIX` + * env variable if its set to a valid absolute path with max length + * `TERMUX__PREFIX_DIR___MAX_LEN`, otherwise if it fails, then return + * `TERMUX__PREFIX` set by `Makefile` if it is readable and executable. + * + * @return Returns the `char *` to directory on success, + * otherwise `NULL`. + */ +const char* get_termux_prefix_dir_from_env_or_default(const char* log_tag, + char *buffer, size_t buffer_len); + + + +/** + * Get cached `sTermuxTmpDir` that is set to the path returned by + * `get_termux_tmp_dir_from_env_or_default()` on its first call. + */ +const char* get_termux_tmp_dir(const char* log_tag); + +/** + * Get the Termux tmp directory from the `ENV__TMPDIR` env + * variable value if its set to a valid absolute path with max length + * `TERMUX__PREFIX__TMP_DIR___MAX_LEN`. + * + * + * @return Returns `0` if a valid tmp directory is found and copied + * to the buffer, `1` if valid tmp directory is not found, otherwise + * `-1` on other failures. + */ +int get_termux_tmp_dir_from_env(const char* log_tag, + char *buffer, size_t buffer_len); + +/** + * Get the Termux tmp directory from the `ENV__TMPDIR` + * env variable if its set to a valid absolute path with max length + * `TERMUX__PREFIX__TMP_DIR___MAX_LEN`, otherwise if it fails, then return + * `TERMUX__PREFIX__TMP_DIR` set by `Makefile` if it is readable and executable. + * + * @return Returns the `char *` to tmp directory on success, + * otherwise `NULL`. + */ +const char* get_termux_tmp_dir_from_env_or_default(const char* log_tag, + char *buffer, size_t buffer_len); + + + + + +/** + * Prefix `bin` directory paths like `/bin` and `/usr/bin` with + * termux prefix directory, like convert `/bin/sh` to `/usr/bin/sh`. + * + * The buffer size must at least be `TERMUX__PREFIX_DIR___MAX_LEN` + strlen(executable_path)`, + * or preferably `PATH_MAX`. + * + * @param termux_prefix_dir The **normalized** path to termux prefix + * directory. If `NULL`, then path returned by + * `get_termux_prefix_dir_from_env_or_default()` + * will be used by calling `get_termux_prefix_dir()`. + * @param executable_path The executable path to prefix. + * @param buffer The output path buffer. + * @param buffer_len The output path buffer length. + * @return Returns the `char *` to original or prefixed directory on + * success, otherwise `NULL` on failures. + */ +char *termux_prefix_path(const char* log_tag, const char *termux_prefix_dir, + const char *executable_path, char *buffer, size_t buffer_len); + + + + + +/** + * Check whether the `path` is in `termux_app_data_dir`. If path is + * a fd path matched by `REGEX__PROC_FD_PATH`, then the real path + * of the fd returned by `get_fd_realpath()` will be checked instead. + * + * **Both `path` and `termux_app_data_dir` must be normalized paths, + * as `is_path_or_fd_path_in_dir_path()` called by this function will + * currently not normalize either path by itself.** + * + * @param log_tag The log tag to use for logging. + * @param path The `path` to check. + * @param termux_app_data_dir The **normalized** path to termux app + * data directory. If `NULL`, then path returned by + * `get_termux_app_data_dir_from_env_or_default()` + * will be used by calling `get_termux_app_data_dir()`. + * @param termux_legacy_app_data_dir The **normalized** path to termux + * legacy app data directory in the format + * `/data/data/`. If `NULL`, then path + * returned by `get_termux_legacy_app_data_dir_from_env_or_default()` + * will be used by calling `get_termux_legacy_app_data_dir()`. + * @return Returns `0` if `path` is in `termux_app_data_dir`, `1` if + * `path` is not in `termux_app_data_dir`, otherwise `-1` on other + * failures. + */ +int is_path_under_termux_app_data_dir(const char* log_tag, const char *path, + const char *termux_app_data_dir, const char *termux_legacy_app_data_dir); + + + + + +/** + * Check whether the `path` is in `termux_rootfs_dir`. If path is + * a fd path matched by `REGEX__PROC_FD_PATH`, then the real path + * of the fd returned by `get_fd_realpath()` will be checked instead. + * + * **Both `path` and `termux_rootfs_dir` must be normalized paths, as + * `is_path_or_fd_path_in_dir_path()` called by this function will + * currently not normalize either path by itself.** + * + * @param log_tag The log tag to use for logging. + * @param path The `path` to check. + * @param termux_rootfs_dir The **normalized** path to termux rootfs + * directory. If `NULL`, then path returned by + * `get_termux_rootfs_dir_from_env_or_default()` + * will be used by calling `get_termux_rootfs_dir()`. + * @return Returns `0` if `path` is in `termux_rootfs_dir`, `1` if + * `path` is not in `termux_rootfs_dir`, otherwise `-1` on other + * failures. + */ +int is_path_under_termux_rootfs_dir(const char* log_tag, const char *path, + const char *termux_rootfs_dir); + +#endif // TERMUX_FILES_H diff --git a/termux-exec-debug.json b/termux-exec-debug.json deleted file mode 100644 index 4b45a7d..0000000 --- a/termux-exec-debug.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "control": { - "Package": "termux-exec", - "Version": "9:9.8", - "Architecture": "aarch64", - "Maintainer": "Termux developers", - "Description": "Development version of termux-exec" - }, - - "installation_prefix": "/data/data/com.termux/files/usr", - - "data_files": { - "lib/libtermux-exec.so": { - "source": "libtermux-exec.so" - } - } -} diff --git a/termux-exec-package.json.in b/termux-exec-package.json.in new file mode 100644 index 0000000..e6e426a --- /dev/null +++ b/termux-exec-package.json.in @@ -0,0 +1,19 @@ +{ + "control": { + "Package": "termux-exec", + "Version": "@TERMUX_EXEC_PKG__VERSION@", + "Architecture": "@TERMUX_EXEC_PKG__ARCH@", + "Maintainer": "@termux", + "Homepage": "https://github.com/termux/termux-exec", + "Description": "A LD_PRELOAD shared library for proper functioning of the Termux execution environment" + }, + + "installation_prefix": "@TERMUX__PREFIX@", + + "data_files": { + "": { + "source": "build/usr", + "source_recurse": true + } + } +} diff --git a/test-program.c b/test-program.c deleted file mode 100644 index a7f559f..0000000 --- a/test-program.c +++ /dev/null @@ -1,75 +0,0 @@ -#include -#include -#include -#include - -#define TERMUX_APT_PATH "/data/data/com.termux/files/usr/bin/apt" - -extern char **environ; - -int main() { - if (fork() == 0) { - char *const argv[] = { "apt", "--version", NULL }; - printf("# execve\n"); - execve(TERMUX_APT_PATH, argv, environ); - return 0; - } - - sleep(1); - if (fork() == 0) { - printf("# execl\n"); - execl(TERMUX_APT_PATH, "apt", "--version", NULL); - return 0; - } - - sleep(1); - if (fork() == 0) { - printf("# execlp\n"); - execlp("apt", "apt", "--version", NULL); - return 0; - } - - sleep(1); - if (fork() == 0) { - printf("# execle\n"); - execle(TERMUX_APT_PATH, "apt", "--version", NULL, environ); - return 0; - } - - sleep(1); - if (fork() == 0) { - char *const argv[] = { "apt", "--version", NULL }; - printf("# execv\n"); - execv(TERMUX_APT_PATH, argv); - return 0; - } - - sleep(1); - if (fork() == 0) { - char *const argv[] = { "apt", "--version", NULL }; - printf("# execvp\n"); - execvp("apt", argv); - return 0; - } - - sleep(1); - if (fork() == 0) { - char *const argv[] = { "apt", "--version", NULL }; - printf("# execvpe\n"); - execvpe("apt", argv, environ); - return 0; - } - - sleep(1); - if (fork() == 0) { - char *const argv[] = { "apt", "--version", NULL }; - int fd = open(TERMUX_APT_PATH, 0); - printf("# fexecve\n"); - fexecve(fd, argv, environ); - return 0; - } - - sleep(1); - - return 0; -} diff --git a/tests/args-with-spaces.sh b/tests/args-with-spaces.sh deleted file mode 100755 index 1d94c65..0000000 --- a/tests/args-with-spaces.sh +++ /dev/null @@ -1 +0,0 @@ -#!/bin/echo hello world bye diff --git a/tests/args-with-spaces.sh-expected b/tests/args-with-spaces.sh-expected deleted file mode 100644 index 0d06dcc..0000000 --- a/tests/args-with-spaces.sh-expected +++ /dev/null @@ -1 +0,0 @@ -hello world bye tests/args-with-spaces.sh myarg1 myarg2 diff --git a/tests/files/exec/print-args-binary.c b/tests/files/exec/print-args-binary.c new file mode 100644 index 0000000..ee55833 --- /dev/null +++ b/tests/files/exec/print-args-binary.c @@ -0,0 +1,11 @@ +#include +#include + +int main(int argc, char* argv[]) { + for (int i = 1; i < argc; i++) { + fprintf(stdout, i == 1 ? "%s" : " %s", argv[i]); + } + + fprintf(stdout, "\n"); + fflush(stdout); +} diff --git a/tests/files/exec/print-args-binary.sym b/tests/files/exec/print-args-binary.sym new file mode 120000 index 0000000..f2573d9 --- /dev/null +++ b/tests/files/exec/print-args-binary.sym @@ -0,0 +1 @@ +./print-args-binary \ No newline at end of file diff --git a/tests/files/exec/print-args-linux-script.sh b/tests/files/exec/print-args-linux-script.sh new file mode 100644 index 0000000..da47567 --- /dev/null +++ b/tests/files/exec/print-args-linux-script.sh @@ -0,0 +1,3 @@ +#!/usr/bin/sh + +echo "$@" diff --git a/tests/files/exec/print-args-linux-script.sh.sym b/tests/files/exec/print-args-linux-script.sh.sym new file mode 120000 index 0000000..258f6f5 --- /dev/null +++ b/tests/files/exec/print-args-linux-script.sh.sym @@ -0,0 +1 @@ +./print-args-linux-script.sh \ No newline at end of file diff --git a/tests/files/exec/print-args-termux-script.sh.in b/tests/files/exec/print-args-termux-script.sh.in new file mode 100644 index 0000000..9fd920a --- /dev/null +++ b/tests/files/exec/print-args-termux-script.sh.in @@ -0,0 +1,3 @@ +#!@TERMUX__PREFIX@/bin/sh + +echo "$@" diff --git a/tests/files/exec/print-args-termux-script.sh.sym b/tests/files/exec/print-args-termux-script.sh.sym new file mode 120000 index 0000000..52bdef9 --- /dev/null +++ b/tests/files/exec/print-args-termux-script.sh.sym @@ -0,0 +1 @@ +./print-args-termux-script.sh \ No newline at end of file diff --git a/tests/initial-whitespace.sh b/tests/initial-whitespace.sh deleted file mode 100755 index 5df0a44..0000000 --- a/tests/initial-whitespace.sh +++ /dev/null @@ -1,3 +0,0 @@ -# !/bin/sh - -echo hi2 diff --git a/tests/initial-whitespace.sh-expected b/tests/initial-whitespace.sh-expected deleted file mode 100644 index 7cc3903..0000000 --- a/tests/initial-whitespace.sh-expected +++ /dev/null @@ -1 +0,0 @@ -hi2 diff --git a/tests/not-executable.sh b/tests/not-executable.sh deleted file mode 100644 index 2a22daa..0000000 --- a/tests/not-executable.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -echo hello diff --git a/tests/not-executable.sh-expected b/tests/not-executable.sh-expected deleted file mode 100644 index 5e7557b..0000000 --- a/tests/not-executable.sh-expected +++ /dev/null @@ -1 +0,0 @@ -./run-tests.sh: line 12: tests/not-executable.sh: Permission denied diff --git a/tests/runtime-binary-tests.c b/tests/runtime-binary-tests.c new file mode 100644 index 0000000..d33de05 --- /dev/null +++ b/tests/runtime-binary-tests.c @@ -0,0 +1,697 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#ifdef __ANDROID__ +#include +#endif + +#include "../src/data/assert_utils.h" +#include "../src/data/data_utils.h" +#include "../src/logger/logger.h" +#include "../src/exec/exec_variants.h" +#include "../src/termux/termux_env.h" + +static const char* LOG_TAG = "bin"; + +static uid_t UID; + +#define TERMUX_EXEC__TESTS__TESTS_PATH TERMUX__PREFIX "/libexec/installed-tests/termux-exec" +#define TERMUX_EXEC__TESTS__TEST_FILES_BASENAME "files" +#define TERMUX_EXEC__TESTS__EXEC_TEST_FILES_BASENAME "exec" + +extern char **environ; + +static void init(); +static void initLogger(); + + + +typedef struct { + bool is_child; + int cpid; + int exit_code; + int status; + int pipe_fds[2]; + FILE *pipe_file; + char *output; + bool return_output; + size_t output_initial_buffer; +} fork_info; + +#define INIT_FORK_INFO(X) fork_info X = {.cpid = -1, .exit_code = -1, .pipe_file = NULL, .output = NULL, .return_output = true, .output_initial_buffer = 255} + +void cleanup_fork(fork_info *info) { + if (info->pipe_fds[0] != -1) + close(info->pipe_fds[0]); + if (info->pipe_fds[1] != -1) + close(info->pipe_fds[1]); + if (info->pipe_file != NULL) + fclose(info->pipe_file); + if (info->output != NULL) + free(info->output); +} + +void exit_fork_with_error(fork_info *info, int exit_code) { + cleanup_fork(info); + if (!info->is_child && info->cpid != -1 && info->exit_code == -1) { + kill(info->cpid, SIGKILL); + } + exit(exit_code); +} + +int fork_child(fork_info *info) { + pid_t cpid, w; + + + // Create pipe for capturing child stdout and stderr. + if (pipe(info->pipe_fds) == -1) { + logStrerror(LOG_TAG, "pipe() failed"); + return -1; + } + + cpid = fork(); + if (cpid == -1) { + logStrerror(LOG_TAG, "fork() failed"); + exit(1); + } + + // If in child. + if (cpid == 0) { + info->is_child = true; + + initLogger(); + updateLogPid(); + + // Redirect stdout/stderr to pipe -> send output to parent. + if (dup2(info->pipe_fds[1], STDOUT_FILENO) == -1) { + logStrerror(LOG_TAG, "Child: Failed to redirect stdout"); + exit_fork_with_error(info, 1); + } + + if (dup2(info->pipe_fds[1], STDERR_FILENO) == -1) { + logStrerror(LOG_TAG, "Child: Failed to redirect stderr"); + exit_fork_with_error(info, 1); + } + + // Close both pipe ends (pipe_fds[0] belongs to parent, + // pipe_fds[1] no longer needed after dup2). + close(info->pipe_fds[0]); + close(info->pipe_fds[1]); + info->pipe_fds[0] = info->pipe_fds[1] = -1; + + // Set no buffering for stdout/stderr. + if (setvbuf(stdout, NULL, _IONBF, 0) != 0) { + logStrerror(LOG_TAG, "Child: Failed to set no buffering for stdout"); + exit_fork_with_error(info, 1); + } + if (setvbuf(stderr, NULL, _IONBF, 0) != 0) { + logStrerror(LOG_TAG, "Child: Failed to set no buffering for stderr"); + exit_fork_with_error(info, 1); + } + + // Let caller child logic continue. + return 0; + } + // If in parent. + else { + info->is_child = false; + + char *buffer = NULL; + long unsigned int buf_size; + long unsigned int buf_data; + long unsigned int buf_free; + + // Close child's side of pipe. + close(info->pipe_fds[1]); + + // Open parent's side of pipe for reading. + info->pipe_file = fdopen(info->pipe_fds[0], "r"); + if (info->pipe_file == NULL) { + logStrerror(LOG_TAG, "Parent: Failed to open pipe for read child output"); + exit_fork_with_error(info, 1); + } + + // Allocate buffer to store child's output. + buf_size = info->output_initial_buffer; + buf_data = 0; + buf_free = buf_size; + buffer = (char *) malloc(buf_size); + if (buffer == NULL) { + logStrerror(LOG_TAG, "Parent: Failed to allocate initial buffer to store child output"); + exit_fork_with_error(info, 1); + } + + // Read child output until EOF. + while (!feof(info->pipe_file)) { + int n = fread(buffer + buf_data, 1, buf_free, info->pipe_file); + if (n <= 0 && ferror(info->pipe_file) != 0) { + logStrerror(LOG_TAG, "Parent: Failed to read from pipe of child output"); + exit_fork_with_error(info, 1); + } + + buf_data += n; + buf_free -= n; + if (buf_free <= 0) { + buf_free += buf_size; + buf_size *= 2; + char *newbuf = (char *) realloc(buffer, buf_size); + if (newbuf == NULL) { + logStrerror(LOG_TAG, "Parent: Failed to reallocate buffer to store child output"); + exit_fork_with_error(info, 1); + } + buffer = newbuf; + } + } + if (buf_data > 0 && buffer[buf_data-1] == '\n') // Remove trailing newline. + buf_data--; + buffer[buf_data++] = '\0'; + + // Resize buffer to final size. + buf_size = buf_data; + buf_free = 0; + char *newbuf = (char *) realloc(buffer, buf_size); + if (newbuf == NULL) { + logStrerror(LOG_TAG, "Parent: Failed to reallocate buffer to store final child output"); + exit_fork_with_error(info, 1); + } + buffer = newbuf; + + // Wait for child to finish. + // If in parent, wait for child to exit. + w = waitpid(cpid, &info->status, WUNTRACED | WCONTINUED); + if (w == -1) { + logStrerror(LOG_TAG, "Parent: waitpid() failed"); + exit(1); + } + + // Close pipe file and pipe. + fclose(info->pipe_file); + close(info->pipe_fds[0]); + + // Return results using provided references. + if (info->return_output) { + info->output = buffer; + } else { + free(buffer); + } + + // Let caller parent logic continue. + info->exit_code = WEXITSTATUS(info->status); + return 0; + } +} + + + + +#if defined __ANDROID__ && __ANDROID_API__ >= 28 +#define FEXECVE_SUPPORTED 1 +#endif + +#ifndef __ANDROID__ +#define FEXECVE_SUPPORTED 1 +#endif + +#if defined FEXECVE_SUPPORTED +#define FEXECVE_CALL_IMPL() \ +int fexecve_call(int fd, char *const *argv, char *const *envp) { \ + return fexecve(fd, argv, envp); \ +} +#else +#define FEXECVE_SUPPORTED 0 +#define FEXECVE_CALL_IMPL() \ +int fexecve_call(int fd, char *const *argv, char *const *envp) { \ + (void)fd; (void)argv; (void)envp; \ + logStrerror(LOG_TAG, "fexecve not supported on __ANDROID_API__ %d and requires api level >= %d", __ANDROID_API__, 28); \ + return -1; \ +} +#endif + +FEXECVE_CALL_IMPL() +#undef FEXECVE_CALL_IMPL + + + +#define exec_wrapper(variant, name, envp, ...) \ + if (1) { \ + /* Construct argv */ \ + char *argv[] = {__VA_ARGS__}; \ + \ + switch (variant) { \ + case ExecVE: { \ + actual_return_value = execve(name, argv, envp); \ + break; \ + } case ExecL: { \ + actual_return_value = execl(name, __VA_ARGS__); \ + break; \ + } case ExecLP: { \ + actual_return_value = execlp(name, __VA_ARGS__); \ + break; \ + } case ExecLE: { \ + actual_return_value = execle(name, __VA_ARGS__, envp); \ + break; \ + } case ExecV: { \ + actual_return_value = execv(name, argv); \ + break; \ + } case ExecVP: { \ + actual_return_value = execvp(name, argv); \ + break; \ + } case ExecVPE: { \ + actual_return_value = execvpe(name, argv, envp); \ + break; \ + } case FExecVE: { \ + int fd = open(name, 0); \ + if (fd == -1) { \ + logStrerror(LOG_TAG, "open() call failed"); \ + exit(1); \ + } \ + \ + actual_return_value = fexecve_call(fd, argv, envp); \ + close(fd); \ + break; \ + } default: { \ + logStrerror(LOG_TAG, "Unknown exec() variant %d", variant); \ + exit(1); \ + } \ + } \ + \ + } else ((void)0) + +#define run_exec_test(test_name, \ + expected_return_value, expected_errno, \ + expected_exit_code, expected_output_regex, expected_output_regex_flags, \ + variant, name, envp, ...) \ + if (1) { \ + logVVVerbose(LOG_TAG, "%s_exec_%s()", test_name, EXEC_VARIANTS_STR[variant]); \ + \ + INIT_FORK_INFO(info); \ + int result = fork_child(&info); \ + if (result != 0) { \ + logError(LOG_TAG, "Unexpected return value for fork_child '%d'", result); \ + exit(1); \ + } \ + \ + if (info.is_child) { \ + int actual_return_value; \ + exec_wrapper(variant, name, envp, __VA_ARGS__); \ + int actual_errno = errno; \ + int test_failed = 0; \ + if (actual_return_value != expected_return_value) { \ + logError(LOG_TAG, "FAILED: '%s' '%s()' test", test_name, EXEC_VARIANTS_STR[variant]); \ + logError(LOG_TAG, "Expected return_value does not equal actual return_value"); \ + test_failed=1; \ + } else if (actual_errno != expected_errno) { \ + logError(LOG_TAG, "FAILED: '%s' '%s()' test", test_name, EXEC_VARIANTS_STR[variant]); \ + logError(LOG_TAG, "Expected errno does not equal actual errno"); \ + test_failed=1; \ + } \ + \ + if (test_failed == 1) { \ + logError(LOG_TAG, "actual_return_value: '%d'", actual_return_value); \ + logError(LOG_TAG, "expected_return_value: '%d'", expected_return_value); \ + logError(LOG_TAG, "actual_errno: '%d'", actual_errno); \ + logError(LOG_TAG, "expected_errno: '%d'", expected_errno); \ + exit_fork_with_error(&info, 100); \ + } else { \ + exit(0); \ + } \ + } else { \ + if (WIFEXITED(info.status)) { \ + ; \ + } else if (WIFSIGNALED(info.status)) { \ + logInfo(LOG_TAG, "Killed by signal %d\n", WTERMSIG(info.status)); \ + } else if (WIFSTOPPED(info.status)) { \ + logInfo(LOG_TAG, "Stopped by signal %d\n", WSTOPSIG(info.status)); \ + } else if (WIFCONTINUED(info.status)) { \ + logInfo(LOG_TAG, "Continued"); \ + } else { \ + logInfo(LOG_TAG, "CANCELLED"); \ + exit(2); \ + } \ + \ + int actual_exit_code = info.exit_code; \ + int test_failed = 0; \ + int regex_match_result = 1; \ + if (expected_output_regex != NULL && \ + (regex_match_result = regex_match(info.output, expected_output_regex, expected_output_regex_flags)) != 0) { \ + logError(LOG_TAG, "FAILED: '%s' '%s()' test", test_name, EXEC_VARIANTS_STR[variant]); \ + logError(LOG_TAG, "Expected output_regex does not equal match actual output"); \ + test_failed=1; \ + } else if (actual_exit_code != expected_exit_code) { \ + logError(LOG_TAG, "FAILED: '%s' '%s()' test", test_name, EXEC_VARIANTS_STR[variant]); \ + logError(LOG_TAG, "Expected exit_code does not equal actual exit_code"); \ + test_failed=1; \ + } \ + \ + if (test_failed == 1) { \ + logError(LOG_TAG, "actual_exit_code: '%d'", actual_exit_code); \ + logError(LOG_TAG, "expected_exit_code: '%d'", expected_exit_code); \ + logError(LOG_TAG, "actual_output: '%s'", info.output); \ + logError(LOG_TAG, "expected_output_regex: '%s' (%d)", expected_output_regex, expected_output_regex_flags); \ + if (regex_match_result != 1) { \ + logError(LOG_TAG, "regex_match_result: '%d'", regex_match_result); \ + } \ + exit_fork_with_error(&info, 100); \ + } else { \ + /* logDebug(LOG_TAG, "PASSED"); */ \ + free(info.output); \ + errno = 0; \ + } \ + } \ + \ + } else ((void)0) + +#define run_all_exec_wrappers_test(test_name, \ + expected_return_value, expected_errno, \ + expected_exit_code, expected_output_regex, expected_output_regex_flags, \ + file, path, envp, ...) \ + if (1) { \ + \ + logVVerbose(LOG_TAG, "%s_exec()", test_name); \ + \ + { \ + /* ExecVE */ \ + run_exec_test(test_name, expected_return_value, expected_errno, \ + expected_exit_code, expected_output_regex, expected_output_regex_flags, \ + ExecVE, path, envp, path, __VA_ARGS__); \ + } \ + \ + \ + { \ + /* ExecL */ \ + run_exec_test(test_name, expected_return_value, expected_errno, \ + expected_exit_code, expected_output_regex, expected_output_regex_flags, \ + ExecL, path, NULL, path, __VA_ARGS__); \ + } \ + { \ + /* ExecLP */ \ + run_exec_test(test_name, expected_return_value, expected_errno, \ + expected_exit_code, expected_output_regex, expected_output_regex_flags, \ + ExecLP, file, NULL, file, __VA_ARGS__); \ + } \ + { \ + /* ExecLE */ \ + run_exec_test(test_name, expected_return_value, expected_errno, \ + expected_exit_code, expected_output_regex, expected_output_regex_flags, \ + ExecLE, path, envp, path, __VA_ARGS__); \ + } \ + \ + \ + { \ + /* ExecV */ \ + run_exec_test(test_name, expected_return_value, expected_errno, \ + expected_exit_code, expected_output_regex, expected_output_regex_flags, \ + ExecV, path, NULL, path, __VA_ARGS__); \ + } \ + { \ + /* ExecVP */ \ + run_exec_test(test_name, expected_return_value, expected_errno, \ + expected_exit_code, expected_output_regex, expected_output_regex_flags, \ + ExecVP, file, NULL, file, __VA_ARGS__); \ + } \ + { \ + /* ExecVPE */ \ + run_exec_test(test_name, expected_return_value, expected_errno, \ + expected_exit_code, expected_output_regex, expected_output_regex_flags, \ + ExecVPE, file, envp, file, __VA_ARGS__); \ + } \ + \ + \ + { \ + if (FEXECVE_SUPPORTED == 1) { \ + /* FExecVE */ \ + run_exec_test(test_name, expected_return_value, expected_errno, \ + expected_exit_code, expected_output_regex, expected_output_regex_flags, \ + FExecVE, path, envp, path, __VA_ARGS__); \ + } \ + } \ + \ + } else ((void)0) + + +#define asprintf_wrapper(strp, fmt, ...) \ + if (1) { \ + if (asprintf(strp, fmt, __VA_ARGS__) == -1) { \ + errno = ENOMEM; \ + logStrerrorDebug(LOG_TAG, "asprintf failed for new '%s'", fmt, __VA_ARGS__); \ + exit(1); \ + } \ + } else ((void)0) + + + + +void testExec__Basic(); +void testExec__Files(const char* termux_exec__tests__tests_path, const char* current_path, char* env_current_path); +void testExec__PackageManager(); + +void runExecTests() { + logDebug(LOG_TAG, "runExecTests()"); + + const char* termux_exec__tests__tests_path = getenv(ENV__TERMUX_EXEC__TESTS__TESTS_PATH); + if (termux_exec__tests__tests_path != NULL) { + size_t termux_exec__tests__tests_path_len = strlen(termux_exec__tests__tests_path); + if (termux_exec__tests__tests_path_len > 0) { + if (termux_exec__tests__tests_path[0] != '/' || termux_exec__tests__tests_path_len >= PATH_MAX) { + logError(LOG_TAG, "Invalid termux_exec__tests__tests_path with length '%zu' set in '%s' env variable: '%s'", + termux_exec__tests__tests_path_len, ENV__TERMUX_EXEC__TESTS__TESTS_PATH, termux_exec__tests__tests_path); + logErrorVVerbose(LOG_TAG, "The termux_exec__tests__tests_path must be an absolute path starting with a '/' with max length '%d' including the null '\0' terminator", + PATH_MAX); + exit(1); + } + } else { + termux_exec__tests__tests_path = NULL; + } + } + + if (termux_exec__tests__tests_path == NULL) { + termux_exec__tests__tests_path = TERMUX_EXEC__TESTS__TESTS_PATH; + } + + const char* current_path = getenv(ENV__PATH); + char* env_current_path = NULL; + + if (current_path == NULL || strlen(current_path) < 1) { + env_current_path = ENV_PREFIX__PATH; + } else { + if (asprintf(&env_current_path, "%s%s", ENV_PREFIX__PATH, current_path) == -1) { + errno = ENOMEM; + logStrerrorDebug(LOG_TAG, "asprintf failed for current '%s%s'", ENV_PREFIX__PATH, current_path); + exit(1); + } + } + + // TODO: Port tests from bionic. + // - https://cs.android.com/android/_/android/platform/bionic/+/refs/tags/android-14.0.0_r18:tests/unistd_test.cpp;l=1364 + // - https://cs.android.com/android/platform/superproject/+/android-14.0.0_r18:bionic/tests/utils.h;l=200 + + testExec__Basic(); + testExec__Files(termux_exec__tests__tests_path, current_path, env_current_path); + testExec__PackageManager(); + + int__AEqual(0, errno); + + // We cannot free this in a test function that sets it as later test cases will use it. + free(env_current_path); +} + + + +void testExec__Basic() { + logVerbose(LOG_TAG, "testExec__Basic()"); + + run_all_exec_wrappers_test("rootfs", + -1, EISDIR, + 0, NULL, 0, + "../../", TERMUX__ROOTFS, environ, + NULL); +} + +void testExec__Files(const char* termux_exec__tests__tests_path, const char* current_path, char* env_current_path) { + logVerbose(LOG_TAG, "testExec__Files()"); + + + + char* termux_exec__exec_test_files_path = NULL; + asprintf_wrapper(&termux_exec__exec_test_files_path, "%s/%s/%s", + termux_exec__tests__tests_path, TERMUX_EXEC__TESTS__TEST_FILES_BASENAME, TERMUX_EXEC__TESTS__EXEC_TEST_FILES_BASENAME); + + + // execlp(), execvp() and execvpe() search for file to be executed in $PATH, + // so set it with test exec files directory appended at end. + char* env_new_path = NULL; + + if (current_path == NULL || strlen(current_path) < 1) { + if (asprintf(&env_new_path, "%s%s", ENV_PREFIX__PATH, termux_exec__exec_test_files_path) == -1) { + errno = ENOMEM; + logStrerrorDebug(LOG_TAG, "asprintf failed for new '%s%s'", ENV_PREFIX__PATH, termux_exec__exec_test_files_path); + exit(1); + } + } else { + if (asprintf(&env_new_path, "%s%s:%s", ENV_PREFIX__PATH, current_path, termux_exec__exec_test_files_path) == -1) { + errno = ENOMEM; + logStrerrorDebug(LOG_TAG, "asprintf failed for new '%s%s:%s'", ENV_PREFIX__PATH, current_path, termux_exec__exec_test_files_path); + exit(1); + } + } + + + putenv(env_new_path); + + + char* test_file_path = NULL; + + asprintf_wrapper(&test_file_path, "%s/%s", termux_exec__exec_test_files_path, "print-args-binary"); + run_all_exec_wrappers_test("print-args-binary", + 0, 0, + 0, "^goodbye-world$", REG_EXTENDED, + "print-args-binary", test_file_path, environ, + "goodbye-world", NULL); + free(test_file_path); + + asprintf_wrapper(&test_file_path, "%s/%s", termux_exec__exec_test_files_path, "print-args-binary.sym"); + run_all_exec_wrappers_test("print-args-binary.sym", + 0, 0, + 0, "^goodbye-world$", REG_EXTENDED, + "print-args-binary.sym", test_file_path, environ, + "goodbye-world", NULL); + free(test_file_path); + + + asprintf_wrapper(&test_file_path, "%s/%s", termux_exec__exec_test_files_path, "print-args-linux-script.sh"); + run_all_exec_wrappers_test("print-args-linux-script.sh", + 0, 0, + 0, "^goodbye-world$", REG_EXTENDED, + "print-args-linux-script.sh", test_file_path, environ, + "goodbye-world", NULL); + free(test_file_path); + + asprintf_wrapper(&test_file_path, "%s/%s", termux_exec__exec_test_files_path, "print-args-linux-script.sh.sym"); + run_all_exec_wrappers_test("print-args-linux-script.sh.sym", + 0, 0, + 0, "^goodbye-world$", REG_EXTENDED, + "print-args-linux-script.sh.sym", test_file_path, environ, + "goodbye-world", NULL); + free(test_file_path); + + + asprintf_wrapper(&test_file_path, "%s/%s", termux_exec__exec_test_files_path, "print-args-termux-script.sh"); + run_all_exec_wrappers_test("print-args-termux-script.sh", + 0, 0, + 0, "^goodbye-world$", REG_EXTENDED, + "print-args-termux-script.sh", test_file_path, environ, + "goodbye-world", NULL); + free(test_file_path); + + asprintf_wrapper(&test_file_path, "%s/%s", termux_exec__exec_test_files_path, "print-args-termux-script.sh.sym"); + run_all_exec_wrappers_test("print-args-termux-script.sh.sym", + 0, 0, + 0, "^goodbye-world$", REG_EXTENDED, + "print-args-termux-script.sh.sym", test_file_path, environ, + "goodbye-world", NULL); + free(test_file_path); + + + putenv(env_current_path); + + + free(termux_exec__exec_test_files_path); + free(env_new_path); + +} + +void testExec__PackageManager() { + logVerbose(LOG_TAG, "testExec__PackageManager()"); + + if (UID == 0) { + logStrerrorVerbose(LOG_TAG, "Not running 'package-manager' 'exec()' wrapper tests since running as root"); + return; + } + + char* termux_package_manager = getenv(ENV__TERMUX_ROOTFS__PACKAGE_MANAGER); + if (termux_package_manager == NULL || strlen(termux_package_manager) < 1) { + logStrerrorVerbose(LOG_TAG, "Not running 'package-manager' 'exec()' wrapper tests since '%s' environment variable not set", + ENV__TERMUX_ROOTFS__PACKAGE_MANAGER); + return; + } + + char* termux_package_manager_path = NULL; + asprintf_wrapper(&termux_package_manager_path, "%s/bin/%s", TERMUX__PREFIX, termux_package_manager); + + // In case bootstrap was built without a package manager. + if (access(termux_package_manager_path, X_OK) != 0) { + logStrerrorVerbose(LOG_TAG, "Not running 'package-manager' 'exec()' wrapper tests since failed to access package manager executable path '%s'", + termux_package_manager_path); + free(termux_package_manager_path); + errno = 0; + return; + } + + + + // apt: `apt x.x.x ()` + // pacman: `Pacman vx.x.x` Also can icon and license info + char* termux_package_manager_version_regex = NULL; + if (asprintf(&termux_package_manager_version_regex, "^.*%s v?[0-9][.][0-9][.][0-9].*$", termux_package_manager) == -1) { + errno = ENOMEM; + logStrerrorDebug(LOG_TAG, "asprintf failed for new '^.*%s v?[0-9][.][0-9][.][0-9].*$'", termux_package_manager); + exit(1); + } + + run_all_exec_wrappers_test("package-manager-version", + 0, 0, + 0, termux_package_manager_version_regex, REG_EXTENDED | REG_ICASE, + termux_package_manager, termux_package_manager_path, environ, + "--version", NULL); + + free(termux_package_manager_version_regex); + + + + free(termux_package_manager_path); + +} + + + + + +int main() { + init(); + + logInfo(LOG_TAG, "Start 'runtime_binary' tests"); + + runExecTests(); + + logInfo(LOG_TAG, "End 'runtime_binary' tests"); + + return 0; +} + + + +static void init() { + errno = 0; + + UID = geteuid(); + + initLogger(); +} + +static void initLogger() { + setDefaultLogTagAndPrefix(TERMUX__LNAME "-exec-tests"); + setCurrentLogLevel(get_termux_exec__tests_log_level()); + setLogFormatMode(LOG_FORMAT_MODE__TAG_AND_MESSAGE); +} diff --git a/tests/runtime-script-tests.in b/tests/runtime-script-tests.in new file mode 100644 index 0000000..7e81f87 --- /dev/null +++ b/tests/runtime-script-tests.in @@ -0,0 +1,875 @@ +#!@TERMUX__PREFIX@/bin/bash +# shellcheck shell=bash + +runExecTests() { + + termux_exec__tests__log 2 "runExecTests()" + + # Setup temp directory for exec tests. + TERMUX_EXEC__TESTS__EXEC_TMPDIR_PATH="$TERMUX_EXEC__TESTS__TMPDIR_PATH/exec" + mkdir -p "$TERMUX_EXEC__TESTS__EXEC_TMPDIR_PATH" || return $? + + TERMUX_EXEC__TESTS__SCRIPT_TEST_FILE_NAME="test-script" + TERMUX_EXEC__TESTS__SCRIPT_TEST_FILE_PATH="$TERMUX_EXEC__TESTS__EXEC_TMPDIR_PATH/$TERMUX_EXEC__TESTS__SCRIPT_TEST_FILE_NAME" + + testExec__Basic || return $? + testExec__Interpreter || return $? + testExec__SingleAndDoubleDotExecutablePaths || return $? + testExec__SingleAndDoubleDotInterpreterPaths || return $? + testExec__Shell || return $? + + [[ "$TERMUX_EXEC__TESTS__ONLY_TERMUX_EXEC_TESTS" == "true" ]] && return 0 + + testExec__AndroidTools || return $? + testExec__TermuxCore || return $? + testExec__TermuxTools || return $? + testExec__TermuxAm || return $? + testExec__TermuxAmSocket || return $? + testExec__TermuxApi || return $? + testExec__Tudo || return $? + testExec__Sudo || return $? + + return 0 + +} + + + +testExec__Basic() { + + termux_exec__tests__log 3 "testExec__Basic()" + + termux_exec__tests__run_script_test "not-executable" \ + "#!/bin/bash${NL}echo hello" "false" \ + "" "." \ + 126 "^.*: $TERMUX_EXEC__TESTS__SCRIPT_TEST_FILE_PATH: Permission denied$" || return $? + + termux_exec__tests__run_script_test "is-executable" \ + "#!/bin/bash${NL}echo hello" "true" \ + "" "." \ + 0 "^hello$" || return $? + + termux_exec__tests__run_script_test "usr-bin-env" \ + "#!/usr/bin/env bash${NL}echo hello-user-bin-env" "true" \ + "" "." \ + 0 "^hello-user-bin-env$" || return $? + + termux_exec__tests__run_script_test "termux-bin-env" \ + "#!$TERMUX__PREFIX/bin/env bash${NL}echo hello-termux-bin-env" "true" \ + "" "." \ + 0 "^hello-termux-bin-env$" || return $? + + termux_exec__tests__run_script_test "empty-file" \ + "" "true" \ + "" "." \ + 0 "^$" || return $? + + return 0 + +} + +testExec__Interpreter() { + + termux_exec__tests__log 3 "testExec__Interpreter()" + + # `termux-exec` will return with the `Not an ELF or no shebang in executable path (ENOEXEC)` + # error, but bash will manually execute the script. + BASH_VERSION="" termux_exec__tests__run_script_test "shebang-with-pre-!-whitespace" \ + "# !/bin/sh${NL}echo \"\$BASH_VERSION\"" "true" \ + "" "." \ + 0 "^$(termux_exec__tests__escape_string_for_regex "$BASH_VERSION")$" || return $? + + termux_exec__tests__run_script_test "shebang-with-pre-path-whitespace" \ + "#! /bin/sh${NL}echo hello" "true" \ + "" "." \ + 0 "^hello$" || return $? + + termux_exec__tests__run_script_test "shebang-with-args-with-spaces" \ + "#!/bin/echo hello world bye${NL}" "true" \ + "" "." \ + 0 "^hello world bye $TERMUX_EXEC__TESTS__SCRIPT_TEST_FILE_PATH arg1 arg2$" \ + "arg1" "arg2" || return $? + + + termux_exec__tests__run_script_test "shebang-path-missing" \ + "#!${NL}" "true" \ + "" "." \ + 0 "^$" || return $? + + termux_exec__tests__run_script_test "shebang-path-whitespace" \ + "#! ${NL}" "true" \ + "" "." \ + 0 "^$" || return $? + + termux_exec__tests__run_script_test "shebang-path-rootfs" \ + "#!/${NL}" "true" \ + "" "." \ + 126 "^.*: $TERMUX_EXEC__TESTS__SCRIPT_TEST_FILE_PATH: /: bad interpreter: Permission denied$" || return $? + + if [ "$ANDROID__BUILD_VERSION_SDK" -ge 24 ]; then + termux_exec__tests__run_script_test "shebang-path-not-found" \ + "#!/x${NL}" "true" \ + "" "." \ + 127 "^.*: $TERMUX_EXEC__TESTS__SCRIPT_TEST_FILE_PATH: cannot execute: required file not found$" || return $? + else + termux_exec__tests__run_script_test "shebang-path-not-found" \ + "#!/x${NL}" "true" \ + "" "." \ + 126 "^.*: $TERMUX_EXEC__TESTS__SCRIPT_TEST_FILE_PATH: /x: bad interpreter: No such file or directory$" || return $? + fi + + return 0 + +} + +testExec__SingleAndDoubleDotExecutablePaths() { + + termux_exec__tests__log 3 "testExec__SingleAndDoubleDotExecutablePaths()" + + # $TMPDIR + # - termux-exec + # - exec + # - dir1 + # - subdir1 + # - dir2 + + local tests_dir_path="$TERMUX_EXEC__TESTS__EXEC_TMPDIR_PATH" + rm -rf "$tests_dir_path" || return $? + + local dir1_name="dir1" + local dir1_path="$tests_dir_path/$dir1_name" + local subdir1_name="subdir1" + local subdir1_path="$dir1_path/$subdir1_name" + mkdir -p "$subdir1_path" || return $? + + local dir2_name="dir2" + local dir2_path="$tests_dir_path/$dir2_name" + mkdir -p "$dir2_path" || return $? + + local original_script_test_file_path="$TERMUX_EXEC__TESTS__SCRIPT_TEST_FILE_PATH" + + + + # Relative: Executable in current directory. + TERMUX_EXEC__TESTS__SCRIPT_TEST_FILE_PATH="$tests_dir_path/$TERMUX_EXEC__TESTS__SCRIPT_TEST_FILE_NAME" + + termux_exec__tests__run_script_test "executable-relative-current-dir-one-single-dot" \ + "#!/bin/bash${NL}echo hello" "true" \ + "./$TERMUX_EXEC__TESTS__SCRIPT_TEST_FILE_NAME" "$tests_dir_path" \ + 0 "^hello$" || return $? + + termux_exec__tests__run_script_test "executable-relative-current-dir-two-single-dot" \ + "#!/bin/bash${NL}echo hello" "true" \ + "././$TERMUX_EXEC__TESTS__SCRIPT_TEST_FILE_NAME" "$tests_dir_path" \ + 0 "^hello$" || return $? + + + # Relative: Executable in parent directory. + TERMUX_EXEC__TESTS__SCRIPT_TEST_FILE_PATH="$tests_dir_path/$TERMUX_EXEC__TESTS__SCRIPT_TEST_FILE_NAME" + + termux_exec__tests__run_script_test "executable-relative-parent-dir-one-double-dot" \ + "#!/bin/bash${NL}echo hello" "true" \ + "../$TERMUX_EXEC__TESTS__SCRIPT_TEST_FILE_NAME" "$dir1_path" \ + 0 "^hello$" || return $? + + termux_exec__tests__run_script_test "executable-relative-parent-dir-two-double-dot" \ + "#!/bin/bash${NL}echo hello" "true" \ + "../../$TERMUX_EXEC__TESTS__SCRIPT_TEST_FILE_NAME" "$subdir1_path" \ + 0 "^hello$" || return $? + + + # Relative: Executable in sibling directory. + TERMUX_EXEC__TESTS__SCRIPT_TEST_FILE_PATH="$dir2_path/$TERMUX_EXEC__TESTS__SCRIPT_TEST_FILE_NAME" + + termux_exec__tests__run_script_test "executable-relative-sibling-dir-one-double-dot" \ + "#!/bin/bash${NL}echo hello" "true" \ + "../$dir2_name/$TERMUX_EXEC__TESTS__SCRIPT_TEST_FILE_NAME" "$dir1_path" \ + 0 "^hello$" || return $? + + termux_exec__tests__run_script_test "executable-relative-sibling-dir-two-double-dot" \ + "#!/bin/bash${NL}echo hello" "true" \ + "../../$dir2_name/$TERMUX_EXEC__TESTS__SCRIPT_TEST_FILE_NAME" "$subdir1_path" \ + 0 "^hello$" || return $? + + + + # Absolute: Executable in current directory. + TERMUX_EXEC__TESTS__SCRIPT_TEST_FILE_PATH="$tests_dir_path/$TERMUX_EXEC__TESTS__SCRIPT_TEST_FILE_NAME" + + termux_exec__tests__run_script_test "executable-absolute-current-dir-one-single-dot" \ + "#!/bin/bash${NL}echo hello" "true" \ + "$tests_dir_path/./$TERMUX_EXEC__TESTS__SCRIPT_TEST_FILE_NAME" "." \ + 0 "^hello$" || return $? + + termux_exec__tests__run_script_test "executable-absolute-current-dir-two-single-dot" \ + "#!/bin/bash${NL}echo hello" "true" \ + "$tests_dir_path/././$TERMUX_EXEC__TESTS__SCRIPT_TEST_FILE_NAME" "." \ + 0 "^hello$" || return $? + + + # Absolute: Executable in parent directory. + TERMUX_EXEC__TESTS__SCRIPT_TEST_FILE_PATH="$tests_dir_path/$TERMUX_EXEC__TESTS__SCRIPT_TEST_FILE_NAME" + + termux_exec__tests__run_script_test "executable-absolute-parent-dir-one-double-dot" \ + "#!/bin/bash${NL}echo hello" "true" \ + "$dir1_path/../$TERMUX_EXEC__TESTS__SCRIPT_TEST_FILE_NAME" "." \ + 0 "^hello$" || return $? + + termux_exec__tests__run_script_test "executable-absolute-parent-dir-two-double-dot" \ + "#!/bin/bash${NL}echo hello" "true" \ + "$subdir1_path/../../$TERMUX_EXEC__TESTS__SCRIPT_TEST_FILE_NAME" "." \ + 0 "^hello$" || return $? + + + # Absolute: Executable in sibling directory. + TERMUX_EXEC__TESTS__SCRIPT_TEST_FILE_PATH="$dir2_path/$TERMUX_EXEC__TESTS__SCRIPT_TEST_FILE_NAME" + + termux_exec__tests__run_script_test "executable-absolute-sibling-dir-one-double-dot" \ + "#!/bin/bash${NL}echo hello" "true" \ + "$dir1_path/../$dir2_name/$TERMUX_EXEC__TESTS__SCRIPT_TEST_FILE_NAME" "." \ + 0 "^hello$" || return $? + + termux_exec__tests__run_script_test "executable-absolute-sibling-dir-two-double-dot" \ + "#!/bin/bash${NL}echo hello" "true" \ + "$subdir1_path/../../$dir2_name/$TERMUX_EXEC__TESTS__SCRIPT_TEST_FILE_NAME" "." \ + 0 "^hello$" || return $? + + + + TERMUX_EXEC__TESTS__SCRIPT_TEST_FILE_PATH="$original_script_test_file_path" + + return 0 + +} + +testExec__SingleAndDoubleDotInterpreterPaths() { + + termux_exec__tests__log 3 "testExec__SingleAndDoubleDotInterpreterPaths()" + + # $TMPDIR + # - termux-exec + # - exec + # - dir1 + # - subdir1 + # - dir2 + + local tests_dir_path="$TERMUX_EXEC__TESTS__EXEC_TMPDIR_PATH" + rm -rf "$tests_dir_path" || return $? + + local dir1_name="dir1" + local dir1_path="$tests_dir_path/$dir1_name" + local subdir1_name="subdir1" + local subdir1_path="$dir1_path/$subdir1_name" + mkdir -p "$subdir1_path" || return $? + + local dir2_name="dir2" + local dir2_path="$tests_dir_path/$dir2_name" + mkdir -p "$dir2_path" || return $? + + + local bash_bin_path="$TERMUX__PREFIX/bin/bash" + local interpreter_file_name="bash" + local interpreter_file_path + + + # Relative: Executable in current directory. + interpreter_file_path="$tests_dir_path/$interpreter_file_name" + ln -s "$bash_bin_path" "$interpreter_file_path" || return $? + + termux_exec__tests__run_script_test "interpreter-relative-current-dir-one-single-dot" \ + "#!./$interpreter_file_name${NL}echo hello" "true" \ + "" "$tests_dir_path" \ + 0 "^hello$" || return $? + + termux_exec__tests__run_script_test "interpreter-relative-current-dir-two-single-dot" \ + "#!././$interpreter_file_name${NL}echo hello" "true" \ + "" "$tests_dir_path" \ + 0 "^hello$" || return $? + + rm -f "$interpreter_file_path" || return $? + + + # Relative: Executable in parent directory. + interpreter_file_path="$tests_dir_path/$interpreter_file_name" + ln -s "$bash_bin_path" "$interpreter_file_path" || return $? + + termux_exec__tests__run_script_test "interpreter-relative-parent-dir-one-double-dot" \ + "#!../$interpreter_file_name${NL}echo hello" "true" \ + "" "$dir1_path" \ + 0 "^hello$" || return $? + + termux_exec__tests__run_script_test "interpreter-relative-parent-dir-two-double-dot" \ + "#!../../$interpreter_file_name${NL}echo hello" "true" \ + "" "$subdir1_path" \ + 0 "^hello$" || return $? + + rm -f "$interpreter_file_path" || return $? + + + # Relative: Executable in sibling directory. + interpreter_file_path="$dir2_path/$interpreter_file_name" + ln -s "$bash_bin_path" "$interpreter_file_path" || return $? + + termux_exec__tests__run_script_test "interpreter-relative-sibling-dir-one-double-dot" \ + "#!../$dir2_name/$interpreter_file_name${NL}echo hello" "true" \ + "" "$dir1_path" \ + 0 "^hello$" || return $? + + termux_exec__tests__run_script_test "interpreter-relative-sibling-dir-two-double-dot" \ + "#!../../$dir2_name/$interpreter_file_name${NL}echo hello" "true" \ + "" "$subdir1_path" \ + 0 "^hello$" || return $? + + rm -f "$interpreter_file_path" || return $? + + + + # Absolute: Executable in current directory. + interpreter_file_path="$tests_dir_path/$interpreter_file_name" + ln -s "$bash_bin_path" "$interpreter_file_path" || return $? + + termux_exec__tests__run_script_test "interpreter-absolute-current-dir-one-single-dot" \ + "#!$tests_dir_path/./$interpreter_file_name${NL}echo hello" "true" \ + "" "." \ + 0 "^hello$" || return $? + + termux_exec__tests__run_script_test "interpreter-absolute-current-dir-two-single-dot" \ + "#!$tests_dir_path/././$interpreter_file_name${NL}echo hello" "true" \ + "" "." \ + 0 "^hello$" || return $? + + rm -f "$interpreter_file_path" || return $? + + + # Absolute: Executable in parent directory. + interpreter_file_path="$tests_dir_path/$interpreter_file_name" + ln -s "$bash_bin_path" "$interpreter_file_path" || return $? + + termux_exec__tests__run_script_test "interpreter-absolute-parent-dir-one-double-dot" \ + "#!$dir1_path/../$interpreter_file_name${NL}echo hello" "true" \ + "" "." \ + 0 "^hello$" || return $? + + termux_exec__tests__run_script_test "interpreter-absolute-parent-dir-two-double-dot" \ + "#!$subdir1_path/../../$interpreter_file_name${NL}echo hello" "true" \ + "" "." \ + 0 "^hello$" || return $? + + rm -f "$interpreter_file_path" || return $? + + + # Absolute: Executable in sibling directory. + interpreter_file_path="$dir2_path/$interpreter_file_name" + ln -s "$bash_bin_path" "$interpreter_file_path" || return $? + + termux_exec__tests__run_script_test "interpreter-absolute-sibling-dir-one-double-dot" \ + "#!$dir1_path/../$dir2_name/$interpreter_file_name${NL}echo hello" "true" \ + "" "." \ + 0 "^hello$" || return $? + + termux_exec__tests__run_script_test "interpreter-absolute-sibling-dir-two-double-dot" \ + "#!$subdir1_path/../../$dir2_name/$interpreter_file_name${NL}echo hello" "true" \ + "" "." \ + 0 "^hello$" || return $? + + rm -f "$interpreter_file_path" || return $? + + return 0 + +} + +testExec__Shell() { + + termux_exec__tests__log 3 "testExec__Shell()" + + # `/dev/stdin` does not exist on Android 7, so use `/proc/self/fd/0` + + termux_exec__tests__run_script_test "bash-heredoc-no-args" \ + "#!/usr/bin/bash${NL}bash <<'EOF'${NL}echo hello${NL}EOF" "true" \ + "" "." \ + 0 "^hello$" || return $? + + termux_exec__tests__run_script_test "cat-bash-heredoc-no-args" \ + "#!/usr/bin/bash${NL}cat <<'EOF' | bash${NL}echo hello${NL}EOF" "true" \ + "" "." \ + 0 "^hello$" || return $? + + + termux_exec__tests__run_script_test "bash-heredoc-with-args" \ + "#!/usr/bin/bash${NL}bash /proc/self/fd/0 hello<<'EOF'${NL}echo \$1${NL}EOF" "true" \ + "" "." \ + 0 "^hello$" || return $? + + termux_exec__tests__run_script_test "cat-bash-heredoc-with-args" \ + "#!/usr/bin/bash${NL}cat <<'EOF' | bash /proc/self/fd/0 hello${NL}echo \$1${NL}EOF" "true" \ + "" "." \ + 0 "^hello$" || return $? + + + termux_exec__tests__run_script_test "bash-herestring-no-args" \ + "#!/usr/bin/bash${NL}bash <<<'echo hello'" "true" \ + "" "." \ + 0 "^hello$" || return $? + + termux_exec__tests__run_script_test "cat-bash-herestring-no-args" \ + "#!/usr/bin/bash${NL}cat <<<'echo hello' | bash" "true" \ + "" "." \ + 0 "^hello$" || return $? + + + termux_exec__tests__run_script_test "bash-herestring-with-args" \ + "#!/usr/bin/bash${NL}bash /proc/self/fd/0 hello <<<'echo \$1'" "true" \ + "" "." \ + 0 "^hello$" || return $? + + termux_exec__tests__run_script_test "cat-bash-herestring-with-args" \ + "#!/usr/bin/bash${NL}cat <<<'echo \$1' | bash /proc/self/fd/0 hello" "true" \ + "" "." \ + 0 "^hello$" || return $? + + + termux_exec__tests__run_script_test "builtin-echo-cat-pipe" \ + "#!/usr/bin/bash${NL}echo hello | cat" "true" \ + "" "." \ + 0 "^hello$" || return $? + + termux_exec__tests__run_script_test "external-echo-cat-pipe" \ + "#!/usr/bin/bash${NL}$TERMUX__PREFIX/bin/echo hello | cat" "true" \ + "" "." \ + 0 "^hello$" || return $? + + termux_exec__tests__run_script_test "external-echo-sed-pipe" \ + "#!/usr/bin/bash${NL}$TERMUX__PREFIX/bin/echo '|hello|' | sed -e 's/|//g'" "true" \ + "" "." \ + 0 "^hello$" || return $? + + + termux_exec__tests__run_script_test "fd-read-write" \ + "#!/usr/bin/bash${NL}exec {fd}< <(echo -n hello)${NL}cat /proc/self/fd/\${fd}${NL}exec {fd}>&-" "true" \ + "" "." \ + 0 "^hello$" || return $? + + return 0 + +} + +testExec__AndroidTools() { + + termux_exec__tests__log 3 "testExec__AndroidTools()" + + # Remove LD_LIBRARY_PATH from environment to avoid conflicting + # with system libraries that system binaries may link against + # Some tools require having /system/bin/app_process in the PATH, + # at least `am` and `pm` on a Nexus 6p running Android `6.0`. + local android_env="LD_LIBRARY_PATH= LD_PRELOAD= PATH=/system/bin" + + termux_exec__tests__run_script_test "android-getprop-ro-build-version-sdk" \ + "#!/usr/bin/bash${NL}${android_env} /system/bin/getprop 'ro.build.version.sdk'" "true" \ + "" "." \ + 0 "^[0-9]+$" || return $? + + termux_exec__tests__run_script_test "android-pm-list-termux-package" \ + "#!/usr/bin/bash${NL}out=\"\$(${android_env} /system/bin/pm list packages -u --user '$TERMUX__USER_ID' 2>&1 &1 0`). + testExec__TermuxAm__wrapper "-user" " --user '$TERMUX__USER_ID'" || return $? + + # If `--user` flag is not passed to `am`, then it should + # automatically use current user as default, so test that. + testExec__TermuxAm__wrapper "-current-user" "" || return $? + + return 0 + +} + +testExec__TermuxAm__wrapper() { + + local test_name_suffix="$1" + local user_id_arg="$2" + + # Set `rw-------` permission for `am.apk` to test if `am` correctly + # makes it read-only to prevent`SIGABRT` on Android `>= 14`. + # - https://github.com/termux/TermuxAm/commit/598a9c06c325db6d41cc840dedcb8ba34564c79f + local am_apk_path="$TERMUX__PREFIX/libexec/termux-am/am.apk" + chmod 0600 "$am_apk_path" || return $? + + + if [ "$ANDROID__BUILD_VERSION_SDK" -ge 34 ]; then + termux_exec__tests__run_script_test "am-broadcast$test_name_suffix" \ + "#!/usr/bin/bash${NL}am broadcast$user_id_arg -a '@TERMUX_APP__NAMESPACE@.test' '$TERMUX_APP__PACKAGE_NAME'" "true" \ + "" "." \ + 0 "^.*Broadcast sent without waiting for result$" || return $? + else + termux_exec__tests__run_script_test "am-broadcast$test_name_suffix" \ + "#!/usr/bin/bash${NL}am broadcast$user_id_arg -a '@TERMUX_APP__NAMESPACE@.test' '$TERMUX_APP__PACKAGE_NAME'" "true" \ + "" "." \ + 0 "^.*Broadcast completed: result=0$" || return $? + fi + + termux_exec__tests__run_script_test "am-start-activity$test_name_suffix" \ + "#!/usr/bin/bash${NL}am start$user_id_arg '@TERMUX_APP__SHELL_ACTIVITY__COMPONENT_NAME@'" "true" \ + "" "." \ + 0 "^.*Starting: Intent \{.*\}.*$" || return $? + + termux_exec__tests__run_script_test "am-start-service$test_name_suffix" \ + "#!/usr/bin/bash${NL}am startservice$user_id_arg '@TERMUX_APP__SHELL_SERVICE__COMPONENT_NAME@'" "true" \ + "" "." \ + 0 "^.*Starting service: Intent \{.*\}.*$" || return $? + + return 0 + +} + +testExec__TermuxAmSocket() { + + termux_exec__tests__log 3 "testExec__TermuxAmSocket()" + + # If cannot connect to `TermuxAmSocketServer` in termux-app to get `am` help. + if [[ "$(termux-am --am-help 2>/dev/null)" != *"usage: am "* ]]; then + termux_exec__tests__log 3 "Skipping termux-am-socket tests since failed to connect to server" + return 0 + fi + + # The `--user` flag must be passed if running in a secondary user (`> 0`). + testExec__TermuxAmSocket__wrapper "-user" " --user '$TERMUX__USER_ID'" || return $? + + # If `--user` flag is not passed to `termux-am`, then it should + # automatically use current user as default, so test that. + if [[ "$TERMUX__USER_ID" == "0" ]]; then + testExec__TermuxAmSocket__wrapper "-current-user" "" || return $? + fi + + return 0 + +} + +testExec__TermuxAmSocket__wrapper() { + + local test_name_suffix="$1" + local user_id_arg="$2" + + termux_exec__tests__run_script_test "termux-am-broadcast$test_name_suffix" \ + "#!/usr/bin/bash${NL}termux-am broadcast$user_id_arg -a '@TERMUX_APP__NAMESPACE@.test' '$TERMUX_APP__PACKAGE_NAME'" "true" \ + "" "." \ + 0 "^.*Broadcast completed: result=.*$" || return $? # resultCode seems to be -1 + + termux_exec__tests__run_script_test "termux-am-start-activity$test_name_suffix" \ + "#!/usr/bin/bash${NL}termux-am start$user_id_arg '@TERMUX_APP__SHELL_ACTIVITY__COMPONENT_NAME@'" "true" \ + "" "." \ + 0 "^.*Starting: Intent \{.*\}.*$" || return $? + + termux_exec__tests__run_script_test "termux-am-start-service$test_name_suffix" \ + "#!/usr/bin/bash${NL}termux-am startservice$user_id_arg '@TERMUX_APP__SHELL_SERVICE__COMPONENT_NAME@'" "true" \ + "" "." \ + 0 "^.*Starting service: Intent \{.*\}.*$" || return $? + + return 0 + +} + +testExec__TermuxApi() { + + termux_exec__tests__log 3 "testExec__TermuxApi()" + + # If running as root. + if [[ "$TERMUX_EXEC__TESTS__UID" == "0" ]]; then + termux_exec__tests__log 3 "Skipping termux-api tests since running as root" + return 0 + fi + + # If `termux-app` did not export the version variable for `termux-api` app. + if [[ ! "$TERMUX_API_APP__APP_VERSION_NAME" =~ ^[0-9].*$ ]]; then + termux_exec__tests__log 3 "Skipping termux-api tests since app not installed" + return 0 + fi + + # If `termux-api-package` is not installed. + if [[ ! -f "$TERMUX__PREFIX/libexec/termux-api" ]]; then + termux_exec__tests__log 3 "Skipping termux-api tests since package not installed" + return 0 + fi + + termux_exec__tests__run_script_test "termux-battery-status" \ + "#!/usr/bin/bash${NL}termux-battery-status" "true" \ + "" "." \ + 0 "^.*\"percentage\": [0-9]+.*$" || return $? + + termux_exec__tests__run_script_test "termux-usb" \ + "#!/usr/bin/bash${NL}termux-usb -l" "true" \ + "" "." \ + 0 "^\[.*\]$" || return $? + + termux_exec__tests__run_script_test "termux-clipboard-set" \ + "#!/usr/bin/bash${NL}termux-clipboard-set termux-api-test" "true" \ + "" "." \ + 0 "^$" || return $? + + # Wait for clipboard to be set. + sleep 3 + + termux_exec__tests__run_script_test "termux-clipboard-get" \ + "#!/usr/bin/bash${NL}termux-clipboard-get" "true" \ + "" "." \ + 0 "^termux-api-test$" || return $? + + return 0 + +} + +testExec__Tudo() { + + termux_exec__tests__log 3 "testExec__Tudo()" + + local tudo_script_file_path="$TERMUX__PREFIX/bin/tudo" + local tudo_tests_file_path="$TERMUX__PREFIX/libexec/installed-tests/tudo/tudo_tests" + + if [[ ! -f "$tudo_script_file_path" ]]; then + termux_exec__tests__log 3 "Skipping tudo tests since tudo not installed" + return 0 + fi + + if [[ ! -f "$tudo_tests_file_path" ]]; then + termux_exec__tests__log 3 "Skipping tudo tests since 'tudo_tests' file not found" + return 0 + fi + + TUDO_TESTS__LOG_LEVEL="$TERMUX_EXEC__TESTS__LOG_LEVEL" "$tudo_tests_file_path" || return $? + + return 0 + +} + +testExec__Sudo() { + + termux_exec__tests__log 3 "testExec__Sudo()" + + local sudo_script_file_path="$TERMUX__PREFIX/bin/sudo" + local sudo_tests_file_path="$TERMUX__PREFIX/libexec/installed-tests/sudo/sudo_tests" + + if [[ ! -f "$sudo_script_file_path" ]]; then + termux_exec__tests__log 3 "Skipping sudo tests since sudo not installed" + return 0 + fi + + if [[ ! -f "$sudo_tests_file_path" ]]; then + termux_exec__tests__log 3 "Skipping sudo tests since 'sudo_tests' file not found" + return 0 + fi + + if [[ "$TERMUX_EXEC__TESTS__UID" != "0" ]]; then + if [[ "$(su -c "id -u")" != "0" ]]; then + termux_exec__tests__log 3 "Skipping sudo tests since root access is not available" + return 0 + fi + fi + + SUDO_TESTS__LOG_LEVEL="$TERMUX_EXEC__TESTS__LOG_LEVEL" "$sudo_tests_file_path" || return $? + + return 0 + +} + + + +## +# `termux_exec__tests__run_script_test` `` `` +# `` `` +# `` \ +# `` `` [``] +## +termux_exec__tests__run_script_test() { + + local return_value + + if [[ $# -lt 7 ]]; then + termux_exec__tests__log_error "Invalid argument count $#. The 'termux_exec__tests__run_script_test' command expects at least 7 arguments: \ + test_name test_file_content test_file_make_executable execution_path working_directory expected_exit_code expected_output_regex [script_args]" + return 1 + fi + + local test_name="$1" + local test_file_content="$2" + local test_file_make_executable="$3" + local execution_path="$4" + local working_directory="$5" + local expected_exit_code="$6" + local expected_output_regex="$7" + shift 7 # Remove args before `script_args` + + local output + local actual_output + local test_failed="false" + + termux_exec__tests__log 4 "$test_name()" + termux_exec__tests__log 5 "TERMUX_EXEC__TESTS__SCRIPT_TEST_FILE_PATH='$TERMUX_EXEC__TESTS__SCRIPT_TEST_FILE_PATH'" + if [[ "$test_file_content" == *"${NL}"* ]]; then + termux_exec__tests__log 5 "test_file_content=${NL}"'```'"${NL}$test_file_content${NL}"'```' + else + termux_exec__tests__log 5 "test_file_content='$test_file_content'" + fi + termux_exec__tests__log 5 "test_file_make_executable='$test_file_make_executable'" + termux_exec__tests__log 5 "execution_path='$execution_path'" + termux_exec__tests__log 5 "working_directory='$working_directory'" + termux_exec__tests__log 5 "expected_exit_code='$expected_exit_code'" + termux_exec__tests__log 5 "expected_output_regex='$expected_output_regex'" + + # If TERMUX_EXEC__TESTS__SCRIPT_TEST_FILE_PATH is not a valid absolute path. + if [[ ! "$TERMUX_EXEC__TESTS__SCRIPT_TEST_FILE_PATH" =~ $TERMUX_EXEC__TESTS__REGEX__ABSOLUTE_PATH ]]; then + termux_exec__tests__log_error "The TERMUX_EXEC__TESTS__SCRIPT_TEST_FILE_PATH '$TERMUX_EXEC__TESTS__SCRIPT_TEST_FILE_PATH' is not a valid absolute path" + return 1 + fi + + rm -f "$TERMUX_EXEC__TESTS__SCRIPT_TEST_FILE_PATH" || return $? + + output="$(printf "%s" "$test_file_content" > "$TERMUX_EXEC__TESTS__SCRIPT_TEST_FILE_PATH" 2>&1)" + return_value=$? + if [ $return_value -ne 0 ]; then + termux_exec__tests__log_error "$output" + termux_exec__tests__log_error "Failed to create the '$TERMUX_EXEC__TESTS__SCRIPT_TEST_FILE_PATH' file for the '$test_name' test" + return $return_value + fi + + if [[ "$test_file_make_executable" == "true" ]]; then + output="$(chmod +x "$TERMUX_EXEC__TESTS__SCRIPT_TEST_FILE_PATH" 2>&1)" + return_value=$? + if [ $return_value -ne 0 ]; then + termux_exec__tests__log_error "$output" + termux_exec__tests__log_error "Failed to set the executable bit for the '$TERMUX_EXEC__TESTS__SCRIPT_TEST_FILE_PATH' file for the '$test_name' test" + return $return_value + fi + fi + + actual_output="$(cd "$working_directory" && "${execution_path:-$TERMUX_EXEC__TESTS__SCRIPT_TEST_FILE_PATH}" "$@" 2>&1)" + actual_exit_code=$? + if [[ -n "$expected_output_regex" ]] && [[ ! "$actual_output" =~ $expected_output_regex ]]; then + termux_exec__tests__log_error "FAILED: '$test_name' test" + termux_exec__tests__log_error "Expected output_regex does not equal match actual output" + test_failed="true" + elif [ $actual_exit_code != "$expected_exit_code" ]; then + termux_exec__tests__log_error "$actual_output" + termux_exec__tests__log_error "FAILED: '$test_name' test" + termux_exec__tests__log_error "Expected result_code does not equal actual result_code" + test_failed="true" + fi + + if [[ "$test_failed" == "true" ]]; then + if [[ "$test_file_content" == *"${NL}"* ]]; then + termux_exec__tests__log_error "test_file_content=${NL}"'```'"${NL}$test_file_content${NL}"'```' + else + termux_exec__tests__log_error "test_file_content='$test_file_content'" + fi + termux_exec__tests__log_error "actual_exit_code: '$actual_exit_code'" + termux_exec__tests__log_error "expected_exit_code: '$expected_exit_code'" + termux_exec__tests__log_error "actual_output: '$actual_output'" + termux_exec__tests__log_error "expected_output_regex: '$expected_output_regex'" + return 100 + else + #termux_exec__tests__log 2 "PASSED" + + # Remove test file so that later tests in like + # `testExec__SingleAndDoubleDotExecutablePaths()` do not accidentally use it. + rm -f "$TERMUX_EXEC__TESTS__SCRIPT_TEST_FILE_PATH" || return $? + + termux_exec__tests__log 5 "" + + return 0 + fi + +} + + + +## +# `runtime_script_tests_main` +## +runtime_script_tests_main() { + + termux_exec__tests__log 1 "Start 'runtime_script' tests" + + runExecTests || return $? + + termux_exec__tests__log 1 "End 'runtime_script' tests" + + return 0 +} diff --git a/tests/simple.sh b/tests/simple.sh deleted file mode 100755 index 704ea85..0000000 --- a/tests/simple.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -echo hi diff --git a/tests/simple.sh-expected b/tests/simple.sh-expected deleted file mode 100644 index 45b983b..0000000 --- a/tests/simple.sh-expected +++ /dev/null @@ -1 +0,0 @@ -hi diff --git a/tests/termux-exec-tests.in b/tests/termux-exec-tests.in new file mode 100644 index 0000000..6347221 --- /dev/null +++ b/tests/termux-exec-tests.in @@ -0,0 +1,629 @@ +#!@TERMUX__PREFIX@/bin/bash +# shellcheck shell=bash + +if [ -z "${BASH_VERSION:-}" ]; then + echo "The 'termux-exec-tests' script must be run from a 'bash' shell."; return 64 2>/dev/null|| exit 64 # EX__USAGE +fi + + + +termux_exec__tests__init() { + +TERMUX_EXEC__TESTS__MAX_LOG_LEVEL=5 # Default: `5` (VVVERBOSE=5) +{ [[ ! "$TERMUX_EXEC__TESTS__LOG_LEVEL" =~ ^[0-9]+$ ]] || [[ "$TERMUX_EXEC__TESTS__LOG_LEVEL" -gt "$TERMUX_EXEC__TESTS__MAX_LOG_LEVEL" ]]; } && \ +TERMUX_EXEC__TESTS__LOG_LEVEL=1 # Default: `1` (OFF=0, NORMAL=1, DEBUG=2, VERBOSE=3, VVERBOSE=4 and VVVERBOSE=5) +TERMUX_EXEC__TESTS__LOG_TAG="" # Default: `` + +TERMUX_EXEC__TESTS__COMMAND_TYPE_ID="" # Default: `` +TERMUX_EXEC__TESTS__COMMAND_TYPE_NOOP="false" # Default: `false` + +NL=$'\n' + +TERMUX_EXEC__TESTS__DETECT_LEAKS=0 # Default: `0` +TERMUX_EXEC__TESTS__NO_CLEAN="false" # Default: `false` +TERMUX_EXEC__TESTS__USE_FSANITIZE_BUILDS="false" # Default: `false` +TERMUX_EXEC__TESTS__ONLY_TERMUX_EXEC_TESTS="false" # Default: `false` +TERMUX_EXEC__TESTS__SKIP_TERMUX_CORE_TESTS="false" # Default: `false` + +TERMUX_EXEC__TESTS__NAME_MAX=255 +TERMUX_EXEC__TESTS__REGEX__ABSOLUTE_PATH='^(/[^/]+)+$' +TERMUX_EXEC__TESTS__REGEX__ROOTFS_OR_ABSOLUTE_PATH='^((/)|((/[^/]+)+))$' +TERMUX_EXEC__TESTS__REGEX__APP_PACKAGE_NAME="^[a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)+$" +TERMUX_EXEC__TESTS__REGEX__UNSIGNED_INT='^[0-9]+$' + + + +# Set `TERMUX_*` variables to environment variables exported by +# Termux app, otherwise default to build time placeholders. +# This is done to support scoped and dynamic variables design. +# The `TERMUX_ENV__*` variables still use build time placeholders. + +TERMUX_APP__PACKAGE_NAME___N="@TERMUX_ENV__S_TERMUX_APP@PACKAGE_NAME" +termux_exec__tests__copy_variable TERMUX_APP__PACKAGE_NAME "$TERMUX_APP__PACKAGE_NAME___N" || return $? +{ [[ ! "$TERMUX_APP__PACKAGE_NAME" =~ $TERMUX_EXEC__TESTS__REGEX__APP_PACKAGE_NAME ]] || \ + [ "${#TERMUX_APP__PACKAGE_NAME}" -gt $TERMUX_EXEC__TESTS__NAME_MAX ]; } && \ +TERMUX_APP__PACKAGE_NAME="@TERMUX_APP__PACKAGE_NAME@" + + +TERMUX__PREFIX___N="@TERMUX_ENV__S_TERMUX@PREFIX" +termux_exec__tests__copy_variable TERMUX__PREFIX "$TERMUX__PREFIX___N" || return $? +[[ ! "$TERMUX__PREFIX" =~ $TERMUX_EXEC__TESTS__REGEX__ABSOLUTE_PATH ]] && \ +TERMUX__PREFIX="@TERMUX__PREFIX@" + + +TERMUX_CORE__TESTS__LOG_LEVEL___N="@TERMUX_ENV__S_TERMUX_CORE__TESTS@LOG_LEVEL" +termux_exec__tests__copy_variable TERMUX_CORE__TESTS__LOG_LEVEL "$TERMUX_CORE__TESTS__LOG_LEVEL___N" || return $? + + +TERMUX_EXEC__TESTS__LOG_LEVEL___N="@TERMUX_ENV__S_TERMUX_EXEC__TESTS@LOG_LEVEL" +termux_exec__tests__copy_variable TERMUX_EXEC__TESTS__LOG_LEVEL "$TERMUX_EXEC__TESTS__LOG_LEVEL___N" || return $? + + +TERMUX_EXEC__TESTS__TESTS_PATH___N="@TERMUX_ENV__S_TERMUX_EXEC__TESTS@TESTS_PATH" +TERMUX_EXEC__TESTS__TESTS_PATH="$TERMUX__PREFIX/libexec/installed-tests/termux-exec" +printf -v "$TERMUX_EXEC__TESTS__TESTS_PATH___N" "%s" "$TERMUX_EXEC__TESTS__TESTS_PATH" || return $? +export "${TERMUX_EXEC__TESTS__TESTS_PATH___N?}" || return $? + + +TERMUX__USER_ID___N="@TERMUX_ENV__S_TERMUX@USER_ID" +termux_exec__tests__copy_variable TERMUX__USER_ID "$TERMUX__USER_ID___N" || return $? +TERMUX__USER_ID="${TERMUX__USER_ID:-0}" + + +TERMUX_CORE__APPS_INFO_ENV_FILE___N="@TERMUX_ENV__S_TERMUX_CORE@APPS_INFO_ENV_FILE" +termux_exec__tests__copy_variable TERMUX_CORE__APPS_INFO_ENV_FILE "$TERMUX_CORE__APPS_INFO_ENV_FILE___N" || return $? + + +TERMUX_API_APP__APP_VERSION_NAME___N="@TERMUX_ENV__S_TERMUX_API_APP@APP_VERSION_NAME" + + + +# Set exit traps. +termux_exec__tests__set_traps || return $? + +} + + + +function termux_exec__tests__log() { local log_level="${1}"; shift; if [[ $TERMUX_EXEC__TESTS__LOG_LEVEL -ge $log_level ]]; then echo "@TERMUX__LNAME@-exec-tests${TERMUX_EXEC__TESTS__LOG_TAG:+".$TERMUX_EXEC__TESTS__LOG_TAG"}:" "$@"; fi } +function termux_exec__tests__log_literal() { local log_level="${1}"; shift; if [[ $TERMUX_EXEC__TESTS__LOG_LEVEL -ge $log_level ]]; then echo -e "@TERMUX__LNAME@-exec-tests${TERMUX_EXEC__TESTS__LOG_TAG:+".$TERMUX_EXEC__TESTS__LOG_TAG"}:" "$@"; fi } +function termux_exec__tests__log_error() { echo "@TERMUX__LNAME@-exec-tests${TERMUX_EXEC__TESTS__LOG_TAG:+".$TERMUX_EXEC__TESTS__LOG_TAG"}:" "$@" 1>&2; } + + + +## +# `termux_exec__tests__main` [``] +## +termux_exec__tests__main() { + + local return_value + + termux_exec__tests__init || return $? + + local run_unit_tests="false" + local run_runtime_tests="false" + + # Process the command arguments passed to the script. + termux_exec__tests__process_script_arguments "$@" || return $? + if [ "$TERMUX_EXEC__TESTS__COMMAND_TYPE_NOOP" = "true" ]; then return 0; fi + + + termux_exec__tests__log 4 "Running 'termux_exec__tests__main'" + + if [[ "$TERMUX_EXEC__TESTS__COMMAND_TYPE_ID" == *,* ]] || \ + [[ ",unit,runtime,all," != *",$TERMUX_EXEC__TESTS__COMMAND_TYPE_ID,"* ]]; then + termux_exec__tests__log_error "Invalid command type id '$TERMUX_EXEC__TESTS__COMMAND_TYPE_ID' passed. Must equal 'unit', 'runtime' or 'all'." + return 1 + fi + + + termux_exec__tests__log 1 "Running 'termux-exec' tests" + + local tests_start_time + local tests_end_time + tests_start_time="$(date "+%s")" || return $? + + + [[ ",unit,all," == *",$TERMUX_EXEC__TESTS__COMMAND_TYPE_ID,"* ]] && run_unit_tests="true" + [[ ",runtime,all," == *",$TERMUX_EXEC__TESTS__COMMAND_TYPE_ID,"* ]] && run_runtime_tests="true" + + + termux_exec__tests__log 5 "$TERMUX_EXEC__TESTS__LOG_LEVEL___N='$TERMUX_EXEC__TESTS__LOG_LEVEL'" + termux_exec__tests__log 5 "$TERMUX_EXEC__TESTS__TESTS_PATH___N='$TERMUX_EXEC__TESTS__TESTS_PATH'" + termux_exec__tests__log 5 "$TERMUX__USER_ID___N='$TERMUX__USER_ID'" + + + # Set `TERMUX_EXEC__TESTS__TESTS_PATH` used by compiled c tests. + if [[ ! "$TERMUX_EXEC__TESTS__TESTS_PATH" =~ $TERMUX_EXEC__TESTS__REGEX__ROOTFS_OR_ABSOLUTE_PATH ]]; then + termux_exec__tests__log_error "The TERMUX_EXEC__TESTS__TESTS_PATH '$TERMUX_EXEC__TESTS__TESTS_PATH' is either not set or is not an absolute path" + return 1 + fi + + + # Setup variables for runtime tests. + if [[ "$run_runtime_tests" == "true" ]]; then + # Check if `termux-apps-info.env` file exists whose path is exported in + # the `$TERMUX_CORE__APPS_INFO_ENV_FILE` environment variable by the + # Termux app running the current shell, which is sourced by + # `termux_core__bash__termux_apps_info_app_version_name` to load the + # latest value in the current environment for + # `$TERMUX_API_APP__APP_VERSION_NAME` before getting it. + # The `termux-apps-app-version-name.bash` (under `$PATH`) and + # `termux-apps-info.env` file (as `$TERMUX_CORE__APPS_INFO_ENV_FILE` not exported) + # will not be available in Termux docker. + termux_exec__tests__log 5 "$TERMUX_CORE__APPS_INFO_ENV_FILE___N='$TERMUX_CORE__APPS_INFO_ENV_FILE'" + if [ -n "$TERMUX_CORE__APPS_INFO_ENV_FILE" ] && [ -f "$TERMUX_CORE__APPS_INFO_ENV_FILE" ]; then + # Source for `termux_core__bash__termux_apps_info_app_version_name` function. + termux_exec__tests__source_file_from_path "termux-apps-app-version-name.bash" || return $? + + termux_core__bash__termux_apps_info_app_version_name get-value \ + "TERMUX_API_APP__APP_VERSION_NAME" cn="termux-api-app" || return $? + termux_exec__tests__log 5 "$TERMUX_API_APP__APP_VERSION_NAME___N='$TERMUX_API_APP__APP_VERSION_NAME'" + else + # Fallback to manually exported variable by caller. + termux_exec__tests__copy_variable TERMUX_API_APP__APP_VERSION_NAME "$TERMUX_API_APP__APP_VERSION_NAME___N" + + TERMUX_API_APP__APP_VERSION_NAME="${TERMUX_API_APP__APP_VERSION_NAME:-}" + termux_exec__tests__log 5 "${TERMUX_API_APP__APP_VERSION_NAME___N}__FALLBACK='$TERMUX_API_APP__APP_VERSION_NAME'" + fi + + + if [[ ! "$TERMUX__USER_ID" =~ ^(([0-9])|([1-9][0-9]{0,2}))$ ]]; then + termux_exec__tests__log_error "The TERMUX__USER_ID '$TERMUX__USER_ID' is not set to a valid user id." + return 1 + fi + + TERMUX_EXEC__TESTS__UID="$(id -u)" + return_value=$? + if [ $return_value -ne 0 ]; then + termux_exec__tests__log_error "Failed to get uid" + return $return_value + fi + + + ANDROID__BUILD_VERSION_SDK="$(getprop "ro.build.version.sdk")" + if [[ ! "$ANDROID__BUILD_VERSION_SDK" =~ $TERMUX_EXEC__TESTS__REGEX__UNSIGNED_INT ]]; then + termux_exec__tests__log_error "Failed to get android build version sdk with getprop" + return 1 + fi + + + if [[ ! -d "$TMPDIR" ]]; then + termux_exec__tests__log_error "The TMPDIR '$TMPDIR' is either not set or not a directory" + return 1 + fi + + + TERMUX_EXEC__TESTS__TMPDIR_PATH="$TMPDIR/termux-exec-tests" + + # Ensure test directory is clean and does not contain files from previous run. + rm -rf "$TERMUX_EXEC__TESTS__TMPDIR_PATH" || return $? + mkdir -p "$TERMUX_EXEC__TESTS__TMPDIR_PATH" || return $? + fi + + + # Run unit tests. + if [[ "$run_unit_tests" == "true" ]]; then + termux_exec__tests__unit_tests__run_command + return_value=$? + if [ $return_value -ne 0 ]; then + return $return_value + fi + fi + + + # Run runtime tests. + if [[ "$run_runtime_tests" == "true" ]]; then + termux_exec__tests__runtime_tests__run_command + return_value=$? + if [ $return_value -ne 0 ]; then + return $return_value + fi + + rm -rf "$TERMUX_EXEC__TESTS__TMPDIR_PATH" + fi + + tests_end_time=$(($(date "+%s") - tests_start_time)) || return $? + termux_exec__tests__log 1 "All 'termux-exec' tests successful in \ +$((tests_end_time / 3600 )) hours $(((tests_end_time % 3600) / 60)) minutes $((tests_end_time % 60)) seconds" + + return 0 + +} + + + +## +# `termux_exec__tests__unit_tests__run_command` +## +termux_exec__tests__unit_tests__run_command() { + + local return_value + + local unit_tests_variant="unit-tests" + + if [[ "$TERMUX_EXEC__TESTS__USE_FSANITIZE_BUILDS" == "true" ]]; then + unit_tests_variant+="-fsanitize" + else + unit_tests_variant+="-nofsanitize" + fi + + termux_exec__tests__log 2 "Running 'unit' tests ($unit_tests_variant)" + + output="$( + printf -v "$TERMUX_EXEC__TESTS__LOG_LEVEL___N" "%s" "$TERMUX_EXEC__TESTS__LOG_LEVEL" || exit $? + export "${TERMUX_EXEC__TESTS__LOG_LEVEL___N?}" || exit $? + ASAN_OPTIONS=fast_unwind_on_malloc=false:detect_leaks="$TERMUX_EXEC__TESTS__DETECT_LEAKS" LSAN_OPTIONS=report_objects="$TERMUX_EXEC__TESTS__DETECT_LEAKS" \ + "$TERMUX_EXEC__TESTS__TESTS_PATH/$unit_tests_variant" 2>&1)" + return_value=$? + if [ $return_value -eq 0 ] || + { [ $return_value -eq 141 ] && + { [[ "$output" == *"WARNING: Can't read from symbolizer at fd"* ]] || + [[ "$output" == *"WARNING: external symbolizer didn't start up correctly!"* ]] + } && + [[ "$output" == *"End 'unit' tests"* ]]; + }; then + termux_exec__tests__log 1 "$output" + else + termux_exec__tests__log_error "$output" + termux_exec__tests__log_error "Unit tests failed" + return $return_value + fi + + return 0 + +} + + + +## +# `termux_exec__tests__runtime_tests__run_command` +## +termux_exec__tests__runtime_tests__run_command() { + + local return_value + + termux_exec__tests__log 2 "Running 'runtime' tests" + + termux_exec__tests__runtime_tests__run_runtime_binary_tests || return $? + + termux_exec__tests__runtime_tests__run_runtime_script_tests || return $? + + return 0 + +} + +## +# `termux_exec__tests__runtime_tests__run_runtime_binary_tests` +## +termux_exec__tests__runtime_tests__run_runtime_binary_tests() { + + local return_value + + local runtime_binary_tests_variant="runtime-binary-tests" + + if [[ "$TERMUX_EXEC__TESTS__USE_FSANITIZE_BUILDS" == "true" ]]; then + runtime_binary_tests_variant+="-fsanitize" + else + runtime_binary_tests_variant+="-nofsanitize" + fi + + if [ "$ANDROID__BUILD_VERSION_SDK" -ge 28 ] && [ -f "$TERMUX_EXEC__TESTS__TESTS_PATH/${runtime_binary_tests_variant}28" ]; then + runtime_binary_tests_variant+="28" + fi + + termux_exec__tests__log 3 "Running 'runtime_binary' tests ($runtime_binary_tests_variant)" + ( + printf -v "$TERMUX_EXEC__TESTS__LOG_LEVEL___N" "%s" "$TERMUX_EXEC__TESTS__LOG_LEVEL" || exit $? + export "${TERMUX_EXEC__TESTS__LOG_LEVEL___N?}" || exit $? + ASAN_OPTIONS=fast_unwind_on_malloc=false:detect_leaks="$TERMUX_EXEC__TESTS__DETECT_LEAKS" LSAN_OPTIONS=report_objects="$TERMUX_EXEC__TESTS__DETECT_LEAKS" \ + "$TERMUX_EXEC__TESTS__TESTS_PATH/$runtime_binary_tests_variant" + ) + return_value=$? + if [ $return_value -ne 0 ]; then + termux_exec__tests__log_error "Runtime binary tests failed" + return $return_value + fi + +} + +## +# `termux_exec__tests__runtime_tests__run_runtime_script_tests` +## +termux_exec__tests__runtime_tests__run_runtime_script_tests() { + + local return_value + + termux_exec__tests__log 3 "Running 'runtime_script' tests" + + # shellcheck source=tests/runtime-script-tests.in + source "$TERMUX_EXEC__TESTS__TESTS_PATH/runtime-script-tests" || return $? + + TERMUX_EXEC__TESTS__LOG_TAG="script" runtime_script_tests_main || return $? + return_value=$? + if [ $return_value -ne 0 ]; then + termux_exec__tests__log_error "Runtime script tests failed" + return $return_value + fi + + return 0 +} + + + + + +## +# Source a file under `$PATH`, like under `TERMUX__PREFIX/bin`. +# +# A separate function is used to source so that arguments passed to +# calling script/function are not passed to the sourced script. +# +# +# termux_exec__tests__source_file_from_path +## +termux_exec__tests__source_file_from_path() { + + local source_file="${1:-}"; [ $# -gt 0 ] && shift 1; + + local source_path + + if source_path="$(command -v "$source_file")" && [ -n "$source_path" ]; then + # shellcheck disable=SC1090 + source "$source_path" || return $? + else + echo "Failed to find the '$source_file' file to source." 1>&2 + return 1 + fi + +} + + + +## +# Copy the value of a variable to another variable. +# +# +# **Parameters:** +# `output_variable_name` - The name of the output variable to set. +# `input_variable_name` - The name of the input variable to read. +# +# **Returns:** +# Returns `0` if successful, otherwise returns with a non-zero exit code. +# +# +# `termux_exec__tests__copy_variable` `` `` +## +termux_exec__tests__copy_variable() { + + local output_variable_name="${1:-}" + local input_variable_name="${2:-}" + + if [[ ! "$output_variable_name" =~ ^[a-zA-Z][a-zA-Z0-9_]*$ ]]; then + echo "The output_variable_name '$output_variable_name' is not a valid shell variable name while running 'termux_exec__tests__copy_variable'." 1>&2 + return 1 + fi + + if [[ ! "$input_variable_name" =~ ^[a-zA-Z][a-zA-Z0-9_]*$ ]]; then + echo "The input_variable_name '$input_variable_name' is not a valid shell variable name while running 'termux_exec__tests__copy_variable'." 1>&2 + return 1 + fi + + eval "$output_variable_name"=\"\$\{"$input_variable_name":-\}\" + +} + + + +## +# Escape '\$[](){}|^.?+*' in a string with backslashes so that it can +# be used as a literal string in regex. +# +# +# `termux_exec__tests__escape_string_for_regex` `` +## +termux_exec__tests__escape_string_for_regex() { + + printf "%s" "$1" | sed -zE -e 's/[][\.|$(){}?+*^]/\\&/g' + +} + + + + + +## +# Set exit traps to `termux_exec__tests__traps()`. +## +termux_exec__tests__set_traps() { + + # Set traps to `termux_exec__tests__traps`. + trap 'termux_exec__tests__traps' EXIT + trap 'termux_exec__tests__traps TERM' TERM + trap 'termux_exec__tests__traps INT' INT + trap 'termux_exec__tests__traps HUP' HUP + trap 'termux_exec__tests__traps QUIT' QUIT + + return 0 + +} + +termux_exec__tests__traps_killtree() { + + local signal="$1"; local pid="$2"; local cpid + for cpid in $(pgrep -P "$pid"); do termux_exec__tests__traps_killtree "$signal" "$cpid"; done + [[ "$pid" != "$$" ]] && signal="${signal:=15}"; kill "-$signal" "$pid" 2>/dev/null + +} + +termux_exec__tests__traps() { + + local exit_code=$? + trap - EXIT + + if [[ "${TERMUX_EXEC__TESTS__TMPDIR_PATH:-}" =~ ^(/[^/]+)+$ ]] && [[ "$TERMUX_EXEC__TESTS__NO_CLEAN" != "true" ]]; then + rm -rf "$TERMUX_EXEC__TESTS__TMPDIR_PATH" + fi + + [ -n "$1" ] && trap - "$1"; + termux_exec__tests__traps_killtree "$1" $$; + exit $exit_code + +} + + + + + +## +# `termux_exec__tests__process_script_arguments` [``] +## +termux_exec__tests__process_script_arguments() { + + local opt; local opt_arg; local OPTARG; local OPTIND + + if [ $# -eq 0 ]; then + TERMUX_EXEC__TESTS__COMMAND_TYPE_NOOP="true" + show_help; return $? + fi + + # Parse options to main command. + while getopts ":hqvfl-:" opt; do + opt_arg="${OPTARG:-}" + case "${opt}" in + -) + case "${OPTARG}" in *?=*) opt_arg="${OPTARG#*=}";; *) opt_arg="";; esac + case "${OPTARG}" in + help) + TERMUX_EXEC__TESTS__COMMAND_TYPE_NOOP="true" + show_help; return $? + ;; + version) + TERMUX_EXEC__TESTS__COMMAND_TYPE_NOOP="true" + echo "@TERMUX_EXEC_PKG__VERSION@"; return $? + ;; + quiet) + TERMUX_EXEC__TESTS__LOG_LEVEL=0 + ;; + ld-preload=?*) + LD_PRELOAD="$(readlink -f -- "$opt_arg")" || return $? + export LD_PRELOAD + ;; + ld-preload | ld-preload=) + termux_exec__tests__log_error "No parameters set for option: '--${OPTARG%=*}'" + return 1 + ;; + no-clean) + TERMUX_EXEC__TESTS__NO_CLEAN="true" + ;; + only-termux-exec-tests) + TERMUX_EXEC__TESTS__ONLY_TERMUX_EXEC_TESTS="true" + ;; + skip-termux-core-tests) + TERMUX_EXEC__TESTS__SKIP_TERMUX_CORE_TESTS="true" + ;; + tests-path=?*) + TERMUX_EXEC__TESTS__TESTS_PATH="$(readlink -f -- "$opt_arg")" || return $? + ;; + tests-path | tests-path=) + termux_exec__tests__log_error "No parameters set for option: '--${OPTARG%=*}'" + return 1 + ;; + '') + # End of options `--`. + break + ;; + *) + termux_exec__tests__log_error "Unknown option: '--${OPTARG:-}'." + return 1 + ;; + esac + ;; + h) + TERMUX_EXEC__TESTS__COMMAND_TYPE_NOOP="true" + show_help; return $? + ;; + q) + TERMUX_EXEC__TESTS__LOG_LEVEL=0 + ;; + v) + if [ "$TERMUX_EXEC__TESTS__LOG_LEVEL" -lt "$TERMUX_EXEC__TESTS__MAX_LOG_LEVEL" ]; then + TERMUX_EXEC__TESTS__LOG_LEVEL=$((TERMUX_EXEC__TESTS__LOG_LEVEL+1)); + else + termux_exec__tests__log_error "Invalid option, max log level is $TERMUX_EXEC__TESTS__MAX_LOG_LEVEL" + return 1 + fi + ;; + f) + TERMUX_EXEC__TESTS__USE_FSANITIZE_BUILDS="true" + ;; + l) + TERMUX_EXEC__TESTS__DETECT_LEAKS=1 + ;; + \?) + :;; + esac + done + shift $((OPTIND - 1)) # Remove already processed arguments from argument list + + if [ $# -eq 0 ]; then + termux_exec__tests__log_error "The command type not passed." + return 1 + elif [ $# -ne 1 ]; then + termux_exec__tests__log_error "Expected 1 argument for command type but passed: $*" + return 1 + fi + + TERMUX_EXEC__TESTS__COMMAND_TYPE_ID="$1" + + return 0; + +} + +## +# `show_help` +## +show_help() { + + cat <<'HELP_EOF' +termux-exec-tests is a script that run tests for the termux-exec. + + +Usage: + termux-exec-tests [command_options] + +Available commands: + unit Run unit tests. + runtime Run runtime on-device tests. + all Run all tests. + +Available command_options: + [ -h | --help ] Display this help screen. + [ --version ] Display version. + [ -q | --quiet ] Set log level to 'OFF'. + [ -v | -vv | -vvv | -vvvvv ] + Set log level to 'DEBUG', 'VERBOSE', + 'VVERBOSE' and 'VVVERBOSE'. + [ -f ] Use fsanitize binaries for AddressSanitizer. + [ -l ] Detect memory leaks with LeakSanitizer. + Requires '-f' to be passed. + [ --ld-preload= ] The path to 'libtermux-exec.so'. + [ --no-clean ] Do not clean test files on failure. + [ --only-termux-exec-tests ] + Only run 'termux-exec' package tests. + [ --skip-termux-core-tests ] + Skip 'termux-core' package tests. + [ --tests-path= ] The path to installed-tests directory. +HELP_EOF + +} + +# If script is sourced, return with success, otherwise call main function. +# - https://stackoverflow.com/a/28776166/14686958 +# - https://stackoverflow.com/a/29835459/14686958 +if (return 0 2>/dev/null); then + return 0 # EX__SUCCESS +else + termux_exec__tests__main "$@" + exit $? +fi diff --git a/tests/unit-tests.c b/tests/unit-tests.c new file mode 100644 index 0000000..79a8e46 --- /dev/null +++ b/tests/unit-tests.c @@ -0,0 +1,1812 @@ +#define _GNU_SOURCE +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../src/data/assert_utils.h" +#include "../src/data/data_utils.h" +#include "../src/exec/exec.h" +#include "../src/file/file_utils.h" +#include "../src/logger/logger.h" +#include "../src/termux/termux_env.h" +#include "../src/termux/termux_files.h" + +static const char* LOG_TAG = "unit"; + +#define TERMUX_EXEC__TESTS__TERMUX_APP_DATA_DIR_1 "/data/user/0/com.foo" +#define TERMUX_EXEC__TESTS__TERMUX_APP_DATA_DIR_2 "/data/data/com.foo" + +#define TERMUX_EXEC__TESTS__TERMUX_ROOTFS_DIR_1 "/data/data/com.foo/foo/rootfs" +#define TERMUX_EXEC__TESTS__TERMUX_PREFIX_DIR_1 TERMUX_EXEC__TESTS__TERMUX_ROOTFS_DIR_1 "/usr" +#define TERMUX_EXEC__TESTS__TERMUX_BIN_DIR_1 TERMUX_EXEC__TESTS__TERMUX_PREFIX_DIR_1 "/bin" + +#define TERMUX_EXEC__TESTS__TERMUX_ROOTFS_DIR_2 "/" +#define TERMUX_EXEC__TESTS__TERMUX_PREFIX_DIR_2 TERMUX_EXEC__TESTS__TERMUX_ROOTFS_DIR_1 "/usr" +#define TERMUX_EXEC__TESTS__TERMUX_BIN_DIR_2 TERMUX_EXEC__TESTS__TERMUX_PREFIX_DIR_1 "/bin" + +static void init(); +static void initLogger(); + + + +void testStringStartsWith__Basic(); + +void runStringStartsWithTests() { + logDebug(LOG_TAG, "runStringStartsWithTests()"); + + testStringStartsWith__Basic(); + + int__AEqual(0, errno); +} + + + +#define ssw__ATrue(string, prefix) \ + if (1) { \ + assert_true_with_error(string_starts_with(string, prefix), \ + LOG_TAG, "%d: string_starts_with('%s', '%s')", __LINE__, string , prefix); \ + assert_int_with_error(0, errno, \ + LOG_TAG, "%d: %s()", __LINE__, __FUNCTION__); \ + } else ((void)0) + +#define ssw__AFalse(string, prefix) \ + if (1) { \ + assert_false_with_error(string_starts_with(string, prefix), \ + LOG_TAG, "%d: string_starts_with('%s', '%s')", __LINE__, string , prefix); \ + assert_int_with_error(0, errno, \ + LOG_TAG, "%d: %s()", __LINE__, __FUNCTION__); \ + } else ((void)0) + + + +void testStringStartsWith__Basic() { + logVerbose(LOG_TAG, "testStringStartsWith__Basic()"); + + ssw__AFalse(NULL, "/path"); + ssw__AFalse("/path", NULL); + ssw__AFalse(NULL, NULL); + ssw__AFalse("", ""); + ssw__AFalse("/", ""); + ssw__AFalse("", "/"); + ssw__AFalse("", " "); + ssw__AFalse(" ", ""); + ssw__ATrue(" ", " "); + ssw__AFalse("", "\0"); + ssw__AFalse("\0", ""); + ssw__AFalse("\0", "\0"); + ssw__AFalse("\0", "a"); + ssw__AFalse("a", "\0"); + + ssw__ATrue("/path/to/file", "/path"); + ssw__AFalse("/path", "/path/to/file"); + + ssw__ATrue("/path", "/"); + ssw__ATrue("/path", "/p"); + ssw__ATrue("/path/to/file", "/"); + ssw__ATrue("/path/to/file", "/p"); + ssw__ATrue("/path/to/file", "/path/"); + ssw__ATrue("/path/to/file", "/path/to"); + ssw__ATrue("/path/to/file", "/path/to/"); + ssw__ATrue("/path/to/file", "/path/to/file"); + ssw__AFalse("/path/to/file", "/path/to/file/"); + +} + + + + + +void testStringEndsWith__Basic(); + +void runStringEndsWithTests() { + logDebug(LOG_TAG, "runStringEndsWithTests()"); + + testStringEndsWith__Basic(); + + int__AEqual(0, errno); +} + + + +#define sew__ATrue(string, prefix) \ + if (1) { \ + assert_true_with_error(string_ends_with(string, prefix), \ + LOG_TAG, "%d: string_ends_with('%s', '%s')", __LINE__, string , prefix); \ + } else ((void)0) + +#define sew__AFalse(string, prefix) \ + if (1) { \ + assert_false_with_error(string_ends_with(string, prefix), \ + LOG_TAG, "%d: string_ends_with('%s', '%s')", __LINE__, string , prefix); \ + } else ((void)0) + + + +void testStringEndsWith__Basic() { + logVerbose(LOG_TAG, "testStringEndsWith__Basic()"); + + sew__AFalse(NULL, "/path"); + sew__AFalse("/path", NULL); + sew__AFalse(NULL, NULL); + sew__AFalse("", ""); + sew__AFalse("/", ""); + sew__AFalse("", "/"); + sew__AFalse("", " "); + sew__AFalse(" ", ""); + sew__ATrue(" ", " "); + sew__AFalse("", "\0"); + sew__AFalse("\0", ""); + sew__AFalse("\0", "\0"); + sew__AFalse("\0", "a"); + sew__AFalse("a", "\0"); + + sew__ATrue("/path/to/file", "/file"); + sew__AFalse("/file", "/path/to/file"); + + sew__ATrue("/path", "h"); + sew__ATrue("/path", "/path"); + sew__AFalse("/path", "hh"); + sew__ATrue("/path/to/file", "/file"); + sew__ATrue("/path/to/file", "to/file"); + sew__ATrue("/path/to/file", "/to/file"); + sew__ATrue("/path/to/file", "path/to/file"); + sew__ATrue("/path/to/file", "/path/to/file"); + +} + + + + + +void testAbsolutizePath__Basic(); + +void runAbsolutizePathTests() { + logDebug(LOG_TAG, "runAbsolutizePathTests()"); + + testAbsolutizePath__Basic(); + + int__AEqual(0, errno); +} + + + +#define absolutizeP__AEqual(expected_path, path) \ + if (1) { \ + char absolute_path[PATH_MAX]; \ + assert_string_equals_with_error(expected_path, absolutize_path(path, absolute_path, sizeof(absolute_path)), \ + LOG_TAG, "%d: absolutize_path('%s')", __LINE__, path); \ + errno = 0; \ + } else ((void)0) + +#define absolutizeP__ANull(path) \ + if (1) { \ + char absolute_path[PATH_MAX]; \ + assert_string_null_with_error(absolutize_path(path, absolute_path, sizeof(absolute_path)), \ + LOG_TAG, "%d: absolutize_path('%s')", __LINE__, path); \ + errno = 0; \ + } else ((void)0) + + + +void testAbsolutizePath__Basic() { + logVerbose(LOG_TAG, "testAbsolutizePath__Basic()"); + + char expected[PATH_MAX]; + char cwd[PATH_MAX]; + + state__ATrue(getcwd(cwd, sizeof(cwd)) != NULL); + + // Ensure enough space for 25 char binary under TERMUX__PREFIX__BIN_DIR for tests. + state__ATrue(strlen(cwd) + strlen(TERMUX__PREFIX__BIN_DIR "/xxxxxxxxxxxxxxxxxxxxxxxxx") < PATH_MAX); + + + absolutizeP__ANull(NULL); + absolutizeP__ANull(""); + + { + char path[PATH_MAX]; + snprintf(path, sizeof(path), "/%0*d", PATH_MAX - 2, 0); + absolutizeP__AEqual(path, path); + } + + { + char path[PATH_MAX + 1]; + snprintf(path, sizeof(path), "/%0*d", PATH_MAX - 1, 0); + absolutizeP__ANull(path); + } + + { + char path[PATH_MAX]; + snprintf(path, sizeof(path), "%0*d", PATH_MAX - 1, 0); + absolutizeP__ANull(path); + } + + { + char path[PATH_MAX]; + snprintf(path, sizeof(path), "%0*d", PATH_MAX - 2, 0); + state__ATrue(strlen(cwd) >= 1); + absolutizeP__ANull(path); + } + + + snprintf(expected, sizeof(expected), "%s/path/to/binary", cwd); + absolutizeP__AEqual(expected, "path/to/binary"); + + snprintf(expected, sizeof(expected), "%s/path/to/../binary", cwd); + absolutizeP__AEqual(expected, "path/to/../binary"); + + + snprintf(expected, sizeof(expected), "%s/../path/to/binary", cwd); + absolutizeP__AEqual(expected, "../path/to/binary"); + + snprintf(expected, sizeof(expected), "%s/../../path/to/binary", cwd); + absolutizeP__AEqual(expected, "../../path/to/binary"); + + + snprintf(expected, sizeof(expected), "%s/./path/to/binary", cwd); + absolutizeP__AEqual(expected, "./path/to/binary"); + + snprintf(expected, sizeof(expected), "%s/././path/to/binary", cwd); + absolutizeP__AEqual(expected, "././path/to/binary"); + + + snprintf(expected, sizeof(expected), "%s/.././path/to/binary", cwd); + absolutizeP__AEqual(expected, ".././path/to/binary"); + + snprintf(expected, sizeof(expected), "%s/./../path/to/binary", cwd); + absolutizeP__AEqual(expected, "./../path/to/binary"); + + + { + char path[PATH_MAX]; + snprintf(expected, sizeof(expected), "%s/path/to/binary", cwd); + snprintf(path, sizeof(path), "%s/path/to/binary", cwd); + absolutizeP__AEqual(expected, path); + } + + + { + char path[PATH_MAX]; + snprintf(expected, sizeof(expected), "%s/../path/to/binary", cwd); + snprintf(path, sizeof(path), "%s/../path/to/binary", cwd); + absolutizeP__AEqual(expected, path); + } + + + snprintf(expected, sizeof(expected), "%s/binary", cwd); + absolutizeP__AEqual(expected, "binary"); + + { + char path[PATH_MAX]; + snprintf(expected, sizeof(expected), "%s///binary", cwd); + snprintf(path, sizeof(path), "%s///binary", cwd); + absolutizeP__AEqual(expected, path); + } + +} + + + + + +void testNormalizePath__KeepEndSeparator(); +void testNormalizePath__NoEndSeparator(); +void testNormalizePath__SingleDots(); +void testNormalizePath__DoubleDots(); +void testNormalizePath__FromMethodDocs(); +void testNormalizePath__BinaryPaths(); + +/** + * Test normalization of file paths. + * + * Most of the tests were taken and modified from following under Apache-2.0 license. + * - https://github.com/apache/commons-io/blob/51894ebab6d260962caacd9e33394226bbf891c4/src/test/java/org/apache/commons/io/FilenameUtilsTest.java#L758 + * - https://github.com/apache/commons-io/blob/master/LICENSE.txt + */ +void runNormalizePathTests() { + logDebug(LOG_TAG, "runNormalizePathTests()"); + + testNormalizePath__KeepEndSeparator(); + testNormalizePath__NoEndSeparator(); + testNormalizePath__SingleDots(); + testNormalizePath__DoubleDots(); + testNormalizePath__FromMethodDocs(); + testNormalizePath__BinaryPaths(); + + int__AEqual(0, errno); +} + + + +#define normalizeP__AEqual(expected_path, path_param, keepEndSeparator, removeDoubleDot) \ + if (1) { \ + const char* path = path_param; \ + if (path != NULL) { \ + char input_path[PATH_MAX]; \ + strncpy(input_path, path, sizeof(input_path) - 1); \ + assert_string_equals_with_error(expected_path, normalize_path(input_path, keepEndSeparator, removeDoubleDot), \ + LOG_TAG, "%d: normalize_path('%s', %s, %s)", __LINE__, input_path, #keepEndSeparator, #removeDoubleDot); \ + } else { \ + assert_string_equals_with_error(expected_path, normalize_path(NULL, keepEndSeparator, removeDoubleDot), \ + LOG_TAG, "%d: normalize_path(null, %s, %s)", __LINE__, #keepEndSeparator, #removeDoubleDot); \ + } \ + } else ((void)0) + +#define normalizeP__ANull(path_param, keepEndSeparator, removeDoubleDot) \ + if (1) { \ + const char* path = path_param; \ + if (path != NULL) { \ + char input_path[PATH_MAX]; \ + strncpy(input_path, path, sizeof(input_path) - 1); \ + assert_string_null_with_error(normalize_path(input_path, keepEndSeparator, removeDoubleDot), \ + LOG_TAG, "%d: normalize_path('%s', %s, %s)", __LINE__, input_path, #keepEndSeparator, #removeDoubleDot); \ + } else { \ + assert_string_null_with_error(normalize_path(NULL, keepEndSeparator, removeDoubleDot), \ + LOG_TAG, "%d: normalize_path(null, %s, %s)", __LINE__, #keepEndSeparator, #removeDoubleDot); \ + } \ + } else ((void)0) + +#define normalizeP__AEqual__KES(expected, path) normalizeP__AEqual(expected, path, true, true) +#define normalizeP__ANull__KES(path) normalizeP__ANull(path, true, true) + +#define normalizeP__AEqual__NES(expected, path) normalizeP__AEqual(expected, path, false, true) +#define normalizeP__ANull__NES(path) normalizeP__ANull(path, false, true) + + + +void testNormalizePath__KeepEndSeparator() { + logVerbose(LOG_TAG, "testNormalizePath__KeepEndSeparator()"); + + normalizeP__ANull(NULL, true, true); + normalizeP__ANull__KES(""); + + normalizeP__AEqual__KES("/", "/"); + normalizeP__AEqual__KES("/", "//"); + normalizeP__AEqual__KES("/", "///"); + normalizeP__AEqual__KES("/", "////"); + + normalizeP__AEqual__KES("/a", "/a"); + normalizeP__AEqual__KES("/a", "//a"); + normalizeP__AEqual__KES("/a/", "//a/"); + normalizeP__AEqual__KES("/a/", "//a//"); + normalizeP__AEqual__KES("/a/", "//a///"); + normalizeP__AEqual__KES("/a/", "///a///"); + normalizeP__AEqual__KES("/a/b", "///a/b"); + normalizeP__AEqual__KES("/a/b/", "///a/b/"); + normalizeP__AEqual__KES("/a/b/", "///a/b//"); + normalizeP__AEqual__KES("/a/b/", "///a//b//"); + normalizeP__AEqual__KES("/a/b/", "///a//b///"); + normalizeP__AEqual__KES("/a/b/", "///a///b///"); + normalizeP__AEqual__KES("a/", "a//"); + normalizeP__AEqual__KES("a/", "a///"); + normalizeP__AEqual__KES("a/", "a///"); + normalizeP__AEqual__KES("a/b", "a/b"); + normalizeP__AEqual__KES("a/b/", "a/b/"); + normalizeP__AEqual__KES("a/b/", "a/b//"); + normalizeP__AEqual__KES("a/b/", "a//b//"); + normalizeP__AEqual__KES("a/b/", "a//b///"); + normalizeP__AEqual__KES("a/b/", "a///b///"); + normalizeP__AEqual__KES("a/b/c", "a/b//c"); + normalizeP__AEqual__KES("/a/b/c", "/a/b//c"); + normalizeP__AEqual__KES("/a/b/c/", "/a/b//c/"); + normalizeP__AEqual__KES("/a/b/c/d/", "///a//b///c/d///"); + normalizeP__AEqual__KES("/a/b/c/d", "///a//b///c/d"); + normalizeP__AEqual__KES("/a/b/c/d/", "//a//b///c/d//"); + + normalizeP__AEqual__KES("~", "~"); + normalizeP__AEqual__KES("~/", "~/"); + normalizeP__AEqual__KES("~/a", "~/a"); + normalizeP__AEqual__KES("~/a/", "~/a/"); + normalizeP__AEqual__KES("~/a/b/d", "~/a/b//d"); + normalizeP__AEqual__KES("~user/a", "~user/a"); + normalizeP__AEqual__KES("~user/a/", "~user/a/"); + normalizeP__AEqual__KES("~user/a/b/d", "~user/a/b//d"); + normalizeP__AEqual__KES("~user/", "~user/"); + normalizeP__AEqual__KES("~user", "~user"); + normalizeP__AEqual__KES("~user/a/b/c.txt", "~user/a/b/c.txt"); + + normalizeP__AEqual__KES("C:/a", "C:/a"); + normalizeP__AEqual__KES("C:/a/", "C:/a/"); + normalizeP__AEqual__KES("C:/a/b/d", "C:/a/b//d"); + normalizeP__AEqual__KES("C:/", "C:/"); + normalizeP__AEqual__KES("C:a", "C:a"); + normalizeP__AEqual__KES("C:a/", "C:a/"); + normalizeP__AEqual__KES("C:a/b/d", "C:a/b//d"); + normalizeP__AEqual__KES("C:", "C:"); + normalizeP__AEqual__KES("C:/a/b/c.txt", "C:/a/b/c.txt"); + + normalizeP__AEqual__KES("/server/a", "//server/a"); + normalizeP__AEqual__KES("/server/a/", "//server/a/"); + normalizeP__AEqual__KES("/server/a/b/d", "//server/a/b//d"); + normalizeP__AEqual__KES("/server/", "//server/"); + normalizeP__AEqual__KES("/server/a/b/c.txt", "//server/a/b/c.txt"); + normalizeP__AEqual__KES("/-server/a/b/c.txt", "/-server/a/b/c.txt"); + + normalizeP__AEqual__KES(":", ":"); + normalizeP__AEqual__KES("1:", "1:"); + normalizeP__AEqual__KES("1:a", "1:a"); + normalizeP__AEqual__KES("1:/a/b/c.txt", "1:/a/b/c.txt"); + normalizeP__AEqual__KES("/a/b/c.txt", "///a/b/c.txt"); + normalizeP__AEqual__KES("a/b/c.txt", "a/b/c.txt"); + normalizeP__AEqual__KES("/a/b/c.txt", "/a/b/c.txt"); + normalizeP__AEqual__KES("~/a/b/c.txt", "~/a/b/c.txt"); + normalizeP__AEqual__KES("/127.0.0.1/a/b/c.txt", "//127.0.0.1/a/b/c.txt"); + normalizeP__AEqual__KES("/::1/a/b/c.txt", "//::1/a/b/c.txt"); + normalizeP__AEqual__KES("/1::/a/b/c.txt", "//1::/a/b/c.txt"); + normalizeP__AEqual__KES("/server.example.org/a/b/c.txt", "//server.example.org/a/b/c.txt"); + normalizeP__AEqual__KES("/server.sub.example.org/a/b/c.txt", "/server.sub.example.org/a/b/c.txt"); + normalizeP__AEqual__KES("/server./a/b/c.txt", "//server./a/b/c.txt"); + normalizeP__AEqual__KES("/1::127.0.0.1/a/b/c.txt", "//1::127.0.0.1/a/b/c.txt"); + + // Not a valid IPv4 addresses but technically a valid "reg-name"s according to RFC1034. + normalizeP__AEqual__KES("/127.0.0.256/a/b/c.txt", "//127.0.0.256/a/b/c.txt"); + normalizeP__AEqual__KES("/127.0.0.01/a/b/c.txt", "//127.0.0.01/a/b/c.txt"); + + normalizeP__AEqual__KES("/::1::2/a/b/c.txt", "//::1::2/a/b/c.txt"); + normalizeP__AEqual__KES("/:1/a/b/c.txt", "//:1/a/b/c.txt"); + normalizeP__AEqual__KES("/1:/a/b/c.txt", "//1:/a/b/c.txt"); + normalizeP__AEqual__KES("/1:2:3:4:5:6:7:8:9/a/b/c.txt", "//1:2:3:4:5:6:7:8:9/a/b/c.txt"); + normalizeP__AEqual__KES("/g:2:3:4:5:6:7:8/a/b/c.txt", "//g:2:3:4:5:6:7:8/a/b/c.txt"); + normalizeP__AEqual__KES("/1ffff:2:3:4:5:6:7:8/a/b/c.txt", "//1ffff:2:3:4:5:6:7:8/a/b/c.txt"); + normalizeP__AEqual__KES("/1:2/a/b/c.txt", "//1:2/a/b/c.txt"); +} + +void testNormalizePath__NoEndSeparator() { + logVerbose(LOG_TAG, "testNormalizePath__NoEndSeparator()"); + + normalizeP__ANull(NULL, false, true); + normalizeP__ANull__NES(""); + + normalizeP__AEqual__NES("/", "/"); + normalizeP__AEqual__NES("/", "//"); + normalizeP__AEqual__NES("/", "///"); + normalizeP__AEqual__NES("/", "////"); + + normalizeP__AEqual__NES("/a", "/a"); + normalizeP__AEqual__NES("/a", "//a"); + normalizeP__AEqual__NES("/a", "//a/"); + normalizeP__AEqual__NES("/a", "//a//"); + normalizeP__AEqual__NES("/a", "//a///"); + normalizeP__AEqual__NES("/a", "///a///"); + normalizeP__AEqual__NES("/a/b", "///a/b"); + normalizeP__AEqual__NES("/a/b", "///a/b/"); + normalizeP__AEqual__NES("/a/b", "///a/b//"); + normalizeP__AEqual__NES("/a/b", "///a//b//"); + normalizeP__AEqual__NES("/a/b", "///a//b///"); + normalizeP__AEqual__NES("/a/b", "///a///b///"); + normalizeP__AEqual__NES("a", "a//"); + normalizeP__AEqual__NES("a", "a///"); + normalizeP__AEqual__NES("a", "a///"); + normalizeP__AEqual__NES("a/b", "a/b"); + normalizeP__AEqual__NES("a/b", "a/b/"); + normalizeP__AEqual__NES("a/b", "a/b//"); + normalizeP__AEqual__NES("a/b", "a//b//"); + normalizeP__AEqual__NES("a/b", "a//b///"); + normalizeP__AEqual__NES("a/b", "a///b///"); + normalizeP__AEqual__NES("a/b/c", "a/b//c"); + normalizeP__AEqual__NES("/a/b/c", "/a/b//c"); + normalizeP__AEqual__NES("/a/b/c", "/a/b//c/"); + normalizeP__AEqual__NES("/a/b/c/d", "///a//b///c/d///"); + normalizeP__AEqual__NES("/a/b/c/d", "///a//b///c/d"); + normalizeP__AEqual__NES("/a/b/c/d", "//a//b///c/d//"); + + normalizeP__AEqual__NES("~", "~"); + normalizeP__AEqual__NES("~", "~/"); + normalizeP__AEqual__NES("~user/a", "~user/a"); + normalizeP__AEqual__NES("~user/a", "~user/a/"); + normalizeP__AEqual__NES("~user/a/b/d", "~user/a/b//d/"); + normalizeP__AEqual__NES("~user", "~user/"); + normalizeP__AEqual__NES("~user", "~user"); + normalizeP__AEqual__NES("~/a", "~/a"); + normalizeP__AEqual__NES("~/a", "~/a/"); + normalizeP__AEqual__NES("~/a/b/d", "~/a/b//d/"); + normalizeP__AEqual__NES("~/a/b/c.txt", "~/a/b/c.txt/"); + normalizeP__AEqual__NES("~user/a/b/c.txt", "~user/a/b/c.txt/"); + + normalizeP__AEqual__NES("C:/a", "C:/a"); + normalizeP__AEqual__NES("C:/a", "C:/a/"); + normalizeP__AEqual__NES("C:/a/b/d", "C:/a/b//d/"); + normalizeP__AEqual__NES("C:", "C://"); + normalizeP__AEqual__NES("C:a", "C:a"); + normalizeP__AEqual__NES("C:a", "C:a/"); + normalizeP__AEqual__NES("C:a/b/d", "C:a/b//d/"); + normalizeP__AEqual__NES("C:", "C:/"); + normalizeP__AEqual__NES("C:/a/b/c.txt", "C:/a/b/c.txt/"); + normalizeP__AEqual__NES("C:/a/b/c.txt", "C://a//b//c.txt/"); + + normalizeP__AEqual__NES("/server/a", "//server/a"); + normalizeP__AEqual__NES("/server/a", "//server/a/"); + normalizeP__AEqual__NES("/server/a/b/d", "//server/a/b//d/"); + normalizeP__AEqual__NES("/server", "//server/"); + normalizeP__AEqual__NES("/server/a/b/c.txt", "//server/a/b/c.txt/"); + + normalizeP__AEqual__NES(":", ":"); + normalizeP__AEqual__NES("1:", "1:"); + normalizeP__AEqual__NES("1:a", "1:a"); + normalizeP__AEqual__NES("1:/a/b/c.txt", "1:/a/b/c.txt"); + normalizeP__AEqual__NES("a/b/c.txt", "a/b/c.txt/"); + normalizeP__AEqual__NES("/a/b/c.txt", "/a/b/c.txt/"); + normalizeP__AEqual__NES("/a/b/c.txt", "///a/b/c.txt"); +} + +void testNormalizePath__SingleDots() { + logVerbose(LOG_TAG, "testNormalizePath__SingleDots()"); + + normalizeP__ANull__KES("."); + normalizeP__ANull__KES("./"); + normalizeP__AEqual__KES("/", "/."); + normalizeP__AEqual__KES("/", "/./"); + normalizeP__ANull__KES("././"); + normalizeP__AEqual__KES("/", "/./."); + + normalizeP__AEqual__KES("/a", "/././a"); + normalizeP__AEqual__KES("/b/", "/./b/./"); + normalizeP__AEqual__KES("/b", "/./b/."); + normalizeP__AEqual__KES("c/", "./c/./"); + normalizeP__AEqual__KES("c", "./c/."); + normalizeP__AEqual__KES("c/", "././c/./"); + normalizeP__AEqual__KES("c", "././c/."); + normalizeP__AEqual__KES("c/", "././c/././"); + normalizeP__AEqual__KES("c", "././c/./."); + normalizeP__AEqual__KES("a/b", "a/b/././."); + normalizeP__AEqual__KES("a/b/", "a/b/./././"); + normalizeP__AEqual__KES("a/", "./a/"); + normalizeP__AEqual__KES("a", "./a"); + normalizeP__AEqual__KES("/a/b/", "/a/b/./././"); + normalizeP__AEqual__KES("/a/b", "/a/b/././."); + normalizeP__AEqual__KES("/a", "/./a"); + normalizeP__AEqual__KES("/", "/./"); + normalizeP__AEqual__KES("/", "/."); + + normalizeP__AEqual__KES("~/a/b/", "~/a/b/./././"); + normalizeP__AEqual__KES("~/a/b", "~/a/b/././."); + normalizeP__AEqual__KES("~/a", "~/./a"); + normalizeP__AEqual__KES("~/", "~/./"); + normalizeP__AEqual__KES("~", "~/."); + normalizeP__AEqual__KES("~/", "~/"); + normalizeP__AEqual__KES("~user/a/b/", "~user/a/b/./././"); + normalizeP__AEqual__KES("~user/a/b", "~user/a/b/././."); + normalizeP__AEqual__KES("~user/a", "~user/./a"); + normalizeP__AEqual__KES("~user/", "~user/./"); + normalizeP__AEqual__KES("~user", "~user/."); + + normalizeP__AEqual__KES("C:/a/b/", "C:/a/b/./././"); + normalizeP__AEqual__KES("C:/a/b", "C:/a/b/././."); + normalizeP__AEqual__KES("C:/a", "C:/./a"); + normalizeP__AEqual__KES("C:/", "C:/./"); + normalizeP__AEqual__KES("C:", "C:/."); + normalizeP__AEqual__KES("C:a/b/", "C:a/b/./././"); + normalizeP__AEqual__KES("C:a/b", "C:a/b/././."); + normalizeP__AEqual__KES("C:./a", "C:./a"); + normalizeP__AEqual__KES("C:./", "C:./"); + normalizeP__AEqual__KES("C:.", "C:."); + + normalizeP__AEqual__KES("/server/a/b/", "//server/a/b/./././"); + normalizeP__AEqual__KES("/server/a/b", "//server/a/b/././."); + normalizeP__AEqual__KES("/server/a", "//server/./a"); + normalizeP__AEqual__KES("/server/", "//server/./"); + normalizeP__AEqual__KES("/server", "//server/."); + + normalizeP__AEqual__KES("/a/b/c.txt", "/./a/b/c.txt"); + normalizeP__AEqual__KES("/127.0.0.256/a/b/c.txt", "//./127.0.0.256/./a/b/c.txt"); + + + + normalizeP__ANull__NES("."); + normalizeP__ANull__NES("./"); + normalizeP__AEqual__NES("/", "/."); + normalizeP__AEqual__NES("/", "/./"); + normalizeP__ANull__NES("././"); + normalizeP__AEqual__NES("/", "/./."); + + normalizeP__AEqual__NES("/a", "/././a/"); + normalizeP__AEqual__NES("/b", "/./b/./"); + normalizeP__AEqual__NES("/b", "/./b/."); + normalizeP__AEqual__NES("c", "./c/./"); + normalizeP__AEqual__NES("c", "./c/."); + normalizeP__AEqual__NES("c", "././c/./"); + normalizeP__AEqual__NES("c", "././c/."); + normalizeP__AEqual__NES("c", "././c/././"); + normalizeP__AEqual__NES("c", "././c/./."); + normalizeP__AEqual__NES("/", "/./."); + normalizeP__AEqual__NES("/", "/."); + normalizeP__AEqual__NES("a/b", "a/b/./././"); + normalizeP__AEqual__NES("a/b", "a/b/./././/"); + normalizeP__AEqual__NES("a", "./a//"); + normalizeP__AEqual__NES("a", "./a/"); + normalizeP__AEqual__NES("/a/b", "/a/b/./././"); + normalizeP__AEqual__NES("/a/b", "/a/b/./././"); + normalizeP__AEqual__NES("/a", "/./a/"); + normalizeP__AEqual__NES("/", "/./"); + normalizeP__AEqual__NES("/", "/./"); + + normalizeP__AEqual__NES("~/a/b", "~/a/b/./././"); + normalizeP__AEqual__NES("~/a/b", "~/a/b/./././"); + normalizeP__AEqual__NES("~/a", "~/./a/"); + normalizeP__AEqual__NES("~", "~/./"); + normalizeP__AEqual__NES("~", "~/./"); + normalizeP__AEqual__NES("~user/a/b", "~user/a/b/./././"); + normalizeP__AEqual__NES("~user/a/b", "~user/a/b/././."); + normalizeP__AEqual__NES("~user/a", "~user/./a/"); + normalizeP__AEqual__NES("~user", "~user/./"); + normalizeP__AEqual__NES("~user", "~user/./"); + + normalizeP__AEqual__NES("C:/a/b", "C:/a/b/./././"); + normalizeP__AEqual__NES("C:/a/b", "C:/a/b/././."); + normalizeP__AEqual__NES("C:/a", "C:/./a/"); + normalizeP__AEqual__NES("C:", "C:/./"); + normalizeP__AEqual__NES("C:", "C:/./"); + normalizeP__AEqual__NES("C:a/b", "C:a/b/./././"); + normalizeP__AEqual__NES("C:a/b", "C:a/b/././."); + normalizeP__AEqual__NES("C:./a", "C:./a/"); + normalizeP__AEqual__NES("C:.", "C:./"); + normalizeP__AEqual__NES("C:.", "C:."); + + normalizeP__AEqual__NES("/server/a/b", "//server/a/b/./././"); + normalizeP__AEqual__NES("/server/a/b", "//server/a/b/././."); + normalizeP__AEqual__NES("/server/a", "//server/./a/"); + normalizeP__AEqual__NES("/server", "//server/./"); + normalizeP__AEqual__NES("/server", "//server/."); + + normalizeP__AEqual__NES("/a/b/c.txt", "/./a/b/c.txt"); + normalizeP__AEqual__NES("/127.0.0.256/a/b/c.txt", "//./127.0.0.256/./a/b/c.txt"); +} + +void testNormalizePath__DoubleDots() { + logVerbose(LOG_TAG, "testNormalizePath__DoubleDots()"); + + normalizeP__ANull__KES(".."); + normalizeP__ANull__KES("../"); + normalizeP__AEqual__KES("/", "/.."); + normalizeP__AEqual__KES("/", "/../"); + normalizeP__AEqual__KES("/", "/../../"); + + normalizeP__AEqual__KES("/a", "/../a"); + normalizeP__AEqual__KES("/a", "/../a/."); + normalizeP__ANull__KES("../"); + normalizeP__ANull__KES("../a"); + normalizeP__ANull__KES("../a/."); + + normalizeP__ANull__KES("./../a"); + normalizeP__ANull__KES("././../a"); + normalizeP__ANull__KES("././.././a"); + + normalizeP__ANull__KES("a/../../b"); + normalizeP__AEqual__KES("b", "a/./../b"); + normalizeP__AEqual__KES("b", "a/./.././b"); + + normalizeP__AEqual__KES("a/c", "a/b/../c"); + normalizeP__AEqual__KES("c", "a/b/../../c"); + normalizeP__AEqual__KES("c/", "a/b/../../c/"); + normalizeP__ANull__KES("a/b/../../../c"); + normalizeP__AEqual__KES("a", "a/b/.."); + normalizeP__AEqual__KES("a/", "a/b/../"); + normalizeP__ANull__KES("a/b/../.."); + normalizeP__ANull__KES("a/b/../../"); + normalizeP__ANull__KES("a/b/../../.."); + normalizeP__AEqual__KES("a/d", "a/b/../c/../d"); + normalizeP__AEqual__KES("a/d/", "a/b/../c/../d/"); + normalizeP__AEqual__KES("/a/c", "/a/b/../c"); + normalizeP__AEqual__KES("/c", "/a/b/../../c"); + normalizeP__AEqual__KES("/c", "/a/b/../../../c"); + normalizeP__AEqual__KES("/a", "/a/b/.."); + normalizeP__AEqual__KES("/", "/a/b/../.."); + normalizeP__AEqual__KES("/", "/a/b/../../.."); + normalizeP__AEqual__KES("/a/d", "/a/b/../c/../d"); + normalizeP__AEqual__KES("/a", "/../a"); + + normalizeP__ANull__KES("~/.."); + normalizeP__ANull__KES("~/../"); + normalizeP__ANull__KES("~/../a"); + normalizeP__ANull__KES("~/../a/."); + normalizeP__AEqual__KES("~/a/c", "~/a/b/../c"); + normalizeP__AEqual__KES("~/c", "~/a/b/../../c"); + normalizeP__ANull__KES("~/a/b/../../../c"); + normalizeP__AEqual__KES("~/a", "~/a/b/.."); + normalizeP__AEqual__KES("~/", "~/a/b/../../"); + normalizeP__AEqual__KES("~", "~/a/b/../.."); + normalizeP__ANull__KES("~/a/b/../../.."); + normalizeP__AEqual__KES("~/a/d", "~/a/b/../c/../d"); + normalizeP__ANull__KES("~/../a"); + normalizeP__ANull__KES("~/.."); + normalizeP__AEqual__KES("~user/a/c", "~user/a/b/../c"); + normalizeP__AEqual__KES("~user/c", "~user/a/b/../../c"); + normalizeP__ANull__KES("~user/a/b/../../../c"); + normalizeP__AEqual__KES("~user/a", "~user/a/b/.."); + normalizeP__AEqual__KES("~user", "~user/a/b/../.."); + normalizeP__ANull__KES("~user/a/b/../../.."); + normalizeP__AEqual__KES("~user/a/d", "~user/a/b/../c/../d"); + normalizeP__ANull__KES("~user/../a"); + normalizeP__ANull__KES("~user/.."); + + normalizeP__AEqual__KES("C:/a/c", "C:/a/b/../c"); + normalizeP__AEqual__KES("C:/c", "C:/a/b/../../c"); + normalizeP__ANull__KES("C:/a/b/../../../../c"); + normalizeP__AEqual__KES("c", "C:/a/b/../../../c"); + normalizeP__AEqual__KES("C:/a", "C:/a/b/.."); + normalizeP__AEqual__KES("C:", "C:/a/b/../.."); + normalizeP__ANull__KES("C:/a/b/../../.."); + normalizeP__AEqual__KES("C:/a/d", "C:/a/b/../c/../d"); + normalizeP__AEqual__KES("a", "C:/../a"); + normalizeP__ANull__KES("C:/.."); + normalizeP__AEqual__KES("C:a/c", "C:a/b/../c"); + normalizeP__AEqual__KES("c", "C:a/b/../../c"); + normalizeP__ANull__KES("C:a/b/../../../c"); + normalizeP__AEqual__KES("C:a", "C:a/b/.."); + normalizeP__ANull__KES("C:a/b/../.."); + normalizeP__ANull__KES("C:a/b/../../.."); + normalizeP__AEqual__KES("C:a/d", "C:a/b/../c/../d"); + normalizeP__AEqual__KES("C:../a", "C:../a"); + normalizeP__AEqual__KES("C:..", "C:.."); + + normalizeP__AEqual__KES("/server/a/c", "//server/a/b/../c"); + normalizeP__AEqual__KES("/server/c", "//server/a/b/../../c"); + normalizeP__AEqual__KES("/c", "/server/a/b/../../../../c"); + normalizeP__AEqual__KES("/c", "//server/a/b/../../../c"); + normalizeP__AEqual__KES("/server/a", "//server/a/b/.."); + normalizeP__AEqual__KES("/server", "//server/a/b/../.."); + normalizeP__AEqual__KES("/", "//server/a/b/../../.."); + normalizeP__AEqual__KES("/server/a/d", "//server/a/b/../c/../d"); + normalizeP__AEqual__KES("/a", "//server/../a"); + normalizeP__AEqual__KES("/", "//server/../.."); + normalizeP__AEqual__KES("/", "//server/.."); + normalizeP__AEqual__KES("/b/c/g", "/a/../../../../b/c/d/e/f/../../../g"); + normalizeP__AEqual__KES("/g", "/a/../../../../b/c/d/e/f/../../../../../g"); + + normalizeP__AEqual__KES("/a/b/c.txt", "//../a/b/c.txt"); + normalizeP__AEqual__KES("/127.0..1/a/b/c.txt", "//127.0..1/a/b/c.txt"); + normalizeP__AEqual__KES("/foo", "//../foo"); + normalizeP__AEqual__KES("/foo", "//../foo"); + + + + normalizeP__ANull__NES(".."); + normalizeP__ANull__NES("../"); + normalizeP__AEqual__NES("/", "/.."); + normalizeP__AEqual__NES("/", "/../"); + normalizeP__AEqual__NES("/", "/../../"); + + normalizeP__AEqual__NES("/a", "/../a/"); + normalizeP__AEqual__NES("/a", "/../a/./"); + normalizeP__ANull__NES("../a"); + normalizeP__ANull__NES("../a/."); + normalizeP__ANull__NES("../a"); + + normalizeP__ANull__NES("./../a"); + normalizeP__ANull__NES("././../a"); + normalizeP__ANull__NES("././.././a"); + + normalizeP__ANull__NES("a/../../b"); + normalizeP__AEqual__NES("b", "a/./../b/"); + normalizeP__AEqual__NES("b", "a/./.././b/"); + + normalizeP__AEqual__NES("a/c", "a/b/../c/"); + normalizeP__AEqual__NES("c", "a/b/../../c"); + normalizeP__AEqual__NES("c", "a/b/../../c/"); + normalizeP__ANull__NES("a/b/../../../c/"); + normalizeP__AEqual__NES("a", "a/b/.."); + normalizeP__AEqual__NES("a", "a/b/../"); + normalizeP__ANull__NES("a/b/../.."); + normalizeP__ANull__NES("a/b/../../"); + normalizeP__ANull__NES("a/b/../../../"); + normalizeP__AEqual__NES("a/d", "a/b/../c/../d"); + normalizeP__AEqual__NES("a/d", "a/b/../c/../d/"); + normalizeP__AEqual__NES("/a/c", "/a/b/../c/"); + normalizeP__AEqual__NES("/c", "/a/b/../../c/"); + normalizeP__AEqual__NES("/c", "/a/b/../../../c/"); + normalizeP__AEqual__NES("/a", "/a/b/../"); + normalizeP__AEqual__NES("/", "/a/b/../../"); + normalizeP__AEqual__NES("/", "/a/b/../../../"); + normalizeP__AEqual__NES("/a/d", "/a/b/../c/../d/"); + normalizeP__AEqual__NES("/a", "/../a/"); + + normalizeP__ANull__NES("~/.."); + normalizeP__ANull__NES("~/../"); + normalizeP__ANull__NES("~/../a"); + normalizeP__ANull__NES("~/../a/."); + normalizeP__AEqual__NES("~/a/c", "~/a/b/../c/"); + normalizeP__AEqual__NES("~/c", "~/a/b/../../c/"); + normalizeP__ANull__NES("~/a/b/../../../c/"); + normalizeP__AEqual__NES("~/a", "~/a/b/../"); + normalizeP__AEqual__NES("~", "~/a/b/../../"); + normalizeP__AEqual__NES("~", "~/a/b/../.."); + normalizeP__ANull__NES("~/a/b/../../../"); + normalizeP__AEqual__NES("~/a/d", "~/a/b/../c/../d/"); + normalizeP__ANull__NES("~/../a/"); + normalizeP__ANull__NES("~/.."); + normalizeP__ANull__NES("~/../"); + normalizeP__AEqual__NES("~user/a/c", "~user/a/b/../c/"); + normalizeP__AEqual__NES("~user/c", "~user/a/b/../../c/"); + normalizeP__ANull__NES("~user/a/b/../../../c/"); + normalizeP__AEqual__NES("~user/a", "~user/a/b/../"); + normalizeP__AEqual__NES("~user", "~user/a/b/../../"); + normalizeP__ANull__NES("~user/a/b/../../../"); + normalizeP__AEqual__NES("~user/a/d", "~user/a/b/../c/../d/"); + normalizeP__ANull__NES("~user/../a/"); + normalizeP__ANull__NES("~user/../"); + + normalizeP__AEqual__NES("C:/a/c", "C:/a/b/../c/"); + normalizeP__AEqual__NES("C:/c", "C:/a/b/../../c/"); + normalizeP__AEqual__NES("c", "C:/a/b/../../../c/"); + normalizeP__AEqual__NES("C:/a", "C:/a/b/../"); + normalizeP__AEqual__NES("C:", "C:/a/b/../../"); + normalizeP__ANull__NES("C:/a/b/../../../"); + normalizeP__AEqual__NES("C:/a/d", "C:/a/b/../c/../d/"); + normalizeP__AEqual__NES("a", "C:/../a/"); + normalizeP__ANull__NES("C:/../"); + normalizeP__AEqual__NES("C:a/c", "C:a/b/../c/"); + normalizeP__AEqual__NES("c", "C:a/b/../../c/"); + normalizeP__ANull__NES("C:a/b/../../../c/"); + normalizeP__AEqual__NES("C:a", "C:a/b/../"); + normalizeP__ANull__NES("C:a/b/../../"); + normalizeP__ANull__NES("C:a/b/../../../"); + normalizeP__AEqual__NES("C:a/d", "C:a/b/../c/../d/"); + normalizeP__AEqual__NES("C:../a", "C:../a/"); + normalizeP__AEqual__NES("C:..", "C:../"); + + normalizeP__AEqual__NES("/server/a/c", "//server/a/b/../c/"); + normalizeP__AEqual__NES("/server/c", "//server/a/b/../../c/"); + normalizeP__AEqual__NES("/c", "//server/a/b/../../../../c/"); + normalizeP__AEqual__NES("/c", "//server/a/b/../../../c/"); + normalizeP__AEqual__NES("/server/a", "//server/a/b/../"); + normalizeP__AEqual__NES("/server", "//server/a/b/../../"); + normalizeP__AEqual__NES("/", "//server/a/b/../../../"); + normalizeP__AEqual__NES("/server/a/d", "//server/a/b/../c/../d/"); + normalizeP__AEqual__NES("/a", "//server/../a"); + normalizeP__AEqual__NES("/", "//server/../../"); + normalizeP__AEqual__NES("/", "//server/../"); +} + +void testNormalizePath__FromMethodDocs() { + logVerbose(LOG_TAG, "testNormalizePath__FromMethodDocs()"); + + normalizeP__ANull(NULL, false, true); + normalizeP__ANull__NES(""); + normalizeP__AEqual__NES("/", "/"); + normalizeP__AEqual__NES("/", "//"); + + normalizeP__AEqual__NES(":", ":"); + normalizeP__AEqual__NES("1:/a/b/c.txt", "1:/a/b/c.txt"); + normalizeP__AEqual__NES("/a/b/c/d", "///a//b///c/d///"); + normalizeP__AEqual__NES("/foo", "/foo//"); + normalizeP__AEqual__NES("/foo", "/foo/./"); + normalizeP__AEqual__NES("/foo/bar", "//foo//./bar"); + normalizeP__AEqual__NES("~", "~"); + normalizeP__AEqual__NES("~", "~/"); + normalizeP__AEqual__NES("~user", "~user/"); + normalizeP__AEqual__NES("~user", "~user"); + normalizeP__AEqual__NES("~user/a", "~user/a"); + + normalizeP__ANull__NES("."); + normalizeP__ANull__NES("./"); + normalizeP__AEqual__NES("/", "/."); + normalizeP__AEqual__NES("/", "/./"); + normalizeP__ANull__NES("././"); + normalizeP__AEqual__NES("/", "/./."); + normalizeP__AEqual__NES("/a", "/././a"); + normalizeP__AEqual__NES("/b", "/./b/./"); + normalizeP__AEqual__NES("c", "././c/./"); + + normalizeP__ANull__NES(".."); + normalizeP__ANull__NES("../"); + normalizeP__AEqual__NES("/", "/.."); + normalizeP__AEqual__NES("/", "/../"); + normalizeP__AEqual__NES("/a", "/../a"); + normalizeP__ANull__NES("../a/."); + normalizeP__ANull__NES("../a"); + normalizeP__AEqual__NES("/a", "/a/b/.."); + normalizeP__AEqual__NES("/", "/a/b/../.."); + normalizeP__AEqual__NES("/", "/a/b/../../.."); + normalizeP__AEqual__NES("/c", "/a/b/../../../c"); + normalizeP__ANull__NES("./../a"); + normalizeP__ANull__NES("././../a"); + normalizeP__ANull__NES("././.././a"); + normalizeP__ANull__NES("a/../../b"); + normalizeP__AEqual__NES("b", "a/./../b"); + normalizeP__AEqual__NES("b", "a/./.././b"); + normalizeP__ANull__NES("foo/../../bar"); + normalizeP__AEqual__NES("/bar", "/foo/../bar"); + normalizeP__AEqual__NES("/bar", "/foo/../bar/"); + normalizeP__AEqual__NES("/baz", "/foo/../bar/../baz"); + normalizeP__AEqual__NES("foo", "foo/bar/.."); + normalizeP__AEqual__NES("bar", "foo/../bar"); + normalizeP__ANull__NES("~/.."); + normalizeP__ANull__NES("~/../"); + normalizeP__ANull__NES("~/../a/."); + normalizeP__ANull__NES("~/.."); + normalizeP__AEqual__NES("~/bar", "~/foo/../bar/"); + normalizeP__AEqual__NES("C:/bar", "C:/foo/../bar"); + normalizeP__AEqual__NES("/server/bar", "//server/foo/../bar"); + normalizeP__AEqual__NES("/bar", "//server/../bar"); +} + +void testNormalizePath__BinaryPaths() { + logVerbose(LOG_TAG, "testNormalizePath__BinaryPaths()"); + + normalizeP__AEqual__NES("binary", "binary"); + normalizeP__AEqual__NES("binary", "binary/"); + normalizeP__AEqual__NES("/binary", "//binary//"); + normalizeP__AEqual__NES("/binary", "/../binary"); + normalizeP__AEqual__NES("/binary", "/../../binary"); + normalizeP__ANull__NES("../binary"); + + normalizeP__AEqual__NES("path/to/binary", "path/to/binary"); + normalizeP__AEqual__NES("path/to/binary", "path/to/binary/"); + normalizeP__AEqual__NES("path/to/binary", "path//to//binary"); + + normalizeP__AEqual__NES("path/path/to/binary", "path/./path/to/binary"); + normalizeP__AEqual__NES("/path/to/binary", "/././path/to/binary"); + normalizeP__AEqual__NES("path/to/binary", "././path/to/binary"); + + normalizeP__AEqual__NES("path/to/binary", "path/../path/to/binary"); + normalizeP__AEqual__NES("/path/to/binary", "/path/../../path/to/binary"); + normalizeP__ANull__NES("path/../../path/to/binary"); + + normalizeP__AEqual__NES("path/to/binary", "path/.././path/to/binary"); + normalizeP__AEqual__NES("path/to/binary", "path/./../path/to/binary"); + normalizeP__AEqual__NES("/path/to/binary", "/path/../../path/to/binary"); + normalizeP__AEqual__NES("/path/to/binary", "/path/./../../path/to/binary"); + normalizeP__AEqual__NES("/path/to/binary", "/path/../.././path/to/binary"); + normalizeP__AEqual__NES("/path/to/binary", "/path/./../.././path/to/binary"); + normalizeP__ANull__NES("path/./../../path/to/binary"); + normalizeP__ANull__NES("path/./../.././path/to/binary"); + + normalizeP__AEqual__NES("/usr/bin/sh", "/../../../../../../../../../../../../usr/./bin/../bin/sh"); + normalizeP__ANull__NES("../../../../../../../../../../../../usr/./bin/../bin/sh"); + + normalizeP__AEqual__NES(TERMUX__PREFIX__BIN_DIR "/sh", TERMUX__PREFIX__BIN_DIR "/sh"); + normalizeP__AEqual__NES(TERMUX__PREFIX__BIN_DIR "/sh", TERMUX__PREFIX__BIN_DIR "/../bin/sh"); +} + + + + + +void testGetTermuxAppDataDirFromEnv__Basic(); + +void runGetTermuxAppDataDirFromEnvTests() { + logDebug(LOG_TAG, "runGetTermuxAppDataDirFromEnvTests()"); + + testGetTermuxAppDataDirFromEnv__Basic(); + + int__AEqual(0, errno); +} + +void testGetTermuxAppDataDirFromEnv__Basic() { + logVerbose(LOG_TAG, "testGetTermuxAppDataDirFromEnv__Basic()"); + + size_t env_var_size = strlen(ENV__TERMUX_APP__DATA_DIR) + 1; + + char buffer[TERMUX_APP__DATA_DIR___MAX_LEN]; + size_t len = sizeof(buffer); + + int result; + + putenv(ENV__TERMUX_APP__DATA_DIR "=" TERMUX_APP__DATA_DIR); + result = get_termux_app_data_dir_from_env(LOG_TAG, buffer, len); + state__ATrue(result == 0); + string__AEqual(TERMUX_APP__DATA_DIR, buffer); + + putenv(ENV__TERMUX_APP__DATA_DIR "=/"); + result = get_termux_app_data_dir_from_env(LOG_TAG, buffer, len); + state__ATrue(result == 0); + string__AEqual("/", buffer); + + putenv(ENV__TERMUX_APP__DATA_DIR "=/a"); + result = get_termux_app_data_dir_from_env(LOG_TAG, buffer, len); + state__ATrue(result == 0); + string__AEqual("/a", buffer); + + putenv(ENV__TERMUX_APP__DATA_DIR "=a"); + result = get_termux_app_data_dir_from_env(LOG_TAG, buffer, len); + state__ATrue(result == 1); + + putenv(ENV__TERMUX_APP__DATA_DIR "=a/"); + result = get_termux_app_data_dir_from_env(LOG_TAG, buffer, len); + state__ATrue(result == 1); + + putenv(ENV__TERMUX_APP__DATA_DIR "=a" TERMUX_APP__DATA_DIR); + result = get_termux_app_data_dir_from_env(LOG_TAG, buffer, len); + state__ATrue(result == 1); + + unsetenv(ENV__TERMUX_APP__DATA_DIR); + result = get_termux_app_data_dir_from_env(LOG_TAG, buffer, len); + state__ATrue(result == 1); + + state__ATrue(PATH_MAX >= TERMUX_APP__DATA_DIR___MAX_LEN); + + char rootfs_dir1[env_var_size + PATH_MAX]; + snprintf(rootfs_dir1, sizeof(rootfs_dir1), "%s=/%0*d", ENV__TERMUX_APP__DATA_DIR, TERMUX_APP__DATA_DIR___MAX_LEN - 1, 0); + + putenv(rootfs_dir1); + result = get_termux_app_data_dir_from_env(LOG_TAG, buffer, len); + state__ATrue(result == 1); + + char rootfs_dir2[env_var_size + PATH_MAX]; + size_t rootfs_dir2_length = snprintf(rootfs_dir2, sizeof(rootfs_dir2), "%s=/%0*d", ENV__TERMUX_APP__DATA_DIR, TERMUX_APP__DATA_DIR___MAX_LEN - 2, 0); + + putenv(rootfs_dir2); + result = get_termux_app_data_dir_from_env(LOG_TAG, buffer, len); + state__ATrue(result == 0); + state__ATrue(rootfs_dir2_length - env_var_size == strlen(buffer)); + + char buffer1[TERMUX_APP__DATA_DIR___MAX_LEN - 1]; + result = get_termux_app_data_dir_from_env(LOG_TAG, buffer1, sizeof(buffer1)); + state__ATrue(result == -1); + errno = 0; + + char buffer2[TERMUX_APP__DATA_DIR___MAX_LEN - 2]; + result = get_termux_app_data_dir_from_env(LOG_TAG, buffer2, sizeof(buffer2)); + state__ATrue(result == -1); + errno = 0; +} + + + + + +void testGetTermuxPrefixDirFromEnv__Basic(); + +void runGetTermuxPrefixDirFromEnvTests() { + logDebug(LOG_TAG, "runGetTermuxPrefixDirFromEnvTests()"); + + testGetTermuxPrefixDirFromEnv__Basic(); + + int__AEqual(0, errno); +} + +void testGetTermuxPrefixDirFromEnv__Basic() { + logVerbose(LOG_TAG, "testGetTermuxPrefixDirFromEnv__Basic()"); + + size_t env_var_size = strlen(ENV__TERMUX__PREFIX) + 1; + + char buffer[TERMUX__PREFIX_DIR___MAX_LEN]; + size_t len = sizeof(buffer); + + int result; + + putenv(ENV__TERMUX__PREFIX "=" TERMUX__PREFIX); + result = get_termux_prefix_dir_from_env(LOG_TAG, buffer, len); + state__ATrue(result == 0); + string__AEqual(TERMUX__PREFIX, buffer); + + putenv(ENV__TERMUX__PREFIX "=/"); + result = get_termux_prefix_dir_from_env(LOG_TAG, buffer, len); + state__ATrue(result == 0); + string__AEqual("/", buffer); + + putenv(ENV__TERMUX__PREFIX "=/a"); + result = get_termux_prefix_dir_from_env(LOG_TAG, buffer, len); + state__ATrue(result == 0); + string__AEqual("/a", buffer); + + putenv(ENV__TERMUX__PREFIX "=a"); + result = get_termux_prefix_dir_from_env(LOG_TAG, buffer, len); + state__ATrue(result == 1); + + putenv(ENV__TERMUX__PREFIX "=a/"); + result = get_termux_prefix_dir_from_env(LOG_TAG, buffer, len); + state__ATrue(result == 1); + + putenv(ENV__TERMUX__PREFIX "=a" TERMUX__PREFIX); + result = get_termux_prefix_dir_from_env(LOG_TAG, buffer, len); + state__ATrue(result == 1); + + unsetenv(ENV__TERMUX__PREFIX); + result = get_termux_prefix_dir_from_env(LOG_TAG, buffer, len); + state__ATrue(result == 1); + + state__ATrue(PATH_MAX >= TERMUX__PREFIX_DIR___MAX_LEN); + + char rootfs_dir1[env_var_size + PATH_MAX]; + snprintf(rootfs_dir1, sizeof(rootfs_dir1), "%s=/%0*d", ENV__TERMUX__PREFIX, TERMUX__PREFIX_DIR___MAX_LEN - 1, 0); + + putenv(rootfs_dir1); + result = get_termux_prefix_dir_from_env(LOG_TAG, buffer, len); + state__ATrue(result == 1); + + char rootfs_dir2[env_var_size + PATH_MAX]; + size_t rootfs_dir2_length = snprintf(rootfs_dir2, sizeof(rootfs_dir2), "%s=/%0*d", ENV__TERMUX__PREFIX, TERMUX__PREFIX_DIR___MAX_LEN - 2, 0); + + putenv(rootfs_dir2); + result = get_termux_prefix_dir_from_env(LOG_TAG, buffer, len); + state__ATrue(result == 0); + state__ATrue(rootfs_dir2_length - env_var_size == strlen(buffer)); + + char buffer1[TERMUX__PREFIX_DIR___MAX_LEN - 1]; + result = get_termux_prefix_dir_from_env(LOG_TAG, buffer1, sizeof(buffer1)); + state__ATrue(result == -1); + errno = 0; + + char buffer2[TERMUX__PREFIX_DIR___MAX_LEN - 2]; + result = get_termux_prefix_dir_from_env(LOG_TAG, buffer2, sizeof(buffer2)); + state__ATrue(result == -1); + errno = 0; +} + + + + + +void testGetTermuxRootfsDirFromEnv__Basic(); + +void runGetTermuxRootfsDirFromEnvTests() { + logDebug(LOG_TAG, "runGetTermuxRootfsDirFromEnvTests()"); + + testGetTermuxRootfsDirFromEnv__Basic(); + + int__AEqual(0, errno); +} + +void testGetTermuxRootfsDirFromEnv__Basic() { + logVerbose(LOG_TAG, "testGetTermuxRootfsDirFromEnv__Basic()"); + + size_t env_var_size = strlen(ENV__TERMUX__ROOTFS) + 1; + + char buffer[TERMUX__ROOTFS_DIR___MAX_LEN]; + size_t len = sizeof(buffer); + + int result; + + putenv(ENV__TERMUX__ROOTFS "=" TERMUX__ROOTFS); + result = get_termux_rootfs_dir_from_env(LOG_TAG, buffer, len); + state__ATrue(result == 0); + string__AEqual(TERMUX__ROOTFS, buffer); + + putenv(ENV__TERMUX__ROOTFS "=/"); + result = get_termux_rootfs_dir_from_env(LOG_TAG, buffer, len); + state__ATrue(result == 0); + string__AEqual("/", buffer); + + putenv(ENV__TERMUX__ROOTFS "=/a"); + result = get_termux_rootfs_dir_from_env(LOG_TAG, buffer, len); + state__ATrue(result == 0); + string__AEqual("/a", buffer); + + putenv(ENV__TERMUX__ROOTFS "=a"); + result = get_termux_rootfs_dir_from_env(LOG_TAG, buffer, len); + state__ATrue(result == 1); + + putenv(ENV__TERMUX__ROOTFS "=a/"); + result = get_termux_rootfs_dir_from_env(LOG_TAG, buffer, len); + state__ATrue(result == 1); + + putenv(ENV__TERMUX__ROOTFS "=a" TERMUX__ROOTFS); + result = get_termux_rootfs_dir_from_env(LOG_TAG, buffer, len); + state__ATrue(result == 1); + + unsetenv(ENV__TERMUX__ROOTFS); + result = get_termux_rootfs_dir_from_env(LOG_TAG, buffer, len); + state__ATrue(result == 1); + + state__ATrue(PATH_MAX >= TERMUX__ROOTFS_DIR___MAX_LEN); + + char rootfs_dir1[env_var_size + PATH_MAX]; + snprintf(rootfs_dir1, sizeof(rootfs_dir1), "%s=/%0*d", ENV__TERMUX__ROOTFS, TERMUX__ROOTFS_DIR___MAX_LEN - 1, 0); + + putenv(rootfs_dir1); + result = get_termux_rootfs_dir_from_env(LOG_TAG, buffer, len); + state__ATrue(result == 1); + + char rootfs_dir2[env_var_size + PATH_MAX]; + size_t rootfs_dir2_length = snprintf(rootfs_dir2, sizeof(rootfs_dir2), "%s=/%0*d", ENV__TERMUX__ROOTFS, TERMUX__ROOTFS_DIR___MAX_LEN - 2, 0); + + putenv(rootfs_dir2); + result = get_termux_rootfs_dir_from_env(LOG_TAG, buffer, len); + state__ATrue(result == 0); + state__ATrue(rootfs_dir2_length - env_var_size == strlen(buffer)); + + char buffer1[TERMUX__ROOTFS_DIR___MAX_LEN - 1]; + result = get_termux_rootfs_dir_from_env(LOG_TAG, buffer1, sizeof(buffer1)); + state__ATrue(result == -1); + errno = 0; + + char buffer2[TERMUX__ROOTFS_DIR___MAX_LEN - 2]; + result = get_termux_rootfs_dir_from_env(LOG_TAG, buffer2, sizeof(buffer2)); + state__ATrue(result == -1); + errno = 0; +} + + + + + +void testConvertTermuxAppDataDirToLegacyPath__Basic(); + +void runConvertTermuxAppDataDirToLegacyPathTests() { + logDebug(LOG_TAG, "runConvertTermuxAppDataDirToLegacyPathTests()"); + + testConvertTermuxAppDataDirToLegacyPath__Basic(); + + int__AEqual(0, errno); +} + + + +#define ctaddtlp__AEqual(expected, termux_app_data_dir, buffer, buffer_len) \ + if (1) { \ + assert_string_equals_with_error(expected, convert_termux_app_data_dir_to_legacy_path(LOG_TAG, termux_app_data_dir, buffer, buffer_len), \ + LOG_TAG, "%d: convert_termux_app_data_dir_to_legacy_path('%s')", __LINE__, termux_app_data_dir); \ + errno = 0; \ + } else ((void)0) + + + +void testConvertTermuxAppDataDirToLegacyPath__Basic() { + logVerbose(LOG_TAG, "testConvertTermuxAppDataDirToLegacyPath__Basic()"); + + char buf[TERMUX_APP__DATA_DIR___MAX_LEN]; + size_t buffer_len = sizeof(buf); + + ctaddtlp__AEqual(NULL, NULL, buf, buffer_len); + ctaddtlp__AEqual(NULL, "a", buf, buffer_len); + ctaddtlp__AEqual(NULL, "a/", buf, buffer_len); + ctaddtlp__AEqual(NULL, "/", buf, buffer_len); + ctaddtlp__AEqual(NULL, "/a", buf, buffer_len); + ctaddtlp__AEqual(NULL, "/a/", buf, buffer_len); + ctaddtlp__AEqual(NULL, TERMUX_EXEC__TESTS__TERMUX_APP_DATA_DIR_1 "/", buf, buffer_len); + ctaddtlp__AEqual(TERMUX_EXEC__TESTS__TERMUX_APP_DATA_DIR_2, TERMUX_EXEC__TESTS__TERMUX_APP_DATA_DIR_1, buf, buffer_len); + + // `11` is length of `/data/data/`. + char termux_app_data_dir1[PATH_MAX]; + snprintf(termux_app_data_dir1, sizeof(termux_app_data_dir1), "/data/user/0/%0*d", TERMUX_APP__DATA_DIR___MAX_LEN - 11, 0); + ctaddtlp__AEqual(NULL, termux_app_data_dir1, buf, buffer_len); + + char termux_app_data_dir2_1[PATH_MAX]; + snprintf(termux_app_data_dir2_1, sizeof(termux_app_data_dir2_1), "/data/user/0/%0*d", TERMUX_APP__DATA_DIR___MAX_LEN - 11 - 1, 0); + char termux_app_data_dir2_2[PATH_MAX]; + snprintf(termux_app_data_dir2_2, sizeof(termux_app_data_dir2_2), "/data/data/%0*d", TERMUX_APP__DATA_DIR___MAX_LEN - 11 - 1, 0); + ctaddtlp__AEqual(termux_app_data_dir2_2, termux_app_data_dir2_1, buf, buffer_len); + +} + + + + + +void testTermuxPrefixPath__Basic(); + +void runTermuxPrefixPathTests() { + logDebug(LOG_TAG, "runTermuxPrefixPathTests()"); + + testTermuxPrefixPath__Basic(); + + int__AEqual(0, errno); +} + + + +#define tpp__AEqual(expected, termux_prefix_dir, executable_path, buffer, buffer_len) \ + if (1) { \ + assert_string_equals_with_error(expected, termux_prefix_path(LOG_TAG, termux_prefix_dir, executable_path, buffer, buffer_len), \ + LOG_TAG, "%d: termux_prefix_path('%s', '%s')", __LINE__, termux_prefix_dir , executable_path); \ + errno = 0; \ + } else ((void)0) + + + +void testTermuxPrefixPath__Basic() { + logVerbose(LOG_TAG, "testTermuxPrefixPath__Basic()"); + + char buf[PATH_MAX]; + + tpp__AEqual(TERMUX__PREFIX__BIN_DIR, TERMUX__PREFIX, "/bin", buf, PATH_MAX); + tpp__AEqual(TERMUX__PREFIX__BIN_DIR "/", TERMUX__PREFIX, "/bin/", buf, PATH_MAX); + tpp__AEqual(TERMUX__PREFIX__BIN_DIR, TERMUX__PREFIX, "/usr/bin", buf, PATH_MAX); + tpp__AEqual(TERMUX__PREFIX__BIN_DIR "/", TERMUX__PREFIX, "/usr/bin/", buf, PATH_MAX); + tpp__AEqual(TERMUX__PREFIX__BIN_DIR "/sh", TERMUX__PREFIX, "/bin/sh", buf, PATH_MAX); + tpp__AEqual(TERMUX__PREFIX__BIN_DIR "/sh", TERMUX__PREFIX, "/usr/bin/sh", buf, PATH_MAX); + tpp__AEqual("/system/bin/sh", TERMUX__PREFIX, "/system/bin/sh", buf, PATH_MAX); + tpp__AEqual("/system/bin/tool", TERMUX__PREFIX, "/system/bin/tool", buf, PATH_MAX); + tpp__AEqual(TERMUX__PREFIX__BIN_DIR "/sh", TERMUX__PREFIX, TERMUX__PREFIX__BIN_DIR "/sh", buf, PATH_MAX); + + tpp__AEqual("./ab/sh", TERMUX__PREFIX, "./ab/sh", buf, PATH_MAX); + +} + + + + + +void testIsPathUnderTermuxAppDataDir__Basic(); + +void runIsPathUnderTermuxAppDataDirTests() { + logDebug(LOG_TAG, "runIsPathUnderTermuxAppDataDirTests()"); + + testIsPathUnderTermuxAppDataDir__Basic(); + + int__AEqual(0, errno); +} + + + +#define iputadd__ABool(expected, path, termux_app_data_dir, termux_legacy_app_data_dir) \ + if (1) { \ + assert_int_with_error(expected ? 0 : 1, is_path_under_termux_app_data_dir(LOG_TAG, path, termux_app_data_dir, termux_legacy_app_data_dir), \ + LOG_TAG, "%d: is_path_under_termux_app_data_dir('%s', '%s', '%s')", __LINE__, path, termux_app_data_dir, termux_legacy_app_data_dir); \ + errno = 0; \ + } else ((void)0) + +#define iputadd__AInt(expected, path, termux_app_data_dir, termux_legacy_app_data_dir) \ + if (1) { \ + assert_int_with_error(expected, is_path_under_termux_app_data_dir(LOG_TAG, path, termux_app_data_dir, termux_legacy_app_data_dir), \ + LOG_TAG, "%d: is_path_under_termux_app_data_dir('%s', '%s', '%s')", __LINE__, path, termux_app_data_dir, termux_legacy_app_data_dir); \ + errno = 0; \ + } else ((void)0) + + + +void testIsPathUnderTermuxAppDataDir__Basic() { + logVerbose(LOG_TAG, "testIsPathUnderTermuxAppDataDir__Basic()"); + + bool is_termux_bin_under_termux_app_data_dir = string_starts_with(TERMUX__PREFIX__BIN_DIR, TERMUX_APP__DATA_DIR "/"); + bool is_termux_app_data_dir_a_legacy_path = string_starts_with(TERMUX_APP__DATA_DIR, "/data/data/"); + + state__ATrue(strlen(TERMUX_APP__DATA_DIR) < PATH_MAX); + state__ATrue(strlen(TERMUX__PREFIX__BIN_DIR) < PATH_MAX); + + + iputadd__ABool(false, NULL, TERMUX_EXEC__TESTS__TERMUX_APP_DATA_DIR_1, TERMUX_EXEC__TESTS__TERMUX_APP_DATA_DIR_2); + iputadd__ABool(false, "", TERMUX_EXEC__TESTS__TERMUX_APP_DATA_DIR_1, TERMUX_EXEC__TESTS__TERMUX_APP_DATA_DIR_2); + iputadd__ABool(false, "/", TERMUX_EXEC__TESTS__TERMUX_APP_DATA_DIR_1, TERMUX_EXEC__TESTS__TERMUX_APP_DATA_DIR_2); + + iputadd__ABool(false, "/a", TERMUX_EXEC__TESTS__TERMUX_APP_DATA_DIR_1, TERMUX_EXEC__TESTS__TERMUX_APP_DATA_DIR_2); + iputadd__ABool(false, "/bin/sh", TERMUX_EXEC__TESTS__TERMUX_APP_DATA_DIR_1, TERMUX_EXEC__TESTS__TERMUX_APP_DATA_DIR_2); + iputadd__ABool(false, "/usr/bin/sh", TERMUX_EXEC__TESTS__TERMUX_APP_DATA_DIR_1, TERMUX_EXEC__TESTS__TERMUX_APP_DATA_DIR_2); + iputadd__ABool(false, "/system/bin/sh", TERMUX_EXEC__TESTS__TERMUX_APP_DATA_DIR_1, TERMUX_EXEC__TESTS__TERMUX_APP_DATA_DIR_2); + + + iputadd__ABool(false, TERMUX_EXEC__TESTS__TERMUX_APP_DATA_DIR_2, TERMUX_EXEC__TESTS__TERMUX_APP_DATA_DIR_1, TERMUX_EXEC__TESTS__TERMUX_APP_DATA_DIR_2); + iputadd__ABool(true, TERMUX_EXEC__TESTS__TERMUX_APP_DATA_DIR_2 "/bar", TERMUX_EXEC__TESTS__TERMUX_APP_DATA_DIR_1, TERMUX_EXEC__TESTS__TERMUX_APP_DATA_DIR_2); + + iputadd__ABool(false, TERMUX_EXEC__TESTS__TERMUX_APP_DATA_DIR_1, TERMUX_EXEC__TESTS__TERMUX_APP_DATA_DIR_1, TERMUX_EXEC__TESTS__TERMUX_APP_DATA_DIR_2); + iputadd__ABool(true, TERMUX_EXEC__TESTS__TERMUX_APP_DATA_DIR_1 "/bar", TERMUX_EXEC__TESTS__TERMUX_APP_DATA_DIR_1, TERMUX_EXEC__TESTS__TERMUX_APP_DATA_DIR_2); + + + if (is_termux_app_data_dir_a_legacy_path) { + iputadd__ABool(false, TERMUX_APP__DATA_DIR, NULL, TERMUX_APP__DATA_DIR); + iputadd__ABool(true, TERMUX_APP__DATA_DIR "/foo", NULL, TERMUX_APP__DATA_DIR); + iputadd__ABool(is_termux_bin_under_termux_app_data_dir, TERMUX__PREFIX__BIN_DIR "/sh", NULL, TERMUX_APP__DATA_DIR); + iputadd__ABool(false, "/" TERMUX__PREFIX__BIN_DIR "/sh", NULL, TERMUX_APP__DATA_DIR); + iputadd__ABool(false, "/./" TERMUX__PREFIX__BIN_DIR "/sh", NULL, TERMUX_APP__DATA_DIR); + } else { + iputadd__ABool(false, TERMUX_APP__DATA_DIR, TERMUX_APP__DATA_DIR, NULL); + iputadd__ABool(true, TERMUX_APP__DATA_DIR "/foo", TERMUX_APP__DATA_DIR, NULL); + iputadd__ABool(is_termux_bin_under_termux_app_data_dir, TERMUX__PREFIX__BIN_DIR "/sh", TERMUX_APP__DATA_DIR, NULL); + iputadd__ABool(false, "/" TERMUX__PREFIX__BIN_DIR "/sh", TERMUX_APP__DATA_DIR, NULL); + iputadd__ABool(false, "/./" TERMUX__PREFIX__BIN_DIR "/sh", TERMUX_APP__DATA_DIR, NULL); + } + + + if (access(TERMUX__PREFIX__BIN_DIR "/sh", X_OK) == 0) { + int fd = open(TERMUX__PREFIX__BIN_DIR "/sh", 0); + state__ATrue(fd != -1); + + char real_path[PATH_MAX]; + ssize_t length = readlink(TERMUX__PREFIX__BIN_DIR "/sh", real_path, sizeof(real_path) - 1); + state__ATrue(length >= 0); + real_path[length] = '\0'; + + // If not running on device, termux-packages `build-package.sh` + // `termux_step_override_config_scripts` step creates a + // symlink from `$TERMUX__PREFIX/bin/sh` to `/bin/sh`. + bool is_path_termux_builder_symlink = is_termux_bin_under_termux_app_data_dir && + strcmp(real_path, "/bin/sh") == 0; + + char proc_fd_path[40]; + snprintf(proc_fd_path, sizeof(proc_fd_path), "/proc/self/fd/%d", fd); + if (is_termux_app_data_dir_a_legacy_path) { + iputadd__ABool(is_termux_bin_under_termux_app_data_dir && !is_path_termux_builder_symlink, + proc_fd_path, NULL, TERMUX_APP__DATA_DIR); + } else { + iputadd__ABool(is_termux_bin_under_termux_app_data_dir && !is_path_termux_builder_symlink, + proc_fd_path, TERMUX_APP__DATA_DIR, NULL); + } + close(fd); + } + errno = 0; + + + if (access("/system/bin/sh", X_OK) == 0) { + int fd = open("/system/bin/sh", 0); + state__ATrue(fd != -1); + char proc_fd_path[40]; + snprintf(proc_fd_path, sizeof(proc_fd_path), "/proc/self/fd/%d", fd); + if (is_termux_app_data_dir_a_legacy_path) { + iputadd__ABool(false, proc_fd_path, NULL, TERMUX_APP__DATA_DIR); + } else { + iputadd__ABool(false, proc_fd_path, TERMUX_APP__DATA_DIR, NULL); + } + close(fd); + } + errno = 0; + + + if (access(TERMUX__PREFIX__BIN_DIR, X_OK) == 0) { + // S_ISREG should fail in `get_fd_realpath()`. + int fd = open(TERMUX__PREFIX__BIN_DIR, 0); + state__ATrue(fd != -1); + char proc_fd_path[40]; + snprintf(proc_fd_path, sizeof(proc_fd_path), "/proc/self/fd/%d", fd); + if (is_termux_app_data_dir_a_legacy_path) { + iputadd__AInt(-1, proc_fd_path, NULL, TERMUX_APP__DATA_DIR); + } else { + iputadd__AInt(-1, proc_fd_path, TERMUX_APP__DATA_DIR, NULL); + } + close(fd); + } + errno = 0; + +} + + + + + +void testIsPathUnderTermuxRootfsDir__Basic(); + +void runIsPathUnderTermuxRootfsDirTests() { + logDebug(LOG_TAG, "runIsPathUnderTermuxRootfsDirTests()"); + + testIsPathUnderTermuxRootfsDir__Basic(); + + int__AEqual(0, errno); +} + + + +#define iputrd__ABool(expected, path, termux_rootfs_dir) \ + if (1) { \ + assert_int_with_error(expected ? 0 : 1, is_path_under_termux_rootfs_dir(LOG_TAG, path, termux_rootfs_dir), \ + LOG_TAG, "%d: is_path_under_termux_rootfs_dir('%s', '%s')", __LINE__, path, termux_rootfs_dir); \ + errno = 0; \ + } else ((void)0) + +#define iputrd__AInt(expected, path, termux_rootfs_dir) \ + if (1) { \ + assert_int_with_error(expected, is_path_under_termux_rootfs_dir(LOG_TAG, path, termux_rootfs_dir), \ + LOG_TAG, "%d: is_path_under_termux_rootfs_dir('%s', '%s')", __LINE__, path, termux_rootfs_dir); \ + errno = 0; \ + } else ((void)0) + + + +void testIsPathUnderTermuxRootfsDir__Basic() { + logVerbose(LOG_TAG, "testIsPathUnderTermuxRootfsDir__Basic()"); + + state__ATrue(strlen(TERMUX_EXEC__TESTS__TERMUX_ROOTFS_DIR_1) < PATH_MAX); + state__ATrue(strlen(TERMUX_EXEC__TESTS__TERMUX_BIN_DIR_1) < PATH_MAX); + + iputrd__ABool(false, NULL, TERMUX_EXEC__TESTS__TERMUX_ROOTFS_DIR_1); + iputrd__ABool(false, "", TERMUX_EXEC__TESTS__TERMUX_ROOTFS_DIR_1); + iputrd__ABool(false, "/", TERMUX_EXEC__TESTS__TERMUX_ROOTFS_DIR_1); + + iputrd__ABool(false, "/a", TERMUX_EXEC__TESTS__TERMUX_ROOTFS_DIR_1); + iputrd__ABool(false, "/bin/sh", TERMUX_EXEC__TESTS__TERMUX_ROOTFS_DIR_1); + iputrd__ABool(false, "/usr/bin/sh", TERMUX_EXEC__TESTS__TERMUX_ROOTFS_DIR_1); + iputrd__ABool(false, "/system/bin/sh", TERMUX_EXEC__TESTS__TERMUX_ROOTFS_DIR_1); + + iputrd__ABool(false, TERMUX_EXEC__TESTS__TERMUX_ROOTFS_DIR_1, TERMUX_EXEC__TESTS__TERMUX_ROOTFS_DIR_1); + iputrd__ABool(true, TERMUX_EXEC__TESTS__TERMUX_BIN_DIR_1 "/sh", TERMUX_EXEC__TESTS__TERMUX_ROOTFS_DIR_1); + iputrd__ABool(false, "/" TERMUX_EXEC__TESTS__TERMUX_BIN_DIR_1 "/sh", TERMUX_EXEC__TESTS__TERMUX_ROOTFS_DIR_1); + iputrd__ABool(false, "/./" TERMUX_EXEC__TESTS__TERMUX_BIN_DIR_1 "/sh", TERMUX_EXEC__TESTS__TERMUX_ROOTFS_DIR_1); + iputrd__ABool(false, "/../" TERMUX_EXEC__TESTS__TERMUX_BIN_DIR_1 "/sh", TERMUX_EXEC__TESTS__TERMUX_ROOTFS_DIR_1); + + + + state__ATrue(strlen(TERMUX_EXEC__TESTS__TERMUX_ROOTFS_DIR_2) < PATH_MAX); + state__ATrue(strlen(TERMUX_EXEC__TESTS__TERMUX_BIN_DIR_2) < PATH_MAX); + + iputrd__ABool(false, NULL, TERMUX_EXEC__TESTS__TERMUX_ROOTFS_DIR_2); + iputrd__ABool(false, "", TERMUX_EXEC__TESTS__TERMUX_ROOTFS_DIR_2); + iputrd__ABool(false, "/", TERMUX_EXEC__TESTS__TERMUX_ROOTFS_DIR_2); + + iputrd__ABool(false, TERMUX_EXEC__TESTS__TERMUX_ROOTFS_DIR_2, TERMUX_EXEC__TESTS__TERMUX_ROOTFS_DIR_2); + iputrd__ABool(true, "/a", TERMUX_EXEC__TESTS__TERMUX_ROOTFS_DIR_2); + iputrd__ABool(true, "/bin/sh", TERMUX_EXEC__TESTS__TERMUX_ROOTFS_DIR_2); + iputrd__ABool(true, "/usr/bin/sh", TERMUX_EXEC__TESTS__TERMUX_ROOTFS_DIR_2); + iputrd__ABool(true, "/system/bin/sh", TERMUX_EXEC__TESTS__TERMUX_ROOTFS_DIR_2); + + iputrd__ABool(true, TERMUX_EXEC__TESTS__TERMUX_BIN_DIR_2 "/sh", TERMUX_EXEC__TESTS__TERMUX_ROOTFS_DIR_2); + iputrd__ABool(true, "/" TERMUX_EXEC__TESTS__TERMUX_BIN_DIR_2 "/sh", TERMUX_EXEC__TESTS__TERMUX_ROOTFS_DIR_2); + iputrd__ABool(true, "/./" TERMUX_EXEC__TESTS__TERMUX_BIN_DIR_2 "/sh", TERMUX_EXEC__TESTS__TERMUX_ROOTFS_DIR_2); + iputrd__ABool(true, "/../" TERMUX_EXEC__TESTS__TERMUX_BIN_DIR_2 "/sh", TERMUX_EXEC__TESTS__TERMUX_ROOTFS_DIR_2); + + + + bool termux_rootfs_is_rootfs = strcmp(TERMUX__ROOTFS, "/") == 0; + bool termux_rootfs_is_system = strcmp(TERMUX__ROOTFS, "/system") == 0; + bool is_termux_bin_under_termux_rootfs = string_starts_with(TERMUX__PREFIX__BIN_DIR, + termux_rootfs_is_rootfs ? "/" : TERMUX__ROOTFS "/"); + + state__ATrue(strlen(TERMUX__ROOTFS) < PATH_MAX); + state__ATrue(strlen(TERMUX__PREFIX__BIN_DIR) < PATH_MAX); + + + if (access(TERMUX__PREFIX__BIN_DIR "/sh", X_OK) == 0) { + int fd = open(TERMUX__PREFIX__BIN_DIR "/sh", 0); + state__ATrue(fd != -1); + + char real_path[PATH_MAX]; + ssize_t length = readlink(TERMUX__PREFIX__BIN_DIR "/sh", real_path, sizeof(real_path) - 1); + state__ATrue(length >= 0); + real_path[length] = '\0'; + + // If not running on device, termux-packages `build-package.sh` + // `termux_step_override_config_scripts` step creates a + // symlink from `$TERMUX__PREFIX/bin/sh` to `/bin/sh`. + bool is_path_termux_builder_symlink = is_termux_bin_under_termux_rootfs && + strcmp(real_path, "/bin/sh") == 0; + + char proc_fd_path[40]; + snprintf(proc_fd_path, sizeof(proc_fd_path), "/proc/self/fd/%d", fd); + iputrd__ABool(is_termux_bin_under_termux_rootfs && !is_path_termux_builder_symlink, + proc_fd_path, TERMUX__ROOTFS); + close(fd); + } + errno = 0; + + + if (access("/system/bin/sh", X_OK) == 0) { + int fd = open("/system/bin/sh", 0); + state__ATrue(fd != -1); + char proc_fd_path[40]; + snprintf(proc_fd_path, sizeof(proc_fd_path), "/proc/self/fd/%d", fd); + iputrd__ABool(termux_rootfs_is_rootfs || termux_rootfs_is_system, proc_fd_path, TERMUX__ROOTFS); + close(fd); + } + errno = 0; + + + if (access(TERMUX__PREFIX__BIN_DIR, X_OK) == 0) { + // S_ISREG should fail in `get_fd_realpath()`. + int fd = open(TERMUX__PREFIX__BIN_DIR, 0); + state__ATrue(fd != -1); + char proc_fd_path[40]; + snprintf(proc_fd_path, sizeof(proc_fd_path), "/proc/self/fd/%d", fd); + iputrd__AInt(-1, proc_fd_path, TERMUX__ROOTFS); + close(fd); + } + errno = 0; +} + + + + + +void testInspectFileHeader__Basic(); + +void runInspectFileHeaderTests() { + logDebug(LOG_TAG, "runInspectFileHeaderTests()"); + + testInspectFileHeader__Basic(); + + int__AEqual(0, errno); +} + +void testInspectFileHeader__Basic() { + logVerbose(LOG_TAG, "testInspectFileHeader__Basic()"); + + char header[FILE_HEADER__BUFFER_LEN]; + size_t hsize = sizeof(header); + + struct file_header_info info = {.interpreter_arg = NULL}; + + snprintf(header, hsize, "#!/bin/sh\n"); + inspect_file_header(TERMUX__PREFIX, header, sizeof(header) , &info); + state__ATrue(!info.is_elf); + state__ATrue(!info.is_non_native_elf); + string__AEqual(TERMUX__PREFIX__BIN_DIR "/sh", info.interpreter_path); + state__ATrue(info.interpreter_arg == NULL); + + snprintf(header, hsize, "#!/bin/sh -x\n"); + inspect_file_header(TERMUX__PREFIX, header, sizeof(header) , &info); + state__ATrue(!info.is_elf); + state__ATrue(!info.is_non_native_elf); + string__AEqual(TERMUX__PREFIX__BIN_DIR "/sh", info.interpreter_path); + string__AEqual("-x", info.interpreter_arg); + + snprintf(header, hsize, "#! /bin/sh -x\n"); + inspect_file_header(TERMUX__PREFIX, header, sizeof(header) , &info); + state__ATrue(!info.is_elf); + state__ATrue(!info.is_non_native_elf); + string__AEqual(TERMUX__PREFIX__BIN_DIR "/sh", info.interpreter_path); + string__AEqual("-x", info.interpreter_arg); + + snprintf(header, hsize, "#!/bin/sh -x \n"); + inspect_file_header(TERMUX__PREFIX, header, sizeof(header) , &info); + state__ATrue(!info.is_elf); + state__ATrue(!info.is_non_native_elf); + string__AEqual(TERMUX__PREFIX__BIN_DIR "/sh", info.interpreter_path); + string__AEqual("-x", info.interpreter_arg); + + snprintf(header, hsize, "#!/bin/sh -x \n"); + inspect_file_header(TERMUX__PREFIX, header, sizeof(header) , &info); + state__ATrue(!info.is_elf); + state__ATrue(!info.is_non_native_elf); + string__AEqual(TERMUX__PREFIX__BIN_DIR "/sh", info.interpreter_path); + string__AEqual("-x", info.interpreter_arg); + + info.interpreter_path = NULL; + info.interpreter_arg = NULL; + // An ELF header for a 32-bit file. + // See https://en.wikipedia.org/wiki/Executable_and_Linkable_Format#File_header + snprintf(header, hsize, "\177ELF"); + // Native instruction set. + header[0x12] = EM_NATIVE; + header[0x13] = 0; + inspect_file_header(TERMUX__PREFIX, header, sizeof(header) , &info); + state__ATrue(info.is_elf); + state__ATrue(!info.is_non_native_elf); + state__ATrue(info.interpreter_path == NULL); + state__ATrue(info.interpreter_arg == NULL); + + info.interpreter_path = NULL; + info.interpreter_arg = NULL; + // An ELF header for a 64-bit file. + // See https://en.wikipedia.org/wiki/Executable_and_Linkable_Format#File_header + snprintf(header, hsize, "\177ELF"); + // 'Fujitsu MMA Multimedia Accelerator' instruction set - likely non-native. + header[0x12] = 0x36; + header[0x13] = 0; + inspect_file_header(TERMUX__PREFIX, header, sizeof(header) , &info); + state__ATrue(info.is_elf); + state__ATrue(info.is_non_native_elf); + state__ATrue(info.interpreter_path == NULL); + state__ATrue(info.interpreter_arg == NULL); +} + + + + + +void testModifyExecEnv__unsetLDVars(); +void testModifyExecEnv__setProcSelfExe(); + +void runModifyExecEnvTests() { + logDebug(LOG_TAG, "runModifyExecEnvTests()"); + + testModifyExecEnv__unsetLDVars(); + testModifyExecEnv__setProcSelfExe(); + + int__AEqual(0, errno); +} + +void testModifyExecEnv__unsetLDVars() { + logVerbose(LOG_TAG, "testModifyExecEnv__unsetLDVars()"); + + { + char *test_env[] = {"MY_ENV=1", NULL}; + char **allocated_envp; + modify_exec_env(test_env, &allocated_envp, NULL, true); + state__ATrue(allocated_envp != NULL); + string__AEqual(allocated_envp[0], "MY_ENV=1"); + state__ATrue(allocated_envp[1] == NULL); + free(allocated_envp); + } + + { + char *test_env[] = {"MY_ENV=1", ENV_PREFIX__LD_PRELOAD "a", NULL}; + char **allocated_envp; + modify_exec_env(test_env, &allocated_envp, NULL, true); + state__ATrue(allocated_envp != NULL); + string__AEqual(allocated_envp[0], "MY_ENV=1"); + state__ATrue(allocated_envp[1] == NULL); + free(allocated_envp); + } + + { + char *test_env[] = {"MY_ENV=1", ENV_PREFIX__LD_PRELOAD "a", "A=B", ENV_PREFIX__LD_LIBRARY_PATH "B", "B=C", NULL}; + char **allocated_envp; + modify_exec_env(test_env, &allocated_envp, NULL, true); + state__ATrue(allocated_envp != NULL); + string__AEqual(allocated_envp[0], "MY_ENV=1"); + string__AEqual(allocated_envp[1], "A=B"); + string__AEqual(allocated_envp[2], "B=C"); + state__ATrue(allocated_envp[3] == NULL); + free(allocated_envp); + } +} + +void testModifyExecEnv__setProcSelfExe() { + logVerbose(LOG_TAG, "testModifyExecEnv__setProcSelfExe()"); + + { + char *termux_proc_self_exe = NULL; + state__ATrue(asprintf(&termux_proc_self_exe, "%s%s", ENV_PREFIX__TERMUX_EXEC__PROC_SELF_EXE, TERMUX__PREFIX__BIN_DIR "/bash") != -1); + + char *test_env[] = {"MY_ENV=1", NULL}; + char **allocated_envp; + modify_exec_env(test_env, &allocated_envp, &termux_proc_self_exe, false); + state__ATrue(allocated_envp != NULL); + string__AEqual(allocated_envp[0], "MY_ENV=1"); + string__AEqual(allocated_envp[1], ENV__TERMUX_EXEC__PROC_SELF_EXE "=" TERMUX__PREFIX__BIN_DIR "/bash"); + free(termux_proc_self_exe); + free(allocated_envp); + } + + { + char *termux_proc_self_exe = NULL; + state__ATrue(asprintf(&termux_proc_self_exe, "%s%s", ENV_PREFIX__TERMUX_EXEC__PROC_SELF_EXE, TERMUX__PREFIX__BIN_DIR "/bash") != -1); + + char *test_env[] = {"MY_ENV=1", ENV__TERMUX_EXEC__PROC_SELF_EXE "=" TERMUX__PREFIX__BIN_DIR "/python", NULL}; + char **allocated_envp; + modify_exec_env(test_env, &allocated_envp, &termux_proc_self_exe, false); + state__ATrue(allocated_envp != NULL); + string__AEqual(allocated_envp[0], "MY_ENV=1"); + string__AEqual(allocated_envp[1], ENV__TERMUX_EXEC__PROC_SELF_EXE "=" TERMUX__PREFIX__BIN_DIR "/bash"); + free(termux_proc_self_exe); + free(allocated_envp); + } + + { + char *test_env[] = {"MY_ENV=1", NULL}; + char **allocated_envp; + modify_exec_env(test_env, &allocated_envp, NULL, false); + state__ATrue(allocated_envp != NULL); + string__AEqual(allocated_envp[0], "MY_ENV=1"); + state__ATrue(allocated_envp[1] == NULL); + free(allocated_envp); + } + + { + char *test_env[] = {"MY_ENV=1", ENV__TERMUX_EXEC__PROC_SELF_EXE "=" TERMUX__PREFIX__BIN_DIR "/python", NULL}; + char **allocated_envp; + modify_exec_env(test_env, &allocated_envp, NULL, false); + state__ATrue(allocated_envp != NULL); + string__AEqual(allocated_envp[0], "MY_ENV=1"); + state__ATrue(allocated_envp[1] == NULL); + free(allocated_envp); + } +} + + + + + +int main() { + init(); + + logInfo(LOG_TAG, "Start 'unit' tests"); + + runStringStartsWithTests(); + runStringEndsWithTests(); + runAbsolutizePathTests(); + runNormalizePathTests(); + runGetTermuxAppDataDirFromEnvTests(); + runGetTermuxPrefixDirFromEnvTests(); + runGetTermuxRootfsDirFromEnvTests(); + runConvertTermuxAppDataDirToLegacyPathTests(); + runTermuxPrefixPathTests(); + runIsPathUnderTermuxAppDataDirTests(); + runIsPathUnderTermuxRootfsDirTests(); + runInspectFileHeaderTests(); + runModifyExecEnvTests(); + + logInfo(LOG_TAG, "End 'unit' tests"); + + return 0; +} + + + +static void init() { + errno = 0; + + initLogger(); +} + +static void initLogger() { + setDefaultLogTagAndPrefix(TERMUX__LNAME "-exec-tests"); + setCurrentLogLevel(get_termux_exec__tests_log_level()); + setLogFormatMode(LOG_FORMAT_MODE__TAG_AND_MESSAGE); +} diff --git a/tests/usr-bin-env.sh b/tests/usr-bin-env.sh deleted file mode 100755 index ec11f38..0000000 --- a/tests/usr-bin-env.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env sh - -echo hello-user-bin-env-sh diff --git a/tests/usr-bin-env.sh-expected b/tests/usr-bin-env.sh-expected deleted file mode 100644 index 8690c3a..0000000 --- a/tests/usr-bin-env.sh-expected +++ /dev/null @@ -1 +0,0 @@ -hello-user-bin-env-sh