diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..842eef73d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,20 @@ +.gradle +.idea +.kotlin +build/ +app/build/ +core/main/build/ +core/components/build/ +core/resources/build/ +terminal-view/build/ +terminal-emulator/build/ +soraX/editor/build/ +soraX/oniguruma-native/build/ +soraX/editor-lsp/build/ +soraX/language-textmate/build/ +baselineprofile/build/ +benchmark/build/ +benchmark2/build/ +core/proot/build/ +local.properties +*.apk diff --git a/.github/scripts/scan-translations.js b/.github/scripts/scan-translations.js new file mode 100644 index 000000000..b1511558a --- /dev/null +++ b/.github/scripts/scan-translations.js @@ -0,0 +1,121 @@ +const fs = require("fs"); +const path = require("path"); +const { execSync } = require("child_process"); +const { GoogleGenerativeAI } = require("@google/generative-ai"); + +const apiKey = process.env.GEMINI_API_KEY; +if (!apiKey) { + console.error("Missing GEMINI_API_KEY environment variable."); + process.exit(1); +} + +const genAI = new GoogleGenerativeAI(apiKey); + +// Helper to safely run git commands and get output string +function runGit(command) { + try { + return execSync(command, { encoding: "utf8" }).trim(); + } catch (error) { + return ""; + } +} + +async function main() { + const eventName = process.env.GITHUB_EVENT_NAME; + let baseDiffTarget = ""; + + if (eventName === "pull_request") { + const baseRef = process.env.GITHUB_BASE_REF; + runGit(`git fetch origin ${baseRef}`); + baseDiffTarget = `origin/${baseRef}...HEAD`; + } else { + let beforeSha = process.env.GITHUB_BEFORE_SHA; + const currentSha = process.env.GITHUB_CURRENT_SHA; + + if (!beforeSha || beforeSha === "0000000000000000000000000000000000000000") { + beforeSha = "HEAD~1"; + } + baseDiffTarget = `${beforeSha} ${currentSha}`; + } + + // 1. Get list of changed files ending with strings.xml + const rawFiles = runGit(`git diff --name-only ${baseDiffTarget}`); + if (!rawFiles) { + console.log("No file changes detected."); + return; + } + + const files = rawFiles.split("\n").filter(file => file.endsWith("strings.xml")); + + if (files.length === 0) { + console.log("No strings.xml changes detected."); + return; + } + + const model = genAI.getGenerativeModel({ model: "gemini-2.0-flash" }); + let workflowFailed = false; + + // 2. Loop through each modified strings.xml file and scan its diff + for (const file of files) { + const diff = runGit(`git diff ${baseDiffTarget} -- "${file}"`); + if (!diff.trim()) continue; + + const prompt = [ + "You are a translation moderation system.", + "", + "Analyze this Android strings.xml git diff.", + "", + "Check ONLY newly added or modified translations.", + "", + "Detect:", + "- profanity", + "- slurs", + "- hate speech", + "- sexual content", + "- abusive language", + "- offensive slang", + "", + "Ignore:", + "- XML syntax", + "- placeholders like %s or %d", + "- harmless casual language", + "", + "Reply ONLY with:", + "SAFE", + "", + "or", + "", + "UNSAFE: ", + "", + "Git diff:", + diff + ].join("\n"); + + try { + const result = await model.generateContent(prompt); + const text = result.response.text().trim(); + + console.log(`\n=== Scanning: ${file} ===`); + console.log(text); + + if (!text.startsWith("SAFE")) { + workflowFailed = true; + } + } catch (err) { + console.error(`Error scanning ${file}:`, err.message); + workflowFailed = true; + } + } + + if (workflowFailed) { + console.error("\nTranslation moderation failed. Unsafe content detected."); + process.exit(1); + } + + console.log("\nAll translations are safe."); +} + +main().catch((err) => { + console.error("Execution error:", err); + process.exit(1); +}); diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index c22053105..bbd72484a 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -27,6 +27,7 @@ jobs: java-version: "21" distribution: "temurin" cache: gradle + - name: Cache Gradle Build Files uses: actions/cache@v5 with: @@ -52,6 +53,9 @@ jobs: - name: Clone submodules run: git submodule update --init --recursive + - name: Accept Android SDK licenses + run: yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses || true + - name: Build with Gradle run: ./gradlew assembleRelease --no-daemon && mv app/build/outputs/apk/release/*.apk app/xed-editor-${{ env.COMMIT_HASH }}.apk env: @@ -73,12 +77,11 @@ jobs: env: COMMIT_MSG: ${{ github.event.head_commit.message }} run: | - CAPTION=$(printf "%s" "$COMMIT_MSG" | jq -Rs .) curl -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_TOKEN }}/sendDocument" \ -F chat_id="-1002408175863" \ -F message_thread_id="582" \ - -F caption="$CAPTION" \ - -F document=@"app/xed-editor-${{ env.COMMIT_HASH }}.apk" + -F document=@"app/xed-editor-${{ env.COMMIT_HASH }}.apk" \ + --form-string "caption=$COMMIT_MSG" - name: Generate Plugin Sdk run: | diff --git a/.github/workflows/ktfmt-check.yml b/.github/workflows/ktfmt-check.yml index 20e556227..e018aa0ee 100644 --- a/.github/workflows/ktfmt-check.yml +++ b/.github/workflows/ktfmt-check.yml @@ -29,6 +29,9 @@ jobs: - name: Clone submodules run: git submodule update --init --recursive + - name: Accept Android SDK licenses + run: yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses || true + - name: Run ktfmt format run: ./gradlew ktfmtFormat --no-daemon diff --git a/.github/workflows/update-soraX.yml b/.github/workflows/update-soraX.yml new file mode 100644 index 000000000..a36078d26 --- /dev/null +++ b/.github/workflows/update-soraX.yml @@ -0,0 +1,33 @@ +# .github/workflows/update-submodule.yml +name: Update soraX Submodule + +on: + workflow_dispatch: + +jobs: + update-submodule: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: true + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Configure Git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Update soraX to latest commit + run: | + git submodule update --remote soraX + + - name: Commit and push if changed + run: | + git diff --quiet && echo "soraX already up to date, nothing to commit." && exit 0 + COMMIT=$(git -C soraX rev-parse --short HEAD) + git add soraX + git commit -m "chore: update soraX submodule to $COMMIT" + git push origin main \ No newline at end of file diff --git a/.gitignore b/.gitignore index ee59cbc64..bf2aaf478 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,17 @@ Todo.md */build **/build .vscode -core/wasm3 \ No newline at end of file +core/wasm3 + +# Binary artifacts +*.exe +*.o +*.so +*.a +*.la +*.lo +*.lai +*.pyc +__pycache__/ +.DS_Store +*~ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..a8e53e7d2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,63 @@ +# ========================================== +# Stage 1: Build Environment +# ========================================== +FROM eclipse-temurin:21-jdk AS builder + +# Set environment variables for Android SDK +ENV ANDROID_HOME=/opt/android-sdk +ENV PATH=${PATH}:${ANDROID_HOME}/cmdline-tools/latest/bin:${ANDROID_HOME}/platform-tools + +# Install system dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + git \ + wget \ + unzip \ + build-essential \ + && rm -rf /var/lib/apt/lists/* + +# Download and install Android SDK command-line tools +RUN mkdir -p ${ANDROID_HOME}/cmdline-tools && \ + wget -q https://dl.google.com/android/repository/commandlinetools-linux-14742923_latest.zip -O /tmp/cmdline-tools.zip && \ + unzip -q /tmp/cmdline-tools.zip -d /tmp && \ + mv /tmp/cmdline-tools ${ANDROID_HOME}/cmdline-tools/latest && \ + rm /tmp/cmdline-tools.zip + +# Accept Android SDK licenses +RUN yes | sdkmanager --licenses || true + +# Set the working directory +WORKDIR /app + +# Copy the project files +# Note: Ensure submodules are checked out on host before building: +# git submodule update --init --recursive +COPY . . + +# Set up build arguments for release signing (optional) +# BUILD_TASK can be assembleDebug, assembleRelease, etc. +ARG BUILD_TASK=assembleDebug +ARG RELEASE_KEYSTORE_BASE64="" +ARG RELEASE_PROPERTIES_BASE64="" + +# Set env to match the signing logic in app/build.gradle.kts +ENV GITHUB_ACTIONS=true + +# Decode credentials (if provided), ensure submodules are present, and run the Gradle build task +RUN if [ -n "$RELEASE_KEYSTORE_BASE64" ] && [ -n "$RELEASE_PROPERTIES_BASE64" ]; then \ + echo "Decoding release keys..." && \ + echo "$RELEASE_KEYSTORE_BASE64" | base64 -d > /tmp/xed.keystore && \ + echo "$RELEASE_PROPERTIES_BASE64" | base64 -d > /tmp/signing.properties; \ + fi && \ + if [ ! -f soraX/editor/build.gradle.kts ]; then \ + echo "soraX submodule not found locally, cloning dynamically..." && \ + rm -rf soraX && \ + git clone --recursive https://github.com/RohitKushvaha01/soraX.git soraX; \ + fi && \ + chmod +x gradlew && \ + ./gradlew ${BUILD_TASK} --no-daemon + +# ========================================== +# Stage 2: Export Built APKs (BuildKit friendly) +# ========================================== +FROM scratch AS export-stage +COPY --from=builder /app/app/build/outputs/apk/ / diff --git a/README.md b/README.md index d2b0aa531..2ecd3a38e 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,27 @@ documentation: https://xed-editor.github.io/Xed-Docs/ --- +## Building the Project + + +1. Build the **Debug APK** (signed with the included testkey): + ```bash + ./gradlew assembleDebug + ``` + The compiled APK will be located at `app/build/outputs/apk/debug/app-debug.apk`. +--- + +### Option 2: Building with Docker + +If you do not have the Android SDK or JDK 21 installed locally, you can compile the project in a container using Docker. +This command builds the APK and exports it directly to your host machine: +```bash +DOCKER_BUILDKIT=1 docker build --target export-stage --output ./out . +``` +The output debug APK will be generated at `out/debug/app-debug.apk`. + +--- + ## Contributing We welcome contributions! Please read the [`/docs/CONTRIBUTING.md`](/docs/CONTRIBUTING.md) file to diff --git a/app/.gitignore b/app/.gitignore index 934892346..6db4ffc0a 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -3,3 +3,6 @@ src/main/jniLibs/arm64-v8a/libproot-loader*.so src/main/jniLibs/armeabi-v7a/libproot-loader*.so src/main/jniLibs/x86_64/libproot-loader*.so +src/main/jniLibs/arm64-v8a/libloader*.so +src/main/jniLibs/armeabi-v7a/libloader*.so +src/main/jniLibs/x86_64/libloader*.so diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9bcd1ef7a..b30bf7a65 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -3,25 +3,23 @@ import java.util.Properties plugins { alias(libs.plugins.android.baselineprofile) alias(libs.plugins.android.application) - alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.compose) alias(libs.plugins.ktfmt) } android { namespace = "com.rk.application" - compileSdk = 36 + compileSdk = 37 defaultConfig { applicationId = "com.rk.xededitor" minSdk = 26 - //noinspection ExpiredTargetSdkVersion - targetSdk = 28 + targetSdk = 37 // versioning - versionCode = 87 - versionName = "3.2.9" + versionCode = 95 + versionName = "3.3.1" vectorDrawables { useSupportLibrary = true } } @@ -40,7 +38,6 @@ android { sourceCompatibility = JavaVersion.VERSION_21 targetCompatibility = JavaVersion.VERSION_21 } - kotlin { jvmToolchain(21) } packaging { jniLibs { useLegacyPackaging = true } } @@ -85,9 +82,10 @@ android { release { isMinifyEnabled = false isShrinkResources = false - isCrunchPngs = false + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + signingConfig = signingConfigs.getByName("release") } @@ -105,6 +103,8 @@ android { } } +kotlin { jvmToolchain(21) } + dependencies { implementation(libs.androidx.profileinstaller) coreLibraryDesugaring(libs.desugar) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index e57f7bf33..037cdb373 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -204,28 +204,107 @@ -keep class * extends com.google.gson.TypeAdapter { *; } --keepclasseswithmembernames class com.rk.plugin.server.api.API {*;} --keepclasseswithmembernames class com.rk.plugin.server.api.PluginLifeCycle {*;} --keepclasseswithmembernames class com.rk.plugin.server.** {*;} --keep class com.rk.xededitor.MainActivity.MainActivity {*;} --keepclasseswithmembernames class com.rk.App {*;} --keepclasseswithmembernames class com.rk.xededitor.BaseActivity {*;} - --keepclassmembernames class com.rk.plugin.server.api.API {*;} --keepclassmembernames class com.rk.plugin.server.api.PluginLifeCycle {*;} --keepclassmembernames class com.rk.plugin.server.** {*;} --keepclassmembernames class com.rk.App {*;} --keepclassmembernames class com.rk.xededitor.BaseActivity {*;} - --keepnames class com.rk.plugin.server.api.API {*;} --keepnames class com.rk.plugin.server.api.PluginLifeCycle {*;} --keepnames class com.rk.plugin.server.** {*;} --keepnames class com.rk.App {*;} --keepnames class com.rk.xededitor.BaseActivity {*;} +-keep class com.rk.plugin.server.** { *; } +-dontwarn com.rk.plugin.server.** + +# Skip shrinking class if it contains the xed extension point or related to extension related code +-keep @com.rk.extension.api.XedExtensionPoint class * { *; } +-keepclassmembers class * { + @com.rk.extension.api.XedExtensionPoint *; +} +-keep class * { + @com.rk.extension.api.XedExtensionPoint ; + @com.rk.extension.api.XedExtensionPoint ; +} + + +# Preserve well-known classes that plugins may try to access and keep their names +-keep class com.rk.commands.** { *; } +-keep class com.rk.editor.** { *; } +-keep class com.rk.file.** { *; } +-keep class com.rk.filetree.** { *; } +-keep class com.rk.lsp.** { *; } +-keep class com.rk.runner.** { *; } +-keep class com.rk.tabs.** { *; } +-keep class com.rk.utils.** { *; } +-keep class com.rk.proot.** { *; } +-keep class com.rk.components.** { *; } +-keep class com.rk.App { *; } +-keep class com.rk.XedConstants { *; } +-keep class com.rk.extension.** { *; } +-keep class java.io.** { *; } +-keep class kotlin.** { *; } +-keep class kotlinx.** { *; } +-keep class okio.** { *; } + +# JVM Libraries Reflection & Compatibility Rules + +# LSP4J +-keep class org.eclipse.lsp4j.** { *; } +-dontwarn org.eclipse.lsp4j.** + +# JGit +-keep class org.eclipse.jgit.** { *; } +-dontwarn org.eclipse.jgit.** + +# Joni / Jcodings (Regex Library) +-keep class org.jruby.joni.** { *; } +-keep class org.jruby.jcodings.** { *; } +-dontwarn org.jruby.joni.** +-dontwarn org.jruby.jcodings.** + +# SnakeYAML +-keep class org.yaml.snakeyaml.** { *; } +-keep class org.snakeyaml.engine.** { *; } +-dontwarn org.yaml.snakeyaml.** +-dontwarn org.snakeyaml.engine.** + +# Moshi +-keep class com.squareup.moshi.** { *; } +-dontwarn com.squareup.moshi.** + +# Ec4j Core +-keep class org.ec4j.core.** { *; } +-dontwarn org.ec4j.core.** + +# Tree-sitter +-keep class com.itsaky.androidide.treesitter.** { *; } +-dontwarn com.itsaky.androidide.treesitter.** + +# Rosemoe Sora Editor +-keep class io.github.rosemoe.sora.** { *; } +-dontwarn io.github.rosemoe.sora.** + +# Oniguruma Native +-keep class io.github.rosemoe.oniguruma.** { *; } +-dontwarn io.github.rosemoe.oniguruma.** + +# UtilCode +-keep class com.blankj.utilcode.** { *; } +-dontwarn com.blankj.utilcode.** + +# Semver +-keep class io.github.z4kn4fein.semver.** { *; } +-dontwarn io.github.z4kn4fein.semver.** + +# ANRWatchdog +-keep class com.github.anrwatchdog.** { *; } +-dontwarn com.github.anrwatchdog.** + +# AndroidSVG +-keep class com.caverock.androidsvg.** { *; } +-dontwarn com.caverock.androidsvg.** -dontwarn sun.security.x509.X509Key --dontobfuscate --dontshrink --keepattributes *Annotation* +-keepattributes *Annotation*,Signature,InnerClasses,EnclosingMethod,Metadata + +# slf4j missing static binders +-dontwarn org.slf4j.impl.** +-dontobfuscate +# Fix for AbstractMethodError: abstract method "double androidx.compose.ui.graphics.colorspace.DoubleFunction.invoke(double)" +-keep,allowoptimization interface androidx.compose.ui.graphics.colorspace.DoubleFunction { *; } +-keep,allowoptimization class androidx.compose.ui.graphics.colorspace.TransferParameters { *; } +-keep,allowoptimization class androidx.compose.ui.graphics.colorspace.Rgb { *; } +-keep class coil.util.** { *; } diff --git a/baselineprofile/build.gradle.kts b/baselineprofile/build.gradle.kts index 0fcd85736..28c84e2a1 100644 --- a/baselineprofile/build.gradle.kts +++ b/baselineprofile/build.gradle.kts @@ -1,16 +1,15 @@ plugins { alias(libs.plugins.android.test) - alias(libs.plugins.kotlin.android) alias(libs.plugins.android.baselineprofile) } android { namespace = "com.rk.baselineprofile" - compileSdk = 36 + compileSdk = 37 defaultConfig { minSdk = 28 - targetSdk = 36 + targetSdk = 37 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/benchmark/build.gradle.kts b/benchmark/build.gradle.kts index 1cbd95f89..ff3c986ad 100644 --- a/benchmark/build.gradle.kts +++ b/benchmark/build.gradle.kts @@ -1,15 +1,14 @@ plugins { alias(libs.plugins.android.test) - alias(libs.plugins.kotlin.android) } android { namespace = "com.rk.benchmark" - compileSdk = 36 + compileSdk = 37 defaultConfig { minSdk = 26 - targetSdk = 36 + targetSdk = 37 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/benchmark2/build.gradle.kts b/benchmark2/build.gradle.kts index cd4f183b5..0f618ccc5 100644 --- a/benchmark2/build.gradle.kts +++ b/benchmark2/build.gradle.kts @@ -1,16 +1,14 @@ plugins { alias(libs.plugins.android.library) alias(libs.plugins.android.benchmark) - alias(libs.plugins.kotlin.android) } android { namespace = "com.rk.benchmark2" - compileSdk = 36 + compileSdk = 37 defaultConfig { minSdk = 26 - targetSdk = 36 testInstrumentationRunner = "androidx.benchmark.junit4.AndroidBenchmarkRunner" } @@ -18,9 +16,6 @@ android { testBuildType = "release" buildTypes { debug { - // Since isDebuggable can"t be modified by gradle for library modules, - // it must be done in a manifest - see src/androidTest/AndroidManifest.xml - isMinifyEnabled = true proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "benchmark-proguard-rules.pro") } release { isDefault = true } @@ -29,6 +24,12 @@ android { sourceCompatibility = JavaVersion.VERSION_21 targetCompatibility = JavaVersion.VERSION_21 } + lint { + targetSdk = 37 + } + testOptions { + targetSdk = 37 + } kotlin { jvmToolchain(21) } } diff --git a/build.gradle.kts b/build.gradle.kts index dbd0351f7..b0a9c7bea 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,7 +2,6 @@ plugins { alias(libs.plugins.android.baselineprofile) apply false alias(libs.plugins.android.application) apply false alias(libs.plugins.android.library) apply false - alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.parcelize) apply false alias(libs.plugins.kotlin.compose) apply false alias(libs.plugins.kotlin.jvm) apply false @@ -18,9 +17,3 @@ subprojects { } } } - -buildscript{ - dependencies { - classpath(libs.r8) - } -} \ No newline at end of file diff --git a/chnagelog.txt b/chnagelog.txt deleted file mode 100644 index 458631663..000000000 --- a/chnagelog.txt +++ /dev/null @@ -1,148 +0,0 @@ -fix: inverted snackbar color -feat: refactor extension system (#1340) -style: reorder imports and remove debug println -feat: add toggle for exit confirmation dialog (Closes #1332) -feat: add auto-closing bracket setting (#1333) -fix: remove issue title workflow -Added translation using Weblate (Dutch) -style: auto-format Kotlin with ktfmt -fix: lsp server config -feat: implement plugin download -feat: improved caching -feat: use new extension registry -feat: add integrated color picker (#1280) -fix: ensure keyboard is hidden on terminal screen disposal -feat: add integrated color picker and improve terminal navigation -feat: enhance properties, use slider and migrate from Gson (#1279) -feat: fix LSP persistence migration issues -feat: replace Gson with Kotlin Serialization and migrate LSP storage -feat: refactor `ValueSlider` and improve settings screens -feat: enhance properties dialog with Git status and file information -chore: enhance file drawer and remove debug extension (#1278) -chore: enhance file drawer and remove debug extensions -perf: optimize code search and indexing in `SearchViewModel` (#1277) -fix: preserve insertion order in code search results -fix: fix code search crashes and improve file stream handling -perf: optimize code search and indexing in `SearchViewModel` -ci: safely handle commit messages in Telegram uploads (#1276) -ci: safely handle commit messages in Telegram uploads -feat(terminal): fix terminal focus issue & modernize (#1275) -feat: extend terminal settings and customization -refactor(terminal): replace legacy `EditText` with Compose `TextField` and fix focus issues -chore: format and cleanup drawable resources (#1273) -chore: format and cleanup drawable resources -feat: implement extension detail screen (#1265) -refactor: refactor and modularize extension and LSP settings UI -feat: add extension repository button and implement placeholder for reviews tab -feat: enhance extension discovery screen with sorting, searching and filtering capabilities -feat: enhance extension details with size and download statistics -Added translation using Weblate (Korean) -Added translation using Weblate (Finnish) -feat: add review section, show author icon, make extension detail page refreshable -feat: enhance extension details and UI -style: enhance UI layout in About and Extension detail screens -feat: enhance about screen and improve extension/language settings -fix: attempt to fix race condition and improving reload command (#1264) -fix: fix duplicate command keys, fix broken session restoration, fix toolbar actions not recomposing, fix syntax highlighting issue -feat: enhance extension management and refactor extension metadata -feat: tiny UI improvements -feat: implement extension detail screen and navigation -fix: attempt to fix race condition and improving reload command -fix: fix tab wipe by adding unique keys to pager (#1261) -fix: fix tab wipe by adding unique keys to pager -feat: implement font management for app, editor, and terminal (#1260) -feat: improve warning UI in terminal font screen -feat: implement comprehensive font management for app, editor, and terminal (Closes #1236) -fix: improve duplicate issue detection (#1257) -fix: improve duplicate issue detection by including closed issues and adding version checking -feat: add URI tap detection and link opening action in editor (#1254) -refactor: cleanup -refactor: decouple tab and editor management in `MainViewModel` -feat: proper focus handling and new `Tab` methods -feat: add URI tap detection and link opening action in editor -feat: minimap support (#1250) -feat: minimap support, theme and font fixes, move OOB and binary settings to category "Other" -support new marker -close duplicate issues -chore: cleanup project (#1242) -feat: implement multi-file selection and refactor (#1240) -fix: fix rememberCoroutineScope left the composition exception on pasting -fix: fix inconsistent background TODO and fix `open_in_terminal` casing -chore: fix merge conflicts -refactor: remove duplicate methods -fix: fix `no_instances` text not showing if no visible instances -fix: hide `NOT_RUNNING` instances that are not forced -feat: improve delete confirmation dialog text for multiple files -fix: revert properties action to be a single file action -chore: resolve file tree focus TODO and fix bug where only one file tree node was marked as cut -fix: fix crash on closing project -feat: implement multi-file selection and refactor -fix: compile -feat: improve drawer UI and animations -feat: fullscreen-mode, smart toolbar and fix various crashes (#1217) -chore: move smart toolbar handling out of EditorTab and change folder color -feat: implement smart toolbar setting -feat: add fullscreen-mode support (Closes #1178) -fix: fix terminal restore/backup crash (Closes #1209) -fix: fix LSP status not showing if no icon is present and fix `MonotonicFrameClock` error (Closes #1214) -feat: enhance LSP management and infrastructure (#1208) -fix: cleanup `DefinitionPrevention` project cache when list/map is empty -fix: fix expensive `isUpdatable` call slowing down lsp connection -fix: fix greptile issues -fix: show warning for unsaved files on close other/close all -fix: enable language servers by default, fix icon inconsistency in toolbar, fix language server not connecting when no TextMate scope exists -feat: enhance LSP management and infrastructure -feat: enhance editor theming and LSP logging capabilities (#1198) -feat: enhance editor theming and LSP logging capabilities -fix: fix editor not loading content (#1196) -fix: fix editor not loading content -fix(lsp): fix LSP project and workspace paths (#1195) -fix(lsp): use project root instead of parent file for workspace and fix workspace URI -feat: added toggles for oom prediction dialog -fix(extension-api): fix extension language server installation (#1193) -fix(extension-api): fix extension language server installation prompt not showing -fix(extension-api): fix extension language servers not connecting (#1192) -fix(extension-api): fix extension language servers not connecting -feat: R language support & dynamic `FileType` registration (#1191) -fix(core): fix color scheme of exclude files editor -feat(extension-api): allow dynamic registration of `FileType`s -feat: add R syntax highlighting support -fix: fix index database crash and improve binary detection (#1190) -fix: fix SQLiteClosable IllegalStateException (Closes #1176) -fix: improve binary detection (Closes #1188) -feat: add proper exclusion system (#1169) -feat(core): improve LSP functionality (#1031) -feat: add proper disable-feature toggles -feat: add syntax highlighting for luau (Closes #1183) -feat: added 'file' output in file properties -feat: auto connect to all editors when language server is installed -fix: fix `setLanguage()` crash, implement auto start/stop for enabling/disabling language server -feat: improve start/stop, refactor -feat: implement proper exclusion system -fix: fix crashes (#1165) -feat: add replace everywhere button (#1164) -feat: add replace everywhere button -feat: center terminal leave warning -feat: improve language server status, separate by instances -fix: remove unwanted `READ_PHONE_STATE` permission (#1163) -fix(core): remove indexing key on project removal -fix: remove unwanted `READ_PHONE_STATE` permission -fix: fix build issue -fix: fix lsp installation info and id of language servers -feat(core): add external server editing (Closes #1116) -feat(core): remove external lsp extension limitation (see #1116) -feat(core): add app logs to debug options -chore: remove TODO -chore: resolve merge conflicts -fix(core): fix `ExternalLspDialog` not following Material Design guidelines, sync extensions value of the dialog -chore: format code -chore: fix merge conflicts and improve `LspRegistry` -fix(core): fix extension settings layout (use `PreferenceLayout` in order for scroll stretch to work) -feat(core): use snackbar for language server installation prompt, fix snackbar overlapping extra keys -refactor(core): move report code into separate new method -fix(core): fix theming -fix(core): fix `IllegalFormatConversionException` -chore(core): extract hardcoded strings into string resources, reduce duplicate code with additional `setThemeColors(...)` method -fix(core): fix build failing because of wrong signature of `onStatusChange` -feat(core): add LSP server detail page, add lsp logging mechanism, add setting for auto-completion on enter, fix duplicate language server connection -feat(core): improve LSP settings UI, add saving mechanism for restoring external LSP servers after restart diff --git a/core/components/build.gradle.kts b/core/components/build.gradle.kts index 2953521d8..16f19162e 100644 --- a/core/components/build.gradle.kts +++ b/core/components/build.gradle.kts @@ -1,13 +1,12 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.compose) alias(libs.plugins.ktfmt) } android { namespace = "org.robok.engine.core.components" - compileSdk = 36 + compileSdk = 37 defaultConfig { minSdk = 26 diff --git a/core/components/src/main/java/com/rk/components/compose/preferences/base/DividerColumn.kt b/core/components/src/main/java/com/rk/components/compose/preferences/base/DividerColumn.kt index 61c48415f..63c439ce5 100644 --- a/core/components/src/main/java/com/rk/components/compose/preferences/base/DividerColumn.kt +++ b/core/components/src/main/java/com/rk/components/compose/preferences/base/DividerColumn.kt @@ -61,8 +61,10 @@ fun DividerColumn( } val width = constraints.maxWidth - val dividersHeight = thicknessPx.roundToInt() * (placeables.size - 1) - val height = placeables.sumOf { it.height } + dividersHeight + val childCount = measurables.size + val dividersHeight = if (childCount <= 1) 0 else thicknessPx.roundToInt() * (childCount - 1) + val contentHeight = placeables.sumOf { it.height } + val height = (contentHeight + dividersHeight).coerceAtLeast(0) layout(width, height) { val dividerPositions = mutableListOf() diff --git a/core/extension/build.gradle.kts b/core/extension/build.gradle.kts deleted file mode 100644 index 5efb222e0..000000000 --- a/core/extension/build.gradle.kts +++ /dev/null @@ -1,42 +0,0 @@ -plugins { - alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.serialization) - alias(libs.plugins.ktfmt) -} - -android { - namespace = "com.rk.extension" - compileSdk = 36 - - defaultConfig { - minSdk = 26 - - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles("consumer-rules.pro") - } - - buildTypes { - release { - isMinifyEnabled = false - proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") - } - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_21 - targetCompatibility = JavaVersion.VERSION_21 - } - kotlin { jvmToolchain(21) } -} - -dependencies { - implementation(libs.androidx.core.ktx) - implementation(libs.gson) - implementation(libs.okhttp) - implementation(libs.androidx.compose.runtime) - implementation(libs.kotlinx.serialization.json) - testImplementation(libs.junit) - androidTestImplementation(libs.androidx.test.junit) - androidTestImplementation(libs.androidx.test.espresso) -} diff --git a/core/extension/src/androidTest/java/com/rk/extension/ExampleInstrumentedTest.kt b/core/extension/src/androidTest/java/com/rk/extension/ExampleInstrumentedTest.kt deleted file mode 100644 index bc8da6099..000000000 --- a/core/extension/src/androidTest/java/com/rk/extension/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.rk.extension - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry -import org.junit.Assert.* -import org.junit.Test -import org.junit.runner.RunWith - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.rk.extension.test", appContext.packageName) - } -} diff --git a/core/extension/src/test/java/com/rk/extension/ExampleUnitTest.kt b/core/extension/src/test/java/com/rk/extension/ExampleUnitTest.kt deleted file mode 100644 index e1b6fec6f..000000000 --- a/core/extension/src/test/java/com/rk/extension/ExampleUnitTest.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.rk.extension - -import org.junit.Assert.* -import org.junit.Test - -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } -} diff --git a/core/extension/.gitignore b/core/link2symlink/.gitignore similarity index 100% rename from core/extension/.gitignore rename to core/link2symlink/.gitignore diff --git a/core/link2symlink/build.gradle.kts b/core/link2symlink/build.gradle.kts new file mode 100644 index 000000000..cd06ceddc --- /dev/null +++ b/core/link2symlink/build.gradle.kts @@ -0,0 +1,43 @@ +plugins { + alias(libs.plugins.android.library) +} + +android { + namespace = "com.rk.link2symlink" + ndkVersion = "29.0.13846066" + compileSdk { + version = release(36) { + minorApiLevel = 1 + } + } + + defaultConfig { + minSdk = 26 + + externalNativeBuild { + cmake { + cppFlags("") + } + } + } + + externalNativeBuild { + cmake { + path("src/main/cpp/CMakeLists.txt") + version = "3.22.1" + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } +} + +dependencies { + implementation(libs.androidx.appcompat) + implementation(libs.androidx.core.ktx) + implementation(libs.material) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.test.espresso) + androidTestImplementation(libs.androidx.test.junit) +} \ No newline at end of file diff --git a/core/extension/src/main/AndroidManifest.xml b/core/link2symlink/src/main/AndroidManifest.xml similarity index 100% rename from core/extension/src/main/AndroidManifest.xml rename to core/link2symlink/src/main/AndroidManifest.xml diff --git a/core/link2symlink/src/main/cpp/CMakeLists.txt b/core/link2symlink/src/main/cpp/CMakeLists.txt new file mode 100644 index 000000000..5ab81f52a --- /dev/null +++ b/core/link2symlink/src/main/cpp/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.22.1) +project("link2symlink") +add_library(${CMAKE_PROJECT_NAME} SHARED + # List C/C++ source files with relative paths to this CMakeLists.txt. + link2symlink.cpp) + +# Ensure reproducible builds by mapping absolute source paths to relative ones in debug info +if (CMAKE_C_COMPILER_ID MATCHES "GNU|Clang" OR CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") + add_compile_options("-ffile-prefix-map=${CMAKE_CURRENT_SOURCE_DIR}=.") +endif() + +target_link_libraries(${CMAKE_PROJECT_NAME} + android log) \ No newline at end of file diff --git a/core/link2symlink/src/main/cpp/link2symlink.cpp b/core/link2symlink/src/main/cpp/link2symlink.cpp new file mode 100644 index 000000000..558e664b2 --- /dev/null +++ b/core/link2symlink/src/main/cpp/link2symlink.cpp @@ -0,0 +1,153 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef __ANDROID__ +#include +#define LOG_TAG "link2symlink" +#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) +#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) +#else +#define LOGI(...) do { fprintf(stderr, "[link2symlink] INFO: "); fprintf(stderr, __VA_ARGS__); fprintf(stderr, "\n"); } while(0) +#define LOGE(...) do { fprintf(stderr, "[link2symlink] ERROR: "); fprintf(stderr, __VA_ARGS__); fprintf(stderr, "\n"); } while(0) +#endif + +#ifndef AT_EMPTY_PATH +#define AT_EMPTY_PATH 0x1000 +#endif +#ifndef AT_SYMLINK_FOLLOW +#define AT_SYMLINK_FOLLOW 0x400 +#endif + +#define EXPORT __attribute__((visibility("default"))) + +// Helper to resolve the absolute path of oldpath relative to dirfd. +// Writes the result to out_path, which must be at least PATH_MAX bytes. +// Returns 0 on success, -1 on error (sets errno). +static int resolve_to_absolute(int dirfd, const char *path, int flags, char *out_path) { + if (!path || !out_path) { + errno = EINVAL; + return -1; + } + + // Handle AT_EMPTY_PATH: if path is empty, reference the fd directly + if ((flags & AT_EMPTY_PATH) && (path[0] == '\0')) { + char fd_path[64]; + snprintf(fd_path, sizeof(fd_path), "/proc/self/fd/%d", dirfd); + ssize_t len = readlink(fd_path, out_path, PATH_MAX - 1); + if (len == -1) { + return -1; + } + out_path[len] = '\0'; + return 0; + } + + // If path is already absolute, just copy it + if (path[0] == '/') { + strncpy(out_path, path, PATH_MAX - 1); + out_path[PATH_MAX - 1] = '\0'; + return 0; + } + + char base_path[PATH_MAX]; + if (dirfd == AT_FDCWD) { + if (getcwd(base_path, sizeof(base_path)) == NULL) { + return -1; + } + } else { + char fd_path[64]; + snprintf(fd_path, sizeof(fd_path), "/proc/self/fd/%d", dirfd); + ssize_t len = readlink(fd_path, base_path, sizeof(base_path) - 1); + if (len == -1) { + return -1; + } + base_path[len] = '\0'; + } + + // Concatenate base_path and path + size_t base_len = strlen(base_path); + if (base_len > 0 && base_path[base_len - 1] == '/') { + snprintf(out_path, PATH_MAX, "%s%s", base_path, path); + } else { + snprintf(out_path, PATH_MAX, "%s/%s", base_path, path); + } + + return 0; +} + +extern "C" { + +EXPORT int link(const char *oldpath, const char *newpath) { + LOGI("link: Intercepted call link(\"%s\", \"%s\")", oldpath ? oldpath : "NULL", newpath ? newpath : "NULL"); + + // Verify source exists + struct stat st; + if (fstatat(AT_FDCWD, oldpath, &st, AT_SYMLINK_NOFOLLOW) != 0) { + LOGE("link: Source file verification failed (errno: %d, %s)", errno, strerror(errno)); + return -1; + } + + char target[PATH_MAX]; + if (resolve_to_absolute(AT_FDCWD, oldpath, 0, target) != 0) { + LOGE("link: Failed to resolve path \"%s\" (errno: %d, %s)", oldpath, errno, strerror(errno)); + return -1; + } + + LOGI("link: Redirecting to symlink(\"%s\", \"%s\")", target, newpath); + int ret = symlink(target, newpath); + if (ret == -1) { + LOGE("link: symlink failed (errno: %d, %s)", errno, strerror(errno)); + } else { + LOGI("link: symlink succeeded"); + } + return ret; +} + +EXPORT int linkat(int olddirfd, const char *oldpath, int newdirfd, const char *newpath, int flags) { + LOGI("linkat: Intercepted call linkat(%d, \"%s\", %d, \"%s\", 0x%x)", + olddirfd, oldpath ? oldpath : "NULL", newdirfd, newpath ? newpath : "NULL", flags); + + // Verify source exists + struct stat st; + int fstat_flags = 0; + if (!(flags & AT_SYMLINK_FOLLOW)) { + fstat_flags |= AT_SYMLINK_NOFOLLOW; + } + if (flags & AT_EMPTY_PATH) { + fstat_flags |= AT_EMPTY_PATH; + } + if (fstatat(olddirfd, oldpath, &st, fstat_flags) != 0) { + LOGE("linkat: Source file verification failed (errno: %d, %s)", errno, strerror(errno)); + return -1; + } + + char target[PATH_MAX]; + if (resolve_to_absolute(olddirfd, oldpath, flags, target) != 0) { + LOGE("linkat: Failed to resolve oldpath (errno: %d, %s)", errno, strerror(errno)); + return -1; + } + + if (flags & AT_SYMLINK_FOLLOW) { + char final_target[PATH_MAX]; + if (realpath(target, final_target) != NULL) { + strncpy(target, final_target, PATH_MAX - 1); + target[PATH_MAX - 1] = '\0'; + } + } + + LOGI("linkat: Redirecting to symlinkat(\"%s\", %d, \"%s\")", target, newdirfd, newpath); + int ret = symlinkat(target, newdirfd, newpath); + if (ret == -1) { + LOGE("linkat: symlinkat failed (errno: %d, %s)", errno, strerror(errno)); + } else { + LOGI("linkat: symlinkat succeeded"); + } + return ret; +} + +} diff --git a/core/main/build.gradle.kts b/core/main/build.gradle.kts index 7f8fc7121..08187b22d 100644 --- a/core/main/build.gradle.kts +++ b/core/main/build.gradle.kts @@ -2,25 +2,44 @@ import groovy.json.JsonOutput plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.compose) alias(libs.plugins.kotlin.ksp) alias(libs.plugins.kotlin.serialization) alias(libs.plugins.ktfmt) } -val gitCommitHash: Provider = - providers.exec { commandLine("git", "rev-parse", "--short=8", "HEAD") }.standardOutput.asText.map { it.trim() } +val isGitRepo = generateSequence(rootProject.projectDir) { it.parentFile }.any { File(it, ".git").exists() } -val fullGitCommitHash: Provider = - providers.exec { commandLine("git", "rev-parse", "HEAD") }.standardOutput.asText.map { it.trim() } +val gitCommitHash: Provider = if (isGitRepo) { + providers.exec { + commandLine("git", "rev-parse", "--short=8", "HEAD") + isIgnoreExitValue = true + }.standardOutput.asText.map { it.trim().ifEmpty { "unknown" } } +} else { + providers.provider { "unknown" } +} -val gitCommitDate: Provider = - providers.exec { commandLine("git", "show", "-s", "--format=%cI", "HEAD") }.standardOutput.asText.map { it.trim() } +val fullGitCommitHash: Provider = if (isGitRepo) { + providers.exec { + commandLine("git", "rev-parse", "HEAD") + isIgnoreExitValue = true + }.standardOutput.asText.map { it.trim().ifEmpty { "unknown" } } +} else { + providers.provider { "unknown" } +} + +val gitCommitDate: Provider = if (isGitRepo) { + providers.exec { + commandLine("git", "show", "-s", "--format=%cI", "HEAD") + isIgnoreExitValue = true + }.standardOutput.asText.map { it.trim().ifEmpty { "unknown" } } +} else { + providers.provider { "unknown" } +} android { namespace = "com.rk.xededitor" - compileSdk = 36 + compileSdk = 37 defaultConfig { minSdk = 26 @@ -111,21 +130,25 @@ dependencies { implementation(libs.ec4j.core) implementation(libs.kotlinx.serialization.json) implementation(libs.jgit) + implementation(libs.semver) debugImplementation(libs.leakcanary) // implementation("androidx.compose.material:material-icons-extended:1.7.8") implementation(libs.androidx.room.runtime) ksp(libs.androidx.room.compiler) + implementation(libs.kotlinx.coroutines) + // Modules implementation(project(":core:resources")) implementation(project(":core:components")) - implementation(project(":core:extension")) - implementation(project(":core:terminal-view")) - implementation(project(":core:terminal-emulator")) + implementation(project(":terminal-view")) + implementation(project(":terminal-emulator")) implementation(project(":editor")) implementation(project(":editor-lsp")) implementation(project(":language-textmate")) + implementation(project(":core:proot")) + implementation(project(":core:link2symlink")) } abstract class GenerateSupportedLocales : DefaultTask() { diff --git a/core/main/src/main/assets/terminal/init.sh b/core/main/src/main/assets/terminal/init.sh index 036d5710e..c8e7ae965 100644 --- a/core/main/src/main/assets/terminal/init.sh +++ b/core/main/src/main/assets/terminal/init.sh @@ -8,6 +8,10 @@ export PS1="\[\e[1;32m\]\u@\h\[\e[0m\]:\[\e[1;34m\]\w\[\e[0m\] \\$ " source "$LOCAL/bin/utils" +if [ -f "$LOCAL/.sandbox_degraded" ]; then + warn "Running in degraded mode. Some features may not work" +fi + # Set timezone CONTAINER_TIMEZONE="UTC" # or any timezone like "Asia/Kolkata" @@ -29,7 +33,7 @@ fi ensure_packages_once() { local marker_file="/.cache/.packages_ensured" - local PACKAGES=("command-not-found" "sudo" "xkb-data") + local PACKAGES=("command-not-found" "sudo" "xkb-data" "libjemalloc-dev") # Exit early if already done [[ -f "$marker_file" ]] && return 0 diff --git a/core/main/src/main/assets/terminal/lsp/json.sh b/core/main/src/main/assets/terminal/lsp/json.sh deleted file mode 100644 index 62e644c81..000000000 --- a/core/main/src/main/assets/terminal/lsp/json.sh +++ /dev/null @@ -1,40 +0,0 @@ -set -e - -source "$LOCAL/bin/utils" - -info 'Preparing...' -apt update && apt upgrade -y - -install() { - if ! command_exists node || ! command_exists npm; then - install_nodejs - fi - - info 'Installing extracted VSCode language servers...' - npm install -g --prefix /usr vscode-langservers-extracted - info 'JSON language server installed successfully.' - exit 0 -} - -uninstall() { - if ask "Are you sure you want to uninstall the extracted VSCode language servers? This will also remove the HTML, CSS and JSON language servers."; then - info 'Uninstalling extracted VSCode language servers...' - npm uninstall -g --prefix /usr vscode-langservers-extracted - info 'Extracted VSCode language servers uninstalled successfully.' - uninstall_nodejs - exit 0 - fi -} - -update() { - info 'Updating extracted VSCode language servers...' - npm update -g --prefix /usr vscode-langservers-extracted - info 'Extracted VSCode language servers updated successfully.' - exit 0 -} - -case "$1" in - --uninstall) uninstall;; - --update) update;; - *) install;; -esac diff --git a/core/main/src/main/assets/terminal/lsp/python.sh b/core/main/src/main/assets/terminal/lsp/python.sh deleted file mode 100644 index a3669b15b..000000000 --- a/core/main/src/main/assets/terminal/lsp/python.sh +++ /dev/null @@ -1,46 +0,0 @@ -set -e - -source "$LOCAL/bin/utils" - -info 'Preparing...' -apt update && apt upgrade -y - -install() { - info 'Installing pipx...' - apt install -y pipx - pipx ensurepath - - info 'Installing Python language server...' - pipx install 'python-lsp-server[all]' - - info 'Python language server installed successfully.' - exit 0 -} - -uninstall() { - info 'Uninstalling Python language server...' - pipx uninstall python-lsp-server - info 'Python language server uninstalled successfully.' - - if ask "Do you want to uninstall pipx? It was installed as a dependency of this language server."; then - info "Uninstalling pipx..." - apt remove -y pipx - apt autoremove -y - info "Pipx uninstalled successfully." - fi - exit 0 -} - -update() { - info 'Updating Python language server...' - pipx upgrade python-lsp-server - info 'Python language server updated successfully.' - exit 0 -} - -case "$1" in - --uninstall) uninstall;; - --update) update;; - *) install;; -esac - diff --git a/core/main/src/main/assets/terminal/lsp/xml.sh b/core/main/src/main/assets/terminal/lsp/xml.sh index 4bb52c7c8..71f980f06 100644 --- a/core/main/src/main/assets/terminal/lsp/xml.sh +++ b/core/main/src/main/assets/terminal/lsp/xml.sh @@ -5,21 +5,24 @@ source "$LOCAL/bin/utils" info 'Preparing...' apt update && apt upgrade -y +LLEMINX_VERSION="0.31.0" +INSTALL_DIR="$HOME/.lsp/lemminx" + install() { info 'Installing LemMinX language server...' - mkdir -p $HOME/.lsp/lemminx - cd $HOME/.lsp/lemminx - apt install -y curl ca-certificates - curl -L -o server.jar https://download.eclipse.org/staging/2025-09/plugins/org.eclipse.lemminx.uber-jar_0.31.0.jar - echo "0.31.0" > version.txt - apt install -y default-jdk + + mkdir -p "$INSTALL_DIR" + cd "$INSTALL_DIR" + apt install -y curl ca-certificates default-jdk + curl -L -o "server.jar" "https://download.eclipse.org/staging/2025-09/plugins/org.eclipse.lemminx.uber-jar_${LLEMINX_VERSION}.jar" + echo "$LLEMINX_VERSION" > version.txt info 'LemMinX language server installed successfully.' exit 0 } uninstall() { info 'Uninstalling LemMinX language server...' - rm -rf $HOME/.lsp/lemminx + rm -rf "$INSTALL_DIR" info 'LemMinX language server uninstalled successfully.' if ask "Do you want to uninstall OpenJDK? It was installed as a dependency of this language server."; then @@ -33,10 +36,10 @@ uninstall() { update() { info 'Updating LemMinX language server...' - cd $HOME/.lsp/lemminx - rm server.jar - curl -L -o server.jar https://download.eclipse.org/staging/2025-09/plugins/org.eclipse.lemminx.uber-jar_0.31.0.jar - echo "0.31.0" > version.txt + cd "$INSTALL_DIR" + rm "server.jar" + curl -L -o "server.jar" "https://download.eclipse.org/staging/2025-09/plugins/org.eclipse.lemminx.uber-jar_${LLEMINX_VERSION}.jar" + echo "$LLEMINX_VERSION" > version.txt info 'LemMinX language server updated successfully.' exit 0 } @@ -45,4 +48,4 @@ case "$1" in --uninstall) uninstall;; --update) update;; *) install;; -esac +esac \ No newline at end of file diff --git a/core/main/src/main/assets/terminal/sandbox.sh b/core/main/src/main/assets/terminal/sandbox.sh index 534a5ad13..4bb5e4ea1 100644 --- a/core/main/src/main/assets/terminal/sandbox.sh +++ b/core/main/src/main/assets/terminal/sandbox.sh @@ -63,17 +63,9 @@ ARGS="$ARGS -L" chmod -R +x $LOCAL/bin -if [ "$FDROID" = false ]; then - if [ $# -gt 0 ]; then - $LINKER $LOCAL/bin/proot $ARGS /bin/bash --rcfile $LOCAL/bin/init -i -c "$*" - else - $LINKER $LOCAL/bin/proot $ARGS /bin/bash --rcfile $LOCAL/bin/init -i - fi +if [ $# -gt 0 ]; then + $PROOT $ARGS /bin/bash --rcfile $LOCAL/bin/init -i -c "$*" else - if [ $# -gt 0 ]; then - $LOCAL/bin/proot $ARGS /bin/bash --rcfile $LOCAL/bin/init -i -c "$*" - else - $LOCAL/bin/proot $ARGS /bin/bash --rcfile $LOCAL/bin/init -i - fi + $PROOT $ARGS /bin/bash --rcfile $LOCAL/bin/init -i fi diff --git a/core/main/src/main/assets/terminal/setup.sh b/core/main/src/main/assets/terminal/setup.sh index e20563363..9ea921979 100644 --- a/core/main/src/main/assets/terminal/setup.sh +++ b/core/main/src/main/assets/terminal/setup.sh @@ -53,12 +53,35 @@ ARGS="$ARGS --link2symlink" ARGS="$ARGS --sysvipc" ARGS="$ARGS -L" + COMMAND="(cd $LOCAL/sandbox && tar -xf $TMP_DIR/sandbox.tar.gz)" -if [ "$FDROID" = false ]; then - $LINKER $LOCAL/bin/proot $ARGS /system/bin/sh -c "$COMMAND" -else - $LOCAL/bin/proot $ARGS /system/bin/sh -c "$COMMAND" + +set +e +# Samsung devices doesn't like running system binaries under proot +$PROOT $ARGS /system/bin/sh -c "$COMMAND" +ret=$? +set -e + +DEGRADED_MARKER="$LOCAL/.sandbox_degraded" + +if [ "$ret" -ne 0 ]; then + warn "PRoot extraction failed (exit code $ret), falling back to direct extraction..." + + set +e + LD_PRELOAD="$(realpath "$NATIVE_LIB_DIR/liblink2symlink.so")" + export LD_PRELOAD + /bin/sh -c "$COMMAND" + unset LD_PRELOAD + ret=$? + set -e + + if [ "$ret" -ne 0 ]; then + warn "Extraction failed (exit code $ret), continuing in degraded mode" + warn "Sandbox may be incomplete and some features may not work" + + touch "$DEGRADED_MARKER" + fi fi @@ -127,6 +150,83 @@ rm "$TMP_DIR"/sandbox.tar.gz # DO NOT REMOVE THIS FILE JUST DON'T, TRUST ME touch $LOCAL/.terminal_setup_ok_DO_NOT_REMOVE + + +info "Installing Node.js APT hook…" + +mkdir -p "$SANDBOX_DIR/etc/apt/apt.conf.d" +mkdir -p "$SANDBOX_DIR/usr/local/bin" + +cat > "$SANDBOX_DIR/etc/apt/apt.conf.d/99node-hook" << 'EOF' +DPkg::Post-Invoke { + "if [ -x /usr/bin/node ]; then /usr/local/bin/node-postinstall.sh; fi"; +}; +EOF + +cat > "$SANDBOX_DIR/usr/local/bin/node-postinstall.sh" << 'EOF' +#!/bin/sh +set -e + +echo "[node-hook] Running Node.js post-install hook..." + +JEMALLOC="" + +echo "[node-hook] Searching for jemalloc..." + +for path in \ + /usr/lib/*/libjemalloc.so* \ + /usr/lib/libjemalloc.so* \ + /lib/*/libjemalloc.so* \ + /lib/libjemalloc.so*; do + + if [ -e "$path" ]; then + JEMALLOC="$path" + echo "[node-hook] Found jemalloc: $JEMALLOC" + break + fi +done + +if [ -z "$JEMALLOC" ]; then + echo "[node-hook] jemalloc not installed, skipping" + exit 0 +fi + +if [ ! -e /usr/bin/node ]; then + echo "[node-hook] Node binary not found, skipping" + exit 0 +fi + +if [ -e /usr/bin/node.distrib ]; then + echo "[node-hook] Node already wrapped, skipping" + exit 0 +fi + +echo "[node-hook] Verifying node binary..." + +if file /usr/bin/node | grep -q ELF; then + echo "[node-hook] Wrapping Node.js with jemalloc..." + + mv /usr/bin/node /usr/bin/node.distrib + + cat > /usr/bin/node << WRAP +#!/bin/sh +LD_PRELOAD=$JEMALLOC exec /usr/bin/node.distrib "\$@" +WRAP + + chmod +x /usr/bin/node + + echo "[node-hook] Node wrapper installed successfully" +else + echo "[node-hook] /usr/bin/node is not an ELF binary, skipping" +fi +EOF + +chmod +x "$SANDBOX_DIR/usr/local/bin/node-postinstall.sh" + +info "Node.js APT hook installed" + + + if [ $# -gt 0 ]; then sh $@ else diff --git a/core/main/src/main/assets/terminal/universal_runner.sh b/core/main/src/main/assets/terminal/universal_runner.sh index 48817f3e1..74b9a598b 100644 --- a/core/main/src/main/assets/terminal/universal_runner.sh +++ b/core/main/src/main/assets/terminal/universal_runner.sh @@ -2,7 +2,7 @@ set -e file="$1" if [ ! -f "$file" ]; then - echo "Error: File not found -> $file" + error "Error: File not found -> $file" exit 1 fi @@ -24,20 +24,28 @@ run_code() { install_package() { local packages="$1" + + # If curl is requested, ensure ca-certificates is also installed + if echo "$packages" | grep -qw curl; then + if ! echo "$packages" | grep -qw ca-certificates; then + packages="$packages ca-certificates" + fi + fi + info "Installing $packages..." apt update -y && apt upgrade -y apt install -y $packages } install_nodejs() { - echo "Installing Node.js LTS..." + info "Installing Node.js LTS..." install_package "curl" curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - apt install -y nodejs } install_rust() { - echo "Installing Rust..." + info "Installing Rust..." install_package "curl" curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y source "$HOME/.cargo/env" @@ -46,7 +54,7 @@ install_rust() { } install_dotnet() { - echo "Installing .NET SDK..." + info "Installing .NET SDK..." wget https://packages.microsoft.com/config/ubuntu/22.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb dpkg -i packages-microsoft-prod.deb rm packages-microsoft-prod.deb @@ -56,7 +64,7 @@ install_dotnet() { install_kotlin() { install_package "unzip curl" - echo "Fetching latest Kotlin compiler..." + info "Fetching latest Kotlin compiler..." url=$(curl -s https://api.github.com/repos/JetBrains/kotlin/releases/latest \ | grep "browser_download_url" \ | grep "kotlin-compiler-.*zip" \ @@ -64,21 +72,21 @@ install_kotlin() { | cut -d '"' -f 4) if [ -z "$url" ]; then - echo "Error: Could not find Kotlin compiler download URL." + error "Error: Could not find Kotlin compiler download URL." return 1 fi - echo "Downloading from: $url" + info "Downloading from: $url" curl -L -o /tmp/kotlin.zip "$url" - echo "Extracting to /opt/kotlinc ..." + info "Extracting to /opt/kotlinc ..." mkdir -p /opt/kotlinc unzip -qo /tmp/kotlin.zip -d /opt ln -sf /opt/kotlinc/bin/kotlinc /usr/local/bin/kotlinc ln -sf /opt/kotlinc/bin/kotlin /usr/local/bin/kotlin - echo "Kotlin installed at /opt/kotlinc" + info "Kotlin installed at /opt/kotlinc" } case "$file" in @@ -102,7 +110,7 @@ case "$file" in fi if ! command_exists tsc; then - echo "Installing TypeScript compiler..." + info "Installing TypeScript compiler..." npm install -g typescript fi @@ -132,11 +140,11 @@ case "$file" in *.kt) if ! command_exists java; then - echo "Installing Java..." + info "Installing Java..." install_package "default-jdk" fi if ! command_exists kotlinc; then - echo "Installing Kotlin..." + info "Installing Kotlin..." install_kotlin fi kotlinc "$file" -include-runtime -d temp.jar && java -jar temp.jar @@ -263,14 +271,14 @@ case "$file" in *.elm) if ! command_exists elm; then - echo "Installing Elm..." + info "Installing Elm..." if ! command_exists node; then install_nodejs fi npm install -g elm fi elm make "$file" --output=temp.html - echo "Elm compiled to temp.html - transfer to browser to view" + info "Elm compiled to temp.html - transfer to browser to view" ;; *.fsx|*.fs) diff --git a/core/main/src/main/assets/textmate/languages.json b/core/main/src/main/assets/textmate/languages.json index 178fdae3f..b4286d7da 100644 --- a/core/main/src/main/assets/textmate/languages.json +++ b/core/main/src/main/assets/textmate/languages.json @@ -308,6 +308,12 @@ "scopeName": "source.sql", "grammar": "textmate/sql/syntaxes/sql.tmLanguage.json", "languageConfiguration": "textmate/sql/language-configuration.json" + }, + { + "grammar": "textmate/nix/syntaxes/nix.tmLanguage.json", + "name": "nix", + "scopeName": "source.nix", + "languageConfiguration": "textmate/nix/language-configuration.json" } ] } diff --git a/core/main/src/main/assets/textmate/nix/language-configuration.json b/core/main/src/main/assets/textmate/nix/language-configuration.json new file mode 100644 index 000000000..9dfca3fc6 --- /dev/null +++ b/core/main/src/main/assets/textmate/nix/language-configuration.json @@ -0,0 +1,40 @@ +{ + "comments": { + "lineComment": "#", + "blockComment": ["/*", "*/"] + }, + "brackets": [ + ["{", "}"], + ["[", "]"], + ["(", ")"], + ["''", "''"], + ["${", "}"] + ], + "autoClosingPairs": [ + { "open": "{", "close": "}" }, + { "open": "[", "close": "]" }, + { "open": "(", "close": ")" }, + { "open": "\"", "close": "\"", "notIn": ["string"] }, + { "open": "''", "close": "''", "notIn": ["string", "comment"] }, + { "open": "${", "close": "}", "notIn": ["string", "comment"] } + ], + "surroundingPairs": [ + ["{", "}"], + ["[", "]"], + ["(", ")"], + ["\"", "\""], + ["''", "''"] + ], + "autoCloseBefore": ";:.,=}])> \n\t", + "wordPattern": "(-?\\d*\\.\\d\\w*)|([^\\=<>\\!\\?\\s\\(\\)\\[\\]\\{\\}\\;\\:\\'\\\"\\,\\.\\~\\@\\#\\%\\^\\&\\*\\+\\|\\\\\\/\\$]+)", + "indentationRules": { + "increaseIndentPattern": "^.*\\{[^}\"']*$", + "decreaseIndentPattern": "^\\s*\\}" + }, + "folding": { + "markers": { + "start": "^\\s*#\\s*region\\b", + "end": "^\\s*#\\s*endregion\\b" + } + } +} diff --git a/core/main/src/main/assets/textmate/nix/syntaxes/nix.tmLanguage.json b/core/main/src/main/assets/textmate/nix/syntaxes/nix.tmLanguage.json new file mode 100644 index 000000000..1d02a5b9e --- /dev/null +++ b/core/main/src/main/assets/textmate/nix/syntaxes/nix.tmLanguage.json @@ -0,0 +1,1188 @@ +{ + "name": "Nix", + "scopeName": "source.nix", + "fileTypes": [ + "nix" + ], + "uuid": "0514fd5f-acb6-436d-b42c-7643e6d36c8f", + "patterns": [ + { + "include": "#expression" + } + ], + "repository": { + "expression": { + "patterns": [ + { + "include": "#parens-and-cont" + }, + { + "include": "#list-and-cont" + }, + { + "include": "#string" + }, + { + "include": "#interpolation" + }, + { + "include": "#with-assert" + }, + { + "include": "#function-for-sure" + }, + { + "include": "#attrset-for-sure" + }, + { + "include": "#attrset-or-function" + }, + { + "include": "#let" + }, + { + "include": "#if" + }, + { + "include": "#operator-unary" + }, + { + "include": "#operator-binary" + }, + { + "include": "#constants" + }, + { + "include": "#bad-reserved" + }, + { + "include": "#parameter-name-and-cont" + }, + { + "include": "#others" + } + ] + }, + "expression-cont": { + "begin": "(?=.?)", + "end": "(?=([\\])};,]|\\b(else|then)\\b))", + "patterns": [ + { + "include": "#parens" + }, + { + "include": "#list" + }, + { + "include": "#string" + }, + { + "include": "#interpolation" + }, + { + "include": "#function-for-sure" + }, + { + "include": "#attrset-for-sure" + }, + { + "include": "#attrset-or-function" + }, + { + "include": "#operator-binary" + }, + { + "include": "#constants" + }, + { + "include": "#bad-reserved" + }, + { + "include": "#parameter-name" + }, + { + "include": "#others" + } + ] + }, + "parens": { + "begin": "\\(", + "beginCaptures": { + "0": { + "name": "punctuation.definition.expression.nix" + } + }, + "end": "\\)", + "endCaptures": { + "0": { + "name": "punctuation.definition.expression.nix" + } + }, + "patterns": [ + { + "include": "#expression" + } + ] + }, + "parens-and-cont": { + "begin": "(?=\\()", + "end": "(?=([\\])};,]|\\b(else|then)\\b))", + "patterns": [ + { + "include": "#parens" + }, + { + "include": "#expression-cont" + } + ] + }, + "list": { + "begin": "\\[", + "beginCaptures": { + "0": { + "name": "punctuation.definition.list.nix" + } + }, + "end": "\\]", + "endCaptures": { + "0": { + "name": "punctuation.definition.list.nix" + } + }, + "patterns": [ + { + "include": "#expression" + } + ] + }, + "list-and-cont": { + "begin": "(?=\\[)", + "end": "(?=([\\])};,]|\\b(else|then)\\b))", + "patterns": [ + { + "include": "#list" + }, + { + "include": "#expression-cont" + } + ] + }, + "attrset-for-sure": { + "patterns": [ + { + "begin": "(?=\\brec\\b)", + "end": "(?=([\\])};,]|\\b(else|then)\\b))", + "patterns": [ + { + "begin": "\\brec\\b", + "end": "(?=\\{)", + "beginCaptures": { + "0": { + "name": "keyword.other.nix" + } + }, + "patterns": [ + { + "include": "#others" + } + ] + }, + { + "include": "#attrset-definition" + }, + { + "include": "#others" + } + ] + }, + { + "begin": "(?=\\{\\s*(\\}|[^,?]*(=|;)))", + "end": "(?=([\\])};,]|\\b(else|then)\\b))", + "patterns": [ + { + "include": "#attrset-definition" + }, + { + "include": "#others" + } + ] + } + ] + }, + "attrset-definition": { + "begin": "(?=\\{)", + "end": "(?=([\\])};,]|\\b(else|then)\\b))", + "patterns": [ + { + "begin": "(\\{)", + "end": "(\\})", + "beginCaptures": { + "0": { + "name": "punctuation.definition.attrset.nix" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.attrset.nix" + } + }, + "patterns": [ + { + "include": "#attrset-contents" + } + ] + }, + { + "begin": "(?<=\\})", + "end": "(?=([\\])};,]|\\b(else|then)\\b))", + "patterns": [ + { + "include": "#expression-cont" + } + ] + } + ] + }, + "attrset-definition-brace-opened": { + "patterns": [ + { + "begin": "(?<=\\})", + "end": "(?=([\\])};,]|\\b(else|then)\\b))", + "patterns": [ + { + "include": "#expression-cont" + } + ] + }, + { + "begin": "(?=.?)", + "end": "\\}", + "endCaptures": { + "0": { + "name": "punctuation.definition.attrset.nix" + } + }, + "patterns": [ + { + "include": "#attrset-contents" + } + ] + } + ] + }, + "attrset-contents": { + "patterns": [ + { + "include": "#attribute-inherit" + }, + { + "include": "#bad-reserved" + }, + { + "include": "#attribute-bind" + }, + { + "include": "#others" + } + ] + }, + "function-header-open-brace": { + "begin": "\\{", + "end": "(?=\\})", + "beginCaptures": { + "0": { + "name": "punctuation.definition.entity.function.2.nix" + } + }, + "patterns": [ + { + "include": "#function-contents" + } + ] + }, + "function-header-close-brace-no-arg": { + "begin": "\\}", + "end": "(?=\\:)", + "beginCaptures": { + "0": { + "name": "punctuation.definition.entity.function.nix" + } + }, + "patterns": [ + { + "include": "#others" + } + ] + }, + "function-header-terminal-arg": { + "begin": "(?=@)", + "end": "(?=\\:)", + "patterns": [ + { + "begin": "\\@", + "end": "(?=\\:)", + "patterns": [ + { + "begin": "(\\b[a-zA-Z\\_][a-zA-Z0-9\\_\\'\\-]*)", + "end": "(?=\\:)", + "name": "variable.parameter.function.3.nix" + }, + { + "include": "#others" + } + ] + }, + { + "include": "#others" + } + ] + }, + "function-header-close-brace-with-arg": { + "begin": "\\}", + "end": "(?=\\:)", + "beginCaptures": { + "0": { + "name": "punctuation.definition.entity.function.nix" + } + }, + "patterns": [ + { + "include": "#function-header-terminal-arg" + }, + { + "include": "#others" + } + ] + }, + "function-header-until-colon-no-arg": { + "begin": "(?=\\{)", + "end": "(?=\\:)", + "patterns": [ + { + "include": "#function-header-open-brace" + }, + { + "include": "#function-header-close-brace-no-arg" + } + ] + }, + "function-header-until-colon-with-arg": { + "begin": "(?=\\{)", + "end": "(?=\\:)", + "patterns": [ + { + "include": "#function-header-open-brace" + }, + { + "include": "#function-header-close-brace-with-arg" + } + ] + }, + "function-body-from-colon": { + "begin": "(\\:)", + "end": "(?=([\\])};,]|\\b(else|then)\\b))", + "beginCaptures": { + "0": { + "name": "punctuation.definition.function.nix" + } + }, + "patterns": [ + { + "include": "#expression" + } + ] + }, + "function-definition": { + "begin": "(?=.?)", + "end": "(?=([\\])};,]|\\b(else|then)\\b))", + "patterns": [ + { + "include": "#function-body-from-colon" + }, + { + "begin": "(?=.?)", + "end": "(?=\\:)", + "patterns": [ + { + "begin": "(\\b[a-zA-Z\\_][a-zA-Z0-9\\_\\'\\-]*)", + "end": "(?=\\:)", + "beginCaptures": { + "0": { + "name": "variable.parameter.function.4.nix" + } + }, + "patterns": [ + { + "begin": "\\@", + "end": "(?=\\:)", + "patterns": [ + { + "include": "#function-header-until-colon-no-arg" + }, + { + "include": "#others" + } + ] + }, + { + "include": "#others" + } + ] + }, + { + "begin": "(?=\\{)", + "end": "(?=\\:)", + "patterns": [ + { + "include": "#function-header-until-colon-with-arg" + } + ] + } + ] + }, + { + "include": "#others" + } + ] + }, + "function-definition-brace-opened": { + "begin": "(?=.?)", + "end": "(?=([\\])};,]|\\b(else|then)\\b))", + "patterns": [ + { + "include": "#function-body-from-colon" + }, + { + "begin": "(?=.?)", + "end": "(?=\\:)", + "patterns": [ + { + "include": "#function-header-close-brace-with-arg" + }, + { + "begin": "(?=.?)", + "end": "(?=\\})", + "patterns": [ + { + "include": "#function-contents" + } + ] + } + ] + }, + { + "include": "#others" + } + ] + }, + "function-for-sure": { + "patterns": [ + { + "begin": "(?=(\\b[a-zA-Z\\_][a-zA-Z0-9\\_\\'\\-]*\\s*[:@]|\\{[^}\\\"']*\\}\\s*:|\\{[^#}\"'/=]*[,\\?]))", + "end": "(?=([\\])};,]|\\b(else|then)\\b))", + "patterns": [ + { + "include": "#function-definition" + } + ] + } + ] + }, + "function-contents": { + "patterns": [ + { + "include": "#bad-reserved" + }, + { + "include": "#function-parameter" + }, + { + "include": "#others" + } + ] + }, + "attrset-or-function": { + "begin": "\\{", + "beginCaptures": { + "0": { + "name": "punctuation.definition.attrset-or-function.nix" + } + }, + "end": "(?=([\\])};]|\\b(else|then)\\b))", + "patterns": [ + { + "begin": "(?=(\\s*\\}|\\\"|\\binherit\\b|\\$\\{|\\b[a-zA-Z\\_][a-zA-Z0-9\\_\\'\\-]*(\\s*\\.|\\s*=[^=])))", + "end": "(?=([\\])};,]|\\b(else|then)\\b))", + "patterns": [ + { + "include": "#attrset-definition-brace-opened" + } + ] + }, + { + "begin": "(?=(\\.\\.\\.|\\b[a-zA-Z\\_][a-zA-Z0-9\\_\\'\\-]*\\s*[,?]))", + "end": "(?=([\\])};,]|\\b(else|then)\\b))", + "patterns": [ + { + "include": "#function-definition-brace-opened" + } + ] + }, + { + "include": "#bad-reserved" + }, + { + "begin": "\\b[a-zA-Z\\_][a-zA-Z0-9\\_\\'\\-]*", + "end": "(?=([\\])};]|\\b(else|then)\\b))", + "beginCaptures": { + "0": { + "name": "variable.parameter.function.maybe.nix" + } + }, + "patterns": [ + { + "begin": "(?=\\.)", + "end": "(?=([\\])};,]|\\b(else|then)\\b))", + "patterns": [ + { + "include": "#attrset-definition-brace-opened" + } + ] + }, + { + "begin": "\\s*(\\,)", + "beginCaptures": { + "1": { + "name": "keyword.operator.nix" + } + }, + "end": "(?=([\\])};,]|\\b(else|then)\\b))", + "patterns": [ + { + "include": "#function-definition-brace-opened" + } + ] + }, + { + "begin": "(?=\\=)", + "end": "(?=([\\])};,]|\\b(else|then)\\b))", + "patterns": [ + { + "include": "#attribute-bind-from-equals" + }, + { + "include": "#attrset-definition-brace-opened" + } + ] + }, + { + "begin": "(?=\\?)", + "end": "(?=([\\])};,]|\\b(else|then)\\b))", + "patterns": [ + { + "include": "#function-parameter-default" + }, + { + "begin": "\\,", + "beginCaptures": { + "0": { + "name": "keyword.operator.nix" + } + }, + "end": "(?=([\\])};,]|\\b(else|then)\\b))", + "patterns": [ + { + "include": "#function-definition-brace-opened" + } + ] + } + ] + }, + { + "include": "#others" + } + ] + }, + { + "include": "#others" + } + ] + }, + "with-assert": { + "begin": "(?)", + "beginCaptures": { + "0": { + "name": "string.unquoted.spath.nix" + } + }, + "end": "(?=([\\])};,]|\\b(else|then)\\b))", + "patterns": [ + { + "include": "#expression-cont" + } + ] + }, + { + "begin": "([a-zA-Z][a-zA-Z0-9\\+\\-\\.]*\\:[a-zA-Z0-9\\%\\/\\?\\:\\@\\&\\=\\+\\$\\,\\-\\_\\.\\!\\~\\*\\']+)", + "beginCaptures": { + "0": { + "name": "string.unquoted.url.nix" + } + }, + "end": "(?=([\\])};,]|\\b(else|then)\\b))", + "patterns": [ + { + "include": "#expression-cont" + } + ] + } + ] + }, + "parameter-name": { + "match": "\\b[a-zA-Z\\_][a-zA-Z0-9\\_\\'\\-]*", + "captures": { + "0": { + "name": "variable.parameter.name.nix" + } + } + }, + "parameter-name-and-cont": { + "begin": "\\b[a-zA-Z\\_][a-zA-Z0-9\\_\\'\\-]*", + "end": "(?=([\\])};,]|\\b(else|then)\\b))", + "beginCaptures": { + "0": { + "name": "variable.parameter.name.nix" + } + }, + "patterns": [ + { + "include": "#expression-cont" + } + ] + }, + "attribute-name-single": { + "match": "\\b[a-zA-Z\\_][a-zA-Z0-9\\_\\'\\-]*", + "name": "entity.other.attribute-name.single.nix" + }, + "attribute-name": { + "patterns": [ + { + "match": "\\b[a-zA-Z\\_][a-zA-Z0-9\\_\\'\\-]*", + "name": "entity.other.attribute-name.multipart.nix" + }, + { + "match": "\\." + }, + { + "include": "#string-quoted" + }, + { + "include": "#interpolation" + } + ] + }, + "function-parameter-default": { + "begin": "\\?", + "beginCaptures": { + "0": { + "name": "keyword.operator.nix" + } + }, + "end": "(?=[,}])", + "patterns": [ + { + "include": "#expression" + } + ] + }, + "function-parameter": { + "patterns": [ + { + "begin": "(\\.\\.\\.)", + "end": "(,|(?=\\}))", + "name": "keyword.operator.nix", + "patterns": [ + { + "include": "#others" + } + ] + }, + { + "begin": "\\b[a-zA-Z\\_][a-zA-Z0-9\\_\\'\\-]*", + "beginCaptures": { + "0": { + "name": "variable.parameter.function.1.nix" + } + }, + "end": "(,|(?=\\}))", + "endCaptures": { + "0": { + "name": "keyword.operator.nix" + } + }, + "patterns": [ + { + "include": "#whitespace" + }, + { + "include": "#comment" + }, + { + "include": "#function-parameter-default" + }, + { + "include": "#expression" + } + ] + }, + { + "include": "#others" + } + ] + }, + "attribute-inherit": { + "begin": "\\binherit\\b", + "beginCaptures": { + "0": { + "name": "keyword.other.inherit.nix" + } + }, + "end": "\\;", + "endCaptures": { + "0": { + "name": "punctuation.terminator.inherit.nix" + } + }, + "patterns": [ + { + "begin": "\\(", + "end": "(?=\\;)", + "beginCaptures": { + "0": { + "name": "punctuation.section.function.arguments.nix" + } + }, + "patterns": [ + { + "begin": "\\)", + "end": "(?=\\;)", + "beginCaptures": { + "0": { + "name": "punctuation.section.function.arguments.nix" + } + }, + "patterns": [ + { + "include": "#bad-reserved" + }, + { + "include": "#attribute-name-single" + }, + { + "include": "#others" + } + ] + }, + { + "include": "#expression" + } + ] + }, + { + "begin": "(?=[a-zA-Z\\_])", + "end": "(?=\\;)", + "patterns": [ + { + "include": "#bad-reserved" + }, + { + "include": "#attribute-name-single" + }, + { + "include": "#others" + } + ] + }, + { + "include": "#others" + } + ] + }, + "attribute-bind-from-equals": { + "begin": "\\=", + "beginCaptures": { + "0": { + "name": "keyword.operator.bind.nix" + } + }, + "end": "\\;", + "endCaptures": { + "0": { + "name": "punctuation.terminator.bind.nix" + } + }, + "patterns": [ + { + "include": "#expression" + } + ] + }, + "attribute-bind": { + "patterns": [ + { + "include": "#attribute-name" + }, + { + "include": "#attribute-bind-from-equals" + } + ] + }, + "operator-unary": { + "name": "keyword.operator.unary.nix", + "match": "(!|-)" + }, + "operator-binary": { + "name": "keyword.operator.nix", + "match": "(\\bor\\b|\\.|\\|\\>|\\<\\||==|!=|!|\\<\\=|\\<|\\>\\=|\\>|&&|\\|\\||-\\>|//|\\?|\\+\\+|-|\\*|/(?=([^*]|$))|\\+)" + }, + "constants": { + "patterns": [ + { + "begin": "\\b(builtins|true|false|null)\\b", + "end": "(?=([\\])};,]|\\b(else|then)\\b))", + "beginCaptures": { + "0": { + "name": "constant.language.nix" + } + }, + "patterns": [ + { + "include": "#expression-cont" + } + ] + }, + { + "beginCaptures": { + "0": { + "name": "support.function.nix" + } + }, + "begin": "\\b(scopedImport|import|isNull|abort|throw|baseNameOf|dirOf|removeAttrs|map|toString|derivationStrict|derivation)\\b", + "end": "(?=([\\])};,]|\\b(else|then)\\b))", + "patterns": [ + { + "include": "#expression-cont" + } + ] + }, + { + "beginCaptures": { + "0": { + "name": "constant.numeric.nix" + } + }, + "begin": "\\b[0-9]+\\b", + "end": "(?=([\\])};,]|\\b(else|then)\\b))", + "patterns": [ + { + "include": "#expression-cont" + } + ] + } + ] + }, + "whitespace": { + "match": "\\s+" + }, + "illegal": { + "match": ".", + "name": "invalid.illegal" + }, + "others": { + "patterns": [ + { + "include": "#whitespace" + }, + { + "include": "#comment" + }, + { + "include": "#illegal" + } + ] + }, + "bad-reserved": { + "match": "(? Unit @@ -58,7 +62,7 @@ class MainActivity : AppCompatActivity() { isPaused = true GlobalScope.launch(Dispatchers.IO) { SessionManager.saveSession(viewModel.tabs, viewModel.currentTabIndex) - DrawerPersistence.saveState() + DrawerPersistence.saveState(drawerViewModel) foregroundListener.values.forEach { it.invoke(false) } LspRegistry.updateConfiguration(this@MainActivity) @@ -73,8 +77,6 @@ class MainActivity : AppCompatActivity() { lifecycleScope.launch(Dispatchers.IO) { handleIntent(intent) foregroundListener.values.forEach { it.invoke(true) } - delay(1000) - handleSupport() val lspConfigChanges = LspRegistry.getConfigurationChanges(this@MainActivity) if (lspConfigChanges.isNotEmpty()) { @@ -84,6 +86,9 @@ class MainActivity : AppCompatActivity() { .filter { affectedExtensions.contains(it.file.getExtension()) } .forEach { tab -> tab.applyHighlightingAndConnectLSP() } } + + delay(1000.milliseconds) + handleSupport() } } @@ -95,7 +100,7 @@ class MainActivity : AppCompatActivity() { suspend fun handleIntent(intent: Intent) { if (Intent.ACTION_VIEW == intent.action || Intent.ACTION_EDIT == intent.action) { if (intent.data == null) { - errorDialog(strings.invalid_intent.getFilledString(intent.toString())) + errorDialog(msg = strings.invalid_intent.getFilledString(intent.toString())) return } @@ -132,18 +137,22 @@ class MainActivity : AppCompatActivity() { setContent { val navController = rememberNavController() + val startDestination = remember { + if (Settings.shown_disclaimer) { + MainRoutes.Main.route + } else { + MainRoutes.Disclaimer.route + } + } NavHost( navController = navController, - startDestination = - if (Settings.shown_disclaimer) { - MainRoutes.Main.route - } else { - MainRoutes.Disclaimer.route - }, + startDestination = startDestination, ) { composable(MainRoutes.Main.route) { MainContentHost() - LaunchedEffect(Unit) { FilePermission.verifyStoragePermission(this@MainActivity) } + LaunchedEffect(Unit) { + FilePermission.verifyStoragePermission(this@MainActivity) + } } composable(MainRoutes.Disclaimer.route) { DisclaimerScreen(navController) { finishAffinity() } } } diff --git a/core/main/src/main/java/com/rk/activities/main/MainContent.kt b/core/main/src/main/java/com/rk/activities/main/MainContent.kt index 84f7e11f0..cc89672db 100644 --- a/core/main/src/main/java/com/rk/activities/main/MainContent.kt +++ b/core/main/src/main/java/com/rk/activities/main/MainContent.kt @@ -48,15 +48,16 @@ import com.mohamedrejeb.compose.dnd.reorder.rememberReorderState import com.rk.commands.CommandPalette import com.rk.commands.CommandProvider import com.rk.components.compose.utils.addIf +import com.rk.drawer.DrawerViewModel import com.rk.editor.preloadSelectionColor import com.rk.filetree.FileAction import com.rk.filetree.FileActionContext import com.rk.filetree.FileActionDialogs +import com.rk.filetree.FileActionProvider import com.rk.filetree.FileIcon import com.rk.filetree.FileTreeViewModel import com.rk.filetree.MultiFileAction import com.rk.filetree.MultiFileActionContext -import com.rk.filetree.getActions import com.rk.icons.XedIcon import com.rk.resources.drawables import com.rk.resources.getString @@ -64,7 +65,7 @@ import com.rk.resources.strings import com.rk.settings.Settings import com.rk.tabs.base.Tab import com.rk.tabs.editor.EditorTab -import com.rk.utils.dialog +import com.rk.utils.dialogRes import com.rk.utils.drawErrorUnderline import com.rk.utils.getGitColor import com.rk.utils.getUnderlineColor @@ -74,6 +75,7 @@ import kotlinx.coroutines.launch fun MainContent( innerPadding: PaddingValues, mainViewModel: MainViewModel, + drawerViewModel: DrawerViewModel, fileTreeViewModel: FileTreeViewModel, drawerState: DrawerState, ) { @@ -82,7 +84,7 @@ fun MainContent( preloadSelectionColor() - FileActionDialogs(fileTreeViewModel, scope, context) + FileActionDialogs(drawerViewModel, fileTreeViewModel, scope, context) if (mainViewModel.isDraggingPalette || mainViewModel.showCommandPalette) { val lastUsedCommand = CommandProvider.getForId(Settings.last_used_command) @@ -146,12 +148,12 @@ fun MainContent( if (tabIndex == -1) return@TabItem if (tabState is EditorTab && tabState.editorState.isDirty) { - dialog( + dialogRes( title = strings.file_unsaved.getString(), msg = strings.ask_unsaved.getString(), onOk = { mainViewModel.tabManager.removeTab(tabIndex) }, onCancel = {}, - okString = strings.discard, + okRes = strings.discard, ) } else { mainViewModel.tabManager.removeTab(tabIndex) @@ -165,12 +167,12 @@ fun MainContent( tabIndex != index && (tab as? EditorTab)?.editorState?.isDirty == true } if (unsavedOtherTabs.isNotEmpty()) { - dialog( + dialogRes( title = strings.files_unsaved.getString(), msg = strings.ask_multiple_unsaved.getString(), onOk = { mainViewModel.tabManager.removeOtherTabs() }, onCancel = {}, - okString = strings.discard, + okRes = strings.discard, ) } else { mainViewModel.tabManager.removeOtherTabs() @@ -182,12 +184,12 @@ fun MainContent( (tab as? EditorTab)?.editorState?.isDirty == true } if (unsavedTabs.isNotEmpty()) { - dialog( + dialogRes( title = strings.files_unsaved.getString(), msg = strings.ask_multiple_unsaved.getString(), onOk = { mainViewModel.tabManager.removeAllTabs() }, onCancel = {}, - okString = strings.discard, + okRes = strings.discard, ) } else { mainViewModel.tabManager.removeAllTabs() @@ -297,6 +299,8 @@ private fun TabItemContent( val context = LocalContext.current val density = LocalDensity.current + val drawerViewModel = (context as MainActivity).drawerViewModel + val isSelected = mainViewModel.currentTabIndex == index val backgroundColor = MaterialTheme.colorScheme.surfaceVariant @@ -372,7 +376,7 @@ private fun TabItemContent( tabState.file?.let { DropdownMenu(expanded = showFileActionMenu, onDismissRequest = { showFileActionMenu = false }) { val root = (tabState as? EditorTab)?.projectRoot - val actions = remember(it) { getActions(it, root) } + val actions = remember(it) { FileActionProvider.getActions(it, root) } actions.forEach { action -> when (action) { @@ -382,7 +386,8 @@ private fun TabItemContent( leadingIcon = { XedIcon(action.icon, contentDescription = action.title) }, enabled = action.isEnabled(it), onClick = { - val context = FileActionContext(it, root, fileTreeViewModel, context) + val context = + FileActionContext(it, root, fileTreeViewModel, drawerViewModel, context) action.action(context) showFileActionMenu = false }, @@ -395,7 +400,8 @@ private fun TabItemContent( leadingIcon = { XedIcon(action.icon, contentDescription = action.title) }, enabled = action.isEnabled(files), onClick = { - val context = MultiFileActionContext(files, root, fileTreeViewModel, context) + val context = + MultiFileActionContext(files, root, fileTreeViewModel, drawerViewModel, context) action.action(context) showFileActionMenu = false }, diff --git a/core/main/src/main/java/com/rk/activities/main/MainContentHost.kt b/core/main/src/main/java/com/rk/activities/main/MainContentHost.kt index 39c171341..adedd27d4 100644 --- a/core/main/src/main/java/com/rk/activities/main/MainContentHost.kt +++ b/core/main/src/main/java/com/rk/activities/main/MainContentHost.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.isImeVisible +import androidx.compose.foundation.layout.padding import androidx.compose.material3.DrawerState import androidx.compose.material3.DrawerValue import androidx.compose.material3.MaterialTheme @@ -30,11 +31,9 @@ import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.lifecycle.viewmodel.compose.viewModel import com.rk.components.ResponsiveDrawer -import com.rk.filetree.DrawerContent -import com.rk.filetree.DrawerPersistence +import com.rk.drawer.DrawerContent +import com.rk.drawer.DrawerPersistence import com.rk.filetree.FileTreeViewModel -import com.rk.filetree.createServices -import com.rk.filetree.isLoading import com.rk.git.GitViewModel import com.rk.resources.getString import com.rk.resources.strings @@ -42,12 +41,12 @@ import com.rk.search.SearchViewModel import com.rk.settings.Settings import com.rk.tabs.editor.EditorTab import com.rk.theme.XedTheme -import com.rk.utils.dialog -import java.lang.ref.WeakReference +import com.rk.utils.dialogRes import kotlinx.coroutines.launch +import java.lang.ref.WeakReference -var fileTreeViewModel = WeakReference(null) var gitViewModel = WeakReference(null) +var fileTreeViewModel = WeakReference(null) var searchViewModel = WeakReference(null) var snackbarHostStateRef: WeakReference = WeakReference(null) @@ -60,8 +59,8 @@ fun MainActivity.MainContentHost( fileTreeViewModel: FileTreeViewModel = viewModel(), searchViewModel: SearchViewModel = viewModel(), ) { - com.rk.activities.main.fileTreeViewModel = WeakReference(fileTreeViewModel) com.rk.activities.main.gitViewModel = WeakReference(gitViewModel) + com.rk.activities.main.fileTreeViewModel = WeakReference(fileTreeViewModel) com.rk.activities.main.searchViewModel = WeakReference(searchViewModel) XedTheme { @@ -113,12 +112,12 @@ fun MainActivity.MainContentHost( if (drawerState.isOpen) { scope.launch { drawerState.close() } } else if (viewModel.tabs.isNotEmpty() && Settings.confirm_exit) { - dialog( + dialogRes( title = strings.attention.getString(), msg = strings.confirm_exit.getString(), onCancel = {}, onOk = { finish() }, - okString = strings.exit, + okRes = strings.exit, ) } else { finish() @@ -140,7 +139,10 @@ fun MainActivity.MainContentHost( contentWindowInsets = if (Settings.fullscreen) WindowInsets() else ScaffoldDefaults.contentWindowInsets, snackbarHost = { - SnackbarHost(hostState = snackbarHostState) { data -> + SnackbarHost( + hostState = snackbarHostState, + modifier = Modifier.padding(bottom = snackbarBottomPadding), + ) { data -> Snackbar( snackbarData = data, containerColor = MaterialTheme.colorScheme.surfaceVariant, @@ -155,6 +157,7 @@ fun MainActivity.MainContentHost( XedTopBar( drawerState = drawerState, viewModel = viewModel, + drawerViewModel = drawerViewModel, fullScreen = Settings.fullscreen, onDrag = { dragAmount -> accumulator += dragAmount @@ -182,19 +185,20 @@ fun MainActivity.MainContentHost( ) { innerPadding -> MainContent( innerPadding = innerPadding, - drawerState = drawerState, mainViewModel = viewModel, + drawerViewModel = drawerViewModel, fileTreeViewModel = fileTreeViewModel, + drawerState = drawerState, ) } } val sheetContent: @Composable ColumnScope.() -> Unit = { LaunchedEffect(Unit) { - isLoading = true - DrawerPersistence.restoreState() - createServices() - isLoading = false + drawerViewModel.isLoading = true + drawerViewModel.setupBuiltinServices(gitViewModel) + DrawerPersistence.restoreState(drawerViewModel) + drawerViewModel.isLoading = false } DrawerContent(Settings.fullscreen) } diff --git a/core/main/src/main/java/com/rk/activities/main/TopBar.kt b/core/main/src/main/java/com/rk/activities/main/TopBar.kt index 902510105..270d50ad4 100644 --- a/core/main/src/main/java/com/rk/activities/main/TopBar.kt +++ b/core/main/src/main/java/com/rk/activities/main/TopBar.kt @@ -19,6 +19,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.input.pointer.pointerInput import com.rk.components.GlobalToolbarActions import com.rk.components.isPermanentDrawer +import com.rk.drawer.DrawerViewModel import com.rk.resources.strings import com.rk.terminal.isV import com.rk.utils.toast @@ -29,6 +30,7 @@ import kotlinx.coroutines.launch fun XedTopBar( drawerState: DrawerState, viewModel: MainViewModel, + drawerViewModel: DrawerViewModel, fullScreen: Boolean, onDrag: (Float) -> Unit = {}, onDragEnd: () -> Unit = {}, @@ -59,7 +61,7 @@ fun XedTopBar( } }, actions = { - GlobalToolbarActions(viewModel) + GlobalToolbarActions(viewModel, drawerViewModel) if (viewModel.tabs.isNotEmpty()) { val tab = diff --git a/core/main/src/main/java/com/rk/activities/settings/SettingsNavHost.kt b/core/main/src/main/java/com/rk/activities/settings/SettingsNavHost.kt index 8a1c75db2..7233b7068 100644 --- a/core/main/src/main/java/com/rk/activities/settings/SettingsNavHost.kt +++ b/core/main/src/main/java/com/rk/activities/settings/SettingsNavHost.kt @@ -1,8 +1,6 @@ package com.rk.activities.settings import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue import androidx.navigation.NavHostController import androidx.navigation.NavType import androidx.navigation.compose.NavHost @@ -23,10 +21,12 @@ import com.rk.settings.editor.EditExtraKeys import com.rk.settings.editor.EditToolbarActions import com.rk.settings.editor.EditorFontScreen import com.rk.settings.editor.ExcludeFiles +import com.rk.settings.editor.FormatterSettings import com.rk.settings.editor.SettingsEditorScreen import com.rk.settings.editor.TerminalFontScreen import com.rk.settings.extension.ExtensionDetail import com.rk.settings.extension.ExtensionScreen +import com.rk.settings.extension.ExtensionSettings import com.rk.settings.git.GitSettings import com.rk.settings.keybinds.KeybindingsScreen import com.rk.settings.language.LanguageScreen @@ -37,6 +37,7 @@ import com.rk.settings.runners.HtmlRunnerSettings import com.rk.settings.runners.RunnerSettings import com.rk.settings.support.Support import com.rk.settings.terminal.SettingsTerminalScreen +import com.rk.settings.terminal.TerminalCheckScreen import com.rk.settings.terminal.TerminalExtraKeys import com.rk.settings.theme.ThemeScreen @@ -56,6 +57,7 @@ fun SettingsNavHost(navController: NavHostController, activity: SettingsActivity composable(SettingsRoutes.Keybindings.route) { KeybindingsScreen() } composable(SettingsRoutes.TerminalSettings.route) { SettingsTerminalScreen() } composable(SettingsRoutes.TerminalExtraKeys.route) { TerminalExtraKeys() } + composable(SettingsRoutes.TerminalCheck.route) { TerminalCheckScreen() } composable(SettingsRoutes.About.route) { AboutScreen() } composable(SettingsRoutes.EditorFontScreen.route) { EditorFontScreen() } composable(SettingsRoutes.AppFontScreen.route) { AppFontScreen() } @@ -77,7 +79,8 @@ fun SettingsNavHost(navController: NavHostController, activity: SettingsActivity composable(SettingsRoutes.LanguageScreen.route) { LanguageScreen() } composable(SettingsRoutes.Runners.route) { RunnerSettings(navController = navController) } composable(SettingsRoutes.HtmlRunner.route) { HtmlRunnerSettings() } - composable(SettingsRoutes.LspSettings.route) { LspSettings(navController = navController) } + composable(SettingsRoutes.Formatters.route) { FormatterSettings(navController) } + composable(SettingsRoutes.LspSettings.route) { LspSettings(navController) } composable( "${SettingsRoutes.LspServerDetail.route}/{serverId}", arguments = listOf(navArgument("serverId", builder = { type = NavType.StringType })), @@ -107,7 +110,15 @@ fun SettingsNavHost(navController: NavHostController, activity: SettingsActivity ) { val extensionId = it.arguments?.getString("extensionId") val extension = extensionId?.let { App.extensionManager.getExtension(it) } - ExtensionDetail(extension) + ExtensionDetail(extension, navController) + } + composable( + "${SettingsRoutes.ExtensionSettings.route}/{extensionId}", + arguments = listOf(navArgument("extensionId", builder = { type = NavType.StringType })), + ) { + val extensionId = it.arguments?.getString("extensionId") + val extension = extensionId?.let { App.extensionManager.getExtension(it) } + ExtensionSettings(extension) } composable(SettingsRoutes.Git.route) { GitSettings() } } diff --git a/core/main/src/main/java/com/rk/activities/settings/SettingsRoutes.kt b/core/main/src/main/java/com/rk/activities/settings/SettingsRoutes.kt index 8f1d3b12d..590a742f4 100644 --- a/core/main/src/main/java/com/rk/activities/settings/SettingsRoutes.kt +++ b/core/main/src/main/java/com/rk/activities/settings/SettingsRoutes.kt @@ -13,6 +13,8 @@ sealed class SettingsRoutes(val route: String) { data object TerminalExtraKeys : SettingsRoutes("terminal_extra_keys") + data object TerminalCheck : SettingsRoutes("terminal_check") + data object About : SettingsRoutes("about") data object EditorFontScreen : SettingsRoutes("editor_font_screen") @@ -35,12 +37,16 @@ sealed class SettingsRoutes(val route: String) { data object ExtensionDetail : SettingsRoutes("extension_detail") + data object ExtensionSettings : SettingsRoutes("extension_settings") + data object DeveloperOptions : SettingsRoutes("developer_options") data object AppLogs : SettingsRoutes("app_logs") data object Support : SettingsRoutes("support") + data object Formatters : SettingsRoutes("formatters") + data object LanguageScreen : SettingsRoutes("language") data object Runners : SettingsRoutes("runners") diff --git a/core/main/src/main/java/com/rk/activities/terminal/Terminal.kt b/core/main/src/main/java/com/rk/activities/terminal/Terminal.kt index bf8578b43..47ec180b2 100644 --- a/core/main/src/main/java/com/rk/activities/terminal/Terminal.kt +++ b/core/main/src/main/java/com/rk/activities/terminal/Terminal.kt @@ -47,7 +47,6 @@ import com.rk.XedConstants import com.rk.exec.isTerminalInstalled import com.rk.file.child import com.rk.file.localBinDir -import com.rk.file.localLibDir import com.rk.file.sandboxDir import com.rk.resources.getString import com.rk.resources.strings @@ -215,36 +214,7 @@ class Terminal : AppCompatActivity() { try { val abi = Build.SUPPORTED_ABIS - val filesToDownload = - listOf( - DownloadFile( - url = - if (abi.contains("x86_64")) { - XedConstants.TALLOC_X64 - } else if (abi.contains("arm64-v8a")) { - XedConstants.TALLOC_ARM64 - } else if (abi.contains("armeabi-v7a")) { - XedConstants.TALLOC_ARM - } else { - throw RuntimeException("Unsupported CPU") - }, - outputFile = localLibDir().child("libtalloc.so.2"), - ), - DownloadFile( - url = - if (abi.contains("x86_64")) { - XedConstants.PROOT_X64 - } else if (abi.contains("arm64-v8a")) { - XedConstants.PROOT_ARM64 - } else if (abi.contains("armeabi-v7a")) { - XedConstants.PROOT_ARM - } else { - throw RuntimeException("Unsupported CPU") - }, - outputFile = localBinDir().child("proot"), - ), - ) - .toMutableList() + val filesToDownload = mutableListOf() if (isTerminalInstalled().not()) { filesToDownload.add( @@ -304,7 +274,7 @@ class Terminal : AppCompatActivity() { File(getTempDir(), "sandbox.tar.gz").delete() } } - errorDialog("Setup failed: ${error.message}") + errorDialog(msg = "Setup failed: ${error.message}") } } finish() diff --git a/core/main/src/main/java/com/rk/commands/CommandAPI.kt b/core/main/src/main/java/com/rk/commands/CommandAPI.kt index 0b3601b82..50b792db3 100644 --- a/core/main/src/main/java/com/rk/commands/CommandAPI.kt +++ b/core/main/src/main/java/com/rk/commands/CommandAPI.kt @@ -1,15 +1,23 @@ package com.rk.commands import android.app.Activity +import com.rk.activities.main.MainActivity import com.rk.activities.main.MainViewModel +import com.rk.drawer.DrawerViewModel import com.rk.editor.Editor import com.rk.icons.Icon import com.rk.lsp.LspConnector import com.rk.tabs.editor.EditorTab -data class CommandContext(private val provider: () -> MainViewModel) { +data class CommandContext( + private val mainViewModelProvider: () -> MainViewModel, + private val drawerViewModelProvider: () -> DrawerViewModel, +) { val mainViewModel: MainViewModel - get() = provider() + get() = mainViewModelProvider() + + val drawerViewModel: DrawerViewModel + get() = drawerViewModelProvider() } data class ActionContext(val currentActivity: Activity) @@ -27,20 +35,25 @@ data class LspActionContext( data class LspNonActionContext(val editorTab: EditorTab, val lspConnector: LspConnector) -abstract class Command(val commandContext: CommandContext) { +abstract class Command { + protected val commandContext = + CommandContext( + mainViewModelProvider = { MainActivity.instance!!.viewModel }, + drawerViewModelProvider = { MainActivity.instance!!.drawerViewModel }, + ) abstract val id: String open val prefix: String? = null abstract fun getLabel(): String + abstract fun getIcon(): Icon + abstract fun action(actionContext: ActionContext) open fun isEnabled(): Boolean = true open fun isSupported(): Boolean = true - abstract fun getIcon(): Icon - open val preferText: Boolean = false open val childCommands: List = emptyList() @@ -73,7 +86,7 @@ abstract class Command(val commandContext: CommandContext) { sectionId: Int = this.sectionId, defaultKeybinds: KeyCombination? = this.defaultKeybinds, ): Command { - return object : Command(commandContext) { + return object : Command() { override val id: String = id override val prefix: String? = prefix @@ -115,9 +128,9 @@ interface ToggleableCommand { fun isOn(): Boolean } -abstract class GlobalCommand(commandContext: CommandContext) : Command(commandContext) +abstract class GlobalCommand : Command() -abstract class EditorCommand(commandContext: CommandContext) : Command(commandContext) { +abstract class EditorCommand : Command() { final override fun action(actionContext: ActionContext) { val currentTab = commandContext.mainViewModel.currentTab val editor = (currentTab as? EditorTab)?.editorState?.editor?.get() ?: return @@ -143,7 +156,7 @@ abstract class EditorCommand(commandContext: CommandContext) : Command(commandCo open fun isEnabled(editorNonActionContext: EditorNonActionContext): Boolean = true } -abstract class LspCommand(commandContext: CommandContext) : EditorCommand(commandContext) { +abstract class LspCommand : EditorCommand() { final override fun action(editorActionContext: EditorActionContext) { val currentTab = editorActionContext.editorTab val editor = editorActionContext.editor diff --git a/core/main/src/main/java/com/rk/commands/CommandPalette.kt b/core/main/src/main/java/com/rk/commands/CommandPalette.kt index de25a9a56..69e4eae21 100644 --- a/core/main/src/main/java/com/rk/commands/CommandPalette.kt +++ b/core/main/src/main/java/com/rk/commands/CommandPalette.kt @@ -80,7 +80,7 @@ fun CommandPalette( derivedStateOf { buildList { lastUsedCommand?.let { add(it) } - addAll(commands.filter { it != lastUsedCommand }) + addAll(commands.filter { it != lastUsedCommand }.sortedBy { it.getLabel() }) } } } @@ -90,10 +90,14 @@ fun CommandPalette( val filteredCommands by remember(visibleCommands, searchQuery) { derivedStateOf { - visibleCommands.filter { - it.getLabel().contains(searchQuery, ignoreCase = true) || - it.prefix?.contains(searchQuery, ignoreCase = true) == true - } + visibleCommands + .filter { + it.getLabel().contains(searchQuery, ignoreCase = true) || + it.prefix?.contains(searchQuery, ignoreCase = true) == true + } + .filter { + it.isSupported() + } } } @@ -188,7 +192,7 @@ fun CommandItem( isSubpage: Boolean, ) { val activity = LocalActivity.current - val enabled by remember { derivedStateOf { command.isSupported() && command.isEnabled() } } + val enabled by remember { derivedStateOf { command.isEnabled() } } val childCommands = command.childCommands val keyCombination = KeybindingsManager.getKeyCombinationForCommand(command.id) diff --git a/core/main/src/main/java/com/rk/commands/CommandProvider.kt b/core/main/src/main/java/com/rk/commands/CommandProvider.kt index bd3a49343..aa057d4c5 100644 --- a/core/main/src/main/java/com/rk/commands/CommandProvider.kt +++ b/core/main/src/main/java/com/rk/commands/CommandProvider.kt @@ -1,6 +1,6 @@ package com.rk.commands -import com.rk.activities.main.MainActivity +import androidx.compose.runtime.mutableStateListOf import com.rk.commands.editor.CopyCommand import com.rk.commands.editor.CutCommand import com.rk.commands.editor.DuplicateLineCommand @@ -17,6 +17,8 @@ import com.rk.commands.editor.SearchCommand import com.rk.commands.editor.SelectAllCommand import com.rk.commands.editor.SelectWordCommand import com.rk.commands.editor.ShareCommand +import com.rk.commands.editor.SortLinesAscendingCommand +import com.rk.commands.editor.SortLinesDescendingCommand import com.rk.commands.editor.SyntaxHighlightingCommand import com.rk.commands.editor.ToggleReadOnlyCommand import com.rk.commands.editor.ToggleWordWrapCommand @@ -35,11 +37,12 @@ import com.rk.commands.lsp.FormatSelectionCommand import com.rk.commands.lsp.GoToDefinitionCommand import com.rk.commands.lsp.GoToReferencesCommand import com.rk.commands.lsp.RenameSymbolCommand +import com.rk.extension.api.XedExtensionPoint object CommandProvider { - private val _commandList = mutableListOf() + private val _commandList = mutableStateListOf() val commandList: List - get() = _commandList.toList() + get() = _commandList lateinit var DocumentationCommand: DocumentationCommand lateinit var TerminalCommand: TerminalCommand @@ -68,6 +71,8 @@ object CommandProvider { lateinit var SyntaxHighlightingCommand: SyntaxHighlightingCommand lateinit var ToggleWordWrapCommand: ToggleWordWrapCommand lateinit var JumpToLineCommand: JumpToLineCommand + lateinit var SortLinesAscendingCommand: SortLinesAscendingCommand + lateinit var SortLinesDescendingCommand: SortLinesDescendingCommand lateinit var ShareCommand: ShareCommand lateinit var EmulateKeyCommand: EmulateKeyCommand lateinit var GoToDefinitionCommand: GoToDefinitionCommand @@ -78,42 +83,42 @@ object CommandProvider { fun buildCommands() = synchronized(this) { - val commandContext = CommandContext { MainActivity.instance!!.viewModel } - - registerBuiltin(DocumentationCommand(commandContext)) { DocumentationCommand = it } - registerBuiltin(TerminalCommand(commandContext)) { TerminalCommand = it } - registerBuiltin(SettingsCommand(commandContext)) { SettingsCommand = it } - registerBuiltin(NewFileCommand(commandContext)) { NewFileCommand = it } - registerBuiltin(CommandPaletteCommand(commandContext)) { CommandPaletteCommand = it } - registerBuiltin(SearchFileFolderCommand(commandContext)) { SearchFileFolderCommand = it } - registerBuiltin(SearchCodeCommand(commandContext)) { SearchCodeCommand = it } - registerBuiltin(CutCommand(commandContext)) { CutCommand = it } - registerBuiltin(CopyCommand(commandContext)) { CopyCommand = it } - registerBuiltin(PasteCommand(commandContext)) { PasteCommand = it } - registerBuiltin(SelectAllCommand(commandContext)) { SelectAllCommand = it } - registerBuiltin(SelectWordCommand(commandContext)) { SelectWordCommand = it } - registerBuiltin(DuplicateLineCommand(commandContext)) { DuplicateLineCommand = it } - registerBuiltin(LowerCaseCommand(commandContext)) { LowerCaseCommand = it } - registerBuiltin(UpperCaseCommand(commandContext)) { UpperCaseCommand = it } - registerBuiltin(SaveCommand(commandContext)) { SaveCommand = it } - registerBuiltin(SaveAllCommand(commandContext)) { SaveAllCommand = it } - registerBuiltin(UndoCommand(commandContext)) { UndoCommand = it } - registerBuiltin(RedoCommand(commandContext)) { RedoCommand = it } - registerBuiltin(RunCommand(commandContext)) { RunCommand = it } - registerBuiltin(ToggleReadOnlyCommand(commandContext)) { ToggleReadOnlyCommand = it } - registerBuiltin(SearchCommand(commandContext)) { SearchCommand = it } - registerBuiltin(ReplaceCommand(commandContext)) { ReplaceCommand = it } - registerBuiltin(RefreshCommand(commandContext)) { RefreshCommand = it } - registerBuiltin(SyntaxHighlightingCommand(commandContext)) { SyntaxHighlightingCommand = it } - registerBuiltin(ToggleWordWrapCommand(commandContext)) { ToggleWordWrapCommand = it } - registerBuiltin(JumpToLineCommand(commandContext)) { JumpToLineCommand = it } - registerBuiltin(ShareCommand(commandContext)) { ShareCommand = it } - registerBuiltin(EmulateKeyCommand(commandContext)) { EmulateKeyCommand = it } - registerBuiltin(GoToDefinitionCommand(commandContext)) { GoToDefinitionCommand = it } - registerBuiltin(GoToReferencesCommand(commandContext)) { GoToReferencesCommand = it } - registerBuiltin(RenameSymbolCommand(commandContext)) { RenameSymbolCommand = it } - registerBuiltin(FormatDocumentCommand(commandContext)) { FormatDocumentCommand = it } - registerBuiltin(FormatSelectionCommand(commandContext)) { FormatSelectionCommand = it } + registerBuiltin(DocumentationCommand()) { DocumentationCommand = it } + registerBuiltin(TerminalCommand()) { TerminalCommand = it } + registerBuiltin(SettingsCommand()) { SettingsCommand = it } + registerBuiltin(NewFileCommand()) { NewFileCommand = it } + registerBuiltin(CommandPaletteCommand()) { CommandPaletteCommand = it } + registerBuiltin(SearchFileFolderCommand()) { SearchFileFolderCommand = it } + registerBuiltin(SearchCodeCommand()) { SearchCodeCommand = it } + registerBuiltin(CutCommand()) { CutCommand = it } + registerBuiltin(CopyCommand()) { CopyCommand = it } + registerBuiltin(PasteCommand()) { PasteCommand = it } + registerBuiltin(SelectAllCommand()) { SelectAllCommand = it } + registerBuiltin(SelectWordCommand()) { SelectWordCommand = it } + registerBuiltin(DuplicateLineCommand()) { DuplicateLineCommand = it } + registerBuiltin(LowerCaseCommand()) { LowerCaseCommand = it } + registerBuiltin(UpperCaseCommand()) { UpperCaseCommand = it } + registerBuiltin(SaveCommand()) { SaveCommand = it } + registerBuiltin(SaveAllCommand()) { SaveAllCommand = it } + registerBuiltin(UndoCommand()) { UndoCommand = it } + registerBuiltin(RedoCommand()) { RedoCommand = it } + registerBuiltin(RunCommand()) { RunCommand = it } + registerBuiltin(ToggleReadOnlyCommand()) { ToggleReadOnlyCommand = it } + registerBuiltin(SearchCommand()) { SearchCommand = it } + registerBuiltin(ReplaceCommand()) { ReplaceCommand = it } + registerBuiltin(RefreshCommand()) { RefreshCommand = it } + registerBuiltin(SyntaxHighlightingCommand()) { SyntaxHighlightingCommand = it } + registerBuiltin(ToggleWordWrapCommand()) { ToggleWordWrapCommand = it } + registerBuiltin(JumpToLineCommand()) { JumpToLineCommand = it } + registerBuiltin(SortLinesAscendingCommand()) { SortLinesAscendingCommand = it } + registerBuiltin(SortLinesDescendingCommand()) { SortLinesDescendingCommand = it } + registerBuiltin(ShareCommand()) { ShareCommand = it } + registerBuiltin(EmulateKeyCommand()) { EmulateKeyCommand = it } + registerBuiltin(GoToDefinitionCommand()) { GoToDefinitionCommand = it } + registerBuiltin(GoToReferencesCommand()) { GoToReferencesCommand = it } + registerBuiltin(RenameSymbolCommand()) { RenameSymbolCommand = it } + registerBuiltin(FormatDocumentCommand()) { FormatDocumentCommand = it } + registerBuiltin(FormatSelectionCommand()) { FormatSelectionCommand = it } } private fun registerBuiltin(command: T, assign: (T) -> Unit) { @@ -122,12 +127,17 @@ object CommandProvider { _commandList.add(command) } + @XedExtensionPoint fun registerCommand(command: Command) { - if (!_commandList.contains(command)) { + val index = _commandList.indexOf(command) + if (index >= 0) { + _commandList[index] = command + } else { _commandList.add(command) } } + @XedExtensionPoint fun unregisterCommand(command: Command) { _commandList.remove(command) } diff --git a/core/main/src/main/java/com/rk/commands/ToolbarConfiguration.kt b/core/main/src/main/java/com/rk/commands/ToolbarConfiguration.kt new file mode 100644 index 000000000..f6755089d --- /dev/null +++ b/core/main/src/main/java/com/rk/commands/ToolbarConfiguration.kt @@ -0,0 +1,102 @@ +package com.rk.commands + +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.snapshots.SnapshotStateList +import com.rk.extension.api.XedExtensionPoint +import com.rk.settings.Settings + +object ToolbarConfiguration { + const val DEFAULT_EDITOR_TOOLBAR_COMMANDS = + "editor.undo|editor.redo|editor.save|editor.run|global.new_file|editor.editable|editor.search|editor.refresh|global.terminal|global.settings" + + val editorCommands: List + get() = Settings.action_items.split("|").mapNotNull { CommandProvider.getForId(it) } + + @XedExtensionPoint + fun addEditorToolbarCommand(commandId: String, index: Int? = null) { + val items = Settings.action_items.split("|").toMutableList() + + if (commandId in items) { + if (index == null) return + + items.remove(commandId) + items.add(index.coerceIn(0, items.size), commandId) + Settings.action_items = items.joinToString("|") + return + } + + if (index != null) { + items.add(index.coerceIn(0, items.size), commandId) + } else { + items.add(commandId) + } + + Settings.action_items = items.joinToString("|") + } + + @XedExtensionPoint + fun addEditorToolbarCommand(command: Command, index: Int? = null) { + addEditorToolbarCommand(command.id, index) + } + + @XedExtensionPoint + fun removeEditorToolbarCommand(commandId: String) { + val items = Settings.action_items.split("|").toMutableList() + + if (!items.remove(commandId)) { + return + } + + Settings.action_items = items.joinToString("|") + } + + @XedExtensionPoint + fun removeEditorToolbarCommand(command: Command) { + removeEditorToolbarCommand(command.id) + } + + private var _globalCommands: SnapshotStateList = + mutableStateListOf( + CommandProvider.NewFileCommand, + CommandProvider.TerminalCommand, + CommandProvider.SettingsCommand, + ) + + val globalCommands: List + get() = _globalCommands + + @XedExtensionPoint + fun addGlobalToolbarCommand(command: Command, index: Int? = null) { + val existingIndex = _globalCommands.indexOf(command) + + if (existingIndex != -1) { + _globalCommands.removeAt(existingIndex) + } + + val insertIndex = + when { + index != null -> index + existingIndex != -1 -> existingIndex + else -> _globalCommands.size + } + + _globalCommands.add(insertIndex.coerceIn(0, _globalCommands.size), command) + } + + @XedExtensionPoint + fun addGlobalToolbarCommand(commandId: String, index: Int? = null) { + val command = CommandProvider.getForId(commandId) ?: return + addGlobalToolbarCommand(command, index) + } + + @XedExtensionPoint + fun removeGlobalToolbarCommand(command: Command) { + _globalCommands.remove(command) + } + + @XedExtensionPoint + fun removeGlobalToolbarCommand(commandId: String) { + val command = CommandProvider.getForId(commandId) ?: return + removeGlobalToolbarCommand(command) + } +} diff --git a/core/main/src/main/java/com/rk/commands/editor/CopyCommand.kt b/core/main/src/main/java/com/rk/commands/editor/CopyCommand.kt index dc66aeac5..48ccf8aff 100644 --- a/core/main/src/main/java/com/rk/commands/editor/CopyCommand.kt +++ b/core/main/src/main/java/com/rk/commands/editor/CopyCommand.kt @@ -1,7 +1,6 @@ package com.rk.commands.editor import android.view.KeyEvent -import com.rk.commands.CommandContext import com.rk.commands.EditorActionContext import com.rk.commands.EditorCommand import com.rk.commands.KeyCombination @@ -10,7 +9,7 @@ import com.rk.resources.drawables import com.rk.resources.getString import com.rk.resources.strings -class CopyCommand(commandContext: CommandContext) : EditorCommand(commandContext) { +class CopyCommand : EditorCommand() { override val id: String = "editor.copy" override fun getLabel(): String = strings.copy.getString() @@ -19,7 +18,7 @@ class CopyCommand(commandContext: CommandContext) : EditorCommand(commandContext editorActionContext.editor.copyText() } - override fun getIcon(): Icon = Icon.DrawableRes(drawables.copy) + override fun getIcon(): Icon = Icon.ResourceIcon(drawables.copy) override val defaultKeybinds: KeyCombination = KeyCombination(keyCode = KeyEvent.KEYCODE_C, ctrl = true) } diff --git a/core/main/src/main/java/com/rk/commands/editor/CutCommand.kt b/core/main/src/main/java/com/rk/commands/editor/CutCommand.kt index 9ace35740..7e0513966 100644 --- a/core/main/src/main/java/com/rk/commands/editor/CutCommand.kt +++ b/core/main/src/main/java/com/rk/commands/editor/CutCommand.kt @@ -1,7 +1,6 @@ package com.rk.commands.editor import android.view.KeyEvent -import com.rk.commands.CommandContext import com.rk.commands.EditorActionContext import com.rk.commands.EditorCommand import com.rk.commands.EditorNonActionContext @@ -11,7 +10,7 @@ import com.rk.resources.drawables import com.rk.resources.getString import com.rk.resources.strings -class CutCommand(commandContext: CommandContext) : EditorCommand(commandContext) { +class CutCommand : EditorCommand() { override val id: String = "editor.cut" override fun getLabel(): String = strings.cut.getString() @@ -24,7 +23,7 @@ class CutCommand(commandContext: CommandContext) : EditorCommand(commandContext) return editorNonActionContext.editorTab.editorState.editable } - override fun getIcon(): Icon = Icon.DrawableRes(drawables.cut) + override fun getIcon(): Icon = Icon.ResourceIcon(drawables.cut) override val defaultKeybinds: KeyCombination = KeyCombination(keyCode = KeyEvent.KEYCODE_X, ctrl = true) } diff --git a/core/main/src/main/java/com/rk/commands/editor/DuplicateLineCommand.kt b/core/main/src/main/java/com/rk/commands/editor/DuplicateLineCommand.kt index 850247939..22ca2450b 100644 --- a/core/main/src/main/java/com/rk/commands/editor/DuplicateLineCommand.kt +++ b/core/main/src/main/java/com/rk/commands/editor/DuplicateLineCommand.kt @@ -1,7 +1,6 @@ package com.rk.commands.editor import android.view.KeyEvent -import com.rk.commands.CommandContext import com.rk.commands.EditorActionContext import com.rk.commands.EditorCommand import com.rk.commands.EditorNonActionContext @@ -11,7 +10,7 @@ import com.rk.resources.drawables import com.rk.resources.getString import com.rk.resources.strings -class DuplicateLineCommand(commandContext: CommandContext) : EditorCommand(commandContext) { +class DuplicateLineCommand : EditorCommand() { override val id: String = "editor.duplicate_line" override fun getLabel(): String = strings.duplicate_line.getString() @@ -24,7 +23,7 @@ class DuplicateLineCommand(commandContext: CommandContext) : EditorCommand(comma return editorNonActionContext.editorTab.editorState.editable } - override fun getIcon(): Icon = Icon.DrawableRes(drawables.duplicate_line) + override fun getIcon(): Icon = Icon.ResourceIcon(drawables.duplicate_line) override val defaultKeybinds: KeyCombination = KeyCombination(keyCode = KeyEvent.KEYCODE_D, ctrl = true) } diff --git a/core/main/src/main/java/com/rk/commands/editor/EmulateKeyCommand.kt b/core/main/src/main/java/com/rk/commands/editor/EmulateKeyCommand.kt index aebe5db0f..a06a98634 100644 --- a/core/main/src/main/java/com/rk/commands/editor/EmulateKeyCommand.kt +++ b/core/main/src/main/java/com/rk/commands/editor/EmulateKeyCommand.kt @@ -5,7 +5,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import com.rk.commands.Command -import com.rk.commands.CommandContext import com.rk.commands.EditorActionContext import com.rk.commands.EditorCommand import com.rk.commands.ToggleableCommand @@ -38,7 +37,7 @@ object EmulateKeyCommandSection { const val KEY_SECTION = 1 } -class EmulateKeyCommand(commandContext: CommandContext) : EditorCommand(commandContext) { +class EmulateKeyCommand : EditorCommand() { val modifierState = ModifierState() override val id: String = "editor.emulate_key" @@ -47,7 +46,7 @@ class EmulateKeyCommand(commandContext: CommandContext) : EditorCommand(commandC override fun action(editorActionContext: EditorActionContext) {} - override fun getIcon(): Icon = Icon.DrawableRes(drawables.keyboard) + override fun getIcon(): Icon = Icon.ResourceIcon(drawables.keyboard) override val childCommands: List by lazy { val keyEvents = KeyEvent::class.java.fields @@ -83,7 +82,7 @@ class EmulateKeyCommand(commandContext: CommandContext) : EditorCommand(commandC val keyDisplayName = KeyUtils.getKeyDisplayName(metaEvent.metaKeyCode) val keyIcon = KeyUtils.getKeyIcon(metaEvent.keyCode) - object : EditorCommand(commandContext), ToggleableCommand { + object : EditorCommand(), ToggleableCommand { override val id: String = "editor.emulate_key.${keyDisplayName.lowercase()}" override val preferText: Boolean = metaEvent.keyCode != KeyEvent.KEYCODE_SHIFT_LEFT @@ -117,7 +116,7 @@ class EmulateKeyCommand(commandContext: CommandContext) : EditorCommand(commandC // To avoid confusion, do not show KEYCODE variants of modifier keys if (KeyUtils.isModifierKey(keyCode)) return@mapNotNull null - object : EditorCommand(commandContext) { + object : EditorCommand() { override val id: String = "editor.emulate_key.${keyName.lowercase()}" override fun getLabel(): String = keyDisplayName diff --git a/core/main/src/main/java/com/rk/commands/editor/JumpToLineCommand.kt b/core/main/src/main/java/com/rk/commands/editor/JumpToLineCommand.kt index 3dee0f8d0..10d652c51 100644 --- a/core/main/src/main/java/com/rk/commands/editor/JumpToLineCommand.kt +++ b/core/main/src/main/java/com/rk/commands/editor/JumpToLineCommand.kt @@ -1,7 +1,6 @@ package com.rk.commands.editor import android.view.KeyEvent -import com.rk.commands.CommandContext import com.rk.commands.EditorActionContext import com.rk.commands.EditorCommand import com.rk.commands.KeyCombination @@ -10,7 +9,7 @@ import com.rk.resources.drawables import com.rk.resources.getString import com.rk.resources.strings -class JumpToLineCommand(commandContext: CommandContext) : EditorCommand(commandContext) { +class JumpToLineCommand : EditorCommand() { override val id: String = "editor.jump_to_line" override fun getLabel(): String = strings.jump_to_line.getString() @@ -19,7 +18,7 @@ class JumpToLineCommand(commandContext: CommandContext) : EditorCommand(commandC editorActionContext.editorTab.editorState.showJumpToLineDialog = true } - override fun getIcon(): Icon = Icon.DrawableRes(drawables.arrow_outward) + override fun getIcon(): Icon = Icon.ResourceIcon(drawables.arrow_outward) override val defaultKeybinds: KeyCombination = KeyCombination(keyCode = KeyEvent.KEYCODE_G, ctrl = true) } diff --git a/core/main/src/main/java/com/rk/commands/editor/LowerCaseCommand.kt b/core/main/src/main/java/com/rk/commands/editor/LowerCaseCommand.kt index c77ffbd2f..03ef19557 100644 --- a/core/main/src/main/java/com/rk/commands/editor/LowerCaseCommand.kt +++ b/core/main/src/main/java/com/rk/commands/editor/LowerCaseCommand.kt @@ -1,6 +1,5 @@ package com.rk.commands.editor -import com.rk.commands.CommandContext import com.rk.commands.EditorActionContext import com.rk.commands.EditorCommand import com.rk.icons.Icon @@ -8,7 +7,7 @@ import com.rk.resources.drawables import com.rk.resources.getString import com.rk.resources.strings -class LowerCaseCommand(commandContext: CommandContext) : EditorCommand(commandContext) { +class LowerCaseCommand : EditorCommand() { override val id: String = "editor.lowercase" override fun getLabel(): String = strings.transform_lowercase.getString() @@ -23,5 +22,5 @@ class LowerCaseCommand(commandContext: CommandContext) : EditorCommand(commandCo } } - override fun getIcon(): Icon = Icon.DrawableRes(drawables.letters) + override fun getIcon(): Icon = Icon.ResourceIcon(drawables.letters) } diff --git a/core/main/src/main/java/com/rk/commands/editor/PasteCommand.kt b/core/main/src/main/java/com/rk/commands/editor/PasteCommand.kt index 735d5600b..533b8b85c 100644 --- a/core/main/src/main/java/com/rk/commands/editor/PasteCommand.kt +++ b/core/main/src/main/java/com/rk/commands/editor/PasteCommand.kt @@ -1,7 +1,6 @@ package com.rk.commands.editor import android.view.KeyEvent -import com.rk.commands.CommandContext import com.rk.commands.EditorActionContext import com.rk.commands.EditorCommand import com.rk.commands.EditorNonActionContext @@ -11,7 +10,7 @@ import com.rk.resources.drawables import com.rk.resources.getString import com.rk.resources.strings -class PasteCommand(commandContext: CommandContext) : EditorCommand(commandContext) { +class PasteCommand : EditorCommand() { override val id: String = "editor.paste" override fun getLabel(): String = strings.paste.getString() @@ -24,7 +23,7 @@ class PasteCommand(commandContext: CommandContext) : EditorCommand(commandContex return editorNonActionContext.editorTab.editorState.editable } - override fun getIcon(): Icon = Icon.DrawableRes(drawables.paste) + override fun getIcon(): Icon = Icon.ResourceIcon(drawables.paste) override val defaultKeybinds: KeyCombination = KeyCombination(keyCode = KeyEvent.KEYCODE_V, ctrl = true) } diff --git a/core/main/src/main/java/com/rk/commands/editor/RedoCommand.kt b/core/main/src/main/java/com/rk/commands/editor/RedoCommand.kt index b35454d00..d8242bf92 100644 --- a/core/main/src/main/java/com/rk/commands/editor/RedoCommand.kt +++ b/core/main/src/main/java/com/rk/commands/editor/RedoCommand.kt @@ -1,7 +1,6 @@ package com.rk.commands.editor import android.view.KeyEvent -import com.rk.commands.CommandContext import com.rk.commands.EditorActionContext import com.rk.commands.EditorCommand import com.rk.commands.EditorNonActionContext @@ -11,7 +10,7 @@ import com.rk.resources.drawables import com.rk.resources.getString import com.rk.resources.strings -class RedoCommand(commandContext: CommandContext) : EditorCommand(commandContext) { +class RedoCommand : EditorCommand() { override val id: String = "editor.redo" override fun getLabel(): String = strings.redo.getString() @@ -27,7 +26,7 @@ class RedoCommand(commandContext: CommandContext) : EditorCommand(commandContext return editorState.editable && editorState.canRedo } - override fun getIcon(): Icon = Icon.DrawableRes(drawables.redo) + override fun getIcon(): Icon = Icon.ResourceIcon(drawables.redo) override val defaultKeybinds: KeyCombination = KeyCombination(keyCode = KeyEvent.KEYCODE_Y, ctrl = true) } diff --git a/core/main/src/main/java/com/rk/commands/editor/RefreshCommand.kt b/core/main/src/main/java/com/rk/commands/editor/RefreshCommand.kt index ae0e29433..41edf04e9 100644 --- a/core/main/src/main/java/com/rk/commands/editor/RefreshCommand.kt +++ b/core/main/src/main/java/com/rk/commands/editor/RefreshCommand.kt @@ -1,7 +1,6 @@ package com.rk.commands.editor import android.view.KeyEvent -import com.rk.commands.CommandContext import com.rk.commands.EditorActionContext import com.rk.commands.EditorCommand import com.rk.commands.KeyCombination @@ -9,9 +8,9 @@ import com.rk.icons.Icon import com.rk.resources.drawables import com.rk.resources.getString import com.rk.resources.strings -import com.rk.utils.dialog +import com.rk.utils.dialogRes -class RefreshCommand(commandContext: CommandContext) : EditorCommand(commandContext) { +class RefreshCommand : EditorCommand() { override val id: String = "editor.refresh" override fun getLabel(): String = strings.refresh.getString() @@ -19,11 +18,11 @@ class RefreshCommand(commandContext: CommandContext) : EditorCommand(commandCont override fun action(editorActionContext: EditorActionContext) { val currentTab = editorActionContext.editorTab if (currentTab.editorState.isDirty) { - dialog( - context = editorActionContext.currentActivity, + dialogRes( + activity = editorActionContext.currentActivity, title = strings.attention.getString(), msg = strings.ask_refresh.getString(), - okString = strings.refresh, + okRes = strings.refresh, onCancel = {}, onOk = { currentTab.refresh() }, ) @@ -32,7 +31,7 @@ class RefreshCommand(commandContext: CommandContext) : EditorCommand(commandCont } } - override fun getIcon(): Icon = Icon.DrawableRes(drawables.refresh) + override fun getIcon(): Icon = Icon.ResourceIcon(drawables.refresh) override val defaultKeybinds: KeyCombination = KeyCombination(keyCode = KeyEvent.KEYCODE_R, ctrl = true, shift = true) diff --git a/core/main/src/main/java/com/rk/commands/editor/ReplaceCommand.kt b/core/main/src/main/java/com/rk/commands/editor/ReplaceCommand.kt index 8964bedea..4e81f41a9 100644 --- a/core/main/src/main/java/com/rk/commands/editor/ReplaceCommand.kt +++ b/core/main/src/main/java/com/rk/commands/editor/ReplaceCommand.kt @@ -1,7 +1,7 @@ package com.rk.commands.editor import android.view.KeyEvent -import com.rk.commands.CommandContext +import androidx.compose.ui.text.TextRange import com.rk.commands.EditorActionContext import com.rk.commands.EditorCommand import com.rk.commands.KeyCombination @@ -10,20 +10,22 @@ import com.rk.resources.drawables import com.rk.resources.getString import com.rk.resources.strings -class ReplaceCommand(commandContext: CommandContext) : EditorCommand(commandContext) { +class ReplaceCommand : EditorCommand() { override val id: String = "editor.replace" override fun getLabel(): String = strings.replace.getString() override fun action(editorActionContext: EditorActionContext) { editorActionContext.editorTab.editorState.apply { - editorActionContext.editor.getSelectedText()?.let { searchKeyword = it } + editorActionContext.editor.getSelectedText()?.let { + searchKeyword = searchKeyword.copy(text = it, selection = TextRange(it.length)) + } isSearching = true isReplaceShown = true } } - override fun getIcon(): Icon = Icon.DrawableRes(drawables.find_replace) + override fun getIcon(): Icon = Icon.ResourceIcon(drawables.find_replace) override val defaultKeybinds: KeyCombination = KeyCombination(keyCode = KeyEvent.KEYCODE_H, ctrl = true) } diff --git a/core/main/src/main/java/com/rk/commands/editor/RunCommand.kt b/core/main/src/main/java/com/rk/commands/editor/RunCommand.kt index 53461257b..a7d35c892 100644 --- a/core/main/src/main/java/com/rk/commands/editor/RunCommand.kt +++ b/core/main/src/main/java/com/rk/commands/editor/RunCommand.kt @@ -2,7 +2,6 @@ package com.rk.commands.editor import android.view.KeyEvent import com.rk.DefaultScope -import com.rk.commands.CommandContext import com.rk.commands.CommandProvider import com.rk.commands.EditorActionContext import com.rk.commands.EditorCommand @@ -12,13 +11,13 @@ import com.rk.icons.Icon import com.rk.resources.drawables import com.rk.resources.getString import com.rk.resources.strings -import com.rk.runner.Runner +import com.rk.runner.RunnerManager import com.rk.settings.Settings import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.launch @OptIn(DelicateCoroutinesApi::class) -class RunCommand(commandContext: CommandContext) : EditorCommand(commandContext) { +class RunCommand : EditorCommand() { override val id: String = "editor.run" override fun getLabel(): String = strings.run.getString() @@ -29,8 +28,8 @@ class RunCommand(commandContext: CommandContext) : EditorCommand(commandContext) CommandProvider.SaveCommand.action(editorActionContext) DefaultScope.launch { Settings.runs += 1 - Runner.run( - context = activity, + RunnerManager.run( + activity = activity, fileObject = editorTab.file, onMultipleRunners = { editorTab.editorState.showRunnerDialog = true @@ -41,10 +40,10 @@ class RunCommand(commandContext: CommandContext) : EditorCommand(commandContext) } override fun isSupported(editorNonActionContext: EditorNonActionContext): Boolean { - return Runner.isRunnable(editorNonActionContext.editorTab.file) + return RunnerManager.isRunnable(editorNonActionContext.editorTab.file) } - override fun getIcon(): Icon = Icon.DrawableRes(drawables.run) + override fun getIcon(): Icon = Icon.ResourceIcon(drawables.run) override val defaultKeybinds: KeyCombination = KeyCombination(keyCode = KeyEvent.KEYCODE_F5) } diff --git a/core/main/src/main/java/com/rk/commands/editor/SaveCommand.kt b/core/main/src/main/java/com/rk/commands/editor/SaveCommand.kt index 9d7610592..96ab4f59f 100644 --- a/core/main/src/main/java/com/rk/commands/editor/SaveCommand.kt +++ b/core/main/src/main/java/com/rk/commands/editor/SaveCommand.kt @@ -1,7 +1,6 @@ package com.rk.commands.editor import android.view.KeyEvent -import com.rk.commands.CommandContext import com.rk.commands.EditorActionContext import com.rk.commands.EditorCommand import com.rk.commands.EditorNonActionContext @@ -14,7 +13,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch -class SaveCommand(commandContext: CommandContext) : EditorCommand(commandContext) { +class SaveCommand : EditorCommand() { override val id: String = "editor.save" override fun getLabel(): String = strings.save.getString() @@ -27,7 +26,7 @@ class SaveCommand(commandContext: CommandContext) : EditorCommand(commandContext return editorNonActionContext.editorTab.file.canWrite() } - override fun getIcon(): Icon = Icon.DrawableRes(drawables.save) + override fun getIcon(): Icon = Icon.ResourceIcon(drawables.save) override val defaultKeybinds: KeyCombination = KeyCombination(keyCode = KeyEvent.KEYCODE_S, ctrl = true) } diff --git a/core/main/src/main/java/com/rk/commands/editor/SearchCommand.kt b/core/main/src/main/java/com/rk/commands/editor/SearchCommand.kt index 28cee56f7..3dc182e89 100644 --- a/core/main/src/main/java/com/rk/commands/editor/SearchCommand.kt +++ b/core/main/src/main/java/com/rk/commands/editor/SearchCommand.kt @@ -1,7 +1,7 @@ package com.rk.commands.editor import android.view.KeyEvent -import com.rk.commands.CommandContext +import androidx.compose.ui.text.TextRange import com.rk.commands.EditorActionContext import com.rk.commands.EditorCommand import com.rk.commands.KeyCombination @@ -10,20 +10,22 @@ import com.rk.resources.drawables import com.rk.resources.getString import com.rk.resources.strings -class SearchCommand(commandContext: CommandContext) : EditorCommand(commandContext) { +class SearchCommand : EditorCommand() { override val id: String = "editor.search" override fun getLabel(): String = strings.search.getString() override fun action(editorActionContext: EditorActionContext) { editorActionContext.editorTab.editorState.apply { - editorActionContext.editor.getSelectedText()?.let { searchKeyword = it } + editorActionContext.editor.getSelectedText()?.let { + searchKeyword = searchKeyword.copy(text = it, selection = TextRange(it.length)) + } isSearching = true isReplaceShown = false } } - override fun getIcon(): Icon = Icon.DrawableRes(drawables.search) + override fun getIcon(): Icon = Icon.ResourceIcon(drawables.search) override val defaultKeybinds: KeyCombination = KeyCombination(keyCode = KeyEvent.KEYCODE_F, ctrl = true) } diff --git a/core/main/src/main/java/com/rk/commands/editor/SelectAllCommand.kt b/core/main/src/main/java/com/rk/commands/editor/SelectAllCommand.kt index b403942e7..1979f73e3 100644 --- a/core/main/src/main/java/com/rk/commands/editor/SelectAllCommand.kt +++ b/core/main/src/main/java/com/rk/commands/editor/SelectAllCommand.kt @@ -1,7 +1,6 @@ package com.rk.commands.editor import android.view.KeyEvent -import com.rk.commands.CommandContext import com.rk.commands.EditorActionContext import com.rk.commands.EditorCommand import com.rk.commands.KeyCombination @@ -10,7 +9,7 @@ import com.rk.resources.drawables import com.rk.resources.getString import com.rk.resources.strings -class SelectAllCommand(commandContext: CommandContext) : EditorCommand(commandContext) { +class SelectAllCommand : EditorCommand() { override val id: String = "editor.select_all" override fun getLabel(): String = strings.select_all.getString() @@ -19,7 +18,7 @@ class SelectAllCommand(commandContext: CommandContext) : EditorCommand(commandCo editorActionContext.editor.selectAll() } - override fun getIcon(): Icon = Icon.DrawableRes(drawables.select_all) + override fun getIcon(): Icon = Icon.ResourceIcon(drawables.select_all) override val defaultKeybinds: KeyCombination = KeyCombination(keyCode = KeyEvent.KEYCODE_A, ctrl = true) } diff --git a/core/main/src/main/java/com/rk/commands/editor/SelectWordCommand.kt b/core/main/src/main/java/com/rk/commands/editor/SelectWordCommand.kt index 983e5c2d2..f9742d636 100644 --- a/core/main/src/main/java/com/rk/commands/editor/SelectWordCommand.kt +++ b/core/main/src/main/java/com/rk/commands/editor/SelectWordCommand.kt @@ -1,7 +1,6 @@ package com.rk.commands.editor import android.view.KeyEvent -import com.rk.commands.CommandContext import com.rk.commands.EditorActionContext import com.rk.commands.EditorCommand import com.rk.commands.KeyCombination @@ -10,7 +9,7 @@ import com.rk.resources.drawables import com.rk.resources.getString import com.rk.resources.strings -class SelectWordCommand(commandContext: CommandContext) : EditorCommand(commandContext) { +class SelectWordCommand : EditorCommand() { override val id: String = "editor.select_word" override fun getLabel(): String = strings.select_word.getString() @@ -19,7 +18,7 @@ class SelectWordCommand(commandContext: CommandContext) : EditorCommand(commandC editorActionContext.editor.selectCurrentWord() } - override fun getIcon(): Icon = Icon.DrawableRes(drawables.select) + override fun getIcon(): Icon = Icon.ResourceIcon(drawables.select) override val defaultKeybinds: KeyCombination = KeyCombination(keyCode = KeyEvent.KEYCODE_W, ctrl = true) } diff --git a/core/main/src/main/java/com/rk/commands/editor/ShareCommand.kt b/core/main/src/main/java/com/rk/commands/editor/ShareCommand.kt index b3f7f2a4b..386cf6da2 100644 --- a/core/main/src/main/java/com/rk/commands/editor/ShareCommand.kt +++ b/core/main/src/main/java/com/rk/commands/editor/ShareCommand.kt @@ -4,7 +4,6 @@ import android.content.Context import android.content.Intent import androidx.core.content.FileProvider import com.rk.DefaultScope -import com.rk.commands.CommandContext import com.rk.commands.EditorActionContext import com.rk.commands.EditorCommand import com.rk.file.FileWrapper @@ -15,7 +14,7 @@ import com.rk.resources.strings import com.rk.utils.toast import kotlinx.coroutines.launch -class ShareCommand(commandContext: CommandContext) : EditorCommand(commandContext) { +class ShareCommand : EditorCommand() { override val id: String = "editor.share" override fun getLabel(): String = strings.share.getString() @@ -49,5 +48,5 @@ class ShareCommand(commandContext: CommandContext) : EditorCommand(commandContex } } - override fun getIcon(): Icon = Icon.DrawableRes(drawables.send) + override fun getIcon(): Icon = Icon.ResourceIcon(drawables.send) } diff --git a/core/main/src/main/java/com/rk/commands/editor/SortLinesAscendingCommand.kt b/core/main/src/main/java/com/rk/commands/editor/SortLinesAscendingCommand.kt new file mode 100644 index 000000000..ecf6b5c4b --- /dev/null +++ b/core/main/src/main/java/com/rk/commands/editor/SortLinesAscendingCommand.kt @@ -0,0 +1,38 @@ +package com.rk.commands.editor + +import com.rk.commands.EditorActionContext +import com.rk.commands.EditorCommand +import com.rk.icons.Icon +import com.rk.resources.drawables +import com.rk.resources.getString +import com.rk.resources.strings + +class SortLinesAscendingCommand : EditorCommand() { + override val id = "editor.sort_lines_ascending" + + override fun getLabel() = strings.sort_lines_ascending.getString() + + override fun action(editorActionContext: EditorActionContext) { + val editor = editorActionContext.editor + + val cursor = editor.cursor + + var startLine: Int + var endLine: Int + if (!cursor.isSelected) { + startLine = 0 + endLine = editor.text.lineCount - 1 + } else { + startLine = minOf(cursor.leftLine, cursor.rightLine) + endLine = maxOf(cursor.leftLine, cursor.rightLine) + } + val endLineColumn = editor.text.getColumnCount(endLine) + + val lines = editor.text.subContent(startLine, 0, endLine, endLineColumn).lines() + val ascendingLines = lines.sorted().joinToString("\n") + + editor.text.replace(startLine, 0, endLine, endLineColumn, ascendingLines) + } + + override fun getIcon() = Icon.ResourceIcon(drawables.sort_by_alphabet) +} diff --git a/core/main/src/main/java/com/rk/commands/editor/SortLinesDescendingCommand.kt b/core/main/src/main/java/com/rk/commands/editor/SortLinesDescendingCommand.kt new file mode 100644 index 000000000..62f481bc3 --- /dev/null +++ b/core/main/src/main/java/com/rk/commands/editor/SortLinesDescendingCommand.kt @@ -0,0 +1,38 @@ +package com.rk.commands.editor + +import com.rk.commands.EditorActionContext +import com.rk.commands.EditorCommand +import com.rk.icons.Icon +import com.rk.resources.drawables +import com.rk.resources.getString +import com.rk.resources.strings + +class SortLinesDescendingCommand : EditorCommand() { + override val id = "editor.sort_lines_descending" + + override fun getLabel() = strings.sort_lines_descending.getString() + + override fun action(editorActionContext: EditorActionContext) { + val editor = editorActionContext.editor + + val cursor = editor.cursor + + var startLine: Int + var endLine: Int + if (!cursor.isSelected) { + startLine = 0 + endLine = editor.text.lineCount - 1 + } else { + startLine = minOf(cursor.leftLine, cursor.rightLine) + endLine = maxOf(cursor.leftLine, cursor.rightLine) + } + val endLineColumn = editor.text.getColumnCount(endLine) + + val lines = editor.text.subContent(startLine, 0, endLine, endLineColumn).lines() + val descendingLine = lines.sortedDescending().joinToString("\n") + + editor.text.replace(startLine, 0, endLine, endLineColumn, descendingLine) + } + + override fun getIcon() = Icon.ResourceIcon(drawables.sort_by_alphabet) +} diff --git a/core/main/src/main/java/com/rk/commands/editor/SyntaxHighlightingCommand.kt b/core/main/src/main/java/com/rk/commands/editor/SyntaxHighlightingCommand.kt index 3a2ea9d3d..45f2c0ca6 100644 --- a/core/main/src/main/java/com/rk/commands/editor/SyntaxHighlightingCommand.kt +++ b/core/main/src/main/java/com/rk/commands/editor/SyntaxHighlightingCommand.kt @@ -1,7 +1,6 @@ package com.rk.commands.editor import com.rk.commands.Command -import com.rk.commands.CommandContext import com.rk.commands.EditorActionContext import com.rk.commands.EditorCommand import com.rk.file.FileTypeManager @@ -10,20 +9,20 @@ import com.rk.resources.drawables import com.rk.resources.getString import com.rk.resources.strings -class SyntaxHighlightingCommand(commandContext: CommandContext) : EditorCommand(commandContext) { +class SyntaxHighlightingCommand : EditorCommand() { override val id: String = "editor.syntax_highlighting" override fun getLabel(): String = strings.highlighting.getString() override fun action(editorActionContext: EditorActionContext) {} - override fun getIcon(): Icon = Icon.DrawableRes(drawables.edit_note) + override fun getIcon(): Icon = Icon.ResourceIcon(drawables.edit_note) override val childCommands: List by lazy { FileTypeManager.allTypes() .filter { it.textmateScope != null } .map { fileType -> - object : EditorCommand(commandContext) { + object : EditorCommand() { override val id: String = "editor.syntax_highlighting.${fileType.name.lowercase()}" override fun getLabel(): String = fileType.title @@ -32,7 +31,7 @@ class SyntaxHighlightingCommand(commandContext: CommandContext) : EditorCommand( editorActionContext.editorTab.editorState.textmateScope = fileType.textmateScope!! } - override fun getIcon(): Icon = fileType.getIcon() + override fun getIcon(): Icon = fileType.getResolvedIcon() } } } diff --git a/core/main/src/main/java/com/rk/commands/editor/ToggleReadOnlyCommand.kt b/core/main/src/main/java/com/rk/commands/editor/ToggleReadOnlyCommand.kt index 46ff63529..2d1f5318c 100644 --- a/core/main/src/main/java/com/rk/commands/editor/ToggleReadOnlyCommand.kt +++ b/core/main/src/main/java/com/rk/commands/editor/ToggleReadOnlyCommand.kt @@ -1,7 +1,6 @@ package com.rk.commands.editor import android.view.KeyEvent -import com.rk.commands.CommandContext import com.rk.commands.EditorActionContext import com.rk.commands.EditorCommand import com.rk.commands.EditorNonActionContext @@ -12,7 +11,7 @@ import com.rk.resources.getString import com.rk.resources.strings import com.rk.tabs.editor.EditorTab -class ToggleReadOnlyCommand(commandContext: CommandContext) : EditorCommand(commandContext) { +class ToggleReadOnlyCommand : EditorCommand() { override val id: String = "editor.editable" override fun getLabel(): String { @@ -37,9 +36,9 @@ class ToggleReadOnlyCommand(commandContext: CommandContext) : EditorCommand(comm override fun getIcon(): Icon { val editorTab = commandContext.mainViewModel.currentTab as? EditorTab return if (editorTab?.editorState?.editable == true) { - Icon.DrawableRes(drawables.lock) + Icon.ResourceIcon(drawables.lock) } else { - Icon.DrawableRes(drawables.edit) + Icon.ResourceIcon(drawables.edit) } } diff --git a/core/main/src/main/java/com/rk/commands/editor/ToggleWordWrapCommand.kt b/core/main/src/main/java/com/rk/commands/editor/ToggleWordWrapCommand.kt index 20c04ca9c..b4d783703 100644 --- a/core/main/src/main/java/com/rk/commands/editor/ToggleWordWrapCommand.kt +++ b/core/main/src/main/java/com/rk/commands/editor/ToggleWordWrapCommand.kt @@ -1,7 +1,6 @@ package com.rk.commands.editor import android.view.KeyEvent -import com.rk.commands.CommandContext import com.rk.commands.EditorActionContext import com.rk.commands.EditorCommand import com.rk.commands.KeyCombination @@ -10,7 +9,7 @@ import com.rk.resources.drawables import com.rk.resources.getString import com.rk.resources.strings -class ToggleWordWrapCommand(commandContext: CommandContext) : EditorCommand(commandContext) { +class ToggleWordWrapCommand : EditorCommand() { override val id: String = "editor.toggle_word_wrap" override fun getLabel(): String = strings.toggle_word_wrap.getString() @@ -20,7 +19,7 @@ class ToggleWordWrapCommand(commandContext: CommandContext) : EditorCommand(comm editor.setWordwrap(!editor.isWordwrap, true, true) } - override fun getIcon(): Icon = Icon.DrawableRes(drawables.edit_note) + override fun getIcon(): Icon = Icon.ResourceIcon(drawables.edit_note) override val defaultKeybinds: KeyCombination = KeyCombination(keyCode = KeyEvent.KEYCODE_Z, alt = true) } diff --git a/core/main/src/main/java/com/rk/commands/editor/UndoCommand.kt b/core/main/src/main/java/com/rk/commands/editor/UndoCommand.kt index b158f976c..229a15ed3 100644 --- a/core/main/src/main/java/com/rk/commands/editor/UndoCommand.kt +++ b/core/main/src/main/java/com/rk/commands/editor/UndoCommand.kt @@ -1,7 +1,6 @@ package com.rk.commands.editor import android.view.KeyEvent -import com.rk.commands.CommandContext import com.rk.commands.EditorActionContext import com.rk.commands.EditorCommand import com.rk.commands.EditorNonActionContext @@ -11,7 +10,7 @@ import com.rk.resources.drawables import com.rk.resources.getString import com.rk.resources.strings -class UndoCommand(commandContext: CommandContext) : EditorCommand(commandContext) { +class UndoCommand : EditorCommand() { override val id: String = "editor.undo" override fun getLabel(): String = strings.undo.getString() @@ -27,7 +26,7 @@ class UndoCommand(commandContext: CommandContext) : EditorCommand(commandContext return editorState.editable && editorState.canUndo } - override fun getIcon(): Icon = Icon.DrawableRes(drawables.undo) + override fun getIcon(): Icon = Icon.ResourceIcon(drawables.undo) override val defaultKeybinds: KeyCombination = KeyCombination(keyCode = KeyEvent.KEYCODE_Z, ctrl = true) } diff --git a/core/main/src/main/java/com/rk/commands/editor/UpperCaseCommand.kt b/core/main/src/main/java/com/rk/commands/editor/UpperCaseCommand.kt index 285663535..dce5e736d 100644 --- a/core/main/src/main/java/com/rk/commands/editor/UpperCaseCommand.kt +++ b/core/main/src/main/java/com/rk/commands/editor/UpperCaseCommand.kt @@ -1,6 +1,5 @@ package com.rk.commands.editor -import com.rk.commands.CommandContext import com.rk.commands.EditorActionContext import com.rk.commands.EditorCommand import com.rk.icons.Icon @@ -8,7 +7,7 @@ import com.rk.resources.drawables import com.rk.resources.getString import com.rk.resources.strings -class UpperCaseCommand(commandContext: CommandContext) : EditorCommand(commandContext) { +class UpperCaseCommand : EditorCommand() { override val id: String = "editor.uppercase" override fun getLabel(): String = strings.transform_uppercase.getString() @@ -23,5 +22,5 @@ class UpperCaseCommand(commandContext: CommandContext) : EditorCommand(commandCo } } - override fun getIcon(): Icon = Icon.DrawableRes(drawables.letters) + override fun getIcon(): Icon = Icon.ResourceIcon(drawables.letters) } diff --git a/core/main/src/main/java/com/rk/commands/global/CommandPaletteCommand.kt b/core/main/src/main/java/com/rk/commands/global/CommandPaletteCommand.kt index 8ae0fb20b..96ae2c6b7 100644 --- a/core/main/src/main/java/com/rk/commands/global/CommandPaletteCommand.kt +++ b/core/main/src/main/java/com/rk/commands/global/CommandPaletteCommand.kt @@ -2,7 +2,6 @@ package com.rk.commands.global import android.view.KeyEvent import com.rk.commands.ActionContext -import com.rk.commands.CommandContext import com.rk.commands.GlobalCommand import com.rk.commands.KeyCombination import com.rk.icons.Icon @@ -10,7 +9,7 @@ import com.rk.resources.drawables import com.rk.resources.getString import com.rk.resources.strings -class CommandPaletteCommand(commandContext: CommandContext) : GlobalCommand(commandContext) { +class CommandPaletteCommand : GlobalCommand() { override val id: String = "global.command_palette" override fun getLabel(): String = strings.command_palette.getString() @@ -19,7 +18,7 @@ class CommandPaletteCommand(commandContext: CommandContext) : GlobalCommand(comm commandContext.mainViewModel.showCommandPalette() } - override fun getIcon(): Icon = Icon.DrawableRes(drawables.command_palette) + override fun getIcon(): Icon = Icon.ResourceIcon(drawables.command_palette) override val defaultKeybinds: KeyCombination = KeyCombination(keyCode = KeyEvent.KEYCODE_P, ctrl = true, shift = true) diff --git a/core/main/src/main/java/com/rk/commands/global/DocumentationCommand.kt b/core/main/src/main/java/com/rk/commands/global/DocumentationCommand.kt index d05ebd172..9577d2746 100644 --- a/core/main/src/main/java/com/rk/commands/global/DocumentationCommand.kt +++ b/core/main/src/main/java/com/rk/commands/global/DocumentationCommand.kt @@ -1,10 +1,7 @@ package com.rk.commands.global -import android.content.Intent import android.view.KeyEvent -import androidx.core.net.toUri import com.rk.commands.ActionContext -import com.rk.commands.CommandContext import com.rk.commands.GlobalCommand import com.rk.commands.KeyCombination import com.rk.icons.Icon @@ -12,19 +9,19 @@ import com.rk.icons.Menu_book import com.rk.icons.XedIcons import com.rk.resources.getString import com.rk.resources.strings +import com.rk.utils.openUrl -class DocumentationCommand(commandContext: CommandContext) : GlobalCommand(commandContext) { +class DocumentationCommand : GlobalCommand() { override val id: String = "global.documentation" override fun getLabel(): String = strings.docs.getString() + override fun getIcon(): Icon = Icon.VectorIcon(XedIcons.Menu_book) + override fun action(actionContext: ActionContext) { val url = "https://xed-editor.github.io/Xed-Docs/" - val intent = Intent(Intent.ACTION_VIEW, url.toUri()) - actionContext.currentActivity.startActivity(intent) + actionContext.currentActivity.openUrl(url) } - override fun getIcon(): Icon = Icon.VectorIcon(XedIcons.Menu_book) - override val defaultKeybinds: KeyCombination = KeyCombination(keyCode = KeyEvent.KEYCODE_F1) } diff --git a/core/main/src/main/java/com/rk/commands/global/NewFileCommand.kt b/core/main/src/main/java/com/rk/commands/global/NewFileCommand.kt index 546a37cc1..1e4887f44 100644 --- a/core/main/src/main/java/com/rk/commands/global/NewFileCommand.kt +++ b/core/main/src/main/java/com/rk/commands/global/NewFileCommand.kt @@ -2,7 +2,6 @@ package com.rk.commands.global import android.view.KeyEvent import com.rk.commands.ActionContext -import com.rk.commands.CommandContext import com.rk.commands.GlobalCommand import com.rk.commands.KeyCombination import com.rk.components.addDialog @@ -11,7 +10,7 @@ import com.rk.resources.drawables import com.rk.resources.getString import com.rk.resources.strings -class NewFileCommand(commandContext: CommandContext) : GlobalCommand(commandContext) { +class NewFileCommand : GlobalCommand() { override val id: String = "global.new_file" override fun getLabel(): String = strings.new_file.getString() @@ -20,7 +19,7 @@ class NewFileCommand(commandContext: CommandContext) : GlobalCommand(commandCont addDialog = true } - override fun getIcon(): Icon = Icon.DrawableRes(drawables.add) + override fun getIcon(): Icon = Icon.ResourceIcon(drawables.add) override val defaultKeybinds: KeyCombination = KeyCombination(keyCode = KeyEvent.KEYCODE_N, ctrl = true) } diff --git a/core/main/src/main/java/com/rk/commands/global/SaveAllCommand.kt b/core/main/src/main/java/com/rk/commands/global/SaveAllCommand.kt index f7467fee7..78c892cd1 100644 --- a/core/main/src/main/java/com/rk/commands/global/SaveAllCommand.kt +++ b/core/main/src/main/java/com/rk/commands/global/SaveAllCommand.kt @@ -2,7 +2,6 @@ package com.rk.commands.global import android.view.KeyEvent import com.rk.commands.ActionContext -import com.rk.commands.CommandContext import com.rk.commands.GlobalCommand import com.rk.commands.KeyCombination import com.rk.icons.Icon @@ -14,7 +13,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch -class SaveAllCommand(commandContext: CommandContext) : GlobalCommand(commandContext) { +class SaveAllCommand : GlobalCommand() { override val id: String = "global.save_all" override fun getLabel(): String = strings.save_all.getString() @@ -29,7 +28,7 @@ class SaveAllCommand(commandContext: CommandContext) : GlobalCommand(commandCont return commandContext.mainViewModel.tabs.isNotEmpty() } - override fun getIcon(): Icon = Icon.DrawableRes(drawables.save) + override fun getIcon(): Icon = Icon.ResourceIcon(drawables.save) override val defaultKeybinds: KeyCombination = KeyCombination(keyCode = KeyEvent.KEYCODE_S, ctrl = true, shift = true) diff --git a/core/main/src/main/java/com/rk/commands/global/SearchCodeCommand.kt b/core/main/src/main/java/com/rk/commands/global/SearchCodeCommand.kt index b059122aa..aba9d6f39 100644 --- a/core/main/src/main/java/com/rk/commands/global/SearchCodeCommand.kt +++ b/core/main/src/main/java/com/rk/commands/global/SearchCodeCommand.kt @@ -2,17 +2,15 @@ package com.rk.commands.global import android.view.KeyEvent import com.rk.commands.ActionContext -import com.rk.commands.CommandContext import com.rk.commands.GlobalCommand import com.rk.commands.KeyCombination import com.rk.components.codeSearchDialog -import com.rk.filetree.currentDrawerTab import com.rk.icons.Icon import com.rk.resources.drawables import com.rk.resources.getString import com.rk.resources.strings -class SearchCodeCommand(commandContext: CommandContext) : GlobalCommand(commandContext) { +class SearchCodeCommand : GlobalCommand() { override val id: String = "global.search_code" override fun getLabel(): String = strings.search_code.getString() @@ -22,10 +20,10 @@ class SearchCodeCommand(commandContext: CommandContext) : GlobalCommand(commandC } override fun isEnabled(): Boolean { - return currentDrawerTab != null + return commandContext.drawerViewModel.currentDrawerTab != null } - override fun getIcon(): Icon = Icon.DrawableRes(drawables.search) + override fun getIcon(): Icon = Icon.ResourceIcon(drawables.search) override val defaultKeybinds: KeyCombination = KeyCombination(keyCode = KeyEvent.KEYCODE_F, ctrl = true, shift = true) diff --git a/core/main/src/main/java/com/rk/commands/global/SearchFileFolderCommand.kt b/core/main/src/main/java/com/rk/commands/global/SearchFileFolderCommand.kt index ad510a6aa..32f6ef840 100644 --- a/core/main/src/main/java/com/rk/commands/global/SearchFileFolderCommand.kt +++ b/core/main/src/main/java/com/rk/commands/global/SearchFileFolderCommand.kt @@ -2,18 +2,16 @@ package com.rk.commands.global import android.view.KeyEvent import com.rk.commands.ActionContext -import com.rk.commands.CommandContext import com.rk.commands.GlobalCommand import com.rk.commands.KeyCombination import com.rk.components.fileSearchDialog import com.rk.filetree.FileTreeTab -import com.rk.filetree.currentDrawerTab import com.rk.icons.Icon import com.rk.resources.drawables import com.rk.resources.getString import com.rk.resources.strings -class SearchFileFolderCommand(commandContext: CommandContext) : GlobalCommand(commandContext) { +class SearchFileFolderCommand : GlobalCommand() { override val id: String = "global.search_file_folder" override fun getLabel(): String = strings.search_file_folder.getString() @@ -23,10 +21,10 @@ class SearchFileFolderCommand(commandContext: CommandContext) : GlobalCommand(co } override fun isEnabled(): Boolean { - return currentDrawerTab is FileTreeTab + return commandContext.drawerViewModel.currentDrawerTab is FileTreeTab } - override fun getIcon(): Icon = Icon.DrawableRes(drawables.search) + override fun getIcon(): Icon = Icon.ResourceIcon(drawables.search) override val defaultKeybinds: KeyCombination = KeyCombination(keyCode = KeyEvent.KEYCODE_P, ctrl = true) } diff --git a/core/main/src/main/java/com/rk/commands/global/SettingsCommand.kt b/core/main/src/main/java/com/rk/commands/global/SettingsCommand.kt index 09d18175d..896f56af5 100644 --- a/core/main/src/main/java/com/rk/commands/global/SettingsCommand.kt +++ b/core/main/src/main/java/com/rk/commands/global/SettingsCommand.kt @@ -4,7 +4,6 @@ import android.content.Intent import android.view.KeyEvent import com.rk.activities.settings.SettingsActivity import com.rk.commands.ActionContext -import com.rk.commands.CommandContext import com.rk.commands.GlobalCommand import com.rk.commands.KeyCombination import com.rk.icons.Icon @@ -12,7 +11,7 @@ import com.rk.resources.drawables import com.rk.resources.getString import com.rk.resources.strings -class SettingsCommand(commandContext: CommandContext) : GlobalCommand(commandContext) { +class SettingsCommand : GlobalCommand() { override val id: String = "global.settings" override fun getLabel(): String = strings.settings.getString() @@ -22,7 +21,7 @@ class SettingsCommand(commandContext: CommandContext) : GlobalCommand(commandCon activity.startActivity(Intent(activity, SettingsActivity::class.java)) } - override fun getIcon(): Icon = Icon.DrawableRes(drawables.settings) + override fun getIcon(): Icon = Icon.ResourceIcon(drawables.settings) override val defaultKeybinds: KeyCombination = KeyCombination(keyCode = KeyEvent.KEYCODE_COMMA, ctrl = true) } diff --git a/core/main/src/main/java/com/rk/commands/global/TerminalCommand.kt b/core/main/src/main/java/com/rk/commands/global/TerminalCommand.kt index b92c33707..167f14fba 100644 --- a/core/main/src/main/java/com/rk/commands/global/TerminalCommand.kt +++ b/core/main/src/main/java/com/rk/commands/global/TerminalCommand.kt @@ -4,7 +4,6 @@ import android.content.Intent import android.view.KeyEvent import com.rk.activities.terminal.Terminal import com.rk.commands.ActionContext -import com.rk.commands.CommandContext import com.rk.commands.GlobalCommand import com.rk.commands.KeyCombination import com.rk.icons.Icon @@ -12,42 +11,22 @@ import com.rk.resources.drawables import com.rk.resources.getString import com.rk.resources.strings import com.rk.settings.app.InbuiltFeatures -import com.rk.utils.showTerminalNotice -class TerminalCommand(commandContext: CommandContext) : GlobalCommand(commandContext) { +class TerminalCommand : GlobalCommand() { override val id: String = "global.terminal" override fun getLabel(): String = strings.terminal.getString() override fun action(actionContext: ActionContext) { val activity = actionContext.currentActivity - showTerminalNotice(activity) { - val intent = - Intent(activity, Terminal::class.java).apply { - commandContext.mainViewModel.currentTab?.file?.let { currentFile -> - // // val currentFile = - // viewModel.currentTab?.file ?: - // // return@apply - // // val currentPath = - // currentFile.getAbsolutePath() - // // val project = - // // tabs - // // .filter { - // // currentPath.startsWith(it.fileObject.getAbsolutePath()) } - // // .maxByOrNull { - // // it.fileObject.getAbsolutePath().length } ?: return@apply - // // putExtra("cwd", - - // TODO: Fix this - } - } - activity.startActivity(intent) - } + val intent = + Intent(activity, Terminal::class.java) + activity.startActivity(intent) } override fun isSupported(): Boolean = InbuiltFeatures.terminal.state.value - override fun getIcon(): Icon = Icon.DrawableRes(drawables.terminal) + override fun getIcon(): Icon = Icon.ResourceIcon(drawables.terminal) override val defaultKeybinds: KeyCombination = KeyCombination(keyCode = KeyEvent.KEYCODE_J, ctrl = true) } diff --git a/core/main/src/main/java/com/rk/commands/lsp/FormatDocumentCommand.kt b/core/main/src/main/java/com/rk/commands/lsp/FormatDocumentCommand.kt index 3325d8be4..e0407331e 100644 --- a/core/main/src/main/java/com/rk/commands/lsp/FormatDocumentCommand.kt +++ b/core/main/src/main/java/com/rk/commands/lsp/FormatDocumentCommand.kt @@ -1,7 +1,6 @@ package com.rk.commands.lsp import com.rk.DefaultScope -import com.rk.commands.CommandContext import com.rk.commands.LspActionContext import com.rk.commands.LspCommand import com.rk.commands.LspNonActionContext @@ -11,7 +10,7 @@ import com.rk.resources.drawables import com.rk.resources.getString import com.rk.resources.strings -class FormatDocumentCommand(commandContext: CommandContext) : LspCommand(commandContext) { +class FormatDocumentCommand : LspCommand() { override val id: String = "lsp.format_document" override fun getLabel(): String = strings.format_document.getString() @@ -24,5 +23,5 @@ class FormatDocumentCommand(commandContext: CommandContext) : LspCommand(command return lspNonActionContext.lspConnector.isFormattingSupported() } - override fun getIcon(): Icon = Icon.DrawableRes(drawables.auto_fix) + override fun getIcon(): Icon = Icon.ResourceIcon(drawables.auto_fix) } diff --git a/core/main/src/main/java/com/rk/commands/lsp/FormatSelectionCommand.kt b/core/main/src/main/java/com/rk/commands/lsp/FormatSelectionCommand.kt index 1cda019c5..265be25ba 100644 --- a/core/main/src/main/java/com/rk/commands/lsp/FormatSelectionCommand.kt +++ b/core/main/src/main/java/com/rk/commands/lsp/FormatSelectionCommand.kt @@ -1,7 +1,6 @@ package com.rk.commands.lsp import com.rk.DefaultScope -import com.rk.commands.CommandContext import com.rk.commands.LspActionContext import com.rk.commands.LspCommand import com.rk.commands.LspNonActionContext @@ -11,7 +10,7 @@ import com.rk.resources.drawables import com.rk.resources.getString import com.rk.resources.strings -class FormatSelectionCommand(commandContext: CommandContext) : LspCommand(commandContext) { +class FormatSelectionCommand : LspCommand() { override val id: String = "lsp.format_selection" override fun getLabel(): String = strings.format_selection.getString() @@ -24,5 +23,5 @@ class FormatSelectionCommand(commandContext: CommandContext) : LspCommand(comman return lspNonActionContext.lspConnector.isRangeFormattingSupported() } - override fun getIcon(): Icon = Icon.DrawableRes(drawables.auto_fix) + override fun getIcon(): Icon = Icon.ResourceIcon(drawables.auto_fix) } diff --git a/core/main/src/main/java/com/rk/commands/lsp/GoToDefinitionCommand.kt b/core/main/src/main/java/com/rk/commands/lsp/GoToDefinitionCommand.kt index 79ed66b59..79b7f14bb 100644 --- a/core/main/src/main/java/com/rk/commands/lsp/GoToDefinitionCommand.kt +++ b/core/main/src/main/java/com/rk/commands/lsp/GoToDefinitionCommand.kt @@ -1,7 +1,6 @@ package com.rk.commands.lsp import com.rk.DefaultScope -import com.rk.commands.CommandContext import com.rk.commands.LspActionContext import com.rk.commands.LspCommand import com.rk.commands.LspNonActionContext @@ -11,7 +10,7 @@ import com.rk.resources.drawables import com.rk.resources.getString import com.rk.resources.strings -class GoToDefinitionCommand(commandContext: CommandContext) : LspCommand(commandContext) { +class GoToDefinitionCommand : LspCommand() { override val id: String = "lsp.go_to_definition" override fun getLabel(): String = strings.go_to_definition.getString() @@ -29,5 +28,5 @@ class GoToDefinitionCommand(commandContext: CommandContext) : LspCommand(command return lspNonActionContext.lspConnector.isGoToDefinitionSupported() } - override fun getIcon(): Icon = Icon.DrawableRes(drawables.jump_to_element) + override fun getIcon(): Icon = Icon.ResourceIcon(drawables.jump_to_element) } diff --git a/core/main/src/main/java/com/rk/commands/lsp/GoToReferencesCommand.kt b/core/main/src/main/java/com/rk/commands/lsp/GoToReferencesCommand.kt index c01d6b102..3295c9b3f 100644 --- a/core/main/src/main/java/com/rk/commands/lsp/GoToReferencesCommand.kt +++ b/core/main/src/main/java/com/rk/commands/lsp/GoToReferencesCommand.kt @@ -1,7 +1,6 @@ package com.rk.commands.lsp import com.rk.DefaultScope -import com.rk.commands.CommandContext import com.rk.commands.LspActionContext import com.rk.commands.LspCommand import com.rk.commands.LspNonActionContext @@ -11,7 +10,7 @@ import com.rk.resources.drawables import com.rk.resources.getString import com.rk.resources.strings -class GoToReferencesCommand(commandContext: CommandContext) : LspCommand(commandContext) { +class GoToReferencesCommand : LspCommand() { override val id: String = "lsp.go_to_references" override fun getLabel(): String = strings.go_to_references.getString() @@ -29,5 +28,5 @@ class GoToReferencesCommand(commandContext: CommandContext) : LspCommand(command return lspNonActionContext.lspConnector.isGoToReferencesSupported() } - override fun getIcon(): Icon = Icon.DrawableRes(drawables.manage_search) + override fun getIcon(): Icon = Icon.ResourceIcon(drawables.manage_search) } diff --git a/core/main/src/main/java/com/rk/commands/lsp/RenameSymbolCommand.kt b/core/main/src/main/java/com/rk/commands/lsp/RenameSymbolCommand.kt index 698b452ea..67590c46a 100644 --- a/core/main/src/main/java/com/rk/commands/lsp/RenameSymbolCommand.kt +++ b/core/main/src/main/java/com/rk/commands/lsp/RenameSymbolCommand.kt @@ -1,7 +1,6 @@ package com.rk.commands.lsp import com.rk.DefaultScope -import com.rk.commands.CommandContext import com.rk.commands.LspActionContext import com.rk.commands.LspCommand import com.rk.commands.LspNonActionContext @@ -11,7 +10,7 @@ import com.rk.resources.drawables import com.rk.resources.getString import com.rk.resources.strings -class RenameSymbolCommand(commandContext: CommandContext) : LspCommand(commandContext) { +class RenameSymbolCommand : LspCommand() { override val id: String = "lsp.rename_symbol" override fun getLabel(): String = strings.rename_symbol.getString() @@ -24,5 +23,5 @@ class RenameSymbolCommand(commandContext: CommandContext) : LspCommand(commandCo return lspNonActionContext.lspConnector.isRenameSymbolSupported() } - override fun getIcon(): Icon = Icon.DrawableRes(drawables.manage_search) + override fun getIcon(): Icon = Icon.ResourceIcon(drawables.manage_search) } diff --git a/core/main/src/main/java/com/rk/components/AddDialogItem.kt b/core/main/src/main/java/com/rk/components/AddDialogItem.kt index f23c0e6a0..9fc837b31 100644 --- a/core/main/src/main/java/com/rk/components/AddDialogItem.kt +++ b/core/main/src/main/java/com/rk/components/AddDialogItem.kt @@ -1,5 +1,6 @@ package com.rk.components +import android.content.res.Resources import androidx.annotation.DrawableRes import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column @@ -19,7 +20,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import com.rk.components.compose.utils.addIf @@ -29,7 +32,7 @@ import java.io.File @Composable fun AddDialogItem( - @DrawableRes icon: Int, + @DrawableRes resId: Int, title: String, description: String? = null, enabled: Boolean = true, @@ -40,7 +43,33 @@ fun AddDialogItem( description = description, onClick = onClick, enabled = enabled, - icon = { Icon(painter = painterResource(icon), contentDescription = null, modifier = Modifier.size(24.dp)) }, + icon = { Icon(painter = painterResource(resId), contentDescription = null, modifier = Modifier.size(24.dp)) }, + ) +} + +@Composable +fun AddDialogItem( + @DrawableRes resId: Int, + resources: Resources, + title: String, + description: String? = null, + enabled: Boolean = true, + onClick: () -> Unit, +) { + AddDialogItem( + title = title, + description = description, + onClick = onClick, + enabled = enabled, + icon = { + val theme = LocalContext.current.theme + + Icon( + imageVector = ImageVector.vectorResource(theme = theme, resId = resId, res = resources), + contentDescription = null, + modifier = Modifier.size(24.dp), + ) + }, ) } @@ -94,9 +123,20 @@ fun AddDialogItem( onClick: () -> Unit, ) { when (icon) { - is Icon.DrawableRes -> { + is Icon.ResourceIcon -> { + AddDialogItem( + resId = icon.drawableRes, + title = title, + description = description, + onClick = onClick, + enabled = enabled, + ) + } + + is Icon.ExternalResourceIcon -> { AddDialogItem( - icon = icon.drawableRes, + resId = icon.drawableRes, + resources = icon.resources, title = title, description = description, onClick = onClick, diff --git a/core/main/src/main/java/com/rk/components/EditorSettingsToggle.kt b/core/main/src/main/java/com/rk/components/EditorSettingsItem.kt similarity index 97% rename from core/main/src/main/java/com/rk/components/EditorSettingsToggle.kt rename to core/main/src/main/java/com/rk/components/EditorSettingsItem.kt index a9e8bf6fd..5e9a5d116 100644 --- a/core/main/src/main/java/com/rk/components/EditorSettingsToggle.kt +++ b/core/main/src/main/java/com/rk/components/EditorSettingsItem.kt @@ -16,7 +16,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @Composable -fun EditorSettingsToggle( +fun EditorSettingsItem( modifier: Modifier = Modifier, label: String, description: String? = null, @@ -30,7 +30,7 @@ fun EditorSettingsToggle( isSwitchLocked: Boolean = false, state: MutableState = remember { mutableStateOf(default) }, ) { - SettingsToggle( + SettingsItem( modifier = modifier, label = label, state = state, diff --git a/core/main/src/main/java/com/rk/components/GlobalToolbarActions.kt b/core/main/src/main/java/com/rk/components/GlobalToolbarActions.kt index c3e5d82d7..7e899a969 100644 --- a/core/main/src/main/java/com/rk/components/GlobalToolbarActions.kt +++ b/core/main/src/main/java/com/rk/components/GlobalToolbarActions.kt @@ -1,21 +1,21 @@ package com.rk.components -import android.app.Activity import android.content.Intent import android.content.pm.PackageManager +import androidx.activity.compose.LocalActivity import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.IconButton import androidx.compose.material3.ModalBottomSheet import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.lifecycleScope @@ -26,14 +26,14 @@ import com.rk.activities.main.drawerStateRef import com.rk.activities.main.fileTreeViewModel import com.rk.activities.main.searchViewModel import com.rk.commands.ActionContext -import com.rk.commands.CommandProvider +import com.rk.commands.ToolbarConfiguration +import com.rk.drawer.DrawerViewModel import com.rk.file.FileObject import com.rk.file.FileWrapper import com.rk.file.child import com.rk.file.createFileIfNot import com.rk.file.toFileObject import com.rk.filetree.FileTreeTab -import com.rk.filetree.currentDrawerTab import com.rk.icons.CreateNewFile import com.rk.icons.XedIcon import com.rk.icons.XedIcons @@ -41,7 +41,6 @@ import com.rk.resources.drawables import com.rk.resources.strings import com.rk.search.CodeSearchDialog import com.rk.search.FileSearchDialog -import com.rk.settings.app.InbuiltFeatures import com.rk.utils.application import com.rk.utils.errorDialog import com.rk.utils.getTempDir @@ -54,35 +53,35 @@ var codeSearchDialog by mutableStateOf(false) @OptIn(ExperimentalMaterial3Api::class) @Composable -fun GlobalToolbarActions(viewModel: MainViewModel) { - val context = LocalContext.current +fun GlobalToolbarActions(viewModel: MainViewModel, drawerViewModel: DrawerViewModel) { + val activity = LocalActivity.current val scope = rememberCoroutineScope() var tempFileNameDialog by remember { mutableStateOf(false) } - if (viewModel.tabs.isEmpty() || viewModel.currentTab?.showGlobalActions == true) { - val newFileCommand = CommandProvider.NewFileCommand - val terminalCommand = CommandProvider.TerminalCommand - val settingsCommand = CommandProvider.SettingsCommand - - IconButton(onClick = { newFileCommand.action(ActionContext(context as Activity)) }) { - XedIcon(newFileCommand.getIcon()) - } + val commands by remember { derivedStateOf { ToolbarConfiguration.globalCommands } } - if (InbuiltFeatures.terminal.state.value) { - IconButton(onClick = { terminalCommand.action(ActionContext(context as Activity)) }) { - XedIcon(terminalCommand.getIcon()) + if (viewModel.tabs.isEmpty() || viewModel.currentTab?.showGlobalActions == true) { + for (command in commands) { + if (command.isSupported()) { + IconButton( + enabled = command.isEnabled(), + onClick = { + activity?.let { + command.performCommand(ActionContext(it)) + } + }, + ) { + XedIcon(command.getIcon()) + } } } - - IconButton(onClick = { settingsCommand.action(ActionContext(context as Activity)) }) { - XedIcon(settingsCommand.getIcon()) - } } - if (fileSearchDialog && currentDrawerTab is FileTreeTab) { + if (fileSearchDialog && drawerViewModel.currentDrawerTab is FileTreeTab) { FileSearchDialog( + mainViewModel = viewModel, searchViewModel = searchViewModel.get()!!, - projectFile = (currentDrawerTab as FileTreeTab).root, + projectFile = (drawerViewModel.currentDrawerTab as FileTreeTab).root, onFinish = { fileSearchDialog = false }, onSelect = { projectFile, fileObject -> scope.launch { @@ -103,11 +102,11 @@ fun GlobalToolbarActions(viewModel: MainViewModel) { ) } - if (codeSearchDialog && currentDrawerTab is FileTreeTab) { + if (codeSearchDialog && drawerViewModel.currentDrawerTab is FileTreeTab) { CodeSearchDialog( mainViewModel = viewModel, searchViewModel = searchViewModel.get()!!, - projectFile = (currentDrawerTab as FileTreeTab).root, + projectFile = (drawerViewModel.currentDrawerTab as FileTreeTab).root, onFinish = { codeSearchDialog = false }, ) } @@ -115,7 +114,7 @@ fun GlobalToolbarActions(viewModel: MainViewModel) { if (addDialog) { ModalBottomSheet(onDismissRequest = { addDialog = false }) { Column(modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp, top = 0.dp)) { - AddDialogItem(icon = drawables.file, title = stringResource(strings.temp_file)) { + AddDialogItem(resId = drawables.file, title = stringResource(strings.temp_file)) { val intent = Intent(Intent.ACTION_CREATE_DOCUMENT) intent.addCategory(Intent.CATEGORY_OPENABLE) intent.setType("application/octet-stream") @@ -162,7 +161,7 @@ fun GlobalToolbarActions(viewModel: MainViewModel) { } } - AddDialogItem(icon = drawables.file_symlink, title = stringResource(strings.open_file)) { + AddDialogItem(resId = drawables.file_symlink, title = stringResource(strings.open_file)) { addDialog = false MainActivity.instance?.apply { fileManager.requestOpenFile(mimeType = "*/*") { diff --git a/core/main/src/main/java/com/rk/components/InfoBlock.kt b/core/main/src/main/java/com/rk/components/InfoBlock.kt index 8151e31b7..633693e62 100644 --- a/core/main/src/main/java/com/rk/components/InfoBlock.kt +++ b/core/main/src/main/java/com/rk/components/InfoBlock.kt @@ -1,6 +1,7 @@ package com.rk.components import android.annotation.SuppressLint +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth @@ -29,10 +30,11 @@ fun InfoBlock( icon: @Composable (() -> Unit)? = null, shape: Shape = RoundedCornerShape(12.dp), warning: Boolean = false, + onClick: (() -> Unit)? = null, ) { PreferenceGroup(modifier = modifier) { Card( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.clickable(enabled = onClick != null, onClick = { onClick?.invoke() }).fillMaxWidth(), shape = shape, colors = if (warning) CardDefaults.cardColors(MaterialTheme.colorScheme.warningSurface) diff --git a/core/main/src/main/java/com/rk/components/SettingsToggle.kt b/core/main/src/main/java/com/rk/components/SettingsItem.kt similarity index 99% rename from core/main/src/main/java/com/rk/components/SettingsToggle.kt rename to core/main/src/main/java/com/rk/components/SettingsItem.kt index 61d34b9e8..e3fe7d608 100644 --- a/core/main/src/main/java/com/rk/components/SettingsToggle.kt +++ b/core/main/src/main/java/com/rk/components/SettingsItem.kt @@ -43,7 +43,7 @@ fun BasicToggle( @OptIn(ExperimentalFoundationApi::class) @Composable -fun SettingsToggle( +fun SettingsItem( modifier: Modifier = Modifier, label: String, default: Boolean, diff --git a/core/main/src/main/java/com/rk/components/StyledTextFeild.kt b/core/main/src/main/java/com/rk/components/StyledTextFeild.kt index c9d4a2a34..31fc6ecb2 100644 --- a/core/main/src/main/java/com/rk/components/StyledTextFeild.kt +++ b/core/main/src/main/java/com/rk/components/StyledTextFeild.kt @@ -1,14 +1,13 @@ package com.rk.components import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.foundation.text.selection.TextSelectionColors import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.LocalTextStyle -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.TextFieldColors import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable @@ -17,10 +16,20 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.takeOrElse import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp +private fun TextFieldColors.textColor(enabled: Boolean, isError: Boolean, focused: Boolean): Color = + when { + !enabled -> disabledTextColor + isError -> errorTextColor + focused -> focusedTextColor + else -> unfocusedTextColor + } + @OptIn(ExperimentalMaterial3Api::class) @Composable fun StyledTextField( @@ -46,29 +55,25 @@ fun StyledTextField( minLines: Int = 1, interactionSource: MutableInteractionSource? = null, shape: Shape = TextFieldDefaults.shape, - colors: TextFieldColors = - TextFieldDefaults.colors( - cursorColor = MaterialTheme.colorScheme.onSurface, - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - disabledIndicatorColor = Color.Transparent, - selectionColors = - TextSelectionColors( - handleColor = MaterialTheme.colorScheme.primary, - backgroundColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.4f), - ), - ), + colors: TextFieldColors = TextFieldDefaults.colors(), ) { val finalInteractionSource = interactionSource ?: remember { MutableInteractionSource() } + val textColor = + textStyle.color.takeOrElse { + val focused = finalInteractionSource.collectIsFocusedAsState().value + colors.textColor(enabled, isError, focused) + } + val mergedTextStyle = textStyle.merge(TextStyle(color = textColor)) + BasicTextField( - cursorBrush = SolidColor(MaterialTheme.colorScheme.primary), + cursorBrush = SolidColor(if (isError) colors.errorCursorColor else colors.cursorColor), value = value, onValueChange = onValueChange, modifier = modifier, enabled = enabled, readOnly = readOnly, - textStyle = TextStyle(color = MaterialTheme.colorScheme.onSurface), + textStyle = mergedTextStyle, keyboardOptions = keyboardOptions, keyboardActions = keyboardActions, visualTransformation = visualTransformation, @@ -85,7 +90,81 @@ fun StyledTextField( enabled = enabled, isError = isError, interactionSource = finalInteractionSource, - contentPadding = PaddingValues(top = 8.dp, start = 8.dp, end = 8.dp), + contentPadding = PaddingValues(top = 8.dp, start = 16.dp, end = 16.dp), + placeholder = placeholder, + leadingIcon = leadingIcon, + trailingIcon = trailingIcon, + prefix = prefix, + suffix = suffix, + supportingText = supportingText, + label = label, + shape = shape, + colors = colors, + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun StyledTextField( + value: TextFieldValue, + onValueChange: (TextFieldValue) -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + readOnly: Boolean = false, + textStyle: TextStyle = LocalTextStyle.current, + label: @Composable (() -> Unit)? = null, + placeholder: @Composable (() -> Unit)? = null, + leadingIcon: @Composable (() -> Unit)? = null, + trailingIcon: @Composable (() -> Unit)? = null, + prefix: @Composable (() -> Unit)? = null, + suffix: @Composable (() -> Unit)? = null, + supportingText: @Composable (() -> Unit)? = null, + isError: Boolean = false, + visualTransformation: VisualTransformation = VisualTransformation.None, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + keyboardActions: KeyboardActions = KeyboardActions.Default, + singleLine: Boolean = false, + maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, + minLines: Int = 1, + interactionSource: MutableInteractionSource? = null, + shape: Shape = TextFieldDefaults.shape, + colors: TextFieldColors = TextFieldDefaults.colors(), +) { + val finalInteractionSource = interactionSource ?: remember { MutableInteractionSource() } + + val textColor = + textStyle.color.takeOrElse { + val focused = finalInteractionSource.collectIsFocusedAsState().value + colors.textColor(enabled, isError, focused) + } + val mergedTextStyle = textStyle.merge(TextStyle(color = textColor)) + + BasicTextField( + cursorBrush = SolidColor(if (isError) colors.errorCursorColor else colors.cursorColor), + value = value, + onValueChange = onValueChange, + modifier = modifier, + enabled = enabled, + readOnly = readOnly, + textStyle = mergedTextStyle, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + visualTransformation = visualTransformation, + interactionSource = finalInteractionSource, + singleLine = singleLine, + maxLines = maxLines, + minLines = minLines, + ) { innerTextField -> + TextFieldDefaults.DecorationBox( + value = value.text, + visualTransformation = visualTransformation, + innerTextField = innerTextField, + singleLine = singleLine, + enabled = enabled, + isError = isError, + interactionSource = finalInteractionSource, + contentPadding = PaddingValues(top = 8.dp, start = 16.dp, end = 16.dp), placeholder = placeholder, leadingIcon = leadingIcon, trailingIcon = trailingIcon, diff --git a/core/main/src/main/java/com/rk/crashhandler/CrashActivity.kt b/core/main/src/main/java/com/rk/crashhandler/CrashActivity.kt index ea61f065d..83e8d8e26 100644 --- a/core/main/src/main/java/com/rk/crashhandler/CrashActivity.kt +++ b/core/main/src/main/java/com/rk/crashhandler/CrashActivity.kt @@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.selection.LocalTextSelectionColors import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Close import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon @@ -33,6 +34,7 @@ import androidx.core.content.pm.PackageInfoCompat import androidx.core.net.toUri import com.rk.crashhandler.CrashHandler.logErrorOrExit import com.rk.editor.Editor +import com.rk.extension.Extension import com.rk.resources.getString import com.rk.resources.strings import com.rk.theme.XedTheme @@ -81,6 +83,34 @@ class CrashActivity : ComponentActivity() { } return true } + + fun start(context: Context, extension: Extension, error: Throwable) { + val intent = Intent(context, CrashActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK + putExtra("is_extension_crash", true) + putExtra("extension_id", extension.id) + putExtra("extension_name", extension.name) + putExtra("extension_version", extension.version) + putExtra("extension_author", extension.author.toString()) + putExtra("repository", extension.repository) + putExtra("thread", Thread.currentThread().name) + putExtra("force_crash", false) + putExtra("msg", error.message) + + var cause = error.cause?.toString() ?: error.toString() + val prefix = "java.lang.Throwable:" + if (cause.startsWith(prefix)) { + cause = cause.removePrefix(prefix) + } + putExtra("error_cause", cause) + + val stringWriter = java.io.StringWriter() + val printWriter = java.io.PrintWriter(stringWriter) + error.printStackTrace(printWriter) + putExtra("stacktrace", stringWriter.toString()) + } + context.startActivity(intent) + } } fun isMainThreadCrashed(): Boolean { @@ -123,7 +153,11 @@ class CrashActivity : ComponentActivity() { } ) { Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowBack, + imageVector = if (mainThreadCrashed){ + Icons.Default.Close + }else{ + Icons.AutoMirrored.Filled.ArrowBack + }, contentDescription = "Back", ) } @@ -193,8 +227,18 @@ class CrashActivity : ComponentActivity() { } private fun reportLogs(crashText: String, context: Context) { + val repo = intent.getStringExtra("repository") + val baseUrl = if (!repo.isNullOrEmpty()) { + if (repo.startsWith("http")) { + repo.removeSuffix("/") + } else { + "https://github.com/$repo" + } + } else { + "https://github.com/Xed-Editor/Xed-Editor" + } val url = - "https://github.com/Xed-Editor/Xed-Editor/issues/new?title=Crash%20Report&body=" + + "$baseUrl/issues/new?title=Crash%20Report&body=" + URLEncoder.encode("``` \n$crashText\n ```", StandardCharsets.UTF_8.toString()) val browserIntent = Intent(Intent.ACTION_VIEW, url.toUri()) context.startActivity(browserIntent) @@ -210,7 +254,19 @@ class CrashActivity : ComponentActivity() { val maxMem = runtime.maxMemory() / (1024 * 1024) return buildString { - append("Unexpected crash occurred").appendLine().appendLine() + val isExtension = intent.getBooleanExtra("is_extension_crash", false) + if (isExtension) { + append("Extension Crashed").appendLine().appendLine() + + + append("Extension ID: ").append(intent.getStringExtra("extension_id")).appendLine() + append("Extension Name: ").append(intent.getStringExtra("extension_name")).appendLine() + append("Extension Version: ").append(intent.getStringExtra("extension_version")).appendLine() + append("Extension Author: ").append(intent.getStringExtra("extension_author")).appendLine() + appendLine() + } else { + append("Unexpected crash occurred").appendLine().appendLine() + } append("Thread: ").append(intent.getStringExtra("thread")).appendLine() append("App version: ").append(versionName).appendLine() diff --git a/core/main/src/main/java/com/rk/crashhandler/CrashHandler.kt b/core/main/src/main/java/com/rk/crashhandler/CrashHandler.kt index 39a467f17..708a596be 100644 --- a/core/main/src/main/java/com/rk/crashhandler/CrashHandler.kt +++ b/core/main/src/main/java/com/rk/crashhandler/CrashHandler.kt @@ -23,10 +23,11 @@ object CrashHandler : Thread.UncaughtExceptionHandler { } if ( - ex.stackTrace.toString().contains("android.view.View${"$"}BaseSavedState") || - ex.stackTrace.toString().contains("android.widget.HorizontalScrollView${"$"}SavedState") + ex.stackTrace.contentToString().contains($$"android.view.View$BaseSavedState") || + ex.stackTrace.contentToString().contains($$"android.widget.HorizontalScrollView$SavedState") ) { Log.w("CrashHandler", "Ignoring crash") + ex.printStackTrace() // return@runCatching } @@ -58,6 +59,7 @@ object CrashHandler : Thread.UncaughtExceptionHandler { exitProcess(1) } + //try to keep main thread alive if (Looper.myLooper() != null) { while (true) { try { diff --git a/core/main/src/main/java/com/rk/drawer/AddProjectSheet.kt b/core/main/src/main/java/com/rk/drawer/AddProjectSheet.kt new file mode 100644 index 000000000..8f758766b --- /dev/null +++ b/core/main/src/main/java/com/rk/drawer/AddProjectSheet.kt @@ -0,0 +1,158 @@ +package com.rk.drawer + +import android.Manifest +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.os.storage.StorageManager +import androidx.activity.compose.ManagedActivityResultLauncher +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat +import androidx.lifecycle.lifecycleScope +import com.rk.activities.main.MainActivity +import com.rk.components.AddDialogItem +import com.rk.file.FileObject +import com.rk.file.FileWrapper +import com.rk.file.sandboxHomeDir +import com.rk.icons.Icon +import com.rk.resources.drawables +import com.rk.resources.strings +import com.rk.settings.Settings +import com.rk.settings.app.InbuiltFeatures +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AddProjectSheet( + onDismiss: () -> Unit, + onAddProject: (FileObject) -> Unit, + openFolder: ManagedActivityResultLauncher, + showPrivateFileWarning: (onOK: () -> Unit) -> Unit, + showGitCloneDialog: () -> Unit, +) { + val context = LocalContext.current + val activity = context as MainActivity + val lifecycleScope = remember { activity.lifecycleScope } + + val viewModel = activity.drawerViewModel + + ModalBottomSheet(onDismissRequest = onDismiss) { + Column(modifier = Modifier.Companion.padding(start = 16.dp, end = 16.dp, bottom = 16.dp, top = 0.dp)) { + AddDialogItem( + icon = Icon.ResourceIcon(drawables.file_symlink), + title = stringResource(strings.open_directory), + description = stringResource(strings.open_dir_desc), + onClick = { + openFolder.launch(null) + onDismiss() + }, + ) + + // Open Path option + val is11Plus = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R + val isManager = is11Plus && Environment.isExternalStorageManager() + val legacyPermission = + ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) != + PackageManager.PERMISSION_GRANTED || + ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) != + PackageManager.PERMISSION_GRANTED + + val storage = Environment.getExternalStorageDirectory() + if ((isManager || (!is11Plus && legacyPermission)) && storage.canWrite() && storage.canRead()) { + AddDialogItem( + icon = Icon.ResourceIcon(drawables.android), + title = stringResource(strings.internal_storage), + description = stringResource(strings.open_internal_storage), + onClick = { + viewModel.addFileTreeTab(FileWrapper(storage)) + onDismiss() + }, + ) + } + + if (isManager) { + val storageManager = context.getSystemService(StorageManager::class.java) + val volumes = storageManager.storageVolumes + + volumes.forEach { volume -> + val root = volume.directory ?: return@forEach + if (root == storage) return@forEach + if (!root.canRead() || !root.canWrite() || root.listFiles() == null) return@forEach + + val name = volume.getDescription(context) + val removable = volume.isRemovable + val description = if (removable) strings.open_removable_storage else strings.open_internal_storage + + AddDialogItem( + icon = Icon.ResourceIcon(drawables.sd_card), + title = name, + description = stringResource(description), + ) { + viewModel.addFileTreeTab(FileWrapper(root)) + onDismiss() + } + } + } + + if (InbuiltFeatures.debugMode.state.value) { + AddDialogItem( + icon = Icon.ResourceIcon(drawables.build), + title = stringResource(strings.private_files), + description = stringResource(strings.private_files_desc), + onClick = { + if (!Settings.has_shown_private_data_dir_warning) { + showPrivateFileWarning { + Settings.has_shown_private_data_dir_warning = true + lifecycleScope.launch { onAddProject(FileWrapper(activity.filesDir.parentFile!!)) } + } + } else { + lifecycleScope.launch { onAddProject(FileWrapper(activity.filesDir.parentFile!!)) } + } + onDismiss() + }, + ) + } + + // Clone repository option + if (InbuiltFeatures.git.state.value) { + AddDialogItem( + icon = Icon.ResourceIcon(drawables.git), + title = stringResource(strings.clone_repo), + description = stringResource(strings.clone_repo_desc), + onClick = { + showGitCloneDialog() + onDismiss() + }, + ) + } + + // Terminal Home option + AddDialogItem( + icon = Icon.ResourceIcon(drawables.terminal), + title = stringResource(strings.terminal_home), + description = stringResource(strings.terminal_home_desc), + onClick = { + if (!Settings.has_shown_terminal_dir_warning) { + showPrivateFileWarning { + Settings.has_shown_terminal_dir_warning = true + lifecycleScope.launch { onAddProject(FileWrapper(sandboxHomeDir())) } + } + } else { + lifecycleScope.launch { onAddProject(FileWrapper(sandboxHomeDir())) } + } + onDismiss() + }, + ) + } + } +} diff --git a/core/main/src/main/java/com/rk/filetree/Drawer.kt b/core/main/src/main/java/com/rk/drawer/Drawer.kt similarity index 56% rename from core/main/src/main/java/com/rk/filetree/Drawer.kt rename to core/main/src/main/java/com/rk/drawer/Drawer.kt index ca908674b..c71a973f9 100644 --- a/core/main/src/main/java/com/rk/filetree/Drawer.kt +++ b/core/main/src/main/java/com/rk/drawer/Drawer.kt @@ -1,13 +1,7 @@ -package com.rk.filetree +package com.rk.drawer -import android.Manifest import android.content.Intent -import android.content.pm.PackageManager -import android.net.Uri -import android.os.Build -import android.os.Environment -import android.os.storage.StorageManager -import androidx.activity.compose.ManagedActivityResultLauncher +import androidx.activity.compose.LocalActivity import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.Crossfade @@ -30,12 +24,10 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Add import androidx.compose.material3.AlertDialog import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.NavigationRail import androidx.compose.material3.NavigationRailDefaults import androidx.compose.material3.NavigationRailItem @@ -48,7 +40,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -61,200 +52,35 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.core.content.ContextCompat -import androidx.lifecycle.lifecycleScope -import com.rk.DefaultScope import com.rk.activities.main.MainActivity -import com.rk.activities.main.fileTreeViewModel import com.rk.activities.main.gitViewModel -import com.rk.components.AddDialogItem import com.rk.components.DoubleInputDialog -import com.rk.file.FileObject -import com.rk.file.FileWrapper -import com.rk.file.child -import com.rk.file.sandboxHomeDir import com.rk.file.toFileObject -import com.rk.git.GitTab +import com.rk.filetree.ProjectCloseConfirmationDialog import com.rk.git.ProgressCoordinator -import com.rk.icons.Icon import com.rk.icons.XedIcon import com.rk.resources.drawables import com.rk.resources.getString import com.rk.resources.strings -import com.rk.settings.Settings -import com.rk.settings.app.InbuiltFeatures -import com.rk.utils.application -import com.rk.utils.dialog -import com.rk.utils.readObject -import com.rk.utils.toast -import com.rk.utils.writeObject +import com.rk.utils.dialogRes import java.io.File -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.withContext - -object DrawerPersistence { - private val saveMutex = Mutex() - - private const val DRAWER_TABS = "drawerTabs" - private const val CURRENT_DRAWER_TAB = "currentDrawerTab" - private const val EXPANDED_FILE_TREE_NODES = "expandedFileTree" - - suspend fun saveState() { - saveMutex.withLock { - val file = FileWrapper(application!!.filesDir.child(DRAWER_TABS)) - val serializableList = ArrayList(drawerTabs) - file.writeObject(serializableList) - - val currentTabFile = FileWrapper(application!!.filesDir.child(CURRENT_DRAWER_TAB)) - if (currentDrawerTab != null) { - currentTabFile.writeObject(currentDrawerTab!!) - } else { - currentTabFile.delete() - } - - val expandedNodeFile = FileWrapper(application!!.filesDir.child(EXPANDED_FILE_TREE_NODES)) - fileTreeViewModel.get()?.getExpandedNodes()?.let { expandedNodeFile.writeObject(it) } - } - } - - suspend fun restoreState() { - saveMutex.withLock { - runCatching { - val loadedTabs = - withContext(Dispatchers.IO) { - val file = FileWrapper(application!!.filesDir.child(DRAWER_TABS)) - - if (file.exists() && file.canRead()) { - file.readObject() as? ArrayList ?: emptyList() - } else { - emptyList() - } - } - - // Update the existing state list on Main thread - withContext(Dispatchers.Main) { - drawerTabs.clear() - drawerTabs.addAll(loadedTabs) - } - - val currentTabFile = FileWrapper(application!!.filesDir.child(CURRENT_DRAWER_TAB)) - if (currentTabFile.exists() && currentTabFile.canRead()) { - selectTab(currentTabFile.readObject() as DrawerTab) - } - - val expandedNodeFile = FileWrapper(application!!.filesDir.child(EXPANDED_FILE_TREE_NODES)) - if (expandedNodeFile.exists() && expandedNodeFile.canRead()) { - fileTreeViewModel - .get() - ?.setExpandedNodes(expandedNodeFile.readObject() as Map) - } - } - .onFailure { - it.printStackTrace() - toast(strings.project_restore_failed) - } - } - } -} - -fun createServices() { - serviceTabs.clear() - serviceTabs.add(GitTab(gitViewModel.get()!!)) -} - -var drawerTabs = mutableStateListOf() -var serviceTabs = mutableStateListOf() -var currentDrawerTab by mutableStateOf(null) -var currentServiceTab by mutableStateOf(null) - -@OptIn(DelicateCoroutinesApi::class) -fun addProject(fileObject: FileObject, save: Boolean = false) { - val alreadyExistingProject = drawerTabs.find { it is FileTreeTab && it.root == fileObject } - if (alreadyExistingProject != null) { - selectTab(alreadyExistingProject) - return - } - val tab = FileTreeTab(fileObject) - tab.onAdded() - drawerTabs.add(tab) - selectTab(tab) - if (save) { - GlobalScope.launch(Dispatchers.IO) { DrawerPersistence.saveState() } - } -} -@OptIn(DelicateCoroutinesApi::class) -fun addProject(tab: DrawerTab, save: Boolean = false) { - tab.onAdded() - drawerTabs.add(tab) - selectTab(tab) - if (save) { - GlobalScope.launch(Dispatchers.IO) { DrawerPersistence.saveState() } - } -} - -@OptIn(DelicateCoroutinesApi::class) -fun removeProject(fileObject: FileObject, save: Boolean = false) { - val index = drawerTabs.indexOfFirst { it is FileTreeTab && it.root == fileObject } - if (index == -1) return - - if (currentDrawerTab == drawerTabs[index]) { - val tabBefore = drawerTabs.getOrNull(index - 1) - val tabAfter = drawerTabs.getOrNull(index + 1) - selectTab(tabBefore ?: tabAfter) - } - - drawerTabs[index].onRemoved() - drawerTabs.removeAt(index) - - if (save) { - GlobalScope.launch(Dispatchers.IO) { DrawerPersistence.saveState() } - } -} - -@OptIn(DelicateCoroutinesApi::class) -fun removeProject(tab: DrawerTab, save: Boolean = false) { - val index = drawerTabs.indexOf(tab) - if (index == -1) return - - if (currentDrawerTab == drawerTabs[index]) { - val tabBefore = drawerTabs.getOrNull(index - 1) - val tabAfter = drawerTabs.getOrNull(index + 1) - selectTab(tabBefore ?: tabAfter) - } - - drawerTabs[index].onRemoved() - drawerTabs.removeAt(index) - - if (save) { - GlobalScope.launch(Dispatchers.IO) { DrawerPersistence.saveState() } - } -} - -fun validateValue(value: String): String? { +private fun validateValue(value: String): String? { return when { value.isBlank() -> strings.value_empty_err.getString() else -> null } } -fun selectTab(tab: DrawerTab?) { - currentDrawerTab = tab - currentServiceTab = null -} - -var isLoading by mutableStateOf(true) - @Composable fun DrawerContent(fullscreen: Boolean) { val context = LocalContext.current val scope = rememberCoroutineScope() + val mainActivity = LocalActivity.current as MainActivity + val viewModel = mainActivity.drawerViewModel + val openFolder = rememberLauncherForActivityResult( contract = ActivityResultContracts.OpenDocumentTree(), @@ -269,13 +95,13 @@ fun DrawerContent(fullscreen: Boolean) { } .onFailure { it.printStackTrace() } - scope.launch { addProject(it.toFileObject(expectedIsFile = false)) } + scope.launch { viewModel.addFileTreeTab(it.toFileObject(expectedIsFile = false)) } } }, ) Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - if (isLoading) { + if (viewModel.isLoading) { CircularProgressIndicator() } else { Row(horizontalArrangement = Arrangement.Start, modifier = Modifier.fillMaxSize()) { @@ -369,7 +195,7 @@ fun DrawerContent(fullscreen: Boolean) { repoURLError = null repoBranchError = null if (success) { - addProject(fileObject) + viewModel.addFileTreeTab(fileObject) } }, ) @@ -387,22 +213,22 @@ fun DrawerContent(fullscreen: Boolean) { ) { Column(modifier = Modifier.fillMaxHeight()) { LazyColumn(modifier = Modifier.weight(1f, fill = true), state = lazyListState) { - items(drawerTabs) { tab -> + items(items = viewModel.drawerTabs) { tab -> if (!tab.isSupported()) return@items NavigationRailItem( - selected = currentDrawerTab == tab, + selected = viewModel.currentDrawerTab == tab, icon = { XedIcon(tab.getIcon()) }, onClick = { - if (currentDrawerTab == tab && currentServiceTab == null) { + if (viewModel.currentDrawerTab == tab && viewModel.currentServiceTab == null) { closeProjectDialog = true } else { - selectTab(tab) + viewModel.selectDrawerTab(tab) } }, label = { Text(tab.getName(), maxLines = 1, overflow = TextOverflow.Ellipsis) }, colors = NavigationRailItemDefaults.colors().let { - if (currentServiceTab == null) it + if (viewModel.currentServiceTab == null) it else it.copy( selectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant, @@ -428,16 +254,16 @@ fun DrawerContent(fullscreen: Boolean) { if (showHorizontalDivider) HorizontalDivider() Column(modifier = Modifier.wrapContentHeight().padding(vertical = 8.dp)) { - serviceTabs.forEach { tab -> + viewModel.serviceTabs.forEach { tab -> if (!tab.isSupported()) return@forEach NavigationRailItem( - selected = currentServiceTab == tab, + selected = viewModel.currentServiceTab == tab, icon = { XedIcon(icon = tab.getIcon()) }, onClick = { - if (currentServiceTab == tab) { - currentServiceTab = null + if (viewModel.currentServiceTab == tab) { + viewModel.unselectServiceTab() } else { - currentServiceTab = tab + viewModel.selectServiceTab(tab) } }, label = { Text(tab.getName(), maxLines = 1, overflow = TextOverflow.Ellipsis) }, @@ -451,8 +277,8 @@ fun DrawerContent(fullscreen: Boolean) { VerticalDivider() Surface { - Crossfade(targetState = currentDrawerTab, label = "file tree") { tab -> - if (currentServiceTab == null) { + Crossfade(targetState = viewModel.currentDrawerTab, label = "file tree") { tab -> + if (viewModel.currentServiceTab == null) { if (tab != null) { tab.Content(modifier = Modifier.fillMaxSize()) } else { @@ -476,18 +302,18 @@ fun DrawerContent(fullscreen: Boolean) { } } - Crossfade(targetState = currentServiceTab) { tab -> + Crossfade(targetState = viewModel.currentServiceTab) { tab -> tab?.Content(modifier = Modifier.fillMaxSize()) } } if (showAddDialog) { - AddProjectDialog( + AddProjectSheet( onDismiss = { showAddDialog = false }, openFolder = openFolder, - onAddProject = { fileObject -> scope.launch { addProject(fileObject, true) } }, + onAddProject = { fileObject -> scope.launch { viewModel.addFileTreeTab(fileObject, true) } }, showPrivateFileWarning = { callback -> - dialog( + dialogRes( title = strings.attention.getString(), msg = strings.warning_private_dir.getString(), onOk = { callback.invoke() }, @@ -553,12 +379,13 @@ fun DrawerContent(fullscreen: Boolean) { ) } + val currentDrawerTab = viewModel.currentDrawerTab if (closeProjectDialog && currentDrawerTab != null) { ProjectCloseConfirmationDialog( - projectName = currentDrawerTab!!.getName(), + projectName = currentDrawerTab.getName(), onConfirm = { closeProjectDialog = false - removeProject(currentDrawerTab!!) + viewModel.removeDrawerTab(currentDrawerTab) }, onDismiss = { closeProjectDialog = false }, ) @@ -567,127 +394,3 @@ fun DrawerContent(fullscreen: Boolean) { } } } - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun AddProjectDialog( - onDismiss: () -> Unit, - onAddProject: (FileObject) -> Unit, - openFolder: ManagedActivityResultLauncher, - showPrivateFileWarning: (onOK: () -> Unit) -> Unit, - showGitCloneDialog: () -> Unit, -) { - val context = LocalContext.current - val activity = context as? MainActivity - val lifecycleScope = remember { activity?.lifecycleScope ?: DefaultScope } - - ModalBottomSheet(onDismissRequest = onDismiss) { - Column(modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp, top = 0.dp)) { - AddDialogItem( - icon = Icon.DrawableRes(drawables.file_symlink), - title = stringResource(strings.open_directory), - description = stringResource(strings.open_dir_desc), - onClick = { - openFolder.launch(null) - onDismiss() - }, - ) - - // Open Path option - val is11Plus = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R - val isManager = is11Plus && Environment.isExternalStorageManager() - val legacyPermission = - ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) != - PackageManager.PERMISSION_GRANTED || - ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) != - PackageManager.PERMISSION_GRANTED - - val storage = Environment.getExternalStorageDirectory() - if ((isManager || (!is11Plus && legacyPermission)) && storage.canWrite() && storage.canRead()) { - AddDialogItem( - icon = Icon.DrawableRes(drawables.android), - title = stringResource(strings.internal_storage), - description = stringResource(strings.open_internal_storage), - onClick = { - addProject(FileWrapper(storage)) - onDismiss() - }, - ) - } - - if (isManager) { - val storageManager = context.getSystemService(StorageManager::class.java) - val volumes = storageManager.storageVolumes - - volumes.forEach { volume -> - val root = volume.directory ?: return@forEach - if (root == storage) return@forEach - if (!root.canRead() || !root.canWrite() || root.listFiles() == null) return@forEach - - val name = volume.getDescription(context) - val removable = volume.isRemovable - val description = if (removable) strings.open_removable_storage else strings.open_internal_storage - - AddDialogItem( - icon = Icon.DrawableRes(drawables.sd_card), - title = name, - description = stringResource(description), - ) { - addProject(FileWrapper(root)) - onDismiss() - } - } - } - - if (InbuiltFeatures.debugMode.state.value) { - AddDialogItem( - icon = Icon.DrawableRes(drawables.build), - title = stringResource(strings.private_files), - description = stringResource(strings.private_files_desc), - onClick = { - if (!Settings.has_shown_private_data_dir_warning) { - showPrivateFileWarning { - Settings.has_shown_private_data_dir_warning = true - lifecycleScope.launch { onAddProject(FileWrapper(activity!!.filesDir.parentFile!!)) } - } - } else { - lifecycleScope.launch { onAddProject(FileWrapper(activity!!.filesDir.parentFile!!)) } - } - onDismiss() - }, - ) - } - - // Clone repository option - if (InbuiltFeatures.git.state.value) { - AddDialogItem( - icon = Icon.DrawableRes(drawables.git), - title = stringResource(strings.clone_repo), - description = stringResource(strings.clone_repo_desc), - onClick = { - showGitCloneDialog() - onDismiss() - }, - ) - } - - // Terminal Home option - AddDialogItem( - icon = Icon.DrawableRes(drawables.terminal), - title = stringResource(strings.terminal_home), - description = stringResource(strings.terminal_home_desc), - onClick = { - if (!Settings.has_shown_terminal_dir_warning) { - showPrivateFileWarning { - Settings.has_shown_terminal_dir_warning = true - lifecycleScope.launch { onAddProject(FileWrapper(sandboxHomeDir())) } - } - } else { - lifecycleScope.launch { onAddProject(FileWrapper(sandboxHomeDir())) } - } - onDismiss() - }, - ) - } - } -} diff --git a/core/main/src/main/java/com/rk/drawer/DrawerPersistence.kt b/core/main/src/main/java/com/rk/drawer/DrawerPersistence.kt new file mode 100644 index 000000000..616224152 --- /dev/null +++ b/core/main/src/main/java/com/rk/drawer/DrawerPersistence.kt @@ -0,0 +1,77 @@ +package com.rk.drawer + +import com.rk.activities.main.fileTreeViewModel +import com.rk.file.FileObject +import com.rk.file.FileWrapper +import com.rk.file.child +import com.rk.resources.strings +import com.rk.utils.application +import com.rk.utils.readObject +import com.rk.utils.toast +import com.rk.utils.writeObject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext + +object DrawerPersistence { + private val saveMutex = Mutex() + + private const val DRAWER_TABS = "drawerTabs" + private const val CURRENT_DRAWER_TAB = "currentDrawerTab" + private const val EXPANDED_FILE_TREE_NODES = "expandedFileTree" + + suspend fun saveState(viewModel: DrawerViewModel) { + saveMutex.withLock { + val file = FileWrapper(application!!.filesDir.child(DRAWER_TABS)) + val serializableList = ArrayList(viewModel.drawerTabs) + file.writeObject(serializableList) + + val currentTabFile = FileWrapper(application!!.filesDir.child(CURRENT_DRAWER_TAB)) + if (viewModel.currentDrawerTab != null) { + currentTabFile.writeObject(viewModel.currentDrawerTab!!) + } else { + currentTabFile.delete() + } + + val expandedNodeFile = FileWrapper(application!!.filesDir.child(EXPANDED_FILE_TREE_NODES)) + fileTreeViewModel.get()?.getExpandedNodes()?.let { expandedNodeFile.writeObject(it) } + } + } + + suspend fun restoreState(viewModel: DrawerViewModel) { + saveMutex.withLock { + runCatching { + val loadedTabs = + withContext(Dispatchers.IO) { + val file = FileWrapper(application!!.filesDir.child(DRAWER_TABS)) + + if (file.exists() && file.canRead()) { + file.readObject() as? ArrayList ?: emptyList() + } else { + emptyList() + } + } + + // Update the existing state list on Main thread + withContext(Dispatchers.Main) { viewModel.forcePushDrawerTabs(loadedTabs) } + + val currentTabFile = FileWrapper(application!!.filesDir.child(CURRENT_DRAWER_TAB)) + if (currentTabFile.exists() && currentTabFile.canRead()) { + viewModel.selectDrawerTab(currentTabFile.readObject() as DrawerTab) + } + + val expandedNodeFile = FileWrapper(application!!.filesDir.child(EXPANDED_FILE_TREE_NODES)) + if (expandedNodeFile.exists() && expandedNodeFile.canRead()) { + fileTreeViewModel + .get() + ?.setExpandedNodes(expandedNodeFile.readObject() as Map>) + } + } + .onFailure { + it.printStackTrace() + toast(strings.project_restore_failed) + } + } + } +} diff --git a/core/main/src/main/java/com/rk/filetree/DrawerTab.kt b/core/main/src/main/java/com/rk/drawer/DrawerTab.kt similarity index 94% rename from core/main/src/main/java/com/rk/filetree/DrawerTab.kt rename to core/main/src/main/java/com/rk/drawer/DrawerTab.kt index 88de7ff1d..48a7207f5 100644 --- a/core/main/src/main/java/com/rk/filetree/DrawerTab.kt +++ b/core/main/src/main/java/com/rk/drawer/DrawerTab.kt @@ -1,4 +1,4 @@ -package com.rk.filetree +package com.rk.drawer import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier diff --git a/core/main/src/main/java/com/rk/drawer/DrawerViewModel.kt b/core/main/src/main/java/com/rk/drawer/DrawerViewModel.kt new file mode 100644 index 000000000..aee82663f --- /dev/null +++ b/core/main/src/main/java/com/rk/drawer/DrawerViewModel.kt @@ -0,0 +1,149 @@ +package com.rk.drawer + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.rk.file.FileObject +import com.rk.filetree.FileTreeTab +import com.rk.git.GitTab +import com.rk.git.GitViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class DrawerViewModel : ViewModel() { + var isLoading by mutableStateOf(true) + + private val _drawerTabs = mutableStateListOf() + private val _serviceTabs = mutableStateListOf() + + val drawerTabs: List + get() = _drawerTabs.toList() + + val serviceTabs: List + get() = _serviceTabs.toList() + + var currentDrawerTabIndex by mutableIntStateOf(0) + private set + + val currentDrawerTab: DrawerTab? + get() = _drawerTabs.getOrNull(currentDrawerTabIndex) + + var currentServiceTabIndex by mutableIntStateOf(0) + private set + + val currentServiceTab: DrawerTab? + get() = _serviceTabs.getOrNull(currentServiceTabIndex) + + internal fun setupBuiltinServices(gitViewModel: GitViewModel) { + _serviceTabs.clear() + _serviceTabs.add(GitTab(gitViewModel)) + currentServiceTabIndex = -1 + } + + fun addFileTreeTab(fileObject: FileObject, save: Boolean = false) { + val existingIndex = _drawerTabs.indexOfFirst { it is FileTreeTab && it.root == fileObject } + + if (existingIndex != -1) { + selectDrawerTab(existingIndex) + return + } + + val tab = FileTreeTab(fileObject) + addDrawerTab(tab, save) + } + + fun addDrawerTab(tab: DrawerTab, save: Boolean = false) { + tab.onAdded() + + _drawerTabs.add(tab) + selectDrawerTab(_drawerTabs.lastIndex) + + if (save) persistAsync() + } + + fun removeFileTreeTab(fileObject: FileObject, save: Boolean = false) { + val index = _drawerTabs.indexOfFirst { it is FileTreeTab && it.root == fileObject } + if (index == -1) return + + removeDrawerTab(index, save) + } + + fun removeDrawerTab(drawerTab: DrawerTab, save: Boolean = false) { + val index = _drawerTabs.indexOf(drawerTab) + if (index == -1) return + + removeDrawerTab(index, save) + } + + fun removeDrawerTab(index: Int, save: Boolean = false) { + if (index !in _drawerTabs.indices) return + + val isActive = currentDrawerTabIndex == index + + _drawerTabs[index].onRemoved() + _drawerTabs.removeAt(index) + + if (_drawerTabs.isEmpty()) { + unselectDrawerTab() + } else if (isActive) { + val newIndex = + when { + index - 1 >= 0 -> index - 1 + index <= _drawerTabs.lastIndex -> index + else -> _drawerTabs.lastIndex + } + selectDrawerTab(newIndex) + } else { + if (currentDrawerTabIndex > index) { + currentDrawerTabIndex -= 1 + } + } + + if (save) persistAsync() + } + + fun selectDrawerTab(drawerTab: DrawerTab) { + val index = _drawerTabs.indexOf(drawerTab) + if (index != -1) selectDrawerTab(index) + } + + fun selectDrawerTab(index: Int) { + if (index !in _drawerTabs.indices) return + + currentDrawerTabIndex = index + currentServiceTabIndex = -1 + } + + fun unselectDrawerTab() { + currentDrawerTabIndex = -1 + currentServiceTabIndex = -1 + } + + fun selectServiceTab(serviceTab: DrawerTab) { + val index = _serviceTabs.indexOf(serviceTab) + if (index != -1) selectServiceTab(index) + } + + fun selectServiceTab(index: Int) { + if (index !in _serviceTabs.indices) return + + currentServiceTabIndex = index + } + + fun unselectServiceTab() { + currentServiceTabIndex = -1 + } + + fun forcePushDrawerTabs(drawerTabs: List) { + _drawerTabs.clear() + _drawerTabs.addAll(drawerTabs) + } + + private fun persistAsync() { + viewModelScope.launch(Dispatchers.IO) { DrawerPersistence.saveState(this@DrawerViewModel) } + } +} diff --git a/core/main/src/main/java/com/rk/editor/Editor.kt b/core/main/src/main/java/com/rk/editor/Editor.kt index b3589fe0a..8bdc73ff9 100644 --- a/core/main/src/main/java/com/rk/editor/Editor.kt +++ b/core/main/src/main/java/com/rk/editor/Editor.kt @@ -14,6 +14,7 @@ import com.rk.settings.editor.DEFAULT_EDITOR_FONT_PATH import com.rk.settings.editor.LineEnding import com.rk.utils.errorDialog import io.github.rosemoe.sora.langs.textmate.TextMateLanguage +import io.github.rosemoe.sora.lsp.editor.LspLanguage import io.github.rosemoe.sora.text.CharPosition import io.github.rosemoe.sora.text.TextRange import io.github.rosemoe.sora.widget.CodeEditor @@ -46,6 +47,7 @@ class Editor : CodeEditor { val editorSurface: Int, val surfaceContainer: Int, val highSurfaceContainer: Int, + val highestSurfaceContainer: Int, val onSurface: Int, val colorPrimary: Int, val selectionBg: Int, @@ -67,6 +69,7 @@ class Editor : CodeEditor { val surfaceColor = if (isDarkMode) colorScheme.surfaceDim else colorScheme.surface val surfaceContainer = colorScheme.surfaceContainer val highSurfaceContainer = colorScheme.surfaceContainerHigh + val highestSurfaceContainer = colorScheme.surfaceContainerHighest val onSurfaceColor = colorScheme.onSurface val colorPrimary = colorScheme.primary val divider = colorScheme.outlineVariant @@ -83,6 +86,7 @@ class Editor : CodeEditor { editorSurface = surfaceColor.toArgb(), surfaceContainer = surfaceContainer.toArgb(), highSurfaceContainer = highSurfaceContainer.toArgb(), + highestSurfaceContainer = highestSurfaceContainer.toArgb(), onSurface = onSurfaceColor.toArgb(), colorPrimary = colorPrimary.toArgb(), selectionBg = selectionBackground.toArgb(), @@ -99,6 +103,7 @@ class Editor : CodeEditor { editorSurface: Int, surfaceContainer: Int, highSurfaceContainer: Int, + highestSurfaceContainer: Int, onSurface: Int, colorPrimary: Int, selectionBg: Int, @@ -114,6 +119,7 @@ class Editor : CodeEditor { editorSurface, surfaceContainer, highSurfaceContainer, + highestSurfaceContainer, onSurface, colorPrimary, selectionBg, @@ -205,13 +211,14 @@ class Editor : CodeEditor { indentStyle?.let { val useTabs = it == PropertyType.IndentStyleValue.tab - (editorLanguage as? TextMateLanguage)?.useTab(useTabs) + getTextMateLanguage()?.useTab(useTabs) } val actualTabSize = indentSize ?: tabSize actualTabSize?.let { props.deleteMultiSpaces = it tabWidth = it + getTextMateLanguage()?.tabSize = it } val endOfLine: PropertyType.EndOfLineValue? = resourceProperties.getValue(PropertyType.end_of_line, null, false) @@ -229,6 +236,21 @@ class Editor : CodeEditor { resourceProperties.getValue(PropertyType.trim_trailing_whitespace, trimTrailingWhitespace, false) } + /** Gets the current [TextMateLanguage], unwrapping it from [LspLanguage] if necessary. */ + fun getTextMateLanguage(): TextMateLanguage? { + val language = editorLanguage + + if (language is TextMateLanguage) { + return language + } + + if (language is LspLanguage) { + return language.wrapperLanguage as? TextMateLanguage + } + + return null + } + fun applyFont() { runCatching { val fontPath = Settings.editor_font_path @@ -243,7 +265,7 @@ class Editor : CodeEditor { typefaceText = font ?: Typeface.DEFAULT typefaceLineNumber = font ?: Typeface.DEFAULT } - .onFailure { errorDialog(it) } + .onFailure { errorDialog(throwable = it) } } fun showSuggestions(yes: Boolean) { @@ -255,9 +277,10 @@ class Editor : CodeEditor { } } - suspend fun setLanguage(textmateScope: String) { + suspend fun configureLanguage(textmateScope: String) { val language = LanguageManager.createLanguage(textmateScope) language.useTab(Settings.actual_tabs) + language.tabSize = Settings.tab_size if (Settings.textmate_suggestions) { val keywords = KeywordManager.getKeywords(textmateScope) diff --git a/core/main/src/main/java/com/rk/editor/Formatters.kt b/core/main/src/main/java/com/rk/editor/Formatters.kt new file mode 100644 index 000000000..92855c91c --- /dev/null +++ b/core/main/src/main/java/com/rk/editor/Formatters.kt @@ -0,0 +1,98 @@ +package com.rk.editor + +import com.rk.extension.api.XedExtensionPoint +import com.rk.file.FileObject +import com.rk.icons.Icon +import com.rk.settings.Preference +import com.rk.settings.Settings +import com.rk.settings.app.InbuiltFeatures +import io.github.rosemoe.sora.lang.format.Formatter + +abstract class FormatterProvider { + abstract val id: String + abstract val label: String + abstract val supportedExtensions: List + abstract val icon: Icon? + + abstract fun getFormatter(): Formatter +} + +sealed class FormatterSource { + data object LSP : FormatterSource() + + data class EXTENSION(val formatter: FormatterProvider) : FormatterSource() +} + +object Formatters { + const val LSP_FORMATTER_ID = "lsp" + + private val _providers = mutableListOf() + val providers: List + get() = _providers.toList() + + @XedExtensionPoint + fun registerFormatter(provider: FormatterProvider) { + if (!_providers.contains(provider)) { + _providers.add(provider) + } + } + + @XedExtensionPoint + fun unregisterFormatter(provider: FormatterProvider) { + _providers.remove(provider) + } + + fun getSourceForId(id: String): FormatterSource? { + if (id == LSP_FORMATTER_ID) return FormatterSource.LSP + + providers + .find { it.id == id } + ?.also { + return FormatterSource.EXTENSION(it) + } + return null + } + + fun getIdOf(source: FormatterSource): String { + return when (source) { + is FormatterSource.LSP -> "lsp" + is FormatterSource.EXTENSION -> source.formatter.id + } + } + + fun isProviderEnabled(provider: FormatterProvider): Boolean { + return Preference.getBoolean("formatter_${provider.id}", true) + } + + fun setProviderEnabled(formatter: FormatterProvider, enabled: Boolean) { + Preference.setBoolean("formatter_${formatter.id}", enabled) + } + + fun getPreferredSourceForFile(file: FileObject): FormatterSource? { + val formatterIds = Settings.formatters.split("|").toTypedArray() + val formatters = formatterIds.mapNotNull { id -> getSourceForId(id) } + + for (formatterType in formatters) { + when (formatterType) { + is FormatterSource.LSP -> return formatterType + is FormatterSource.EXTENSION -> { + val formatter = formatterType.formatter + val fileExt = file.getExtension() + val isSupported = formatter.supportedExtensions.contains(fileExt) + + if (isSupported) return formatterType + } + } + } + + return null + } + + fun isLspFormatterEnabled(): Boolean { + return Preference.getBoolean("formatter_$LSP_FORMATTER_ID", true) && InbuiltFeatures.terminal.state.value + } + + fun setLspFormatterEnabled(enabled: Boolean) { + Preference.setBoolean("formatter_$LSP_FORMATTER_ID", enabled) + } +} diff --git a/core/main/src/main/java/com/rk/editor/XedColorScheme.kt b/core/main/src/main/java/com/rk/editor/XedColorScheme.kt index 8d044b598..10fad4bc7 100644 --- a/core/main/src/main/java/com/rk/editor/XedColorScheme.kt +++ b/core/main/src/main/java/com/rk/editor/XedColorScheme.kt @@ -44,6 +44,7 @@ class XedColorScheme( editorSurface, surfaceContainer, highSurfaceContainer, + highestSurfaceContainer, onSurface, colorPrimary, selectionBg, @@ -70,12 +71,17 @@ class XedColorScheme( SIGNATURE_BACKGROUND, HOVER_BACKGROUND, LINE_NUMBER_PANEL, + TEXT_INLAY_HINT_BACKGROUND, ) setColors(setAlpha(surfaceContainer, 0.4f), MINIMAP_BACKGROUND) setColors(setAlpha(highSurfaceContainer, 0.6f), MINIMAP_VIEWPORT, MINIMAP_VIEWPORT_BORDER) - setColors(highSurfaceContainer, COMPLETION_WND_ITEM_CURRENT) + setColors(highSurfaceContainer, COMPLETION_WND_ITEM_CURRENT, TEXT_HIGHLIGHT_BACKGROUND) + + setColors(highestSurfaceContainer, SNIPPET_BACKGROUND_EDITING, TEXT_HIGHLIGHT_STRONG_BACKGROUND) + setColors(setAlpha(highestSurfaceContainer, 0.8f), SNIPPET_BACKGROUND_RELATED) + setColors(setAlpha(highestSurfaceContainer, 0.6f), SNIPPET_BACKGROUND_INACTIVE) setColors( onSurface, @@ -89,6 +95,7 @@ class XedColorScheme( LINE_NUMBER, LINE_NUMBER_CURRENT, LINE_NUMBER_PANEL_TEXT, + TEXT_INLAY_HINT_FOREGROUND, ) setColors(handleColor, SELECTION_HANDLE) diff --git a/core/main/src/main/java/com/rk/editor/intelligent/IntelligentFeature.kt b/core/main/src/main/java/com/rk/editor/intelligent/IntelligentFeature.kt index 77d0f8450..a0f140d7d 100644 --- a/core/main/src/main/java/com/rk/editor/intelligent/IntelligentFeature.kt +++ b/core/main/src/main/java/com/rk/editor/intelligent/IntelligentFeature.kt @@ -2,6 +2,7 @@ package com.rk.editor.intelligent import androidx.compose.runtime.mutableStateListOf import com.rk.editor.Editor +import com.rk.extension.api.XedExtensionPoint import io.github.rosemoe.sora.event.EditorKeyEvent import io.github.rosemoe.sora.event.KeyBindingEvent @@ -15,12 +16,14 @@ object IntelligentFeatureRegistry { val allFeatures: List get() = builtInFeatures + mutableFeatures + @XedExtensionPoint fun registerFeature(feature: IntelligentFeature) { if (!mutableFeatures.contains(feature)) { mutableFeatures.add(feature) } } + @XedExtensionPoint fun unregisterFeature(feature: IntelligentFeature) { mutableFeatures.remove(feature) } diff --git a/core/main/src/main/java/com/rk/exec/NpmUtils.kt b/core/main/src/main/java/com/rk/exec/NpmUtils.kt index 67c0463de..7943e25b8 100644 --- a/core/main/src/main/java/com/rk/exec/NpmUtils.kt +++ b/core/main/src/main/java/com/rk/exec/NpmUtils.kt @@ -1,5 +1,6 @@ package com.rk.exec +import io.github.z4kn4fein.semver.toVersionOrNull import org.json.JSONObject object NpmUtils { @@ -26,8 +27,8 @@ object NpmUtils { } suspend fun hasUpdate(packageName: String): Boolean { - val currentVersion = getInstalledVersion(packageName) ?: return false - val latestVersion = getLatestVersion(packageName) ?: return false - return currentVersion != latestVersion + val currentVersion = getInstalledVersion(packageName)?.toVersionOrNull(false) ?: return false + val latestVersion = getLatestVersion(packageName)?.toVersionOrNull(false) ?: return false + return currentVersion < latestVersion } } diff --git a/core/main/src/main/java/com/rk/exec/PipxUtils.kt b/core/main/src/main/java/com/rk/exec/PipxUtils.kt index 274ca3bf9..f9119c085 100644 --- a/core/main/src/main/java/com/rk/exec/PipxUtils.kt +++ b/core/main/src/main/java/com/rk/exec/PipxUtils.kt @@ -1,5 +1,6 @@ package com.rk.exec +import io.github.z4kn4fein.semver.toVersionOrNull import org.json.JSONObject object PipxUtils { @@ -45,7 +46,10 @@ object PipxUtils { val obj = JSONObject(result.output) val latest = obj.getString("latest") val installed = obj.getString("installed_version") - installed != latest + + val latestVersion = latest.toVersionOrNull(false) ?: return false + val installedVersion = installed.toVersionOrNull(false) ?: return false + installedVersion < latestVersion } .getOrDefault(false) } diff --git a/core/main/src/main/java/com/rk/exec/TerminalUtils.kt b/core/main/src/main/java/com/rk/exec/TerminalUtils.kt index 73b9383b1..64a5f1d89 100644 --- a/core/main/src/main/java/com/rk/exec/TerminalUtils.kt +++ b/core/main/src/main/java/com/rk/exec/TerminalUtils.kt @@ -1,14 +1,12 @@ package com.rk.exec -import android.content.Context +import android.app.Activity import android.content.Intent -import com.rk.activities.main.MainActivity import com.rk.activities.terminal.Terminal import com.rk.file.child import com.rk.file.localDir import com.rk.file.sandboxDir import com.rk.file.sandboxHomeDir -import com.rk.utils.showTerminalNotice import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -28,9 +26,7 @@ suspend fun isTerminalWorking(): Boolean = return@withContext process.waitFor() == 0 } -fun launchTerminal(context: Context, terminalCommand: TerminalCommand) { - showTerminalNotice(activity = MainActivity.instance!!) { - pendingCommand = terminalCommand - context.startActivity(Intent(context, Terminal::class.java)) - } +fun launchTerminal(activity: Activity, terminalCommand: TerminalCommand) { + pendingCommand = terminalCommand + activity.startActivity(Intent(activity, Terminal::class.java)) } diff --git a/core/main/src/main/java/com/rk/exec/ubuntuProcess.kt b/core/main/src/main/java/com/rk/exec/ubuntuProcess.kt index 2cce1c93f..07790b7fa 100644 --- a/core/main/src/main/java/com/rk/exec/ubuntuProcess.kt +++ b/core/main/src/main/java/com/rk/exec/ubuntuProcess.kt @@ -1,7 +1,6 @@ package com.rk.exec import android.annotation.SuppressLint -import android.os.Build import android.util.Log import com.rk.file.child import com.rk.file.localBinDir @@ -13,7 +12,6 @@ import com.rk.settings.Settings import com.rk.utils.application import com.rk.utils.getSourceDirOfPackage import com.rk.utils.getTempDir -import com.rk.utils.isFDroid import com.rk.xededitor.BuildConfig import java.io.File import java.io.IOException @@ -93,7 +91,7 @@ suspend fun ubuntuProcess( val args = mutableListOf().apply { - add(localBinDir().child("proot").absolutePath) + add("${application!!.applicationInfo.nativeLibraryDir}/libproot.so") add("--kill-on-exit") if (workingDir != null) { @@ -145,7 +143,8 @@ suspend fun ubuntuProcess( "/system/bin/linker" } env["NATIVE_LIB_DIR"] = application!!.applicationInfo.nativeLibraryDir - env["FDROID"] = isFDroid.toString() + env["PROOT"] = "${application!!.applicationInfo.nativeLibraryDir}/libproot.so" + env["PROOT_LOADER"] = "${application!!.applicationInfo.nativeLibraryDir}/libloader.so" env["SANDBOX"] = Settings.sandbox.toString() env["TMP_DIR"] = getTempDir().absolutePath env["TMPDIR"] = getTempDir().absolutePath @@ -170,18 +169,14 @@ suspend fun ubuntuProcess( env["PATH"] = "/bin:/sbin:/usr/bin:/usr/sbin:/usr/games:/usr/local/bin:/usr/local/sbin:${localBinDir()}:${System.getenv("PATH")}" - if (!isFDroid) { - env["PROOT_LOADER"] = "${application!!.applicationInfo.nativeLibraryDir}/libproot-loader.so" - if ( - Build.SUPPORTED_32_BIT_ABIS.isNotEmpty() && - File(application!!.applicationInfo.nativeLibraryDir).child("libproot-loader32.so").exists() - ) { - env["PROOT_LOADER32"] = "${application!!.applicationInfo.nativeLibraryDir}/libproot-loader32.so" - } + val loader32 = "${application!!.applicationInfo.nativeLibraryDir}/libloader32.so" + if (File(loader32).exists()) { + env["PROOT_LOADER_32"] = loader32 } - if (Settings.seccomp) { - env["SECCOMP"] = "1" + when (Settings.seccomp_mode) { + "yes" -> env["SECCOMP"] = "1" + "no" -> env["PROOT_NO_SECCOMP"] = "1" } } diff --git a/core/main/src/main/java/com/rk/extension/ExtensionAPI.kt b/core/main/src/main/java/com/rk/extension/ExtensionAPI.kt deleted file mode 100644 index a75baeada..000000000 --- a/core/main/src/main/java/com/rk/extension/ExtensionAPI.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.rk.extension - -import android.app.Application - -abstract class ExtensionAPI : Application.ActivityLifecycleCallbacks { - abstract fun onExtensionLoaded(extension: Extension) - - abstract fun onUninstalled(extension: Extension) -} diff --git a/core/main/src/main/java/com/rk/extension/ExtensionAPIManager.kt b/core/main/src/main/java/com/rk/extension/ExtensionAPIManager.kt deleted file mode 100644 index 28f6b90c9..000000000 --- a/core/main/src/main/java/com/rk/extension/ExtensionAPIManager.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.rk.extension - -import android.app.Activity -import android.os.Bundle -import com.rk.DefaultScope -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock - -object ExtensionAPIManager : ExtensionAPI(), CoroutineScope by DefaultScope { - private val mutex = Mutex() - - override fun onExtensionLoaded(extension: Extension) { - throw IllegalStateException("This function not be called from here") - } - - override fun onUninstalled(extension: Extension) { - throw IllegalStateException("This function not be called from here") - } - - override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { - launch { - mutex.withLock { - launch { loadedExtensions.values.forEach { it?.onActivityCreated(activity, savedInstanceState) } } - } - } - } - - override fun onActivityDestroyed(activity: Activity) { - launch { mutex.withLock { launch { loadedExtensions.values.forEach { it?.onActivityDestroyed(activity) } } } } - } - - override fun onActivityPaused(activity: Activity) { - launch { mutex.withLock { launch { loadedExtensions.values.forEach { it?.onActivityPaused(activity) } } } } - } - - override fun onActivityResumed(activity: Activity) { - launch { mutex.withLock { launch { loadedExtensions.values.forEach { it?.onActivityResumed(activity) } } } } - } - - override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) { - launch { - mutex.withLock { - launch { loadedExtensions.values.forEach { it?.onActivitySaveInstanceState(activity, outState) } } - } - } - } - - override fun onActivityStarted(activity: Activity) { - launch { mutex.withLock { launch { loadedExtensions.values.forEach { it?.onActivityStarted(activity) } } } } - } - - override fun onActivityStopped(activity: Activity) { - launch { mutex.withLock { launch { loadedExtensions.values.forEach { it?.onActivityStopped(activity) } } } } - } -} diff --git a/core/main/src/main/java/com/rk/extension/ExtensionRegistry.kt b/core/main/src/main/java/com/rk/extension/ExtensionRegistry.kt deleted file mode 100644 index 12a8b021e..000000000 --- a/core/main/src/main/java/com/rk/extension/ExtensionRegistry.kt +++ /dev/null @@ -1,90 +0,0 @@ -package com.rk.extension - -import android.util.Log -import com.rk.utils.errorDialog -import java.io.File -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json -import okhttp3.OkHttpClient -import okhttp3.Request - -@Serializable private data class ExtensionListResponse(val extensions: List) - -@Serializable private data class ExtensionEntry(val manifest: ExtensionManifest) - -@Serializable private data class ExtensionDetail(val downloads: Int? = null, val download: DownloadUrls) - -@Serializable private data class DownloadUrls(val icon: String? = null, val readme: String? = null, val zip: String) - -object ExtensionRegistry { - private const val TAG = "ExtensionRegistry" - private const val BASE_URL = "https://xed-editor.app/api/extensions" - - private val client: OkHttpClient = OkHttpClient() - private val json = Json { ignoreUnknownKeys = true } - - suspend fun fetchExtensions(): List = - withContext(Dispatchers.IO) { - runCatching { - val jsonString = requestJson(BASE_URL) - val response = json.decodeFromString(jsonString) - response.extensions.map { it.manifest } - } - .onFailure { - it.printStackTrace() - throw it - } - .getOrElse { emptyList() } - } - - fun getIconUrl(id: String): String = "$BASE_URL/$id/icon.png" - - fun getReadmeUrl(id: String): String = "$BASE_URL/$id/README.md" - - fun getChangelogUrl(id: String): String = "$BASE_URL/$id/CHANGELOG.md" - - suspend fun getDownloadCount(id: String): Int? = getDetails(id)?.downloads - - private suspend fun getDetails(id: String): ExtensionDetail? = - runCatching { - withContext(Dispatchers.IO) { - val jsonString = requestJson("$BASE_URL/$id") - json.decodeFromString(jsonString) - } - } - .getOrNull() - - private fun requestJson(url: String): String { - val req = Request.Builder().url(url).build() - return client.newCall(req).execute().use { res -> - if (!res.isSuccessful) error("HTTP ${res.code}") - val body = res.body.string() - Log.d(TAG, body) - body - } - } - - suspend fun downloadZip(manifest: ExtensionManifest, destFile: File): Boolean = - withContext(Dispatchers.IO) { - runCatching { - val zipUrl = getDetails(manifest.id)?.download?.zip ?: error("Extension ZIP file was not found.") - - val request = Request.Builder().url(zipUrl).build() - client.newCall(request).execute().use { response -> - if (!response.isSuccessful) error("HTTP ${response.code}") - destFile.parentFile?.mkdirs() - response.body.byteStream().use { input -> - destFile.outputStream().use { output -> input.copyTo(output) } - } - } - true - } - .onFailure { - it.printStackTrace() - errorDialog(it) - } - .getOrElse { false } - } -} diff --git a/core/main/src/main/java/com/rk/extension/ExtensionUtils.kt b/core/main/src/main/java/com/rk/extension/ExtensionUtils.kt deleted file mode 100644 index b33568f37..000000000 --- a/core/main/src/main/java/com/rk/extension/ExtensionUtils.kt +++ /dev/null @@ -1,120 +0,0 @@ -package com.rk.extension - -import android.app.Application -import androidx.core.content.pm.PackageInfoCompat -import com.rk.file.FileObject -import com.rk.file.copyToTempDir -import com.rk.utils.application -import com.rk.utils.errorDialog -import com.rk.utils.isMainThread -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -fun LocalExtension.load(application: Application) = run { - val classLoader = - try { - classLoader(application.classLoader) - } catch (err: Exception) { - return@run Result.failure( - RuntimeException( - "Failed to create ClassLoader for extension '${manifest.name}'. Details: ${err.message}", - err, - ) - ) - } - - if (isMainThread()) { - return@run Result.failure( - RuntimeException( - "Attempted to load extension '${manifest.name}' on the main thread. Extension loading must be performed on a background thread." - ) - ) - } - - val minAppVersion = manifest.minAppVersion - val maxAppVersion = manifest.targetAppVersion - - val xedVersionCode = - PackageInfoCompat.getLongVersionCode(application.packageManager.getPackageInfo(application.packageName, 0)) - - if (!(minAppVersion <= xedVersionCode && maxAppVersion <= xedVersionCode)) { - return@run Result.failure( - RuntimeException( - "Extension '${manifest.name}' (${manifest.version}) is not compatible with this version of Xed-Editor (min: $minAppVersion, max: $maxAppVersion, Xed-Editor: $xedVersionCode)" - ) - ) - } - - val mainClassInstance = - try { - classLoader.loadClass(manifest.mainClass) - } catch (err: Exception) { - return@run Result.failure( - RuntimeException( - "Failed to load main class '${manifest.mainClass}' for extension '${manifest.name}'. Details: ${err.message}", - err, - ) - ) - } - - if (ExtensionAPI::class.java.isAssignableFrom(mainClassInstance)) { - val instance = - mainClassInstance.getDeclaredConstructor().newInstance() as? ExtensionAPI - ?: return@run Result.failure( - RuntimeException( - "Failed to instantiate main class '${mainClassInstance.name}' for extension '${manifest.name}'. The class could not be cast to ExtensionAPI. Ensure it implements the ExtensionAPI interface and has a public no-argument constructor." - ) - ) - - try { - instance.onExtensionLoaded(this) - } catch (err: ClassNotFoundException) { - return@run Result.failure( - RuntimeException( - "Failed to initialize extension '${manifest.name}': A required class was not found. This might indicate a missing dependency or an issue with the extension's packaging. Details: ${err.message}", - err, - ) - ) - } catch (err: NoClassDefFoundError) { - return@run Result.failure( - RuntimeException( - "Failed to initialize extension '${manifest.name}': A class definition was not found. This usually means a class was available at compile time but is missing at runtime. Check the extension's dependencies. Details: ${err.message}", - err, - ) - ) - } catch (err: Exception) { - return@run Result.failure( - RuntimeException( - "Failed to initialize extension '${manifest.name}': An unexpected error occurred during the onExtensionLoaded call. Details: ${err.message}", - err, - ) - ) - } - - loadedExtensions[this] = instance - Result.success(instance) - } else { - Result.failure( - RuntimeException( - "The main class '${manifest.mainClass}' of extension '${manifest.name}' does not implement the ExtensionAPI interface. Please ensure the main class correctly implements this interface." - ) - ) - } -} - -suspend fun ExtensionManager.installExtensionFromZip(fileObject: FileObject) = run { - val file = fileObject.copyToTempDir() - installExtensionFromZip(file).also { file.delete() } -} - -suspend fun ExtensionManager.loadAllExtensions() = - withContext(Dispatchers.IO) { - for ((_, extension) in localExtensions) { - launch(Dispatchers.IO) { - extension.load(application!!).onFailure { - errorDialog(it.message ?: "Failed to load extension '${extension.name}'") - } - } - } - } diff --git a/core/main/src/main/java/com/rk/extension/api/ActivityProvider.kt b/core/main/src/main/java/com/rk/extension/api/ActivityProvider.kt new file mode 100644 index 000000000..83e5ad969 --- /dev/null +++ b/core/main/src/main/java/com/rk/extension/api/ActivityProvider.kt @@ -0,0 +1,39 @@ + + + + +//DO NOT UPDATE PACKAGE NAME OTHERWISE EXTENSIONS WILL BREAK +package com.rk.extension + +import android.app.Activity +import android.app.Application +import android.os.Bundle +import java.lang.ref.WeakReference + +object ActivityProvider : Application.ActivityLifecycleCallbacks { + + private var currentActivityRef: WeakReference? = null + + val currentActivity: Activity? + get() = currentActivityRef?.get() + + override fun onActivityResumed(activity: Activity) { + currentActivityRef = WeakReference(activity) + } + + override fun onActivityPaused(activity: Activity) { + if (currentActivityRef?.get() == activity) { + currentActivityRef = null + } + } + + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) = Unit + + override fun onActivityStarted(activity: Activity) = Unit + + override fun onActivityStopped(activity: Activity) = Unit + + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) = Unit + + override fun onActivityDestroyed(activity: Activity) = Unit +} diff --git a/core/main/src/main/java/com/rk/extension/api/AppResources.kt b/core/main/src/main/java/com/rk/extension/api/AppResources.kt new file mode 100644 index 000000000..06a36c128 --- /dev/null +++ b/core/main/src/main/java/com/rk/extension/api/AppResources.kt @@ -0,0 +1,139 @@ + + + +//DO NOT UPDATE PACKAGE NAME OTHERWISE EXTENSIONS WILL BREAK +package com.rk.extension + +import android.content.Context +import android.content.res.Resources +import android.graphics.Typeface +import android.graphics.drawable.Drawable +import androidx.core.content.res.ResourcesCompat +import java.io.InputStream + +class AppResources(private val context: Context, private val resources: Resources, private val packageName: String) { + + private fun id(name: String, type: String): Int { + return resources.getIdentifier(name, type, packageName) + } + + fun getString(name: String): String? { + val resId = id(name, "string") + return if (resId != 0) resources.getString(resId) else null + } + + fun getStringId(name: String): Int? { + val resId = id(name, "string") + return if (resId != 0) resId else null + } + + fun getText(name: String): CharSequence? { + val resId = id(name, "string") + return if (resId != 0) resources.getText(resId) else null + } + + fun getStringArray(name: String): Array? { + val resId = id(name, "array") + return if (resId != 0) resources.getStringArray(resId) else null + } + + fun getTextArray(name: String): Array? { + val resId = id(name, "array") + return if (resId != 0) resources.getTextArray(resId) else null + } + + fun getIntArray(name: String): IntArray? { + val resId = id(name, "array") + return if (resId != 0) resources.getIntArray(resId) else null + } + + fun getArrayId(name: String): Int? { + val resId = id(name, "array") + return if (resId != 0) resId else null + } + + fun getQuantityString(name: String, quantity: Int): String? { + val resId = id(name, "plurals") + return if (resId != 0) resources.getQuantityString(resId, quantity) else null + } + + fun getQuantityText(name: String, quantity: Int): CharSequence? { + val resId = id(name, "plurals") + return if (resId != 0) resources.getQuantityText(resId, quantity) else null + } + + fun getPluralId(name: String): Int? { + val resId = id(name, "plurals") + return if (resId != 0) resId else null + } + + fun getDrawable(name: String): Drawable? { + val resId = id(name, "drawable") + return if (resId != 0) ResourcesCompat.getDrawable(resources, resId, context.theme) else null + } + + fun getDrawableId(name: String): Int? { + val resId = id(name, "drawable") + return if (resId != 0) resId else null + } + + fun getDimension(name: String): Float? { + val resId = id(name, "dimen") + return if (resId != 0) resources.getDimension(resId) else null + } + + fun getDimensionId(name: String): Int? { + val resId = id(name, "dimen") + return if (resId != 0) resId else null + } + + fun getInteger(name: String): Int? { + val resId = id(name, "integer") + return if (resId != 0) resources.getInteger(resId) else null + } + + fun getIntegerId(name: String): Int? { + val resId = id(name, "integer") + return if (resId != 0) resId else null + } + + fun getBoolean(name: String): Boolean? { + val resId = id(name, "bool") + return if (resId != 0) resources.getBoolean(resId) else null + } + + fun getBooleanId(name: String): Int? { + val resId = id(name, "bool") + return if (resId != 0) resId else null + } + + fun openRaw(name: String): InputStream? { + val resId = id(name, "raw") + return if (resId != 0) resources.openRawResource(resId) else null + } + + fun getRawId(name: String): Int? { + val resId = id(name, "raw") + return if (resId != 0) resId else null + } + + fun getFont(name: String): Typeface? { + val resId = id(name, "font") + return if (resId != 0) ResourcesCompat.getFont(context, resId) else null + } + + fun getFontId(name: String): Int? { + val resId = id(name, "font") + return if (resId != 0) resId else null + } + + fun getAnimationId(name: String): Int? { + val resId = id(name, "anim") + return if (resId != 0) resId else null + } + + fun getAnimatorId(name: String): Int? { + val resId = id(name, "animator") + return if (resId != 0) resId else null + } +} diff --git a/core/main/src/main/java/com/rk/extension/api/ExtensionAPI.kt b/core/main/src/main/java/com/rk/extension/api/ExtensionAPI.kt new file mode 100644 index 000000000..d9bffc56f --- /dev/null +++ b/core/main/src/main/java/com/rk/extension/api/ExtensionAPI.kt @@ -0,0 +1,24 @@ + + + +//DO NOT UPDATE PACKAGE NAME OTHERWISE EXTENSIONS WILL BREAK +package com.rk.extension + +import android.app.Application +import androidx.compose.runtime.Composable + +abstract class ExtensionAPI(protected val context: ExtensionContext) : Application.ActivityLifecycleCallbacks { + /** Called only once when the extension is installed for the first time. */ + abstract fun onInstalled() + + /** Called every time the extension is loaded (app start or extension installation). */ + abstract fun onExtensionLoaded() + + /** Called when the extension is updated to a new version. */ + abstract fun onUpdated() + + /** Called when the extension is uninstalled. */ + abstract fun onUninstalled() + + @Composable open fun SettingsContent() {} +} diff --git a/core/main/src/main/java/com/rk/extension/api/ExtensionContext.kt b/core/main/src/main/java/com/rk/extension/api/ExtensionContext.kt new file mode 100644 index 000000000..9d8dbb126 --- /dev/null +++ b/core/main/src/main/java/com/rk/extension/api/ExtensionContext.kt @@ -0,0 +1,44 @@ + + + + +//DO NOT UPDATE PACKAGE NAME OTHERWISE EXTENSIONS WILL BREAK +package com.rk.extension + +import android.content.Context +import android.content.res.AssetManager +import android.content.res.Resources +import androidx.annotation.Keep +import com.rk.extension.api.XedExtensionPoint +import com.rk.extension.api.logDebug +import com.rk.extension.api.logError +import com.rk.extension.api.logInfo +import com.rk.extension.api.logWarn +import kotlinx.coroutines.CoroutineScope + +@XedExtensionPoint +@Keep +class ExtensionContext(val extension: LocalExtension, val appContext: Context, val scope: CoroutineScope) { + val settings = SharedPrefExtensionSettings(extension.id) + + val currentActivity + get() = ActivityProvider.currentActivity + + val appResources + get() = AppResources(appContext, appContext.resources, appContext.packageName) + + val assets: AssetManager by lazy { + AssetManager::class.java.getDeclaredConstructor().newInstance().apply { + val method = javaClass.getMethod("addAssetPath", String::class.java) + method.invoke(this, extension.apkFile.absolutePath) + } + } + + val resources by lazy { Resources(assets, appContext.resources.displayMetrics, appContext.resources.configuration) } + + //Kept for backward compatibility + fun logDebug(msg: String) = extension.id.logDebug(msg) + fun logInfo(msg: String) = extension.id.logInfo(msg) + fun logWarn(msg: String) = extension.id.logWarn(msg) + fun logError(msg: String) = extension.id.logError(msg) +} diff --git a/core/main/src/main/java/com/rk/extension/api/ExtensionSettings.kt b/core/main/src/main/java/com/rk/extension/api/ExtensionSettings.kt new file mode 100644 index 000000000..b8b7853ae --- /dev/null +++ b/core/main/src/main/java/com/rk/extension/api/ExtensionSettings.kt @@ -0,0 +1,35 @@ + + + +//DO NOT UPDATE PACKAGE NAME OTHERWISE EXTENSIONS WILL BREAK +package com.rk.extension + +import com.rk.settings.Preference + +interface ExtensionSettings { + fun getString(key: String, default: String): String? + + fun getBoolean(key: String, default: Boolean): Boolean + + fun getInt(key: String, default: Int): Int + + fun putString(key: String, value: String) + + fun putBoolean(key: String, value: Boolean) + + fun putInt(key: String, value: Int) +} + +class SharedPrefExtensionSettings(private val id: String) : ExtensionSettings { + override fun getString(key: String, default: String) = Preference.getString("$id.$key", default) + + override fun getBoolean(key: String, default: Boolean) = Preference.getBoolean("$id.$key", default) + + override fun getInt(key: String, default: Int) = Preference.getInt("$id.$key", default) + + override fun putString(key: String, value: String) = Preference.setString("$id.$key", value) + + override fun putBoolean(key: String, value: Boolean) = Preference.setBoolean("$id.$key", value) + + override fun putInt(key: String, value: Int) = Preference.setInt("$id.$key", value) +} diff --git a/core/main/src/main/java/com/rk/extension/api/Logger.kt b/core/main/src/main/java/com/rk/extension/api/Logger.kt new file mode 100644 index 000000000..c69cd025d --- /dev/null +++ b/core/main/src/main/java/com/rk/extension/api/Logger.kt @@ -0,0 +1,24 @@ +package com.rk.extension.api +import android.util.Log +import com.rk.extension.ExtensionId +import com.rk.settings.debugOptions.LogCollector + +fun ExtensionId.logDebug(msg: String) { + Log.d(this, msg) + LogCollector.reportDebug("[${this}]$msg") +} + +fun ExtensionId.logInfo(msg: String) { + Log.i(this, msg) + LogCollector.reportInfo("[${this}]$msg") +} + +fun ExtensionId.logWarn(msg: String) { + Log.w(this, msg) + LogCollector.reportWarn("[${this}]$msg") +} + +fun ExtensionId.logError(msg: String) { + Log.e(this, msg) + LogCollector.reportError("[${this}]$msg") +} \ No newline at end of file diff --git a/core/main/src/main/java/com/rk/extension/api/XedExtensionPoint.kt b/core/main/src/main/java/com/rk/extension/api/XedExtensionPoint.kt new file mode 100644 index 000000000..28865761f --- /dev/null +++ b/core/main/src/main/java/com/rk/extension/api/XedExtensionPoint.kt @@ -0,0 +1,5 @@ +package com.rk.extension.api + +@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY) +@Retention(AnnotationRetention.RUNTIME) +annotation class XedExtensionPoint diff --git a/core/main/src/main/java/com/rk/extension/loader/ExtensionLoader.kt b/core/main/src/main/java/com/rk/extension/loader/ExtensionLoader.kt new file mode 100644 index 000000000..141b87280 --- /dev/null +++ b/core/main/src/main/java/com/rk/extension/loader/ExtensionLoader.kt @@ -0,0 +1,183 @@ +package com.rk.extension.loader + +import android.app.Application +import androidx.core.content.pm.PackageInfoCompat +import com.rk.App +import com.rk.crashhandler.CrashActivity +import com.rk.extension.ExtensionAPI +import com.rk.extension.ExtensionContext +import com.rk.extension.LocalExtension +import com.rk.extension.apkFile +import com.rk.extension.manager.ExtensionManager +import com.rk.extension.manager.LoadedExtension +import com.rk.file.FileObject +import com.rk.file.copyToTempDir +import com.rk.utils.application +import com.rk.utils.isMainThread +import dalvik.system.PathClassLoader +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.lang.reflect.InvocationTargetException +import kotlin.collections.iterator + +/** + * Loads a locally installed extension. + * + * This function performs compatibility checks, instantiates the extension's main class, + * initializes the extension lifecycle, and caches the result. + * + * @param application The main Android [Application] instance. + * @param initialInstallation True if this is the first time the extension is installed/loaded. + * @return A [Result] enclosing the loaded [com.rk.extension.ExtensionAPI] instance, or a failure exception. + */ +fun LocalExtension.load( + application: Application, + initialInstallation: Boolean = false +): Result { + if (isMainThread()) { + return Result.failure( + IllegalStateException( + "Attempted to load extension '${manifest.name}' on the main thread. Extension loading must be performed on a background thread." + ) + ) + } + + return runCatching { + // 1. Verify compatibility with the current app version + verifyCompatibility(application) + + // 2. Create the class loader to load the extension's code + val classLoader = createClassLoader(application) + + // 3. Load the extension's main class and verify it implements ExtensionAPI + val mainClass = loadMainClass(classLoader) + + // 4. Set up the coroutine scope and instantiate the API class + val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO + CoroutineName("Extension: $id")) + val instance = instantiateAPI(mainClass, application, scope) + + // 5. Invoke lifecycle callback methods + if (initialInstallation) { + instance.onInstalled() + } + instance.onExtensionLoaded() + + // 6. Cache the loaded extension in the manager + App.extensionManager.loadedExtensions[this] = LoadedExtension(instance, scope) + instance + } +} + +/** + * Verifies if the extension is compatible with the running version of the editor. + * Throws an [IllegalStateException] if the app version does not satisfy the extension's requirements. + */ +private fun LocalExtension.verifyCompatibility(application: Application) { + val minAppVersion = manifest.minAppVersion + val maxAppVersion = manifest.maxAppVersion + + val xedVersionCode = PackageInfoCompat.getLongVersionCode( + application.packageManager.getPackageInfo(application.packageName, 0) + ) + + val isBelowMin = minAppVersion != null && xedVersionCode < minAppVersion + val isAboveMax = maxAppVersion != null && xedVersionCode > maxAppVersion + + if (isBelowMin || isAboveMax) { + throw IllegalStateException( + "Extension '${manifest.name}' (${manifest.version}) is not compatible with this version of Xed-Editor (min: $minAppVersion, max: $maxAppVersion, Xed-Editor: $xedVersionCode)" + ) + } +} + +/** + * Creates a class loader specifically configured for this extension's APK/package file. + * Uses a child-first delegation strategy so extension-specific libraries take precedence. + */ +private fun LocalExtension.createClassLoader(application: Application): ClassLoader { + return try { + PathClassLoader(apkFile.absolutePath, application.classLoader) + } catch (err: Exception) { + throw IllegalStateException( + "Failed to create ClassLoader for extension '${manifest.name}'. Details: ${err.message}", + err + ) + } +} + + + +/** + * Loads the main entry point class of the extension and asserts that it implements [ExtensionAPI]. + */ +private fun LocalExtension.loadMainClass(classLoader: ClassLoader): Class<*> { + val mainClass = try { + classLoader.loadClass(manifest.mainClass) + } catch (err: Throwable) { + throw err + } + + if (!ExtensionAPI::class.java.isAssignableFrom(mainClass)) { + throw IllegalStateException( + "The main class '${manifest.mainClass}' of extension '${manifest.name}' does not implement the ExtensionAPI interface. Please ensure the main class correctly implements this interface." + ) + } + + return mainClass +} + +/** + * Instantiates the extension's main [ExtensionAPI] class by calling its public constructor + * that accepts an [com.rk.extension.ExtensionContext]. + */ +private fun LocalExtension.instantiateAPI( + mainClassInstance: Class<*>, + application: Application, + scope: CoroutineScope +): ExtensionAPI { + val extContext = ExtensionContext(extension = this, appContext = application, scope = scope) + return try { + val constructor = mainClassInstance.getDeclaredConstructor(ExtensionContext::class.java) + (constructor.newInstance(extContext) as? ExtensionAPI) + ?: throw IllegalStateException( + "Failed to instantiate main class '${mainClassInstance.name}' for extension '${manifest.name}'. Ensure the class implements the ExtensionAPI interface and declares a public constructor accepting ExtensionContext." + ) + } catch (err: Throwable) { + // Unpack Java reflection wrapping to show the real root exception if available + val realError = if (err is InvocationTargetException) err.cause ?: err else err + throw realError + } +} + +/** + * Installs an extension directly from a file object by copying it to a temporary directory first. + */ +suspend fun ExtensionManager.installExtensionFromZip(fileObject: FileObject) = run { + val file = fileObject.copyToTempDir() + installExtensionFromZip(file).also { file.delete() } +} + +/** + * Scans all local extensions and loads any that are not disabled. + * If an extension fails to load, it is marked as disabled and a crash screen is shown. + */ +suspend fun ExtensionManager.loadAllExtensions() = + withContext(Dispatchers.IO) { + for ((_, extension) in localExtensions) { + if (isExtensionDisabled(extension.id)) { + continue + } + launch(Dispatchers.IO) { + extension.load(application!!).onFailure { error -> + setExtensionDisabled(extension.id, true) + withContext(Dispatchers.Main) { + CrashActivity.start(application!!, extension, error) + } + } + } + } + } diff --git a/core/main/src/main/java/com/rk/extension/manager/ExtensionAPIManager.kt b/core/main/src/main/java/com/rk/extension/manager/ExtensionAPIManager.kt new file mode 100644 index 000000000..a304c54a1 --- /dev/null +++ b/core/main/src/main/java/com/rk/extension/manager/ExtensionAPIManager.kt @@ -0,0 +1,49 @@ +package com.rk.extension.manager + +import android.app.Activity +import android.app.Application +import android.os.Bundle +import com.rk.App +import com.rk.DefaultScope +import com.rk.extension.ExtensionAPI +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +object ExtensionAPIManager : Application.ActivityLifecycleCallbacks, CoroutineScope by DefaultScope { + + private fun runForAll(block: ExtensionAPI.() -> Unit) { + App.extensionManager.loadedExtensions.values.forEach { loaded -> loaded?.api?.block() } + } + + private fun runForAllAsync(block: ExtensionAPI.() -> Unit) { + launch { runForAll(block) } + } + + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { + runForAllAsync { onActivityCreated(activity, savedInstanceState) } + } + + override fun onActivityStarted(activity: Activity) { + runForAllAsync { onActivityStarted(activity) } + } + + override fun onActivityResumed(activity: Activity) { + runForAllAsync { onActivityResumed(activity) } + } + + override fun onActivityPaused(activity: Activity) { + runForAllAsync { onActivityPaused(activity) } + } + + override fun onActivityStopped(activity: Activity) { + runForAllAsync { onActivityStopped(activity) } + } + + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) { + runForAllAsync { onActivitySaveInstanceState(activity, outState) } + } + + override fun onActivityDestroyed(activity: Activity) { + runForAllAsync { onActivityDestroyed(activity) } + } +} diff --git a/core/main/src/main/java/com/rk/extension/ExtensionManager.kt b/core/main/src/main/java/com/rk/extension/manager/ExtensionManager.kt similarity index 56% rename from core/main/src/main/java/com/rk/extension/ExtensionManager.kt rename to core/main/src/main/java/com/rk/extension/manager/ExtensionManager.kt index c5860165d..4d2ba0b37 100644 --- a/core/main/src/main/java/com/rk/extension/ExtensionManager.kt +++ b/core/main/src/main/java/com/rk/extension/manager/ExtensionManager.kt @@ -1,21 +1,34 @@ -package com.rk.extension +package com.rk.extension.manager import android.app.Application import android.content.Context import androidx.compose.runtime.mutableStateMapOf import androidx.core.content.pm.PackageInfoCompat import com.rk.file.child +import com.rk.resources.getString +import com.rk.resources.strings import com.rk.utils.errorDialog -import java.io.File -import java.util.zip.ZipFile import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json +import java.io.File +import java.util.zip.ZipFile +import androidx.core.content.edit +import com.rk.extension.Extension +import com.rk.extension.ExtensionAPI +import com.rk.extension.ExtensionError +import com.rk.extension.ExtensionId +import com.rk.extension.ExtensionManifest +import com.rk.extension.InstallResult +import com.rk.extension.LocalExtension +import com.rk.extension.StoreExtension +import com.rk.extension.UpdatableExtension private val Context.localDir: File get() = filesDir.parentFile!!.resolve("local").apply { if (!exists()) mkdirs() } @@ -25,12 +38,16 @@ val Context.extensionDir: File internal fun Context.compiledDexDir() = extensionDir.resolve("oat") +data class LoadedExtension(val api: ExtensionAPI, val scope: CoroutineScope) + open class ExtensionManager(private val context: Application) : CoroutineScope by CoroutineScope(Dispatchers.IO) { private val mutex = Mutex() val localExtensions = mutableStateMapOf() val storeExtension = mutableStateMapOf() val json = Json { ignoreUnknownKeys = true } + val loadedExtensions = mutableStateMapOf() + init { launch(Dispatchers.IO) { runCatching { @@ -40,30 +57,80 @@ open class ExtensionManager(private val context: Application) : CoroutineScope b } } - suspend fun indexLocalExtensions() = - mutex.withLock { - localExtensions.clear() - - withContext(Dispatchers.IO) { - val extensionFolders = context.extensionDir.listFiles()?.filter { it.isDirectory } - extensionFolders?.forEach { dir -> - val extensionJson = dir.resolve("manifest.json") - if (extensionJson.exists()) { - runCatching { - val extensionManifest = json.decodeFromString(extensionJson.readText()) - val extension = LocalExtension(manifest = extensionManifest, installPath = dir.absolutePath) - localExtensions[extensionManifest.id] = extension - } + private val disabledPrefs by lazy { + context.getSharedPreferences("disabled_extensions", Context.MODE_PRIVATE) + } + + fun isExtensionDisabled(id: ExtensionId): Boolean { + return disabledPrefs.getBoolean(id, false) + } + + fun setExtensionDisabled(id: ExtensionId, disabled: Boolean) { + disabledPrefs.edit { putBoolean(id, disabled) } + } + + fun isInstalled(extensionId: ExtensionId) = localExtensions.containsKey(extensionId) + + fun getExtension(extensionId: ExtensionId): Extension? { + val local = localExtensions[extensionId] + val store = storeExtension[extensionId] + + return when { + local != null && store != null -> UpdatableExtension(local, store) + local != null -> local + store != null -> store + else -> null + } + } + + fun getSyncedExtensions(): List { + val allIds = localExtensions.keys + storeExtension.keys + return allIds.mapNotNull { id -> getExtension(id) } + } + + fun getLocalExtensions(): List { + return getSyncedExtensions().filterIsInstance() + } + + fun getStoreExtensions(): List { + return getSyncedExtensions().filter { it is StoreExtension || it is UpdatableExtension } + } + + suspend fun indexLocalExtensions() = mutex.withLock { + val newExtensions = withContext(Dispatchers.IO) { + val map = mutableMapOf() + val extensionFolders = context.extensionDir.listFiles()?.filter { it.isDirectory } + extensionFolders?.forEach { dir -> + val extensionJson = dir.resolve("manifest.json") + if (extensionJson.exists()) { + runCatching { + val extensionManifest = json.decodeFromString(extensionJson.readText()) + val extension = LocalExtension( + manifest = extensionManifest, + installPath = dir.absolutePath + ) + map[extensionManifest.id] = extension } } } + map + } + withContext(Dispatchers.Main) { + val toRemove = localExtensions.keys.filter { it !in newExtensions } + toRemove.forEach { localExtensions.remove(it) } + localExtensions.putAll(newExtensions) } + } suspend fun indexStoreExtensions() = withContext(Dispatchers.IO) { val extensions = ExtensionRegistry.fetchExtensions() - storeExtension.clear() - storeExtension.putAll(extensions.associate { it.id to StoreExtension(it) }) + val newExtensions = extensions.associate { it.id to StoreExtension(it) } + withContext(Dispatchers.Main) { + val toRemove = storeExtension.keys.filter { it !in newExtensions } + toRemove.forEach { storeExtension.remove(it) } + storeExtension.putAll(newExtensions) + } } suspend fun installStoreExtension(context: Context, extension: StoreExtension) = runCatching { @@ -130,34 +197,46 @@ open class ExtensionManager(private val context: Application) : CoroutineScope b val extensionInfo = validation.getOrThrow() val targetDir = context.extensionDir.resolve(extensionInfo.id) + var performedUpdate = false if (targetDir.exists()) { - uninstallExtension(extensionInfo.id) + uninstallExtension(extensionInfo.id, update = true) + performedUpdate = true } val pm = context.packageManager val xedVersionCode = PackageInfoCompat.getLongVersionCode(pm.getPackageInfo(context.packageName, 0)) - if (extensionInfo.minAppVersion != -1 && xedVersionCode < extensionInfo.minAppVersion) { + if (extensionInfo.minAppVersion != null && xedVersionCode < extensionInfo.minAppVersion) { return@withContext InstallResult.Error(ExtensionError.OUTDATED_CLIENT) - } else if (extensionInfo.targetAppVersion != -1 && xedVersionCode > extensionInfo.targetAppVersion) { + } else if (extensionInfo.maxAppVersion != null && xedVersionCode > extensionInfo.maxAppVersion) { return@withContext InstallResult.Error(ExtensionError.OUTDATED_EXTENSION) } dir.copyRecursively(targetDir, overwrite = true) - val extension = LocalExtension(manifest = extensionInfo, installPath = targetDir.absolutePath) + val extension = + LocalExtension(manifest = extensionInfo, installPath = targetDir.absolutePath) localExtensions[extensionInfo.id] = extension - InstallResult.Success(extension) + InstallResult.Success(extension, performedUpdate) } - suspend fun uninstallExtension(extensionId: ExtensionId) = + suspend fun uninstallExtension(extensionId: ExtensionId, update: Boolean = false) = withContext(Dispatchers.IO) { try { val extension = localExtensions[extensionId] ?: return@withContext Result.failure(Exception("Extension not found")) - loadedExtensions[extension]?.onUninstalled(extension) + val loadedExtension = loadedExtensions[extension] + runCatching { + if (update) { + loadedExtension?.api?.onUpdated() + } else { + loadedExtension?.api?.onUninstalled() + } + } + .onFailure { errorDialog(title = strings.ext_cleanup_failed.getString(), throwable = it) } + loadedExtensions[extension]?.scope?.cancel() val extensionDir = File(extension.installPath) if (!extensionDir.exists()) { @@ -167,6 +246,7 @@ open class ExtensionManager(private val context: Application) : CoroutineScope b extensionDir.deleteRecursively() localExtensions.remove(extensionId) context.compiledDexDir().deleteWithPackageName(extension.manifest.id) + disabledPrefs.edit { remove(extensionId) } Result.success(Unit) } catch (err: Exception) { @@ -180,11 +260,4 @@ open class ExtensionManager(private val context: Application) : CoroutineScope b delete() } else if (name.startsWith(pkgName)) delete() } - - fun isInstalled(extensionId: ExtensionId) = localExtensions.containsKey(extensionId) - - fun getExtension(extensionId: ExtensionId) = localExtensions[extensionId] ?: storeExtension[extensionId] - - fun getExtensionManifest(extensionId: ExtensionId) = - localExtensions[extensionId]?.manifest ?: storeExtension[extensionId]?.manifest } diff --git a/core/main/src/main/java/com/rk/extension/manager/ExtensionRegistry.kt b/core/main/src/main/java/com/rk/extension/manager/ExtensionRegistry.kt new file mode 100644 index 000000000..e9072b171 --- /dev/null +++ b/core/main/src/main/java/com/rk/extension/manager/ExtensionRegistry.kt @@ -0,0 +1,213 @@ +package com.rk.extension.manager + +import android.util.Log +import com.rk.XedConstants +import com.rk.extension.ExtensionManifest +import com.rk.extension.ExtensionStats +import com.rk.utils.errorDialog +import java.io.File +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import com.rk.utils.okHttpClient +import okhttp3.OkHttpClient +import okhttp3.Request +import com.google.gson.GsonBuilder +import com.rk.theme.ThemeConfig +import com.rk.icons.pack.IconPackManifest + +import androidx.compose.runtime.mutableStateMapOf +import com.rk.settings.extension.InstallState + +@Serializable private data class ExtensionListResponse(val extensions: List) + +@Serializable private data class ExtensionEntry(val manifest: ExtensionManifest) + +@Serializable private data class ExtensionDetail(val downloads: Int? = null, val download: DownloadUrls) + +@Serializable +private data class DownloadUrls(val icon: String? = null, val readme: String? = null, val zip: String, val size: Int) + +@Serializable +data class ThemeStoreEntry( + val id: String, + val userId: String, + val manifest: JsonObject +) + +@Serializable +data class ThemesResponse(val themes: List) + +@Serializable +data class IconPackStoreEntry( + val id: String, + val userId: String, + val manifest: IconPackManifest +) + +@Serializable +data class IconPacksResponse(val iconPacks: List) + +object ExtensionRegistry { + private const val TAG = "ExtensionRegistry" + private const val BASE_URL = XedConstants.EXTENSION_API_BASE + + private val client: OkHttpClient = okHttpClient + private val json = Json { ignoreUnknownKeys = true } + + val downloadProgress = mutableStateMapOf() + val activeInstalls = mutableStateMapOf() + + suspend fun downloadFileWithProgress( + url: String, + destFile: File, + onProgress: (progress: Float) -> Unit + ): Boolean = withContext(Dispatchers.IO) { + runCatching { + val request = Request.Builder().url(url).build() + client.newCall(request).execute().use { response -> + if (!response.isSuccessful) error("HTTP ${response.code}") + val totalBytes = response.body.contentLength() + destFile.parentFile?.mkdirs() + response.body.byteStream().use { input -> + destFile.outputStream().use { output -> + val buffer = ByteArray(8192) + var bytesRead: Int + var totalBytesRead = 0L + while (input.read(buffer).also { bytesRead = it } != -1) { + output.write(buffer, 0, bytesRead) + totalBytesRead += bytesRead + if (totalBytes > 0) { + val progress = totalBytesRead.toFloat() / totalBytes + onProgress(progress) + } else { + onProgress(-1f) + } + } + } + } + } + true + }.onFailure { + it.printStackTrace() + }.getOrElse { false } + } + + suspend fun fetchExtensions(): List = + withContext(Dispatchers.IO) { + runCatching { + val jsonString = requestJson(BASE_URL) + val response = json.decodeFromString(jsonString) + response.extensions.map { it.manifest } + } + .onFailure { + it.printStackTrace() + throw it + } + .getOrElse { emptyList() } + } + + fun getIconUrl(id: String): String = "$BASE_URL/$id/icon.png" + + fun getReadmeUrl(id: String): String = "$BASE_URL/$id/README.md" + + fun getChangelogUrl(id: String): String = "$BASE_URL/$id/CHANGELOG.md" + + suspend fun getStats(id: String): ExtensionStats { + val details = + runCatching { + withContext(Dispatchers.IO) { + val jsonString = requestJson("$BASE_URL/$id") + json.decodeFromString(jsonString) + } + } + .getOrNull() + + return ExtensionStats( + downloadCount = details?.downloads, + rating = null, + size = details?.download?.size?.toLong(), + ) + } + + private fun requestJson(url: String): String { + val req = Request.Builder().url(url).build() + return client.newCall(req).execute().use { res -> + if (!res.isSuccessful) error("HTTP ${res.code}") + val body = res.body.string() + Log.d(TAG, body) + body + } + } + + suspend fun downloadZip(manifest: ExtensionManifest, destFile: File): Boolean = + withContext(Dispatchers.IO) { + runCatching { + val zipUrl = "$BASE_URL/${manifest.id}/plugin.zip" + + val request = Request.Builder().url(zipUrl).build() + client.newCall(request).execute().use { response -> + if (!response.isSuccessful) error("HTTP ${response.code}") + destFile.parentFile?.mkdirs() + response.body.byteStream().use { input -> + destFile.outputStream().use { output -> input.copyTo(output) } + } + } + true + } + .onFailure { + it.printStackTrace() + errorDialog(throwable = it) + } + .getOrElse { false } + } + + suspend fun fetchThemes(): List = + withContext(Dispatchers.IO) { + runCatching { + val jsonString = requestJson(XedConstants.THEMES_API_BASE) + val response = json.decodeFromString(jsonString) + response.themes + } + .onFailure { + it.printStackTrace() + } + .getOrElse { emptyList() } + } + + suspend fun fetchIconPacks(): List = + withContext(Dispatchers.IO) { + runCatching { + val jsonString = requestJson(XedConstants.ICONPACKS_API_BASE) + val response = json.decodeFromString(jsonString) + response.iconPacks + } + .onFailure { + it.printStackTrace() + } + .getOrElse { emptyList() } + } + + suspend fun downloadIconPackZip(id: String, destFile: File): Boolean = + withContext(Dispatchers.IO) { + runCatching { + val zipUrl = "${XedConstants.ICONPACKS_API_BASE}/$id/iconpack.zip" + val request = Request.Builder().url(zipUrl).build() + client.newCall(request).execute().use { response -> + if (!response.isSuccessful) error("HTTP ${response.code}") + destFile.parentFile?.mkdirs() + response.body.byteStream().use { input -> + destFile.outputStream().use { output -> input.copyTo(output) } + } + } + true + } + .onFailure { + it.printStackTrace() + errorDialog(throwable = it) + } + .getOrElse { false } + } +} diff --git a/core/main/src/main/java/com/rk/extension/Extension.kt b/core/main/src/main/java/com/rk/extension/model/Extension.kt similarity index 51% rename from core/main/src/main/java/com/rk/extension/Extension.kt rename to core/main/src/main/java/com/rk/extension/model/Extension.kt index 559dc6ade..36c4137dc 100644 --- a/core/main/src/main/java/com/rk/extension/Extension.kt +++ b/core/main/src/main/java/com/rk/extension/model/Extension.kt @@ -1,14 +1,14 @@ package com.rk.extension - -import android.content.Context -import android.content.pm.PackageManager -import androidx.compose.runtime.mutableStateMapOf -import dalvik.system.PathClassLoader +import com.rk.extension.manager.ExtensionRegistry +import com.rk.xededitor.BuildConfig +import io.github.z4kn4fein.semver.toVersionOrNull import java.io.File import java.util.Date +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import kotlinx.serialization.Serializable -val loadedExtensions = mutableStateMapOf() +data class ExtensionStats(val downloadCount: Int?, val rating: Float?, val size: Long?) sealed interface Extension { val id: ExtensionId @@ -19,19 +19,20 @@ sealed interface Extension { val tags: List val repository: String val license: String? + val hasSettings: Boolean val iconUrl: String val readmeUrl: String val changelogUrl: String - suspend fun calcSize(): Long + val minAppVersion: Int? - suspend fun getRating(): Float? + val maxAppVersion: Int? - suspend fun getReviews(): List + suspend fun getStats(): ExtensionStats - suspend fun getDownloadCount(): Int? + suspend fun getReviews(): List } data class Review(val rating: Int, val text: String, val author: String, val date: Date, val authorResponse: String?) @@ -68,7 +69,8 @@ data class StoreExtension(val manifest: ExtensionManifest, val verified: Boolean override val license get() = manifest.license - override suspend fun getRating() = null + override val hasSettings: Boolean + get() = manifest.hasSettings override val iconUrl: String get() = ExtensionRegistry.getIconUrl(manifest.id) @@ -79,11 +81,15 @@ data class StoreExtension(val manifest: ExtensionManifest, val verified: Boolean override val changelogUrl get() = ExtensionRegistry.getChangelogUrl(manifest.id) - override suspend fun calcSize() = 0L // TODO + override val minAppVersion + get() = manifest.minAppVersion - override suspend fun getReviews(): List = emptyList() + override val maxAppVersion + get() = manifest.maxAppVersion + + override suspend fun getStats() = ExtensionRegistry.getStats(manifest.id) - override suspend fun getDownloadCount() = ExtensionRegistry.getDownloadCount(manifest.id) + override suspend fun getReviews(): List = emptyList() } /** Extensions that are installed locally (from disk). */ @@ -132,6 +138,9 @@ data class LocalExtension( override val license get() = manifest.license + override val hasSettings: Boolean + get() = manifest.hasSettings + override val iconUrl get() = "$installPath/icon.png" @@ -141,91 +150,120 @@ data class LocalExtension( override val changelogUrl get() = "$installPath/CHANGELOG.md" - override suspend fun calcSize(): Long { - var totalSize = 0L - val stack = ArrayDeque() - stack.add(File(installPath)) - - loop@ while (stack.isNotEmpty()) { - val current = stack.removeLast() - runCatching { - if (current.isDirectory()) { - val files = current.listFiles() ?: continue@loop - stack.addAll(files) - } else { - totalSize += current.length() + override val minAppVersion + get() = manifest.minAppVersion + + override val maxAppVersion + get() = manifest.maxAppVersion + + override suspend fun getStats(): ExtensionStats { + return ExtensionStats(null, null, calcSize()) + } + + private suspend fun calcSize(): Long { + return withContext(Dispatchers.IO) { + var totalSize = 0L + val stack = ArrayDeque() + stack.add(File(installPath)) + + loop@ while (stack.isNotEmpty()) { + val current = stack.removeLast() + runCatching { + if (current.isDirectory()) { + val files = current.listFiles() ?: continue@loop + stack.addAll(files) + } else { + totalSize += current.length() + } } } + totalSize } - return totalSize } - override suspend fun getRating() = null - override suspend fun getReviews(): List = emptyList() - - override suspend fun getDownloadCount() = null } data class UpdatableExtension(val installed: LocalExtension, val store: StoreExtension) : Extension { override val id - get() = installed.id + get() = store.id override val name - get() = installed.name + get() = store.name override val version get() = installed.version + val newVersion: String + get() = store.version + override val author - get() = installed.author + get() = store.author override val description - get() = installed.description + get() = store.description override val tags - get() = installed.tags + get() = store.tags override val repository - get() = installed.repository + get() = store.repository override val license - get() = installed.license + get() = store.license + + override val hasSettings: Boolean + get() = installed.hasSettings override val iconUrl - get() = installed.iconUrl + get() = if (isUpdatable()) store.iconUrl else installed.iconUrl override val readmeUrl - get() = installed.readmeUrl + get() = if (isUpdatable()) store.readmeUrl else installed.readmeUrl override val changelogUrl - get() = installed.changelogUrl + get() = if (isUpdatable()) store.changelogUrl else installed.changelogUrl + + override val minAppVersion + get() = store.minAppVersion - override suspend fun calcSize() = installed.calcSize() + override val maxAppVersion + get() = store.maxAppVersion - override suspend fun getRating() = store.getRating() + override suspend fun getStats() = store.getStats() override suspend fun getReviews() = store.getReviews() - override suspend fun getDownloadCount() = store.getDownloadCount() + fun isUpdatable(): Boolean { + val installedVersion = installed.version.toVersionOrNull() ?: return false + val storeVersion = store.version.toVersionOrNull() ?: return false + return installedVersion < storeVersion + } } -fun LocalExtension.classLoader(parent: ClassLoader?) = PathClassLoader(apkFile.absolutePath, parent) -val LocalExtension.apkFile +val LocalExtension.apkFile: File get() = run { val dir = File(installPath) if (!dir.isDirectory) error("Extension [$name, $id] directory not found") - dir.listFiles { it.extension == "apk" }?.first()?.also { it.setReadOnly() } ?: error("APK not found") - } - -fun LocalExtension.getApkPackageInfo(context: Context) = run { - val pm = context.packageManager - pm.getPackageArchiveInfo(apkFile.absolutePath, PackageManager.GET_META_DATA or PackageManager.GET_ACTIVITIES)!! - .apply { - applicationInfo!!.sourceDir = apkFile.absolutePath - applicationInfo!!.publicSourceDir = apkFile.absolutePath + val apks = dir.listFiles { it.extension == "apk" } ?: emptyArray() + if (apks.isEmpty()) error("APK not found") + + val apk = if (apks.size == 1) { + apks.first() + } else { + val isDebug = BuildConfig.DEBUG + if (isDebug) { + apks.find { it.name.contains("debug", ignoreCase = true) } + ?: apks.find { !it.name.contains("release", ignoreCase = true) } + ?: apks.first() + } else { + apks.find { it.name.contains("release", ignoreCase = true) } + ?: apks.find { !it.name.contains("debug", ignoreCase = true) } + ?: apks.first() + } } -} + apk.also { it.setReadOnly() } + } diff --git a/core/main/src/main/java/com/rk/extension/ExtensionManifest.kt b/core/main/src/main/java/com/rk/extension/model/ExtensionManifest.kt similarity index 85% rename from core/main/src/main/java/com/rk/extension/ExtensionManifest.kt rename to core/main/src/main/java/com/rk/extension/model/ExtensionManifest.kt index 0cf308140..a76637d22 100644 --- a/core/main/src/main/java/com/rk/extension/ExtensionManifest.kt +++ b/core/main/src/main/java/com/rk/extension/model/ExtensionManifest.kt @@ -13,11 +13,12 @@ data class ExtensionManifest( val version: String = "1.0.0", val description: String? = null, val author: ExtensionAuthor, - val minAppVersion: Int = -1, // -1 means supports all versions - val targetAppVersion: Int = -1, // -1 means supports all versions + val minAppVersion: Int? = null, // null means no minimum restriction + val maxAppVersion: Int? = null, // null means no maximum restriction val repository: String, val license: String? = null, val tags: List = emptyList(), + val hasSettings: Boolean = false, ) { override fun equals(other: Any?): Boolean { if (this === other) return true diff --git a/core/main/src/main/java/com/rk/extension/InstallResult.kt b/core/main/src/main/java/com/rk/extension/model/InstallResult.kt similarity index 70% rename from core/main/src/main/java/com/rk/extension/InstallResult.kt rename to core/main/src/main/java/com/rk/extension/model/InstallResult.kt index 1a160a96f..995c43dab 100644 --- a/core/main/src/main/java/com/rk/extension/InstallResult.kt +++ b/core/main/src/main/java/com/rk/extension/model/InstallResult.kt @@ -1,12 +1,10 @@ package com.rk.extension sealed interface InstallResult { - data class Success(val extension: LocalExtension) : InstallResult + data class Success(val extension: LocalExtension, val performedUpdate: Boolean) : InstallResult data class ValidationFailed(val error: Throwable?) : InstallResult - class AlreadyInstalled : InstallResult - data class Error(val error: ExtensionError) : InstallResult } diff --git a/core/main/src/main/java/com/rk/file/BuiltinFileType.kt b/core/main/src/main/java/com/rk/file/BuiltinFileType.kt index f37bdde67..ea661b6ed 100644 --- a/core/main/src/main/java/com/rk/file/BuiltinFileType.kt +++ b/core/main/src/main/java/com/rk/file/BuiltinFileType.kt @@ -1,5 +1,6 @@ package com.rk.file +import com.rk.extension.api.XedExtensionPoint import com.rk.icons.Icon import com.rk.icons.pack.currentIconPack import com.rk.resources.drawables @@ -52,6 +53,7 @@ private val diff = drawables.diff private val cmake = drawables.cmake private val powershell = drawables.powershell private val r = drawables.r +private val nix = drawables.nix // TODO: Add icon for FileType.EXECUTABLE // TODO: Add icon for FileType.PASCAL @@ -81,8 +83,8 @@ interface FileType { get() = null val textmateScope: String? - val icon: Int? - val iconOverride: Map? + val icon: Icon? + val iconOverride: Map? get() = null val name: String @@ -104,11 +106,9 @@ interface FileType { * * @return An [Icon] representing the file type icon. */ - fun getIcon(): Icon { + fun getResolvedIcon(): Icon { val iconPackFile = currentIconPack.value?.getIconFileForFileType(this) - return iconPackFile?.let { Icon.SvgIcon(it) } - ?: icon?.let { Icon.DrawableRes(it) } - ?: Icon.DrawableRes(drawables.file) + return iconPackFile?.let { Icon.SvgIcon(it) } ?: icon ?: Icon.ResourceIcon(drawables.file) } } @@ -123,8 +123,17 @@ object FileTypeManager { private val dynamicRegistry = mutableListOf() /** Register a new file type dynamically. */ + @XedExtensionPoint fun register(fileType: FileType) { - dynamicRegistry.add(fileType) + if (!dynamicRegistry.contains(fileType)) { + dynamicRegistry.add(fileType) + } + } + + /** Unregister a file type. */ + @XedExtensionPoint + fun unregister(fileType: FileType) { + dynamicRegistry.remove(fileType) } /** Get all dynamically registered file types + built-in file types together */ @@ -158,8 +167,8 @@ enum class BuiltinFileType( override val extensions: List, override val names: List? = null, override val textmateScope: String?, - override val icon: Int?, - override val iconOverride: Map? = null, + override val icon: Icon?, + override val iconOverride: Map? = null, override val title: String, override val markdownNames: List = emptyList(), ) : FileType { @@ -167,46 +176,71 @@ enum class BuiltinFileType( JAVASCRIPT( extensions = listOf("js", "mjs", "cjs", "jscsrc", "jshintrc", "mut"), textmateScope = "source.js", - icon = js, + icon = Icon.ResourceIcon(js), title = "JavaScript", markdownNames = listOf("javascript"), ), TYPESCRIPT( extensions = listOf("ts", "mts", "cts"), textmateScope = "source.ts", - icon = ts, + icon = Icon.ResourceIcon(ts), title = "TypeScript", markdownNames = listOf("typescript"), ), - JSX(extensions = listOf("jsx"), textmateScope = "source.js.jsx", icon = react, title = "JavaScript JSX"), - TSX(extensions = listOf("tsx"), textmateScope = "source.tsx", icon = react, title = "TypeScript JSX"), + JSX( + extensions = listOf("jsx"), + textmateScope = "source.js.jsx", + icon = Icon.ResourceIcon(react), + title = "JavaScript JSX", + ), + TSX( + extensions = listOf("tsx"), + textmateScope = "source.tsx", + icon = Icon.ResourceIcon(react), + title = "TypeScript JSX", + ), HTML( extensions = listOf("html", "htm", "xhtml", "xht"), textmateScope = "text.html.basic", - icon = html, + icon = Icon.ResourceIcon(html), title = "HTML", ), - HTMX(extensions = listOf("htmx"), textmateScope = "text.html.htmx", icon = html, title = "HTMX"), - CSS(extensions = listOf("css"), textmateScope = "source.css", icon = css, title = "CSS"), - SCSS(extensions = listOf("scss", "sass"), textmateScope = "source.css.scss", icon = sass, title = "SCSS"), - LESS(extensions = listOf("less"), textmateScope = "source.css.less", icon = less, title = "Less"), - JSON(extensions = listOf("json", "jsonl", "jsonc"), textmateScope = "source.json", icon = json, title = "JSON"), + HTMX(extensions = listOf("htmx"), textmateScope = "text.html.htmx", icon = Icon.ResourceIcon(html), title = "HTMX"), + CSS(extensions = listOf("css"), textmateScope = "source.css", icon = Icon.ResourceIcon(css), title = "CSS"), + SCSS( + extensions = listOf("scss", "sass"), + textmateScope = "source.css.scss", + icon = Icon.ResourceIcon(sass), + title = "SCSS", + ), + LESS( + extensions = listOf("less"), + textmateScope = "source.css.less", + icon = Icon.ResourceIcon(less), + title = "Less", + ), + JSON( + extensions = listOf("json", "jsonl", "jsonc"), + textmateScope = "source.json", + icon = Icon.ResourceIcon(json), + title = "JSON", + ), MARKDOWN( extensions = listOf("md", "markdown", "mdown", "mkd", "mkdn", "mdoc", "mdtext", "mdtxt", "mdwn"), textmateScope = "text.html.markdown", - icon = markdown, + icon = Icon.ResourceIcon(markdown), title = "Markdown", ), XML( extensions = listOf("xml", "xaml", "dtd", "plist", "ascx", "csproj", "wxi", "wxl", "wxs", "svg"), textmateScope = "text.xml", - icon = xml, + icon = Icon.ResourceIcon(xml), title = "XML", ), YAML( extensions = listOf("yaml", "yml", "eyaml", "eyml", "cff"), textmateScope = "source.yaml", - icon = yaml, + icon = Icon.ResourceIcon(yaml), title = "YAML", ), @@ -214,46 +248,51 @@ enum class BuiltinFileType( PYTHON( extensions = listOf("py", "pyi"), textmateScope = "source.python", - icon = python, + icon = Icon.ResourceIcon(python), title = "Python", markdownNames = listOf("python"), ), - JAVA(extensions = listOf("java", "jav", "bsh"), textmateScope = "source.java", icon = java, title = "Java"), + JAVA( + extensions = listOf("java", "jav", "bsh"), + textmateScope = "source.java", + icon = Icon.ResourceIcon(java), + title = "Java", + ), GROOVY( extensions = listOf("gsh", "groovy", "gradle", "gvy", "gy"), textmateScope = "source.groovy", - icon = groovy, - iconOverride = mapOf("gradle" to gradle), + icon = Icon.ResourceIcon(groovy), + iconOverride = mapOf("gradle" to Icon.ResourceIcon(gradle)), title = "Groovy", ), - C(extensions = listOf("c"), textmateScope = "source.c", icon = c, title = "C"), + C(extensions = listOf("c"), textmateScope = "source.c", icon = Icon.ResourceIcon(c), title = "C"), CPP( extensions = listOf("cpp", "cxx", "cc", "c++", "h", "hpp", "hh", "hxx", "h++"), textmateScope = "source.cpp", - icon = cpp, + icon = Icon.ResourceIcon(cpp), title = "C++", ), CSHARP( extensions = listOf("cs", "csx"), textmateScope = "source.cs", - icon = csharp, + icon = Icon.ResourceIcon(csharp), title = "C#", markdownNames = listOf("csharp"), ), RUBY( extensions = listOf("rb", "erb", "gemspec"), textmateScope = "source.ruby", - icon = ruby, + icon = Icon.ResourceIcon(ruby), title = "Ruby", markdownNames = listOf("ruby"), ), - LUA(extensions = listOf("lua", "luau"), textmateScope = "source.lua", icon = lua, title = "Lua"), - GO(extensions = listOf("go"), textmateScope = "source.go", icon = go, title = "Go"), - PHP(extensions = listOf("php"), textmateScope = "source.php", icon = php, title = "PHP"), + LUA(extensions = listOf("lua", "luau"), textmateScope = "source.lua", icon = Icon.ResourceIcon(lua), title = "Lua"), + GO(extensions = listOf("go"), textmateScope = "source.go", icon = Icon.ResourceIcon(go), title = "Go"), + PHP(extensions = listOf("php"), textmateScope = "source.php", icon = Icon.ResourceIcon(php), title = "PHP"), RUST( extensions = listOf("rs"), textmateScope = "source.rust", - icon = rust, + icon = Icon.ResourceIcon(rust), title = "Rust", markdownNames = listOf("rust"), ), @@ -264,19 +303,29 @@ enum class BuiltinFileType( title = "Pascal", markdownNames = listOf("pascal"), ), - ZIG(extensions = listOf("zig"), textmateScope = "source.zig", icon = zig, title = "Zig"), - NIM(extensions = listOf("nim"), textmateScope = "source.nim", icon = nim, title = "Nim"), - SWIFT(extensions = listOf("swift"), textmateScope = "source.swift", icon = swift, title = "Swift"), - DART(extensions = listOf("dart"), textmateScope = "source.dart", icon = dart, title = "Dart"), + ZIG(extensions = listOf("zig","zon"), textmateScope = "source.zig", icon = Icon.ResourceIcon(zig), title = "Zig"), + NIM(extensions = listOf("nim"), textmateScope = "source.nim", icon = Icon.ResourceIcon(nim), title = "Nim"), + SWIFT( + extensions = listOf("swift"), + textmateScope = "source.swift", + icon = Icon.ResourceIcon(swift), + title = "Swift", + ), + DART(extensions = listOf("dart"), textmateScope = "source.dart", icon = Icon.ResourceIcon(dart), title = "Dart"), ROCQ(extensions = listOf("v", "coq"), textmateScope = "source.coq", icon = null, title = "Rocq (Coq)"), KOTLIN( extensions = listOf("kt", "kts"), textmateScope = "source.kotlin", - icon = kotlin, + icon = Icon.ResourceIcon(kotlin), title = "Kotlin", markdownNames = listOf("kotlin"), ), - LISP(extensions = listOf("lisp", "clisp"), textmateScope = "source.lisp", icon = lisp, title = "Lisp"), + LISP( + extensions = listOf("lisp", "clisp"), + textmateScope = "source.lisp", + icon = Icon.ResourceIcon(lisp), + title = "Lisp", + ), SHELL( extensions = listOf( @@ -299,15 +348,20 @@ enum class BuiltinFileType( "ksh", ), textmateScope = "source.shell", - icon = shell, + icon = Icon.ResourceIcon(shell), title = "Shell script", markdownNames = listOf("shell", "console"), ), - WINDOWS_SHELL(extensions = listOf("cmd", "bat"), textmateScope = "source.batchfile", icon = shell, title = "Batch"), + WINDOWS_SHELL( + extensions = listOf("cmd", "bat"), + textmateScope = "source.batchfile", + icon = Icon.ResourceIcon(shell), + title = "Batch", + ), POWERSHELL( extensions = listOf("ps1", "psm1", "psd1"), textmateScope = "source.powershell", - icon = powershell, + icon = Icon.ResourceIcon(powershell), title = "PowerShell", markdownNames = listOf("powershell", "ps"), ), @@ -317,58 +371,95 @@ enum class BuiltinFileType( extensions = emptyList(), names = listOf("cmakelists.txt"), textmateScope = "source.cmake", - icon = cmake, + icon = Icon.ResourceIcon(cmake), title = "CMake", ), - R(extensions = listOf("r"), textmateScope = "source.r", icon = r, title = "R", markdownNames = listOf("r")), + R( + extensions = listOf("r"), + textmateScope = "source.r", + icon = Icon.ResourceIcon(r), + title = "R", + markdownNames = listOf("r"), + ), + NIX( + extensions = listOf("nix"), + textmateScope = "source.nix", + icon = Icon.ResourceIcon(nix), + title = "Nix" + ), // Data Files - SQL(extensions = listOf("sql", "dsql", "sqllite"), textmateScope = "source.sql", icon = sql, title = "SQL"), - TOML(extensions = listOf("toml"), textmateScope = "source.toml", icon = toml, title = "TOML"), - INI(extensions = listOf("ini"), textmateScope = "source.ini", icon = prop, title = "INI"), + SQL( + extensions = listOf("sql", "dsql", "sqllite"), + textmateScope = "source.sql", + icon = Icon.ResourceIcon(sql), + title = "SQL", + ), + TOML(extensions = listOf("toml"), textmateScope = "source.toml", icon = Icon.ResourceIcon(toml), title = "TOML"), + INI(extensions = listOf("ini"), textmateScope = "source.ini", icon = Icon.ResourceIcon(prop), title = "INI"), PROPERTIES( extensions = listOf("properties", "cfg", "conf", "config", "editorconfig", "gitconfig", "gitmodules", "gitattributes"), textmateScope = "source.properties", - icon = prop, - iconOverride = mapOf("gitmodules" to git, "gitattributes" to git, "gitconfig" to git), + icon = Icon.ResourceIcon(prop), + iconOverride = + mapOf( + "gitmodules" to Icon.ResourceIcon(git), + "gitattributes" to Icon.ResourceIcon(git), + "gitconfig" to Icon.ResourceIcon(git), + ), title = "Properties", ), IGNORE( extensions = listOf("gitignore", "gitignore_global", "gitkeep", "git-blame-ignore-revs"), textmateScope = "source.ignore", - icon = git, + icon = Icon.ResourceIcon(git), title = "Ignore", ), - DIFF(extensions = listOf("diff", "patch", "rej"), textmateScope = "source.diff", icon = diff, title = "Diff"), + DIFF( + extensions = listOf("diff", "patch", "rej"), + textmateScope = "source.diff", + icon = Icon.ResourceIcon(diff), + title = "Diff", + ), // Documents TEXT( extensions = listOf("txt"), textmateScope = null, - icon = text, + icon = Icon.ResourceIcon(text), title = "Plain text", markdownNames = listOf("plaintext", "text"), ), LOG(extensions = listOf("log"), textmateScope = "text.log", icon = null, title = "Log"), - LATEX(extensions = listOf("latex", "tex", "ltx"), textmateScope = "text.tex.latex", icon = latex, title = "LaTeX"), + LATEX( + extensions = listOf("latex", "tex", "ltx"), + textmateScope = "text.tex.latex", + icon = Icon.ResourceIcon(latex), + title = "LaTeX", + ), IMAGE( extensions = listOf("jpg", "jpeg", "png", "gif", "bmp", "tiff", "webp", "ico", "heic", "heif", "avif"), textmateScope = null, - icon = image, + icon = Icon.ResourceIcon(image), title = "Image", ), AUDIO( extensions = listOf("mp3", "wav", "flac", "ogg", "aac", "m4a", "wma", "opus"), textmateScope = null, - icon = audio, + icon = Icon.ResourceIcon(audio), title = "Audio", ), - VIDEO(extensions = listOf("mp4", "avi", "mov", "mkv", "webm"), textmateScope = null, icon = video, title = "Video"), + VIDEO( + extensions = listOf("mp4", "avi", "mov", "mkv", "webm"), + textmateScope = null, + icon = Icon.ResourceIcon(video), + title = "Video", + ), ARCHIVE( extensions = listOf("zip", "rar", "7z", "tar", "gz", "bz2", "xy"), textmateScope = null, - icon = archive, + icon = Icon.ResourceIcon(archive), title = "Archive", ), EXECUTABLE( @@ -377,6 +468,6 @@ enum class BuiltinFileType( icon = null, title = "Executable", ), - APK(extensions = listOf("apk", "xapk", "apks"), textmateScope = null, icon = apk, title = "APK"), + APK(extensions = listOf("apk", "xapk", "apks"), textmateScope = null, icon = Icon.ResourceIcon(apk), title = "APK"), UNKNOWN(extensions = emptyList(), textmateScope = null, icon = null, title = strings.unknown.getString()), } diff --git a/core/main/src/main/java/com/rk/file/FilePermission.kt b/core/main/src/main/java/com/rk/file/FilePermission.kt index f54dca254..ef15d35ae 100644 --- a/core/main/src/main/java/com/rk/file/FilePermission.kt +++ b/core/main/src/main/java/com/rk/file/FilePermission.kt @@ -14,13 +14,15 @@ import androidx.core.net.toUri import com.rk.resources.getString import com.rk.resources.strings import com.rk.settings.Settings -import com.rk.utils.dialog -import java.lang.ref.WeakReference +import com.rk.utils.dialogRes import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch +import java.lang.ref.WeakReference object FilePermission { private const val REQUEST_CODE_STORAGE_PERMISSIONS = 1259 + private var isRequesting = false + private var activeActivity = WeakReference(null) fun onRequestPermissionsResult( requestCode: Int, @@ -41,6 +43,11 @@ object FilePermission { private var dialogRef = WeakReference(null) fun verifyStoragePermission(activity: Activity) { + if (isRequesting && activeActivity.get() == activity) { + return + } + activeActivity = WeakReference(activity) + dialogRef.get()?.apply { if (isShowing) { dismiss() @@ -71,17 +78,18 @@ object FilePermission { } } if (shouldAsk) { - dialog( - context = activity, - onDialog = { dialogRef = WeakReference(it) }, + isRequesting = true + dialogRes( + activity = activity, title = strings.manage_storage.getString(), msg = strings.manage_storage_reason.getString(), - cancelString = strings.ignore, - okString = strings.ok, + cancelRes = strings.ignore, + okRes = strings.ok, onOk = { + isRequesting = false if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) { val intent = Intent(ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) - intent.setData("package:${activity.packageName}".toUri()) + intent.data = "package:${activity.packageName}".toUri() activity.startActivity(intent) } else { // below 11 @@ -94,7 +102,10 @@ object FilePermission { ActivityCompat.requestPermissions(activity, perms, REQUEST_CODE_STORAGE_PERMISSIONS) } }, - onCancel = { Settings.ignore_storage_permission = true }, + onCancel = { + Settings.ignore_storage_permission = true + isRequesting = false + }, cancelable = false, ) } diff --git a/core/main/src/main/java/com/rk/file/UriWrapper.kt b/core/main/src/main/java/com/rk/file/UriWrapper.kt index 58a0b32d5..cca0ece71 100644 --- a/core/main/src/main/java/com/rk/file/UriWrapper.kt +++ b/core/main/src/main/java/com/rk/file/UriWrapper.kt @@ -80,7 +80,7 @@ class UriWrapper : FileObject { URLDecoder.decode(file.uri.toString(), "UTF-8") .removePrefix("content://com.termux.documents/tree//data/data/com.termux/files/home/document/") if (path.startsWith("/data").not()) { - errorDialog("Converting termux uri into realpath failed: \nURI : ${file.uri}\n\nPATH : $path") + errorDialog(msg = "Converting termux uri into realpath failed: \nURI : ${file.uri}\n\nPATH : $path") } // dialog(title = "PATH", msg = path, onOk = {}) return File(path) diff --git a/core/main/src/main/java/com/rk/filetree/FileAction.kt b/core/main/src/main/java/com/rk/filetree/FileAction.kt index 32d685c31..2c1d134c6 100644 --- a/core/main/src/main/java/com/rk/filetree/FileAction.kt +++ b/core/main/src/main/java/com/rk/filetree/FileAction.kt @@ -11,6 +11,7 @@ import androidx.compose.material.icons.outlined.Refresh import androidx.lifecycle.viewModelScope import com.rk.activities.main.MainActivity import com.rk.activities.terminal.Terminal +import com.rk.drawer.DrawerViewModel import com.rk.file.FileObject import com.rk.file.FileOperations import com.rk.file.FileWrapper @@ -23,7 +24,6 @@ import com.rk.resources.getString import com.rk.resources.strings import com.rk.settings.app.InbuiltFeatures import com.rk.tabs.editor.EditorTab -import com.rk.utils.showTerminalNotice import com.rk.utils.toast import kotlinx.coroutines.launch @@ -31,6 +31,7 @@ data class FileActionContext( val file: FileObject, val root: FileObject?, val viewModel: FileTreeViewModel, + val drawerViewModel: DrawerViewModel, val context: Context, ) @@ -38,6 +39,7 @@ data class MultiFileActionContext( val files: List, val root: FileObject?, val viewModel: FileTreeViewModel, + val drawerViewModel: DrawerViewModel, val context: Context, ) @@ -103,18 +105,16 @@ object RefreshAction : MultiFileAction() { } object TerminalAction : FileAction() { - override val icon = Icon.DrawableRes(drawables.terminal) + override val icon = Icon.ResourceIcon(drawables.terminal) override val title = strings.open_in_terminal.getString() override fun action(context: FileActionContext) { val file = context.file val context = context.context - showTerminalNotice(activity = MainActivity.instance!!) { - val intent = Intent(context, Terminal::class.java) - intent.putExtra("cwd", file.getAbsolutePath()) - context.startActivity(intent) - } + val intent = Intent(context, Terminal::class.java) + intent.putExtra("cwd", file.getAbsolutePath()) + context.startActivity(intent) } override fun isSupported(file: FileObject): Boolean { @@ -170,7 +170,7 @@ object DeleteAction : MultiFileAction() { } object CopyAction : MultiFileAction() { - override val icon = Icon.DrawableRes(drawables.copy) + override val icon = Icon.ResourceIcon(drawables.copy) override val title = strings.copy.getString() override fun action(context: MultiFileActionContext) { @@ -183,7 +183,7 @@ object CopyAction : MultiFileAction() { } object CutAction : MultiFileAction() { - override val icon = Icon.DrawableRes(drawables.cut) + override val icon = Icon.ResourceIcon(drawables.cut) override val title = strings.cut.getString() override fun action(context: MultiFileActionContext) { @@ -195,7 +195,7 @@ object CutAction : MultiFileAction() { } object PasteAction : FileAction() { - override val icon = Icon.DrawableRes(drawables.paste) + override val icon = Icon.ResourceIcon(drawables.paste) override val title = strings.paste.getString() override fun action(context: FileActionContext) { @@ -240,7 +240,7 @@ object PasteAction : FileAction() { } object OpenWithAction : FileAction() { - override val icon = Icon.DrawableRes(drawables.open_in_new) + override val icon = Icon.ResourceIcon(drawables.open_in_new) override val title = strings.open_with.getString() override fun action(context: FileActionContext) { @@ -251,7 +251,7 @@ object OpenWithAction : FileAction() { } object SaveAsAction : FileAction() { - override val icon = Icon.DrawableRes(drawables.file_symlink) + override val icon = Icon.ResourceIcon(drawables.file_symlink) override val title = strings.save_as.getString() override fun action(context: FileActionContext) { @@ -262,7 +262,7 @@ object SaveAsAction : FileAction() { } object AddFileAction : FileAction() { - override val icon = Icon.DrawableRes(drawables.arrow_downward) + override val icon = Icon.ResourceIcon(drawables.arrow_downward) override val title = strings.add_file.getString() override fun action(context: FileActionContext) { @@ -273,15 +273,16 @@ object AddFileAction : FileAction() { } object OpenAsProjectAction : FileAction() { - override val icon = Icon.DrawableRes(drawables.folder_code) + override val icon = Icon.ResourceIcon(drawables.folder_code) override val title = strings.open_as_project.getString() override fun action(context: FileActionContext) { - addProject(context.file, true) + context.drawerViewModel.addFileTreeTab(context.file, true) } override fun isEnabled(file: FileObject): Boolean { - return drawerTabs.none { it is FileTreeTab && it.root == file } + val drawerViewModel = MainActivity.instance?.drawerViewModel ?: return false + return drawerViewModel.drawerTabs.none { it is FileTreeTab && it.root == file } } override val type = FileActionType(file = false, folder = true, rootFolder = true) diff --git a/core/main/src/main/java/com/rk/filetree/FileActionDialogs.kt b/core/main/src/main/java/com/rk/filetree/FileActionDialogs.kt index 574ac9412..21269f54a 100644 --- a/core/main/src/main/java/com/rk/filetree/FileActionDialogs.kt +++ b/core/main/src/main/java/com/rk/filetree/FileActionDialogs.kt @@ -13,6 +13,7 @@ import com.rk.activities.main.MainActivity import com.rk.activities.main.drawerStateRef import com.rk.components.PropertiesDialog import com.rk.components.SingleInputDialog +import com.rk.drawer.DrawerViewModel import com.rk.file.FileObject import com.rk.file.FileOperations import com.rk.resources.fillPlaceholders @@ -27,7 +28,12 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @Composable -fun FileActionDialogs(viewModel: FileTreeViewModel, scope: CoroutineScope, context: Context) { +fun FileActionDialogs( + drawerViewModel: DrawerViewModel, + viewModel: FileTreeViewModel, + scope: CoroutineScope, + context: Context, +) { if (viewModel.showRenameDialog) { val file = viewModel.renameFile ?: return SingleInputDialog( @@ -91,7 +97,7 @@ fun FileActionDialogs(viewModel: FileTreeViewModel, scope: CoroutineScope, conte } if (file == root) { - removeProject(file, true) + drawerViewModel.removeFileTreeTab(file, true) } MainActivity.instance?.viewModel?.also { viewModel -> @@ -180,7 +186,7 @@ fun FileActionDialogs(viewModel: FileTreeViewModel, scope: CoroutineScope, conte viewModel.updateCache(file) viewModel.createValue = "" } - .onFailure { errorDialog(it) } + .onFailure { errorDialog(throwable = it) } } }, onFinish = { viewModel.closeCreateDialog() }, @@ -192,7 +198,7 @@ fun FileActionDialogs(viewModel: FileTreeViewModel, scope: CoroutineScope, conte ProjectCloseConfirmationDialog( projectName = root.getAppropriateName(), onConfirm = { - removeProject(root) + drawerViewModel.removeFileTreeTab(root) viewModel.closeCloseProjectConfirmation() }, onDismiss = { viewModel.closeCloseProjectConfirmation() }, diff --git a/core/main/src/main/java/com/rk/filetree/FileActionProvider.kt b/core/main/src/main/java/com/rk/filetree/FileActionProvider.kt new file mode 100644 index 000000000..8b65a5ec9 --- /dev/null +++ b/core/main/src/main/java/com/rk/filetree/FileActionProvider.kt @@ -0,0 +1,85 @@ +package com.rk.filetree + +import com.rk.extension.api.XedExtensionPoint +import com.rk.file.FileObject + +object FileActionProvider { + private val _fileActions = + mutableListOf( + CloseAction, + RefreshAction, + TerminalAction, + CreateNewFileAction, + CreateNewFolderAction, + RenameAction, + DeleteAction, + CopyAction, + CutAction, + PasteAction, + OpenWithAction, + SaveAsAction, + // AddFileAction, + OpenAsProjectAction, + PropertiesAction, + ) + + val fileActions: List + get() = _fileActions.toList() + + @XedExtensionPoint + fun registerAction(action: BaseFileAction) { + if (!_fileActions.contains(action)) { + _fileActions.add(action) + } + } + + @XedExtensionPoint + fun unregisterAction(action: BaseFileAction) { + _fileActions.remove(action) + } + + fun getActions(file: FileObject, root: FileObject?) = getActions(listOf(file), root) + + fun getActions(files: List, root: FileObject?): List { + if (files.isEmpty()) return emptyList() + + val suitableActions = mutableListOf() + + fileActions.forEach { action -> + val isBulkAction = files.size > 1 + + if (action is MultiFileAction) { + val isSupported = action.isSupported(files) + if (!isSupported) return@forEach + + val hasFolders = files.any { it.isDirectory() } + val hasFiles = files.any { it.isFile() } + val hasRoot = files.any { it == root } + + val folderValid = if (!hasFolders) true else action.type.folder + val fileValid = if (!hasFiles) true else action.type.file + val rootValid = if (!hasRoot) true else action.type.rootFolder + + if (folderValid && fileValid && rootValid) { + suitableActions.add(action) + } + } else if (!isBulkAction) { + val file = files.first() + val action = action as FileAction + + val isSupported = action.isSupported(file) + if (!isSupported) return@forEach + + val isRootAction = file == root && action.type.rootFolder + val isFileAction = file.isFile() && action.type.file + val isFolderAction = file.isDirectory() && action.type.folder + + if (isRootAction || isFileAction || isFolderAction) { + suitableActions.add(action) + } + } + } + + return suitableActions + } +} diff --git a/core/main/src/main/java/com/rk/filetree/FileActionUtils.kt b/core/main/src/main/java/com/rk/filetree/FileActionUtils.kt deleted file mode 100644 index 202d75842..000000000 --- a/core/main/src/main/java/com/rk/filetree/FileActionUtils.kt +++ /dev/null @@ -1,67 +0,0 @@ -package com.rk.filetree - -import com.rk.file.FileObject - -val fileActions = - listOf( - CloseAction, - RefreshAction, - TerminalAction, - CreateNewFileAction, - CreateNewFolderAction, - RenameAction, - DeleteAction, - CopyAction, - CutAction, - PasteAction, - OpenWithAction, - SaveAsAction, - // AddFileAction, - OpenAsProjectAction, - PropertiesAction, - ) - -fun getActions(file: FileObject, root: FileObject?) = getActions(listOf(file), root) - -fun getActions(files: List, root: FileObject?): List { - if (files.isEmpty()) return emptyList() - - val suitableActions = mutableListOf() - - fileActions.forEach { action -> - val isBulkAction = files.size > 1 - - if (action is MultiFileAction) { - val isSupported = action.isSupported(files) - if (!isSupported) return@forEach - - val hasFolders = files.any { it.isDirectory() } - val hasFiles = files.any { it.isFile() } - val hasRoot = files.any { it == root } - - val folderValid = if (!hasFolders) true else action.type.folder - val fileValid = if (!hasFiles) true else action.type.file - val rootValid = if (!hasRoot) true else action.type.rootFolder - - if (folderValid && fileValid && rootValid) { - suitableActions.add(action) - } - } else if (!isBulkAction) { - val file = files.first() - val action = action as FileAction - - val isSupported = action.isSupported(file) - if (!isSupported) return@forEach - - val isRootAction = file == root && action.type.rootFolder - val isFileAction = file.isFile() && action.type.file - val isFolderAction = file.isDirectory() && action.type.folder - - if (isRootAction || isFileAction || isFolderAction) { - suitableActions.add(action) - } - } - } - - return suitableActions -} diff --git a/core/main/src/main/java/com/rk/filetree/FileIcon.kt b/core/main/src/main/java/com/rk/filetree/FileIcon.kt index 4d842cd86..818566836 100644 --- a/core/main/src/main/java/com/rk/filetree/FileIcon.kt +++ b/core/main/src/main/java/com/rk/filetree/FileIcon.kt @@ -2,25 +2,22 @@ package com.rk.filetree import android.content.Context import android.graphics.drawable.Drawable -import android.graphics.drawable.PictureDrawable import androidx.compose.foundation.layout.size -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import coil.compose.AsyncImage -import com.caverock.androidsvg.SVG import com.rk.file.FileObject import com.rk.file.FileTypeManager +import com.rk.icons.Icon +import com.rk.icons.XedIcon import com.rk.icons.pack.currentIconPack import com.rk.icons.rememberSvgImageLoader import com.rk.resources.drawables -import com.rk.resources.getDrawable -import java.io.InputStream +import com.rk.utils.loadSvg private val plain_file = drawables.file private val folder = drawables.folder @@ -53,18 +50,18 @@ fun FileIcon(file: FileObject, iconTint: Color? = null, isExpanded: Boolean = fa val icon = when { file.isFile() -> getBuiltInFileIcon(file) - file.isDirectory() -> folder - file.isSymlink() -> fileSymlink - else -> unknown + file.isDirectory() -> Icon.ResourceIcon(folder) + file.isSymlink() -> Icon.ResourceIcon(fileSymlink) + else -> Icon.ResourceIcon(unknown) } val tint = iconTint - ?: if (icon == folder || icon == archive) { + ?: if (icon is Icon.ResourceIcon && (icon.drawableRes == folder || icon.drawableRes == archive)) { MaterialTheme.colorScheme.primary } else MaterialTheme.colorScheme.secondary - val useTint = currentIconPack.value?.info?.applyTint == true + val useTint = currentIconPack.value?.manifest?.applyTint == true if (iconPackFile != null) { AsyncImage( @@ -75,12 +72,7 @@ fun FileIcon(file: FileObject, iconTint: Color? = null, isExpanded: Boolean = fa modifier = Modifier.size(20.dp), ) } else { - Icon( - painter = painterResource(id = icon), - contentDescription = null, - tint = tint, - modifier = Modifier.size(20.dp), - ) + XedIcon(icon = icon, contentDescription = null, tint = tint, modifier = Modifier.size(20.dp)) } } @@ -104,15 +96,15 @@ fun FileNameIcon(fileName: String, isDirectory: Boolean, iconTint: Color? = null val iconPackFile = currentIconPack.value?.getIconFileForName(fileName, isDirectory, isExpanded) val imageLoader = rememberSvgImageLoader() - val icon = if (isDirectory) folder else getBuiltInFileIcon(fileName) + val icon = if (isDirectory) Icon.ResourceIcon(folder) else getBuiltInFileIcon(fileName) val tint = iconTint - ?: if (icon == folder || icon == archive) { + ?: if (icon is Icon.ResourceIcon && (icon.drawableRes == folder || icon.drawableRes == archive)) { MaterialTheme.colorScheme.primary } else MaterialTheme.colorScheme.secondary - val useTint = currentIconPack.value?.info?.applyTint == true + val useTint = currentIconPack.value?.manifest?.applyTint == true if (iconPackFile != null) { AsyncImage( @@ -123,12 +115,7 @@ fun FileNameIcon(fileName: String, isDirectory: Boolean, iconTint: Color? = null modifier = Modifier.size(20.dp), ) } else { - Icon( - painter = painterResource(id = icon), - contentDescription = null, - tint = tint, - modifier = Modifier.size(20.dp), - ) + XedIcon(icon = icon, contentDescription = null, tint = tint, modifier = Modifier.size(20.dp)) } } @@ -144,47 +131,30 @@ fun FileNameIcon(fileName: String, isDirectory: Boolean, iconTint: Color? = null * @param isDirectory Whether the file is a directory. * @return A [Drawable] representing the file icon. */ -fun getDrawableFileIcon( - context: Context, - fileName: String, - isDirectory: Boolean, - isExpanded: Boolean = false, -): Drawable? { - fun loadSvg(inputStream: InputStream): Drawable? { - val svg = - try { - SVG.getFromInputStream(inputStream) - } catch (_: Exception) { - return null - } - - val picture = svg.renderToPicture() - return PictureDrawable(picture) - } - +fun getDrawableFileIcon(fileName: String, isDirectory: Boolean, isExpanded: Boolean = false): Drawable? { val iconPackFile = currentIconPack.value?.getIconFileForName(fileName, isDirectory, isExpanded) - val icon = if (isDirectory) folder else getBuiltInFileIcon(fileName) + val icon = if (isDirectory) Icon.ResourceIcon(folder) else getBuiltInFileIcon(fileName) - val builtinIcon = icon.getDrawable(context) + val builtinIcon = icon.toDrawable() val iconPackIcon = iconPackFile?.inputStream()?.let { loadSvg(it) } return iconPackIcon ?: builtinIcon } -private fun getBuiltInFileIcon(fileName: String): Int = +private fun getBuiltInFileIcon(fileName: String): Icon = when (fileName) { "contract.sol", "LICENSE", - "NOTICE" -> text + "NOTICE" -> Icon.ResourceIcon(text) "gradlew", - "gradlew.bat" -> gradle - "README.md" -> info + "gradlew.bat" -> Icon.ResourceIcon(gradle) + "README.md" -> Icon.ResourceIcon(info) else -> { val ext = fileName.substringAfterLast('.', "") val type = FileTypeManager.fromExtension(ext) - type.iconOverride?.get(ext) ?: type.icon ?: plain_file + type.iconOverride?.get(ext) ?: type.icon ?: Icon.ResourceIcon(plain_file) } } -private fun getBuiltInFileIcon(file: FileObject): Int = getBuiltInFileIcon(file.getName()) +private fun getBuiltInFileIcon(file: FileObject): Icon = getBuiltInFileIcon(file.getName()) diff --git a/core/main/src/main/java/com/rk/filetree/FileTree.kt b/core/main/src/main/java/com/rk/filetree/FileTree.kt index 2df55a698..9bc1af37e 100644 --- a/core/main/src/main/java/com/rk/filetree/FileTree.kt +++ b/core/main/src/main/java/com/rk/filetree/FileTree.kt @@ -46,6 +46,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.viewModelScope import com.rk.activities.main.MainActivity import com.rk.activities.main.searchViewModel +import com.rk.drawer.DrawerViewModel import com.rk.file.FileObject import com.rk.icons.XedIcon import com.rk.resources.getString @@ -67,11 +68,12 @@ fun FileTree( onFileClick: FileTreeNode.(FileTreeNode) -> Unit, onSearchClick: () -> Unit = {}, viewModel: FileTreeViewModel, + drawerViewModel: DrawerViewModel, ) { // Auto-expand root node on first composition LaunchedEffect(rootNode.file) { - if (!viewModel.isNodeExpanded(rootNode.file)) { - viewModel.toggleNodeExpansion(rootNode.file) + if (!viewModel.isNodeExpanded(rootNode.file, rootNode.file)) { + viewModel.toggleNodeExpansion(rootNode.file, rootNode.file) viewModel.loadChildrenForNode(rootNode) } } @@ -107,7 +109,7 @@ fun FileTree( modifier = Modifier.weight(1f), ) { if (viewModel.isAnyFileSelected(rootNode.file)) { - SelectionActions(viewModel, rootNode) + SelectionActions(viewModel, drawerViewModel, rootNode) } else { FileTreeActions(viewModel, onSearchClick) } @@ -143,9 +145,9 @@ fun FileTree( } @Composable -private fun SelectionActions(viewModel: FileTreeViewModel, rootNode: FileTreeNode) { +private fun SelectionActions(viewModel: FileTreeViewModel, drawerViewModel: DrawerViewModel, rootNode: FileTreeNode) { val selectedFiles = viewModel.getSelectedFiles(rootNode.file) - val actions = remember(selectedFiles, rootNode.file) { getActions(selectedFiles, rootNode.file) } + val actions = remember(selectedFiles, rootNode.file) { FileActionProvider.getActions(selectedFiles, rootNode.file) } var expanded by remember { mutableStateOf(false) } val context = LocalContext.current @@ -174,7 +176,8 @@ private fun SelectionActions(viewModel: FileTreeViewModel, rootNode: FileTreeNod IconButton( enabled = action.isEnabled(file), onClick = { - val context = FileActionContext(file, rootNode.file, viewModel, context) + val context = + FileActionContext(file, rootNode.file, viewModel, drawerViewModel, context) action.action(context) viewModel.unselectAllFiles(rootNode.file) @@ -187,7 +190,14 @@ private fun SelectionActions(viewModel: FileTreeViewModel, rootNode: FileTreeNod IconButton( enabled = action.isEnabled(selectedFiles), onClick = { - val context = MultiFileActionContext(selectedFiles, rootNode.file, viewModel, context) + val context = + MultiFileActionContext( + selectedFiles, + rootNode.file, + viewModel, + drawerViewModel, + context, + ) action.action(context) viewModel.unselectAllFiles(rootNode.file) @@ -215,7 +225,14 @@ private fun SelectionActions(viewModel: FileTreeViewModel, rootNode: FileTreeNod leadingIcon = { XedIcon(action.icon, contentDescription = action.title) }, enabled = action.isEnabled(file), onClick = { - val context = FileActionContext(file, rootNode.file, viewModel, context) + val context = + FileActionContext( + file, + rootNode.file, + viewModel, + drawerViewModel, + context, + ) action.action(context) viewModel.unselectAllFiles(rootNode.file) @@ -230,7 +247,13 @@ private fun SelectionActions(viewModel: FileTreeViewModel, rootNode: FileTreeNod enabled = action.isEnabled(selectedFiles), onClick = { val context = - MultiFileActionContext(selectedFiles, rootNode.file, viewModel, context) + MultiFileActionContext( + selectedFiles, + rootNode.file, + viewModel, + drawerViewModel, + context, + ) action.action(context) viewModel.unselectAllFiles(rootNode.file) diff --git a/core/main/src/main/java/com/rk/filetree/FileTreeNodeItem.kt b/core/main/src/main/java/com/rk/filetree/FileTreeNodeItem.kt index 025ecdbc8..8dcc28fda 100644 --- a/core/main/src/main/java/com/rk/filetree/FileTreeNodeItem.kt +++ b/core/main/src/main/java/com/rk/filetree/FileTreeNodeItem.kt @@ -55,7 +55,7 @@ fun FileTreeNodeItem( val isHidden = node.file.getName().startsWith(".") if (isHidden && !Settings.show_hidden_files_drawer) return - val isExpanded = viewModel.isNodeExpanded(node.file) + val isExpanded = viewModel.isNodeExpanded(root, node.file) val horizontalPadding = (depth * 16).dp val isLoading = viewModel.isNodeLoading(node.file) @@ -101,7 +101,7 @@ fun FileTreeNodeItem( LaunchedEffect(children, Settings.compact_folders_drawer) { displayedChildren = if (Settings.compact_folders_drawer && children.size == 1 && children[0].isDirectory) { - val collapsedNode = viewModel.collapseNode(node) + val collapsedNode = viewModel.collapseNode(root, node) viewModel.getNodeChildren(collapsedNode) } else children displayName = @@ -123,7 +123,7 @@ fun FileTreeNodeItem( } if (node.isDirectory) { - viewModel.toggleNodeExpansion(node.file) + viewModel.toggleNodeExpansion(root, node.file) return@combinedClickable } diff --git a/core/main/src/main/java/com/rk/filetree/FileTreeTab.kt b/core/main/src/main/java/com/rk/filetree/FileTreeTab.kt index 769cc482f..69b629251 100644 --- a/core/main/src/main/java/com/rk/filetree/FileTreeTab.kt +++ b/core/main/src/main/java/com/rk/filetree/FileTreeTab.kt @@ -45,6 +45,7 @@ import com.rk.activities.main.searchViewModel import com.rk.components.AddDialogItem import com.rk.components.codeSearchDialog import com.rk.components.fileSearchDialog +import com.rk.drawer.DrawerTab import com.rk.file.FileObject import com.rk.file.FileWrapper import com.rk.file.UriWrapper @@ -78,6 +79,7 @@ class FileTreeTab(val root: FileObject) : DrawerTab() { val scope = rememberCoroutineScope() val context = LocalContext.current val mainViewModel = MainActivity.instance?.viewModel + val drawerViewModel = MainActivity.instance?.drawerViewModel LaunchedEffect(root) { if (InbuiltFeatures.git.state.value) { @@ -100,6 +102,7 @@ class FileTreeTab(val root: FileObject) : DrawerTab() { modifier = Modifier.fillMaxSize().systemBarsPadding(), rootNode = root.toFileTreeNode(), viewModel = fileTreeViewModel.get()!!, + drawerViewModel = drawerViewModel!!, onFileClick = { node -> scope.launch(Dispatchers.IO) { mainViewModel?.editorManager?.openFile(node.file, projectRoot = root, switchToTab = true) @@ -249,7 +252,7 @@ class FileTreeTab(val root: FileObject) : DrawerTab() { drawables.outline_folder } - return Icon.DrawableRes(iconId) + return Icon.ResourceIcon(iconId) } override fun equals(other: Any?): Boolean { @@ -266,7 +269,8 @@ class FileTreeTab(val root: FileObject) : DrawerTab() { override fun isSupported(): Boolean { if (runBlocking { !root.exists() } || !root.isDirectory()) { - removeProject(this@FileTreeTab, true) + val drawerViewModel = MainActivity.instance?.drawerViewModel ?: return false + drawerViewModel.removeDrawerTab(this@FileTreeTab, true) return false } return true diff --git a/core/main/src/main/java/com/rk/filetree/FileTreeViewModel.kt b/core/main/src/main/java/com/rk/filetree/FileTreeViewModel.kt index a49d3ea6f..4bc6f7199 100644 --- a/core/main/src/main/java/com/rk/filetree/FileTreeViewModel.kt +++ b/core/main/src/main/java/com/rk/filetree/FileTreeViewModel.kt @@ -14,6 +14,7 @@ import com.rk.activities.main.searchViewModel import com.rk.file.FileObject import com.rk.search.GlobExcluder import com.rk.settings.Settings +import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -133,18 +134,19 @@ class FileTreeViewModel : ViewModel() { private val selectedFiles = mutableStateMapOf>() private val focusedFile = mutableStateMapOf() private val fileListCache = mutableStateMapOf>() - private val expandedNodes = mutableStateMapOf() + private val expandedNodes = mutableStateMapOf>() private val collapsedNameCache = mutableStateMapOf() private var fileOperationsCount by mutableIntStateOf(0) private val excluder by derivedStateOf { GlobExcluder(Settings.excluded_files_drawer) } - fun getExpandedNodes(): Map { - return mutableMapOf().apply { expandedNodes.forEach { set(it.key, it.value) } } + fun getExpandedNodes(): Map> { + // Convert to java `Set` to make serialization possible + return expandedNodes.mapValues { (_, value) -> HashSet(value) } } - fun setExpandedNodes(map: Map) { - map.forEach { expandedNodes[it.key] = it.value } + fun setExpandedNodes(map: Map>) { + map.forEach { (key, value) -> expandedNodes[key] = expandedNodes[key]?.plus(value) ?: value } } fun toggleSelection(projectRoot: FileObject, fileObject: FileObject) { @@ -215,7 +217,8 @@ class FileTreeViewModel : ViewModel() { // Track loading states to avoid showing spinners incorrectly private val _loadingStates = mutableStateMapOf() - fun isNodeExpanded(fileObject: FileObject): Boolean = expandedNodes[fileObject] == true + fun isNodeExpanded(projectRoot: FileObject, fileObject: FileObject): Boolean = + expandedNodes[projectRoot]?.contains(fileObject) ?: false fun isNodeLoading(fileObject: FileObject): Boolean = _loadingStates[fileObject] == true @@ -241,12 +244,27 @@ class FileTreeViewModel : ViewModel() { return diagnosedNodes[fileObject] ?: -1 } - fun toggleNodeExpansion(fileObject: FileObject) { - val wasExpanded = expandedNodes[fileObject] == true - expandedNodes[fileObject] = !wasExpanded + fun toggleNodeExpansion(projectRoot: FileObject, fileObject: FileObject) { + val wasExpanded = isNodeExpanded(projectRoot, fileObject) + if (wasExpanded) { + collapseFile(projectRoot, fileObject) + } else { + expandFile(projectRoot, fileObject) + } + } + + private fun collapseFile(projectRoot: FileObject, fileObject: FileObject) { + expandedNodes[projectRoot] = expandedNodes[projectRoot]?.minus(fileObject) ?: emptySet() + if (expandedNodes[projectRoot]?.isEmpty() == true) { + expandedNodes.remove(projectRoot) + } + } + + private fun expandFile(projectRoot: FileObject, fileObject: FileObject) { + expandedNodes[projectRoot] = expandedNodes[projectRoot]?.plus(fileObject) ?: setOf(fileObject) // If we're expanding and haven't loaded yet, trigger a load - if (!wasExpanded && !fileListCache.containsKey(fileObject)) { + if (!fileListCache.containsKey(fileObject)) { _loadingStates[fileObject] = true } } @@ -255,13 +273,11 @@ class FileTreeViewModel : ViewModel() { return collapsedNameCache[node.file] ?: node.name } - suspend fun collapseNode(node: FileTreeNode): FileTreeNode { + suspend fun collapseNode(projectFile: FileObject, node: FileTreeNode): FileTreeNode { var currentNode = node var collapsedName = node.name while (true) { - if (!isNodeExpanded(currentNode.file)) { - toggleNodeExpansion(currentNode.file) - } + expandFile(projectFile, currentNode.file) loadChildrenForNodeSynchronous(currentNode) val children = getNodeChildren(currentNode) if (children.size != 1) { @@ -302,11 +318,8 @@ class FileTreeViewModel : ViewModel() { fileListCache[file] = sortedFiles - viewModelScope.launch { - delay(300) - _loadingStates[file] = false - } - } catch (e: Exception) { + viewModelScope.launch { clearLoadingState(file) } + } catch (_: Exception) { _loadingStates[file] = false } } @@ -317,22 +330,23 @@ class FileTreeViewModel : ViewModel() { suspend fun goToFolder(projectFile: FileObject, fileObject: FileObject) { focusedFile[projectFile] = fileObject viewModelScope.launch { - delay(1000) + delay(1000.milliseconds) focusedFile.remove(projectFile) } var currentFile: FileObject? = fileObject while (currentFile != null && currentFile != projectFile) { - expandedNodes[currentFile] = true + expandFile(projectFile, currentFile) // If we're expanding and haven't loaded yet, trigger a load - if (!fileListCache.containsKey(fileObject)) { + if (!fileListCache.containsKey(currentFile)) { _loadingStates[currentFile] = true } currentFile = currentFile.getParentFile() } - expandedNodes[projectFile] = true + + expandFile(projectFile, projectFile) } suspend fun refreshEverything() = @@ -367,10 +381,7 @@ class FileTreeViewModel : ViewModel() { val sortedFiles = sortAndFilterFiles(fileList) fileListCache[node.file] = sortedFiles - viewModelScope.launch { - delay(300) - _loadingStates[node.file] = false - } + viewModelScope.launch { clearLoadingState(node.file) } } catch (_: Exception) { _loadingStates[node.file] = false } @@ -401,15 +412,18 @@ class FileTreeViewModel : ViewModel() { val sortedFiles = sortAndFilterFiles(fileList) fileListCache[node.file] = sortedFiles - viewModelScope.launch { - delay(300) - _loadingStates[node.file] = false - } + viewModelScope.launch { clearLoadingState(node.file) } } catch (_: Exception) { _loadingStates[node.file] = false } } + private suspend fun clearLoadingState(file: FileObject) { + // Use delay to avoid flickering + delay(300.milliseconds) + _loadingStates[file] = false + } + private suspend fun calculateFileSizes(fileObjects: List): Map { val fileSizes = mutableMapOf() if (sortMode != SortMode.SORT_BY_SIZE) return fileSizes diff --git a/core/main/src/main/java/com/rk/git/GitTab.kt b/core/main/src/main/java/com/rk/git/GitTab.kt index c02214263..b7c03e51f 100644 --- a/core/main/src/main/java/com/rk/git/GitTab.kt +++ b/core/main/src/main/java/com/rk/git/GitTab.kt @@ -66,11 +66,10 @@ import com.rk.activities.main.fileTreeViewModel import com.rk.components.SingleInputDialog import com.rk.components.compose.utils.addIf import com.rk.components.getDrawerWidth +import com.rk.drawer.DrawerTab import com.rk.file.toFileWrapper -import com.rk.filetree.DrawerTab import com.rk.filetree.FileNameIcon import com.rk.filetree.FileTreeTab -import com.rk.filetree.currentDrawerTab import com.rk.icons.Icon import com.rk.resources.drawables import com.rk.resources.getString @@ -655,12 +654,13 @@ class GitTab(val viewModel: GitViewModel) : DrawerTab() { } override fun getIcon(): Icon { - return Icon.DrawableRes(drawables.git) + return Icon.ResourceIcon(drawables.git) } override fun isSupported(): Boolean { if (!InbuiltFeatures.git.state.value) return false - val tab = currentDrawerTab ?: return false + val drawerViewModel = MainActivity.instance?.drawerViewModel ?: return false + val tab = drawerViewModel.currentDrawerTab ?: return false if (tab !is FileTreeTab) return false val rootDir = File(tab.root.getAbsolutePath()) diff --git a/core/main/src/main/java/com/rk/icons/Icon.kt b/core/main/src/main/java/com/rk/icons/Icon.kt index a39a4393e..19a9962fd 100644 --- a/core/main/src/main/java/com/rk/icons/Icon.kt +++ b/core/main/src/main/java/com/rk/icons/Icon.kt @@ -1,14 +1,34 @@ package com.rk.icons +import android.content.res.Resources +import android.graphics.drawable.Drawable import androidx.compose.ui.graphics.vector.ImageVector +import androidx.core.content.res.ResourcesCompat +import com.rk.utils.application +import com.rk.utils.loadSvg import java.io.File sealed class Icon { - class DrawableRes(@androidx.annotation.DrawableRes val drawableRes: Int) : Icon() + class ResourceIcon(@androidx.annotation.DrawableRes val drawableRes: Int) : Icon() + + class ExternalResourceIcon(@androidx.annotation.DrawableRes val drawableRes: Int, val resources: Resources) : Icon() class VectorIcon(val vector: ImageVector) : Icon() class SvgIcon(val file: File) : Icon() class TextIcon(val text: String) : Icon() + + fun toDrawable(): Drawable? { + return when (this) { + is ResourceIcon -> { + ResourcesCompat.getDrawable(application!!.resources, drawableRes, null) + } + is ExternalResourceIcon -> { + ResourcesCompat.getDrawable(resources, drawableRes, null) + } + is SvgIcon -> loadSvg(file.inputStream()) + else -> null + } + } } diff --git a/core/main/src/main/java/com/rk/icons/QuestionMarkRound.kt b/core/main/src/main/java/com/rk/icons/QuestionMarkRound.kt new file mode 100644 index 000000000..621a31b1e --- /dev/null +++ b/core/main/src/main/java/com/rk/icons/QuestionMarkRound.kt @@ -0,0 +1,109 @@ +package com.rk.icons + +/* +ISC License + +Copyright (c) 2026 Lucide Icons and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +--- + +The following Lucide icons are derived from the Feather project: + +airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out + +The MIT License (MIT) (for the icons listed above) + +Copyright (c) 2013-present Cole Bemis + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.StrokeJoin +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +val LucideCircleQuestionMark: ImageVector + get() { + if (_LucideCircleQuestionMark != null) return _LucideCircleQuestionMark!! + + _LucideCircleQuestionMark = + ImageVector.Builder( + name = "circle-question-mark", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 24f, + viewportHeight = 24f, + ) + .apply { + path( + fill = SolidColor(Color.Transparent), + stroke = SolidColor(Color.Black), + strokeLineWidth = 2f, + strokeLineCap = StrokeCap.Round, + strokeLineJoin = StrokeJoin.Round, + ) { + moveTo(22f, 12f) + arcTo(10f, 10f, 0f, false, true, 12f, 22f) + arcTo(10f, 10f, 0f, false, true, 2f, 12f) + arcTo(10f, 10f, 0f, false, true, 22f, 12f) + close() + } + path( + fill = SolidColor(Color.Transparent), + stroke = SolidColor(Color.Black), + strokeLineWidth = 2f, + strokeLineCap = StrokeCap.Round, + strokeLineJoin = StrokeJoin.Round, + ) { + moveTo(9.09f, 9f) + arcToRelative(3f, 3f, 0f, false, true, 5.83f, 1f) + curveToRelative(0f, 2f, -3f, 3f, -3f, 3f) + } + path( + fill = SolidColor(Color.Transparent), + stroke = SolidColor(Color.Black), + strokeLineWidth = 2f, + strokeLineCap = StrokeCap.Round, + strokeLineJoin = StrokeJoin.Round, + ) { + moveTo(12f, 17f) + horizontalLineToRelative(0.01f) + } + } + .build() + + return _LucideCircleQuestionMark!! + } + +private var _LucideCircleQuestionMark: ImageVector? = null diff --git a/core/main/src/main/java/com/rk/icons/XedIcon.kt b/core/main/src/main/java/com/rk/icons/XedIcon.kt index 54b9393d3..51acfe3f5 100644 --- a/core/main/src/main/java/com/rk/icons/XedIcon.kt +++ b/core/main/src/main/java/com/rk/icons/XedIcon.kt @@ -8,8 +8,10 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.sp import coil.ImageLoader @@ -24,7 +26,7 @@ fun XedIcon( tint: Color = LocalContentColor.current, ) { when (icon) { - is Icon.DrawableRes -> { + is Icon.ResourceIcon -> { Icon( painter = painterResource(icon.drawableRes), contentDescription = contentDescription, @@ -33,6 +35,17 @@ fun XedIcon( ) } + is Icon.ExternalResourceIcon -> { + val theme = LocalContext.current.theme + + Icon( + imageVector = ImageVector.vectorResource(theme = theme, resId = icon.drawableRes, res = icon.resources), + contentDescription = contentDescription, + modifier = modifier, + tint = tint, + ) + } + is Icon.VectorIcon -> { Icon(imageVector = icon.vector, contentDescription = contentDescription, modifier = modifier, tint = tint) } diff --git a/core/main/src/main/java/com/rk/icons/pack/IconPack.kt b/core/main/src/main/java/com/rk/icons/pack/IconPack.kt index a7ddba28d..ecdfdd766 100644 --- a/core/main/src/main/java/com/rk/icons/pack/IconPack.kt +++ b/core/main/src/main/java/com/rk/icons/pack/IconPack.kt @@ -11,7 +11,13 @@ typealias IconPackId = String typealias IconPackPath = String @Serializable -data class IconPackInfo(val id: IconPackId, val name: String, val applyTint: Boolean = false, val icons: IconPackList) +data class IconPackManifest( + val id: IconPackId, + val name: String, + val minAppVersion: Int? = null, + val applyTint: Boolean = false, + val icons: IconPackList, +) @Serializable data class IconPackList( @@ -25,7 +31,7 @@ data class IconPackList( val languageNames: Map = emptyMap(), ) -data class IconPack(val info: IconPackInfo, val installDir: File) { +data class IconPack(val manifest: IconPackManifest, val installDir: File) { fun getIconFileForFile(file: FileObject, isExpanded: Boolean = false): File? { val fileName = file.getName() val isDirectory = file.isDirectory() @@ -37,26 +43,27 @@ data class IconPack(val info: IconPackInfo, val installDir: File) { if (isDirectory) { if (isExpanded) { // First use folderNamesExpanded, then defaultFolderExpanded - info.icons.folderNamesExpanded[fileName.lowercase()] + manifest.icons.folderNamesExpanded[fileName.lowercase()] ?.let { installDir.resolve(it) } - ?.takeIf { it.exists() } ?: installDir.resolve(info.icons.defaultFolderExpanded) + ?.takeIf { it.exists() } ?: installDir.resolve(manifest.icons.defaultFolderExpanded) } else { // First use folderNames, then defaultFolder - info.icons.folderNames[fileName.lowercase()]?.let { installDir.resolve(it) }?.takeIf { it.exists() } - ?: installDir.resolve(info.icons.defaultFolder) + manifest.icons.folderNames[fileName.lowercase()] + ?.let { installDir.resolve(it) } + ?.takeIf { it.exists() } ?: installDir.resolve(manifest.icons.defaultFolder) } } else { // First use fileNames, then fileExtensions, then languageNames, then defaultFile val ext = fileName.substringAfterLast(".", "") - info.icons.fileNames[fileName.lowercase()]?.let { installDir.resolve(it) }?.takeIf { it.exists() } - ?: info.icons.fileExtensions[ext.lowercase()] + manifest.icons.fileNames[fileName.lowercase()]?.let { installDir.resolve(it) }?.takeIf { it.exists() } + ?: manifest.icons.fileExtensions[ext.lowercase()] ?.let { installDir.resolve(it) } ?.takeIf { it.exists() } - ?: info.icons.languageNames[FileTypeManager.fromExtension(ext).name.lowercase()] + ?: manifest.icons.languageNames[FileTypeManager.fromExtension(ext).name.lowercase()] ?.let { installDir.resolve(it) } ?.takeIf { it.exists() } - ?: installDir.resolve(info.icons.defaultFile) + ?: installDir.resolve(manifest.icons.defaultFile) } // If no icon was working (even the fallback ones) @@ -68,11 +75,13 @@ data class IconPack(val info: IconPackInfo, val installDir: File) { fun getIconFileForExt(fileExtension: String): File? { val path = // First use fileExtensions, then languageNames, then defaultFile - info.icons.fileExtensions[fileExtension.lowercase()]?.let { installDir.resolve(it) }?.takeIf { it.exists() } - ?: info.icons.languageNames[FileTypeManager.fromExtension(fileExtension).name.lowercase()] + manifest.icons.fileExtensions[fileExtension.lowercase()] + ?.let { installDir.resolve(it) } + ?.takeIf { it.exists() } + ?: manifest.icons.languageNames[FileTypeManager.fromExtension(fileExtension).name.lowercase()] ?.let { installDir.resolve(it) } ?.takeIf { it.exists() } - ?: installDir.resolve(info.icons.defaultFile) + ?: installDir.resolve(manifest.icons.defaultFile) // If no icon was working (even the fallback ones) if (!path.exists()) return null @@ -86,9 +95,9 @@ data class IconPack(val info: IconPackInfo, val installDir: File) { val path = // First use fileExtensions, then languageNames, then defaultFile - extension?.let { info.icons.fileExtensions[it] }?.let { installDir.resolve(it) }?.takeIf { it.exists() } - ?: info.icons.languageNames[typeName]?.let { installDir.resolve(it) }?.takeIf { it.exists() } - ?: installDir.resolve(info.icons.defaultFile) + extension?.let { manifest.icons.fileExtensions[it] }?.let { installDir.resolve(it) }?.takeIf { it.exists() } + ?: manifest.icons.languageNames[typeName]?.let { installDir.resolve(it) }?.takeIf { it.exists() } + ?: installDir.resolve(manifest.icons.defaultFile) // If no icon was working (even the fallback ones) if (!path.exists()) return null diff --git a/core/main/src/main/java/com/rk/icons/pack/IconPackManager.kt b/core/main/src/main/java/com/rk/icons/pack/IconPackManager.kt index 85053441d..000e6e876 100644 --- a/core/main/src/main/java/com/rk/icons/pack/IconPackManager.kt +++ b/core/main/src/main/java/com/rk/icons/pack/IconPackManager.kt @@ -3,6 +3,7 @@ package com.rk.icons.pack import android.app.Application import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateOf +import androidx.core.content.pm.PackageInfoCompat import com.rk.activities.settings.SettingsActivity import com.rk.file.child import com.rk.file.createDirIfNot @@ -11,7 +12,8 @@ import com.rk.resources.getFilledString import com.rk.resources.getString import com.rk.resources.strings import com.rk.settings.Settings -import com.rk.utils.dialog +import com.rk.utils.application +import com.rk.utils.dialogRes import java.io.File import java.util.zip.ZipFile import kotlinx.coroutines.Dispatchers @@ -53,24 +55,43 @@ class IconPackManager(private val context: Application) { } private fun installIconPackFromDir(dir: File) { - val iconPackInfo = validateIconPack(dir) ?: return + val iconPackManifest = validateIconPack(dir) ?: return + + val packageName = application!!.packageName + val packageManager = application!!.packageManager + val currentVersionCode = PackageInfoCompat.getLongVersionCode(packageManager.getPackageInfo(packageName, 0)) + if (iconPackManifest.minAppVersion != null && iconPackManifest.minAppVersion.toLong() > currentVersionCode) { + dialogRes( + activity = SettingsActivity.instance, + title = strings.warning.getString(), + msg = strings.incompatible_theme_warning.getString(), + cancelRes = strings.cancel, + okRes = strings.continue_action, + onOk = { writeIconPackToDisk(iconPackManifest, dir) }, + ) + return + } + + writeIconPackToDisk(iconPackManifest, dir) + } - val installDir = iconPackDir.child(iconPackInfo.id) + private fun writeIconPackToDisk(iconPackManifest: IconPackManifest, dir: File) { + val installDir = iconPackDir.child(iconPackManifest.id) if (installDir.exists()) { - uninstallIconPack(iconPackInfo.id) + uninstallIconPack(iconPackManifest.id) } dir.copyRecursively(installDir, overwrite = true) - val iconPack = IconPack(iconPackInfo, installDir) - iconPacks[iconPackInfo.id] = iconPack + val iconPack = IconPack(iconPackManifest, installDir) + iconPacks[iconPackManifest.id] = iconPack } @OptIn(ExperimentalSerializationApi::class) - internal fun validateIconPack(dir: File): IconPackInfo? { + internal fun validateIconPack(dir: File): IconPackManifest? { val iconPackJson = dir.resolve("manifest.json") if (!iconPackJson.exists()) { - dialog( + dialogRes( SettingsActivity.instance, strings.icon_pack_install_failed.getString(), strings.manifest_missing.getString(), @@ -79,12 +100,12 @@ class IconPackManager(private val context: Application) { return null } - val iconPackInfo = - runCatching { json.decodeFromString(iconPackJson.readText()) } + val iconPackManifest = + runCatching { json.decodeFromString(iconPackJson.readText()) } .getOrElse { e -> if (e is MissingFieldException) { val fields = e.missingFields.joinToString("\n") { "• $it" } - dialog( + dialogRes( SettingsActivity.instance, strings.icon_pack_install_failed.getString(), strings.manifest_missing_fields.getFilledString(fields), @@ -92,7 +113,7 @@ class IconPackManager(private val context: Application) { ) return null } - dialog( + dialogRes( SettingsActivity.instance, strings.icon_pack_install_failed.getString(), e.localizedMessage ?: strings.unknown_err.getString(), @@ -101,7 +122,7 @@ class IconPackManager(private val context: Application) { return null } - return iconPackInfo + return iconPackManifest } fun uninstallIconPack(iconPackId: IconPackId) { @@ -118,10 +139,10 @@ class IconPackManager(private val context: Application) { val manifestJson = dir.resolve("manifest.json") if (manifestJson.exists()) { runCatching { - val iconPackInfo = json.decodeFromString(manifestJson.readText()) - val installDir = iconPackDir.child(iconPackInfo.id) - val iconPack = IconPack(iconPackInfo, installDir) - iconPacks[iconPackInfo.id] = iconPack + val iconPackManifest = json.decodeFromString(manifestJson.readText()) + val installDir = iconPackDir.child(iconPackManifest.id) + val iconPack = IconPack(iconPackManifest, installDir) + iconPacks[iconPackManifest.id] = iconPack } } } diff --git a/core/main/src/main/java/com/rk/lsp/AndroidProcessConnection.kt b/core/main/src/main/java/com/rk/lsp/AndroidProcessConnection.kt new file mode 100644 index 000000000..244b1d685 --- /dev/null +++ b/core/main/src/main/java/com/rk/lsp/AndroidProcessConnection.kt @@ -0,0 +1,93 @@ +package com.rk.lsp + +import android.util.Log +import com.rk.file.localBinDir +import com.rk.file.localLibDir +import java.io.File +import java.io.InputStream +import java.io.OutputStream +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch + +class AndroidProcessConnection(private val cmd: Array, instance: LspServerInstance) : + BaseLspConnectionProvider(instance) { + + private var process: Process? = null + private var loggingInput: InputStream? = null + private var loggingOutput: OutputStream? = null + + private var scope: CoroutineScope? = null + + override val inputStream: InputStream + get() = loggingInput ?: throw IllegalStateException("Process not running") + + override val outputStream: OutputStream + get() = loggingOutput ?: throw IllegalStateException("Process not running") + + override val isClosed: Boolean + get() = process == null || process?.isAlive == false + + override fun start() { + if (process != null) return + scope = CoroutineScope(Dispatchers.IO) + + val pb = ProcessBuilder(*cmd) + + val rootPath = instance.projectRoot.getAbsolutePath() + val rootFile = File(rootPath) + if (rootFile.exists() && rootFile.isDirectory) { + pb.directory(rootFile) + } + + pb.environment().apply { + put("ANDROID_ART_ROOT", System.getenv("ANDROID_ART_ROOT").orEmpty()) + put("ANDROID_DATA", System.getenv("ANDROID_DATA").orEmpty()) + put("ANDROID_I18N_ROOT", System.getenv("ANDROID_I18N_ROOT").orEmpty()) + put("ANDROID_ROOT", System.getenv("ANDROID_ROOT").orEmpty()) + put("ANDROID_RUNTIME_ROOT", System.getenv("ANDROID_RUNTIME_ROOT").orEmpty()) + put("ANDROID_TZDATA_ROOT", System.getenv("ANDROID_TZDATA_ROOT").orEmpty()) + put("BOOTCLASSPATH", System.getenv("BOOTCLASSPATH").orEmpty()) + put("DEX2OATBOOTCLASSPATH", System.getenv("DEX2OATBOOTCLASSPATH").orEmpty()) + put("EXTERNAL_STORAGE", System.getenv("EXTERNAL_STORAGE").orEmpty()) + put("PATH", "${System.getenv("PATH")}:${localBinDir().absolutePath}") + put("LD_LIBRARY_PATH", localLibDir().absolutePath) + } + + process = pb.start() + + loggingInput = + LoggingInputStream(process!!.inputStream) { json -> + Log.d("AndroidProcessConnection", "[stdout] $json") + if (com.rk.settings.Preference.getBoolean("debug_mode", com.rk.xededitor.BuildConfig.DEBUG)) { + instance.addLog(LspLogEntry(MessageSource.RPC, null, "→ $json")) + } + } + loggingOutput = + LoggingOutputStream(process!!.outputStream) { json -> + Log.d("AndroidProcessConnection", "[stdin] $json") + if (com.rk.settings.Preference.getBoolean("debug_mode", com.rk.xededitor.BuildConfig.DEBUG)) { + instance.addLog(LspLogEntry(MessageSource.RPC, null, "← $json")) + } + } + + scope!!.launch { + runCatching { + process!!.errorStream.bufferedReader().forEachLine { line -> + Log.e("AndroidProcessConnection", "[stderr] $line") + instance.addLog(LspLogEntry(MessageSource.Runtime, null, line)) + } + } + } + } + + override fun close() { + scope?.cancel() + scope = null + process?.destroy() + process = null + loggingInput = null + loggingOutput = null + } +} diff --git a/core/main/src/main/java/com/rk/lsp/FileIconProvider.kt b/core/main/src/main/java/com/rk/lsp/FileIconProvider.kt index d93b51194..2eed053fa 100644 --- a/core/main/src/main/java/com/rk/lsp/FileIconProvider.kt +++ b/core/main/src/main/java/com/rk/lsp/FileIconProvider.kt @@ -1,7 +1,6 @@ package com.rk.lsp import android.graphics.drawable.Drawable -import com.rk.activities.main.MainActivity import com.rk.filetree.getDrawableFileIcon import io.github.rosemoe.sora.lang.completion.SimpleCompletionIconDrawer @@ -29,7 +28,6 @@ class FileIconProvider : io.github.rosemoe.sora.lang.completion.FileIconProvider * @return A [Drawable] if successful, or null if no icon can be loaded. */ override fun load(src: String, isFolder: Boolean): Drawable? { - val context = MainActivity.instance ?: return null - return getDrawableFileIcon(context, src.substringAfterLast('/'), isFolder) + return getDrawableFileIcon(src.substringAfterLast('/'), isFolder) } } diff --git a/core/main/src/main/java/com/rk/lsp/LspConnectionConfig.kt b/core/main/src/main/java/com/rk/lsp/LspConnectionConfig.kt index 624fc7f39..c41a32ebb 100644 --- a/core/main/src/main/java/com/rk/lsp/LspConnectionConfig.kt +++ b/core/main/src/main/java/com/rk/lsp/LspConnectionConfig.kt @@ -32,6 +32,23 @@ sealed interface LspConnectionConfig { } } + data class AndroidProcess(val command: Array) : LspConnectionConfig { + override fun providerFactory() = ConnectionProviderFactory { AndroidProcessConnection(command, it) } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as AndroidProcess + + return command.contentEquals(other.command) + } + + override fun hashCode(): Int { + return command.contentHashCode() + } + } + data class Custom(val provider: ConnectionProviderFactory) : LspConnectionConfig { override fun providerFactory() = provider } diff --git a/core/main/src/main/java/com/rk/lsp/LspConnector.kt b/core/main/src/main/java/com/rk/lsp/LspConnector.kt index 54130d86c..cd4ae5279 100644 --- a/core/main/src/main/java/com/rk/lsp/LspConnector.kt +++ b/core/main/src/main/java/com/rk/lsp/LspConnector.kt @@ -13,9 +13,9 @@ import com.rk.resources.getString import com.rk.resources.strings import com.rk.settings.Preference import com.rk.tabs.editor.EditorTab -import com.rk.utils.dialog +import com.rk.utils.dialogRes import com.rk.utils.errorDialog -import com.rk.utils.info +import com.rk.utils.logInfo import io.github.rosemoe.sora.langs.textmate.TextMateLanguage import io.github.rosemoe.sora.lsp.client.languageserver.LspFeature import io.github.rosemoe.sora.lsp.client.languageserver.ServerStatus @@ -130,7 +130,7 @@ class LspConnector( suspend fun connect(wrapperLanguage: TextMateLanguage?) = withContext(Dispatchers.IO) { if (isConnected()) { - info("LSP servers already connected skipping...") + logInfo("LSP servers already connected skipping...") return@withContext } @@ -233,6 +233,9 @@ class LspConnector( override val disabledFeatures: Set get() = buildSet { + if (!Preference.getBoolean("lsp_${id}_document_highlight", true)) { + add(LspFeature.DocumentHighlight) + } if (!Preference.getBoolean("lsp_${id}_hover", true)) { add(LspFeature.Hover) } @@ -262,24 +265,30 @@ class LspConnector( get() = object : EventHandler.EventListener { override fun onEventException(eventListener: AsyncEventListener, exception: Exception) { - instance.addLog(LspLogEntry(MessageType.Error, "Event ${eventListener.eventName} failed")) + instance.addLog( + LspLogEntry( + MessageSource.Client, + MessageType.Error, + "Event ${eventListener.eventName} failed", + ) + ) exception.localizedMessage?.let { message -> - instance.addLog(LspLogEntry(MessageType.Error, message)) + instance.addLog(LspLogEntry(MessageSource.Client, MessageType.Error, message)) } } override fun onHandlerException(exception: Exception) { exception.cause?.localizedMessage?.let { message -> - instance.addLog(LspLogEntry(MessageType.Error, message)) + instance.addLog(LspLogEntry(MessageSource.Client, MessageType.Error, message)) } exception.localizedMessage?.let { message -> - instance.addLog(LspLogEntry(MessageType.Error, message)) + instance.addLog(LspLogEntry(MessageSource.Client, MessageType.Error, message)) } } override fun onLogMessage(messageParams: MessageParams?) { if (messageParams == null) return - info(messageParams.message) + logInfo(messageParams.message) instance.addLog(messageParams) } @@ -287,14 +296,14 @@ class LspConnector( if (messageParams == null) return instance.addLog(messageParams) when (messageParams.type) { - MessageType.Error -> errorDialog(messageParams.message) + MessageType.Error -> errorDialog(msg = messageParams.message) MessageType.Warning -> - dialog(title = strings.warning.getString(), msg = messageParams.message) + dialogRes(title = strings.warning.getString(), msg = messageParams.message) MessageType.Info -> - dialog(title = strings.info.getString(), msg = messageParams.message) + dialogRes(title = strings.info.getString(), msg = messageParams.message) - MessageType.Log -> info(messageParams.message) + MessageType.Log -> logInfo(messageParams.message) MessageType.Debug -> {} } } @@ -322,7 +331,7 @@ class LspConnector( is ServerStatus.STOPPED -> "Disconnected from LSP server (reason: ${newStatus.reason})\n" } - instance.addLog(LspLogEntry(MessageType.Info, statusMessage)) + instance.addLog(LspLogEntry(MessageSource.Client, MessageType.Info, statusMessage)) if ( oldStatus is ServerStatus.STOPPED && diff --git a/core/main/src/main/java/com/rk/lsp/LspRegistry.kt b/core/main/src/main/java/com/rk/lsp/LspRegistry.kt index ebd723482..89709ff72 100644 --- a/core/main/src/main/java/com/rk/lsp/LspRegistry.kt +++ b/core/main/src/main/java/com/rk/lsp/LspRegistry.kt @@ -2,12 +2,11 @@ package com.rk.lsp import android.content.Context import androidx.compose.runtime.mutableStateListOf +import com.rk.extension.api.XedExtensionPoint import com.rk.lsp.servers.Bash import com.rk.lsp.servers.CSS import com.rk.lsp.servers.Emmet import com.rk.lsp.servers.HTML -import com.rk.lsp.servers.JSON -import com.rk.lsp.servers.Python import com.rk.lsp.servers.TypeScript import com.rk.lsp.servers.XML @@ -16,7 +15,7 @@ object LspRegistry { val extensionServers: List get() = _extensionServers.toList() - val builtInServer = listOf(Python, HTML, Emmet, CSS, TypeScript, JSON, Bash, XML) + val builtInServer = listOf(HTML, Emmet, CSS, TypeScript, Bash, XML) private val _externalServers = mutableStateListOf() val externalServers: List @@ -57,12 +56,14 @@ object LspRegistry { ?: _extensionServers.find { it.id == id } } + @XedExtensionPoint fun registerServer(server: LspServer) { if (!_extensionServers.contains(server)) { _extensionServers.add(server) } } + @XedExtensionPoint fun unregisterServer(server: LspServer) { _extensionServers.remove(server) } diff --git a/core/main/src/main/java/com/rk/lsp/LspServer.kt b/core/main/src/main/java/com/rk/lsp/LspServer.kt index 828b750d2..7db1601e3 100644 --- a/core/main/src/main/java/com/rk/lsp/LspServer.kt +++ b/core/main/src/main/java/com/rk/lsp/LspServer.kt @@ -1,5 +1,6 @@ package com.rk.lsp +import android.app.Activity import android.content.Context import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable @@ -9,6 +10,7 @@ import com.rk.activities.main.MainActivity import com.rk.exec.TerminalCommand import com.rk.exec.launchTerminal import com.rk.file.FileObject +import com.rk.icons.Icon import com.rk.tabs.editor.EditorTab import com.rk.tabs.editor.applyHighlightingAndConnectLSP import com.rk.theme.greenStatus @@ -21,15 +23,15 @@ abstract class ScriptedLspServer : LspServer() { abstract val installScript: File abstract val installId: String - override fun install(context: Context) = launchInstaller(context) + override fun install(activity: Activity) = launchInstaller(activity) - override fun uninstall(context: Context) = launchInstaller(context, "--uninstall") + override fun uninstall(activity: Activity) = launchInstaller(activity, "--uninstall") - override fun update(context: Context) = launchInstaller(context, "--update") + override fun update(activity: Activity) = launchInstaller(activity, "--update") - protected fun launchInstaller(context: Context, vararg flags: String) { + protected fun launchInstaller(activity: Activity, vararg flags: String) { launchTerminal( - context = context, + activity = activity, terminalCommand = TerminalCommand( exe = "/bin/bash", @@ -42,6 +44,16 @@ abstract class ScriptedLspServer : LspServer() { } abstract class LspServer { + abstract val id: String + abstract val languageName: String + abstract val serverName: String + abstract val supportedExtensions: List + abstract val icon: Icon? + + open val canBeUninstalled = true + + open val expectedCapabilities: ServerCapabilities? = null + suspend fun startAllInstances(): List { val connectedEditors = mutableListOf() _instances.forEach { connectedEditors.addAll(it.start()) } @@ -84,13 +96,13 @@ abstract class LspServer { abstract suspend fun isInstalled(context: Context): Boolean - abstract fun install(context: Context) + abstract fun install(activity: Activity) - abstract fun uninstall(context: Context) + abstract fun uninstall(activity: Activity) abstract suspend fun isUpdatable(context: Context): Boolean - abstract fun update(context: Context) + abstract fun update(activity: Activity) abstract fun getConnectionConfig(): LspConnectionConfig @@ -113,16 +125,6 @@ abstract class LspServer { } override fun hashCode(): Int = id.hashCode() - - open val expectedCapabilities: ServerCapabilities? = null - - open val canBeUninstalled = true - - abstract val id: String - abstract val languageName: String - abstract val serverName: String - abstract val supportedExtensions: List - abstract val icon: Int? } @Composable diff --git a/core/main/src/main/java/com/rk/lsp/LspServerInstance.kt b/core/main/src/main/java/com/rk/lsp/LspServerInstance.kt index bbd79c918..2a61b6560 100644 --- a/core/main/src/main/java/com/rk/lsp/LspServerInstance.kt +++ b/core/main/src/main/java/com/rk/lsp/LspServerInstance.kt @@ -21,8 +21,6 @@ import com.rk.file.FileObject import com.rk.icons.Error import com.rk.icons.XedIcons import com.rk.resources.strings -import com.rk.settings.debugOptions.LogEntry -import com.rk.settings.debugOptions.LogLevel import com.rk.tabs.editor.EditorTab import com.rk.tabs.editor.applyHighlightingAndConnectLSP import com.rk.theme.greenStatus @@ -44,21 +42,20 @@ enum class LspConnectionStatus { TIMEOUT, } -private fun MessageType.toLogLevel() = - when (this) { - MessageType.Error -> LogLevel.ERROR - MessageType.Warning -> LogLevel.WARN - MessageType.Info -> LogLevel.INFO - MessageType.Log -> LogLevel.DEBUG - MessageType.Debug -> LogLevel.DEBUG - } - -data class LspLogEntry(val level: MessageType, val message: String, val timestamp: Long = System.currentTimeMillis()) { - fun toLogEntry(): LogEntry { - return LogEntry(level = level.toLogLevel(), message = message, timestamp = timestamp) - } +enum class MessageSource { + RPC, + LSP, + Runtime, + Client, } +data class LspLogEntry( + val source: MessageSource, + val type: MessageType?, + val message: String, + val timestamp: Long = System.currentTimeMillis(), +) + data class LspServerInstance(val server: LspServer, internal val lspProject: LspProject, val projectRoot: FileObject) { val id = "${server.id}_${projectRoot.getAbsolutePath().hashCode()}" @@ -68,25 +65,23 @@ data class LspServerInstance(val server: LspServer, internal val lspProject: Lsp var hasError by mutableStateOf(false) fun addLog(messageParams: MessageParams) { - logs.add(LspLogEntry(level = messageParams.type, message = messageParams.message)) + logs.add(LspLogEntry(type = messageParams.type, message = messageParams.message, source = MessageSource.LSP)) } fun addLog(lspLogEntry: LspLogEntry) { - if (lspLogEntry.level == MessageType.Error) hasError = true + if (lspLogEntry.type == MessageType.Error) hasError = true logs.add(lspLogEntry) } fun getLspLogs() = logs.toList() - fun getLogs() = getLspLogs().map { it.toLogEntry() } - fun getWrapper(): LanguageServerWrapper? { return server.supportedExtensions.firstOrNull()?.let { lspProject.getLanguageServerWrapper(it, server.serverName) } ?: run { hasError = true - addLog(LspLogEntry(MessageType.Error, "Language server instance not found...")) + addLog(LspLogEntry(MessageSource.Client, MessageType.Error, "Language server instance not found...")) return null } } @@ -94,7 +89,7 @@ data class LspServerInstance(val server: LspServer, internal val lspProject: Lsp /** Stops this language server instance */ suspend fun stop() { withContext(Dispatchers.IO) { - addLog(LspLogEntry(MessageType.Info, "User stopped language server instance...")) + addLog(LspLogEntry(MessageSource.Client, MessageType.Info, "User stopped language server instance...")) val wrapper = getWrapper() ?: return@withContext DefinitionPrevention.register(lspProject, server) try { @@ -108,7 +103,7 @@ data class LspServerInstance(val server: LspServer, internal val lspProject: Lsp /** Restarts this language server instance */ suspend fun restart() { withContext(Dispatchers.IO) { - addLog(LspLogEntry(MessageType.Info, "User restarted language server instance...")) + addLog(LspLogEntry(MessageSource.Client, MessageType.Info, "User restarted language server instance...")) val wrapper = getWrapper() ?: return@withContext try { wrapper.restartAndReconnect() @@ -125,7 +120,7 @@ data class LspServerInstance(val server: LspServer, internal val lspProject: Lsp */ suspend fun start(): List { return withContext(Dispatchers.IO) { - addLog(LspLogEntry(MessageType.Info, "User started language server instance...")) + addLog(LspLogEntry(MessageSource.Client, MessageType.Info, "User started language server instance...")) val wrapper = getWrapper() ?: return@withContext emptyList() hasError = false DefinitionPrevention.unregister(lspProject, server) diff --git a/core/main/src/main/java/com/rk/lsp/ProcessConnection.kt b/core/main/src/main/java/com/rk/lsp/ProcessConnection.kt index c304c0b6c..7a1bf95aa 100644 --- a/core/main/src/main/java/com/rk/lsp/ProcessConnection.kt +++ b/core/main/src/main/java/com/rk/lsp/ProcessConnection.kt @@ -9,7 +9,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import org.eclipse.lsp4j.MessageType class ProcessConnection(private val cmd: Array, instance: LspServerInstance) : BaseLspConnectionProvider(instance) { @@ -37,19 +36,23 @@ class ProcessConnection(private val cmd: Array, instance: LspServerInsta loggingInput = LoggingInputStream(process!!.inputStream) { json -> Log.d("ProcessConnection", "[stdout] $json") - instance.addLog(LspLogEntry(MessageType.Log, "→ $json")) + if (com.rk.settings.Preference.getBoolean("debug_mode", com.rk.xededitor.BuildConfig.DEBUG)) { + instance.addLog(LspLogEntry(MessageSource.RPC, null, "→ $json")) + } } loggingOutput = LoggingOutputStream(process!!.outputStream) { json -> Log.d("ProcessConnection", "[stdin] $json") - instance.addLog(LspLogEntry(MessageType.Log, "← $json")) + if (com.rk.settings.Preference.getBoolean("debug_mode", com.rk.xededitor.BuildConfig.DEBUG)) { + instance.addLog(LspLogEntry(MessageSource.RPC, null, "← $json")) + } } scope!!.launch { runCatching { process!!.errorStream.bufferedReader().forEachLine { line -> Log.e("ProcessConnection", "[stderr] $line") - instance.addLog(LspLogEntry(MessageType.Error, line)) + instance.addLog(LspLogEntry(MessageSource.Runtime, null, line)) } } } diff --git a/core/main/src/main/java/com/rk/lsp/SocketConnection.kt b/core/main/src/main/java/com/rk/lsp/SocketConnection.kt index c7a838267..946b284af 100644 --- a/core/main/src/main/java/com/rk/lsp/SocketConnection.kt +++ b/core/main/src/main/java/com/rk/lsp/SocketConnection.kt @@ -35,12 +35,16 @@ class SocketConnection(private val port: Int, private val host: String? = null, loggingInput = LoggingInputStream(socket!!.getInputStream()) { json -> Log.d("SocketConnection", "[stdout] $json") - instance.addLog(LspLogEntry(MessageType.Log, "→ $json")) + if (com.rk.settings.Preference.getBoolean("debug_mode", com.rk.xededitor.BuildConfig.DEBUG)) { + instance.addLog(LspLogEntry(MessageSource.RPC, null, "→ $json")) + } } loggingOutput = LoggingOutputStream(socket!!.getOutputStream()) { json -> Log.d("SocketConnection", "[stdin] $json") - instance.addLog(LspLogEntry(MessageType.Log, "← $json")) + if (com.rk.settings.Preference.getBoolean("debug_mode", com.rk.xededitor.BuildConfig.DEBUG)) { + instance.addLog(LspLogEntry(MessageSource.RPC, null, "← $json")) + } } } diff --git a/core/main/src/main/java/com/rk/lsp/servers/ExternalProcessServer.kt b/core/main/src/main/java/com/rk/lsp/servers/ExternalProcessServer.kt index 50c617554..40468bf0c 100644 --- a/core/main/src/main/java/com/rk/lsp/servers/ExternalProcessServer.kt +++ b/core/main/src/main/java/com/rk/lsp/servers/ExternalProcessServer.kt @@ -1,5 +1,6 @@ package com.rk.lsp.servers +import android.app.Activity import android.content.Context import com.rk.file.FileTypeManager import com.rk.lsp.LspConnectionConfig @@ -7,7 +8,7 @@ import com.rk.lsp.LspServer import kotlin.random.Random // DO not put this in lsp registry -class ExternalProcessServer( +data class ExternalProcessServer( val command: String, override val supportedExtensions: List, override val id: String = "${supportedExtensions.firstOrNull()}_${Random.nextInt()}", @@ -17,19 +18,15 @@ class ExternalProcessServer( override val icon = supportedExtensions.firstOrNull()?.let { FileTypeManager.fromExtension(it).icon } override val canBeUninstalled = false - override suspend fun isInstalled(context: Context): Boolean { - return true - } + override suspend fun isInstalled(context: Context) = true - override fun install(context: Context) {} + override fun install(activity: Activity) {} - override fun uninstall(context: Context) {} + override fun uninstall(activity: Activity) {} - override suspend fun isUpdatable(context: Context): Boolean { - return false - } + override suspend fun isUpdatable(context: Context) = false - override fun update(context: Context) {} + override fun update(activity: Activity) {} override fun getConnectionConfig(): LspConnectionConfig { return LspConnectionConfig.Process( @@ -37,35 +34,5 @@ class ExternalProcessServer( ) } - override fun toString(): String { - return serverName - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - if (!super.equals(other)) return false - - other as ExternalProcessServer - - if (icon != other.icon) return false - if (command != other.command) return false - if (supportedExtensions != other.supportedExtensions) return false - if (languageName != other.languageName) return false - if (id != other.id) return false - if (serverName != other.serverName) return false - - return true - } - - override fun hashCode(): Int { - var result = super.hashCode() - result = 31 * result + (icon ?: 0) - result = 31 * result + command.hashCode() - result = 31 * result + supportedExtensions.hashCode() - result = 31 * result + languageName.hashCode() - result = 31 * result + id.hashCode() - result = 31 * result + serverName.hashCode() - return result - } + override fun toString() = serverName } diff --git a/core/main/src/main/java/com/rk/lsp/servers/ExternalSocketServer.kt b/core/main/src/main/java/com/rk/lsp/servers/ExternalSocketServer.kt index a874d9096..462ef2823 100644 --- a/core/main/src/main/java/com/rk/lsp/servers/ExternalSocketServer.kt +++ b/core/main/src/main/java/com/rk/lsp/servers/ExternalSocketServer.kt @@ -1,5 +1,6 @@ package com.rk.lsp.servers +import android.app.Activity import android.content.Context import com.rk.file.FileTypeManager import com.rk.lsp.LspConnectionConfig @@ -7,7 +8,7 @@ import com.rk.lsp.LspServer import kotlin.random.Random // DO not put this in lsp registry -class ExternalSocketServer( +data class ExternalSocketServer( val host: String, val port: Int, override val supportedExtensions: List, @@ -19,55 +20,19 @@ class ExternalSocketServer( override val icon = supportedExtensions.firstOrNull()?.let { FileTypeManager.fromExtension(it).icon } override val canBeUninstalled = false - override suspend fun isInstalled(context: Context): Boolean { - return true - } + override suspend fun isInstalled(context: Context) = true - override fun install(context: Context) {} + override fun install(activity: Activity) {} - override fun uninstall(context: Context) {} + override fun uninstall(activity: Activity) {} - override suspend fun isUpdatable(context: Context): Boolean { - return false - } + override suspend fun isUpdatable(context: Context) = false - override fun update(context: Context) {} + override fun update(activity: Activity) {} override fun getConnectionConfig(): LspConnectionConfig { return LspConnectionConfig.Socket(host = host, port = port) } - override fun toString(): String { - return serverName - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - if (!super.equals(other)) return false - - other as ExternalSocketServer - - if (port != other.port) return false - if (icon != other.icon) return false - if (host != other.host) return false - if (supportedExtensions != other.supportedExtensions) return false - if (languageName != other.languageName) return false - if (id != other.id) return false - if (serverName != other.serverName) return false - - return true - } - - override fun hashCode(): Int { - var result = super.hashCode() - result = 31 * result + port - result = 31 * result + (icon ?: 0) - result = 31 * result + host.hashCode() - result = 31 * result + supportedExtensions.hashCode() - result = 31 * result + languageName.hashCode() - result = 31 * result + id.hashCode() - result = 31 * result + serverName.hashCode() - return result - } + override fun toString() = serverName } diff --git a/core/main/src/main/java/com/rk/lsp/servers/JSON.kt b/core/main/src/main/java/com/rk/lsp/servers/JSON.kt deleted file mode 100644 index 5f5e42ea6..000000000 --- a/core/main/src/main/java/com/rk/lsp/servers/JSON.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.rk.lsp.servers - -import android.content.Context -import com.rk.exec.NpmUtils -import com.rk.exec.isTerminalInstalled -import com.rk.file.BuiltinFileType -import com.rk.file.child -import com.rk.file.localBinDir -import com.rk.file.sandboxDir -import com.rk.lsp.LspConnectionConfig -import com.rk.lsp.ScriptedLspServer - -object JSON : ScriptedLspServer() { - override val id: String = "json" - override val languageName: String = "JSON" - override val serverName = "vscode-json-language-server" - override val supportedExtensions = BuiltinFileType.JSON.extensions - override val icon = BuiltinFileType.JSON.icon - - override val installScript = localBinDir().child("lsp/json") - override val installId = "JSON language server" - - override suspend fun isInstalled(context: Context): Boolean { - if (!isTerminalInstalled()) { - return false - } - - return sandboxDir().child("/usr/bin/$serverName").exists() - } - - override suspend fun isUpdatable(context: Context): Boolean { - return NpmUtils.hasUpdate("vscode-langservers-extracted") - } - - override fun getConnectionConfig(): LspConnectionConfig { - return LspConnectionConfig.Process(arrayOf("/usr/bin/node", "/usr/bin/$serverName", "--stdio")) - } -} diff --git a/core/main/src/main/java/com/rk/lsp/servers/Python.kt b/core/main/src/main/java/com/rk/lsp/servers/Python.kt deleted file mode 100644 index 6a3ea83c1..000000000 --- a/core/main/src/main/java/com/rk/lsp/servers/Python.kt +++ /dev/null @@ -1,64 +0,0 @@ -package com.rk.lsp.servers - -import android.content.Context -import com.rk.exec.PipxUtils -import com.rk.exec.isTerminalInstalled -import com.rk.file.BuiltinFileType -import com.rk.file.child -import com.rk.file.localBinDir -import com.rk.file.sandboxHomeDir -import com.rk.lsp.LspConnectionConfig -import com.rk.lsp.LspConnector -import com.rk.lsp.ScriptedLspServer -import org.eclipse.lsp4j.DidChangeConfigurationParams - -object Python : ScriptedLspServer() { - override val id: String = "python" - override val languageName: String = "Python" - override val serverName = "python-lsp-server" - override val supportedExtensions = BuiltinFileType.PYTHON.extensions - override val icon = BuiltinFileType.PYTHON.icon - - override val installScript = localBinDir().child("lsp/python") - override val installId = "Python language server" - - override suspend fun isInstalled(context: Context): Boolean { - if (!isTerminalInstalled()) { - return false - } - - return sandboxHomeDir().child(".local/share/pipx/venvs/python-lsp-server/bin/pylsp").exists() - } - - override suspend fun isUpdatable(context: Context): Boolean { - return PipxUtils.hasUpdate(serverName) - } - - override fun getConnectionConfig(): LspConnectionConfig { - return LspConnectionConfig.Process(arrayOf("/home/.local/share/pipx/venvs/python-lsp-server/bin/pylsp")) - } - - override suspend fun onInitialize(lspConnector: LspConnector) { - val requestManager = lspConnector.lspEditor!!.requestManager - - val params = - DidChangeConfigurationParams( - mapOf( - "pylsp" to - mapOf( - "plugins" to - mapOf( - "pycodestyle" to - mapOf( - "enabled" to true, - "ignore" to listOf("E501", "W291", "W293"), - "maxLineLength" to 999, - ) - ) - ) - ) - ) - - requestManager.didChangeConfiguration(params) - } -} diff --git a/core/main/src/main/java/com/rk/lsp/servers/XML.kt b/core/main/src/main/java/com/rk/lsp/servers/XML.kt index 92dd4620d..967660009 100644 --- a/core/main/src/main/java/com/rk/lsp/servers/XML.kt +++ b/core/main/src/main/java/com/rk/lsp/servers/XML.kt @@ -8,10 +8,12 @@ import com.rk.file.localBinDir import com.rk.file.sandboxHomeDir import com.rk.lsp.LspConnectionConfig import com.rk.lsp.ScriptedLspServer +import io.github.z4kn4fein.semver.toVersion +import io.github.z4kn4fein.semver.toVersionOrNull object XML : ScriptedLspServer() { - override val id: String = "xml" - override val languageName: String = "XML" + override val id = "xml" + override val languageName = "XML" override val serverName = "lemminx" override val supportedExtensions = BuiltinFileType.XML.extensions override val icon = BuiltinFileType.XML.icon @@ -32,8 +34,9 @@ object XML : ScriptedLspServer() { override suspend fun isUpdatable(context: Context): Boolean { val versionFile = sandboxHomeDir().child(".lsp/lemminx/version.txt") - val currentVersion = runCatching { versionFile.readText().trim() }.getOrNull() - return currentVersion != LATEST_VERSION + val currentVersionText = runCatching { versionFile.readText().trim() }.getOrNull() + val currentVersion = currentVersionText?.toVersionOrNull() ?: return false + return currentVersion < LATEST_VERSION.toVersion() } override fun getConnectionConfig(): LspConnectionConfig { diff --git a/core/main/src/main/java/com/rk/runner/Runner.kt b/core/main/src/main/java/com/rk/runner/Runner.kt deleted file mode 100644 index ef6c51e41..000000000 --- a/core/main/src/main/java/com/rk/runner/Runner.kt +++ /dev/null @@ -1,136 +0,0 @@ -package com.rk.runner - -import android.content.Context -import com.rk.file.BuiltinFileType -import com.rk.file.FileObject -import com.rk.icons.Icon -import com.rk.runner.runners.UniversalRunner -import com.rk.runner.runners.web.html.HtmlRunner -import com.rk.runner.runners.web.markdown.MarkdownRunner -import com.rk.settings.Settings -import com.rk.utils.errorDialog -import java.lang.ref.WeakReference - -abstract class RunnerImpl { - - abstract suspend fun run(context: Context, fileObject: FileObject) - - abstract fun getName(): String - - abstract fun getIcon(context: Context): Icon? - - abstract suspend fun isRunning(): Boolean - - abstract suspend fun stop() -} - -var currentRunner = WeakReference(null) - -abstract class RunnerBuilder( - val regex: Regex, - val enabled: () -> Boolean = { true }, - val clazz: Class, -) { - fun build(): RunnerImpl { - return clazz.getDeclaredConstructor().newInstance() - } -} - -object Runner { - val runnerBuilders = mutableListOf() - - init { - val htmlExtensions = BuiltinFileType.HTML.extensions.joinToString("|") - val markdownExtensions = BuiltinFileType.MARKDOWN.extensions.joinToString("|") - - runnerBuilders.apply { - add( - object : - RunnerBuilder( - regex = Regex(".*\\.($htmlExtensions|svg)$"), - enabled = { Settings.enable_html_runner }, - clazz = HtmlRunner::class.java, - ) {} - ) - add( - object : - RunnerBuilder( - regex = Regex(".*\\.($markdownExtensions)$"), - enabled = { Settings.enable_md_runner }, - clazz = MarkdownRunner::class.java, - ) {} - ) - add( - object : - RunnerBuilder( - regex = - Regex( - ".*\\.(py|js|ts|java|kt|rs|rb|php|c|cpp|cc|cxx|cs|sh|bash|zsh|fish|pl|lua|r|R|hs|f90|f95|f03|f08|pas|tcl|elm|fsx|fs)$" - ), - enabled = { Settings.enable_universal_runner }, - clazz = UniversalRunner::class.java, - ) {} - ) - } - } - - fun isRunnable(fileObject: FileObject): Boolean { - ShellBasedRunners.runners.forEach { - val name = fileObject.getName() - val regex = Regex(it.regex) - - if (regex.matches(name)) { - return true - } - } - - runnerBuilders.forEach { - if (!it.enabled()) return@forEach - - val name = fileObject.getName() - val regex = it.regex - - if (regex.matches(name)) { - return true - } - } - return false - } - - suspend fun run(context: Context, fileObject: FileObject, onMultipleRunners: (List) -> Unit) { - val availableRunners = mutableListOf() - - ShellBasedRunners.runners.forEach { - val name = fileObject.getName() - val regex = Regex(it.regex) - - if (regex.matches(name)) { - availableRunners.add(it) - } - } - - runnerBuilders.forEach { - val name = fileObject.getName() - val regex = it.regex - - if (regex.matches(name)) { - availableRunners.add(it.build()) - } - } - - if (availableRunners.isEmpty()) { - errorDialog("No runners available") - return - } - - if (availableRunners.size == 1) { - availableRunners[0].run(context, fileObject) - } else { - onMultipleRunners.invoke(availableRunners) - } - } - - suspend fun onMainActivityResumed() { - currentRunner.get()?.stop() - } -} diff --git a/core/main/src/main/java/com/rk/runner/RunnerManager.kt b/core/main/src/main/java/com/rk/runner/RunnerManager.kt new file mode 100644 index 000000000..cb107a7c1 --- /dev/null +++ b/core/main/src/main/java/com/rk/runner/RunnerManager.kt @@ -0,0 +1,92 @@ +package com.rk.runner + +import android.app.Activity +import android.content.Context +import androidx.compose.runtime.mutableStateListOf +import com.rk.extension.api.XedExtensionPoint +import com.rk.file.FileObject +import com.rk.icons.Icon +import com.rk.runner.runners.UniversalRunner +import com.rk.runner.runners.web.html.HtmlRunner +import com.rk.runner.runners.web.markdown.MarkdownRunner +import com.rk.settings.Preference +import com.rk.utils.errorDialog + +abstract class Runner { + abstract val id: String + abstract val label: String + open val description: String? = null + open val onConfigure: (() -> Unit)? = null + + abstract fun getIcon(context: Context): Icon? + + abstract fun matcher(fileObject: FileObject): Boolean + + abstract suspend fun run(activity: Activity, fileObject: FileObject) + + abstract suspend fun isRunning(): Boolean + + abstract suspend fun stop() + + fun isEnabled(): Boolean { + return Preference.getBoolean("runner_$id", true) + } + + fun setEnabled(enabled: Boolean) { + Preference.setBoolean("runner_$id", enabled) + } +} + +object RunnerManager { + + private val _extensionRunners = mutableStateListOf() + + val extensionRunners: List + get() = _extensionRunners.toList() + + val builtinRunners = listOf(HtmlRunner, MarkdownRunner, UniversalRunner) + + @XedExtensionPoint + fun registerRunner(runner: Runner) { + if (!_extensionRunners.contains(runner)) { + _extensionRunners.add(runner) + } + } + + @XedExtensionPoint + fun unregisterRunner(runner: Runner) { + _extensionRunners.remove(runner) + } + + fun isRunnable(fileObject: FileObject): Boolean { + return getAvailableRunners(fileObject).isNotEmpty() + } + + fun getAvailableRunners(fileObject: FileObject): List { + val result = mutableListOf() + + val runners = builtinRunners + extensionRunners + ShellBasedRunners.runners + runners.forEach { + if (it.isEnabled() && it.matcher(fileObject)) { + result.add(it) + } + } + + return result + } + + suspend fun run(activity: Activity, fileObject: FileObject, onMultipleRunners: (List) -> Unit) { + val availableRunners = getAvailableRunners(fileObject) + + if (availableRunners.isEmpty()) { + errorDialog(activity, msg = "No runners available") + return + } + + if (availableRunners.size == 1) { + availableRunners[0].run(activity, fileObject) + } else { + onMultipleRunners.invoke(availableRunners) + } + } +} diff --git a/core/main/src/main/java/com/rk/runner/ShellBasedRunners.kt b/core/main/src/main/java/com/rk/runner/ShellBasedRunners.kt index 5b8f266c4..2e9f66261 100644 --- a/core/main/src/main/java/com/rk/runner/ShellBasedRunners.kt +++ b/core/main/src/main/java/com/rk/runner/ShellBasedRunners.kt @@ -1,5 +1,6 @@ package com.rk.runner +import android.app.Activity import android.content.Context import androidx.compose.runtime.mutableStateListOf import com.google.gson.Gson @@ -15,6 +16,7 @@ import com.rk.file.runnerDir import com.rk.icons.Icon import com.rk.resources.drawables import java.io.File +import kotlin.random.Random import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -28,10 +30,10 @@ object ShellBasedRunners { suspend fun newRunner(runner: ShellBasedRunner): Boolean { return withContext(Dispatchers.IO) { - if (runners.find { it.getName() == runner.getName() } == null) { + if (runners.find { it.label == runner.label } == null) { withContext(Dispatchers.Main) { runners.add(runner) } runnerDir() - .child("${runner.getName()}.sh") + .child("${runner.label}.sh") .createFileIfNot() .writeText("echo \"This runner has no implementation. Click the runner and add your own script.\"") saveRunners() @@ -47,10 +49,10 @@ object ShellBasedRunners { localDir().child("runners.json").writeText(json) } - suspend fun deleteRunner(runner: ShellBasedRunner, deleteScript: Boolean = true) { + suspend fun deleteRunner(runner: ShellBasedRunner) { runners.remove(runner) saveRunners() - runnerDir().child("${runner.getName()}.sh").createFileIfNot().delete() + runnerDir().child("${runner.label}.sh").createFileIfNot().delete() } suspend fun indexRunners() { @@ -66,29 +68,32 @@ object ShellBasedRunners { } } -data class ShellBasedRunner(private val name: String, val regex: String) : RunnerImpl() { - override suspend fun run(context: Context, fileObject: FileObject) { - val script = runnerDir().child("${name}.sh").createFileIfNot() +data class ShellBasedRunner(override val label: String, val regex: String) : Runner() { + + override val id = Random.nextInt().toString() + + override fun matcher(fileObject: FileObject): Boolean { + return Regex(regex).matches(fileObject.getName()) + } + + override suspend fun run(activity: Activity, fileObject: FileObject) { + val script = runnerDir().child("${label}.sh").createFileIfNot() launchTerminal( - context, + activity = activity, TerminalCommand( exe = "/bin/bash", args = arrayOf(script.absolutePath, fileObject.getAbsolutePath()), - id = name, + id = label, ), ) } - override fun getName(): String { - return name - } - fun getScript(): File { - return runnerDir().child("${getName()}.sh").createFileIfNot() + return runnerDir().child("$label.sh").createFileIfNot() } override fun getIcon(context: Context): Icon { - return Icon.DrawableRes(drawables.bash) + return Icon.ResourceIcon(drawables.bash) } override suspend fun isRunning(): Boolean { diff --git a/core/main/src/main/java/com/rk/runner/runners/Shell.kt b/core/main/src/main/java/com/rk/runner/runners/Shell.kt deleted file mode 100644 index 373e6f68e..000000000 --- a/core/main/src/main/java/com/rk/runner/runners/Shell.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.rk.runner.runners - -import android.content.Context -import com.rk.exec.TerminalCommand -import com.rk.exec.launchTerminal -import com.rk.file.FileObject -import com.rk.file.FileWrapper -import com.rk.icons.Icon -import com.rk.resources.drawables -import com.rk.resources.strings -import com.rk.runner.RunnerImpl -import com.rk.utils.errorDialog - -class Shell : RunnerImpl() { - override suspend fun run(context: Context, fileObject: FileObject) { - if (fileObject !is FileWrapper) { - errorDialog(msgRes = strings.native_runner) - return - } - - launchTerminal( - context, - terminalCommand = - TerminalCommand( - sandbox = true, - exe = "/bin/bash", - args = arrayOf(fileObject.getAbsolutePath()), - id = "shell-runner", - terminatePreviousSession = true, - workingDir = fileObject.getParentFile()?.getAbsolutePath() ?: "/", - ), - ) - } - - override fun getName(): String { - return "Shell Runner" - } - - override fun getIcon(context: Context): Icon { - return Icon.DrawableRes(drawables.bash) - } - - override suspend fun isRunning(): Boolean { - return false - } - - override suspend fun stop() {} -} diff --git a/core/main/src/main/java/com/rk/runner/runners/UniversalRunner.kt b/core/main/src/main/java/com/rk/runner/runners/UniversalRunner.kt index ecdd9a136..a21432a38 100644 --- a/core/main/src/main/java/com/rk/runner/runners/UniversalRunner.kt +++ b/core/main/src/main/java/com/rk/runner/runners/UniversalRunner.kt @@ -1,6 +1,7 @@ package com.rk.runner.runners import android.annotation.SuppressLint +import android.app.Activity import android.content.Context import android.os.Environment import com.rk.DefaultScope @@ -14,18 +15,30 @@ import com.rk.icons.Icon import com.rk.resources.drawables import com.rk.resources.getString import com.rk.resources.strings -import com.rk.runner.RunnerImpl +import com.rk.runner.Runner import com.rk.terminal.setupAssetFile -import com.rk.utils.dialog +import com.rk.utils.dialogRes import kotlinx.coroutines.launch -class UniversalRunner : RunnerImpl() { +object UniversalRunner : Runner() { + + override val id = "universal" + override val label = strings.universal_runner.getString() + override val description = strings.universal_runner_desc.getString() + + override fun matcher(fileObject: FileObject): Boolean { + return Regex( + ".*\\.(py|js|ts|java|kt|rs|rb|php|c|cpp|cc|cxx|cs|sh|bash|zsh|fish|pl|lua|r|R|hs|f90|f95|f03|f08|pas|tcl|elm|fsx|fs)$" + ) + .matches(fileObject.getName()) + } + @SuppressLint("SdCardPath") - override suspend fun run(context: Context, fileObject: FileObject) { + override suspend fun run(activity: Activity, fileObject: FileObject) { setupAssetFile("universal_runner") if (fileObject !is FileWrapper) { - dialog(title = strings.attention.getString(), msg = strings.non_native_filetype.getString(), onOk = {}) + dialogRes(title = strings.attention.getString(), msg = strings.non_native_filetype.getString(), onOk = {}) return } @@ -35,40 +48,36 @@ class UniversalRunner : RunnerImpl() { path.startsWith("/storage/") || path.startsWith(Environment.getExternalStorageDirectory().absolutePath) ) { - dialog( + dialogRes( title = strings.attention.getString(), msg = strings.sdcard_filetype.getString(), - okString = strings.continue_action, + okRes = strings.continue_action, onCancel = {}, - onOk = { DefaultScope.launch { launchUniversalRunner(context, fileObject) } }, + onOk = { DefaultScope.launch { launchUniversalRunner(activity, fileObject) } }, ) return } - launchUniversalRunner(context, fileObject) + launchUniversalRunner(activity, fileObject) } - suspend fun launchUniversalRunner(context: Context, fileObject: FileObject) { + suspend fun launchUniversalRunner(activity: Activity, fileObject: FileObject) { launchTerminal( - context, + activity = activity, terminalCommand = TerminalCommand( sandbox = true, exe = "/bin/bash", args = arrayOf(localBinDir().child("universal_runner").absolutePath, fileObject.getAbsolutePath()), - id = "universal_runner", + id = strings.universal_runner.getString(), terminatePreviousSession = true, workingDir = fileObject.getParentFile()?.getAbsolutePath() ?: "/", ), ) } - override fun getName(): String { - return strings.universal_runner.getString() - } - override fun getIcon(context: Context): Icon { - return Icon.DrawableRes(drawables.run) + return Icon.ResourceIcon(drawables.run) } override suspend fun isRunning(): Boolean { diff --git a/core/main/src/main/java/com/rk/runner/runners/web/HttpServer.kt b/core/main/src/main/java/com/rk/runner/runners/web/HttpServer.kt index 87b9c34b6..d35056978 100644 --- a/core/main/src/main/java/com/rk/runner/runners/web/HttpServer.kt +++ b/core/main/src/main/java/com/rk/runner/runners/web/HttpServer.kt @@ -22,6 +22,10 @@ class HttpServer( start() } + override fun useGzipWhenAccepted(r: Response?): Boolean { + return false + } + override fun serve(session: IHTTPSession?): Response { return runBlocking { val uri = session!!.uri @@ -57,6 +61,11 @@ class HttpServer( private suspend fun serveFileWithEruda(file: FileObject): Response { return try { + val mime = URLConnection.guessContentTypeFromName(file.getName()) ?: "application/octet-stream" + if (mime != MIME_HTML) { + return serveFileWithoutEruda(file) + } + val darkTheme = isDarkTheme(context) val erudaTheme = if (amoled.value && darkTheme) "AMOLED" else if (darkTheme) "Dark" else "Light" val erudaScript = @@ -70,17 +79,13 @@ class HttpServer( val html = file.getInputStream().bufferedReader().use { it.readText() } val injected = - if (html.contains("", ignoreCase = true)) { - html.replace("", "$erudaScript", ignoreCase = true) + if (html.contains("", ignoreCase = true)) { + html.replace("", "$erudaScript", ignoreCase = true) } else { - html + erudaScript + erudaScript + html } - newFixedLengthResponse( - Status.OK, - URLConnection.guessContentTypeFromName(file.getName()) ?: "text/html", - injected, - ) + newFixedLengthResponse(Status.OK, mime, injected) } catch (_: SecurityException) { forbiddenError(file) } catch (e: Exception) { diff --git a/core/main/src/main/java/com/rk/runner/runners/web/html/HtmlRunner.kt b/core/main/src/main/java/com/rk/runner/runners/web/html/HtmlRunner.kt index 1467de082..af334fa52 100644 --- a/core/main/src/main/java/com/rk/runner/runners/web/html/HtmlRunner.kt +++ b/core/main/src/main/java/com/rk/runner/runners/web/html/HtmlRunner.kt @@ -1,32 +1,45 @@ package com.rk.runner.runners.web.html +import android.app.Activity import android.content.Context import android.content.Intent import androidx.browser.customtabs.CustomTabsIntent import androidx.core.net.toUri +import com.rk.activities.settings.SettingsRoutes +import com.rk.activities.settings.settingsNavController import com.rk.file.BuiltinFileType import com.rk.file.FileObject import com.rk.icons.Icon import com.rk.resources.getFilledString import com.rk.resources.getString import com.rk.resources.strings -import com.rk.runner.RunnerImpl +import com.rk.runner.Runner import com.rk.runner.runners.web.HttpServer import com.rk.settings.Settings import com.rk.utils.toast import java.net.BindException -class HtmlRunner : RunnerImpl() { - companion object { - var httpServer: HttpServer? = null +object HtmlRunner : Runner() { + + var httpServer: HttpServer? = null + + override val id = "html_preview" + override val label = strings.html_preview.getString() + override val description = strings.html_preview_desc.getString() + + override fun matcher(fileObject: FileObject): Boolean { + val htmlExtensions = BuiltinFileType.HTML.extensions.joinToString("|") + return Regex(".*\\.($htmlExtensions|svg)$").matches(fileObject.getName()) } - override suspend fun run(context: Context, fileObject: FileObject) { + override val onConfigure: () -> Unit = { settingsNavController.get()?.navigate(SettingsRoutes.HtmlRunner.route) } + + override suspend fun run(activity: Activity, fileObject: FileObject) { stop() val port = Settings.http_server_port try { - httpServer = HttpServer(context, port, fileObject.getParentFile() ?: fileObject) + httpServer = HttpServer(activity, port, fileObject.getParentFile() ?: fileObject) } catch (_: BindException) { toast(strings.http_server_port_error.getFilledString(port)) return @@ -38,22 +51,18 @@ class HtmlRunner : RunnerImpl() { val url = "$address/${fileObject.getName()}" if (Settings.launch_in_browser) { val intent = Intent(Intent.ACTION_VIEW, url.toUri()) - context.startActivity(intent) + activity.startActivity(intent) return } CustomTabsIntent.Builder() .setShowTitle(true) .setShareState(CustomTabsIntent.SHARE_STATE_OFF) .build() - .launchUrl(context, url.toUri()) - } - - override fun getName(): String { - return strings.html_preview.getString() + .launchUrl(activity, url.toUri()) } - override fun getIcon(context: Context): Icon { - return Icon.DrawableRes(BuiltinFileType.HTML.icon!!) + override fun getIcon(context: Context): Icon? { + return BuiltinFileType.HTML.icon } override suspend fun isRunning(): Boolean = httpServer?.isAlive == true diff --git a/core/main/src/main/java/com/rk/runner/runners/web/markdown/MDViewer.kt b/core/main/src/main/java/com/rk/runner/runners/web/markdown/MDViewer.kt index 6905225b0..d4469b056 100644 --- a/core/main/src/main/java/com/rk/runner/runners/web/markdown/MDViewer.kt +++ b/core/main/src/main/java/com/rk/runner/runners/web/markdown/MDViewer.kt @@ -127,12 +127,17 @@ class MDViewer : WebActivity() { private suspend fun fetchMarkdownFile(url: String): String { return withContext(Dispatchers.IO) { - val connection = URL(url).openConnection() as HttpURLConnection try { - connection.requestMethod = "GET" - connection.inputStream.bufferedReader().use { it.readText() } - } finally { - connection.disconnect() + val connection = URL(url).openConnection() as HttpURLConnection + try { + connection.requestMethod = "GET" + connection.inputStream.bufferedReader().use { it.readText() } + } finally { + connection.disconnect() + } + } catch (e: Exception) { + e.printStackTrace() + "" } } } diff --git a/core/main/src/main/java/com/rk/runner/runners/web/markdown/MarkdownRunner.kt b/core/main/src/main/java/com/rk/runner/runners/web/markdown/MarkdownRunner.kt index 0d8db87cd..f439eb3e5 100644 --- a/core/main/src/main/java/com/rk/runner/runners/web/markdown/MarkdownRunner.kt +++ b/core/main/src/main/java/com/rk/runner/runners/web/markdown/MarkdownRunner.kt @@ -1,5 +1,6 @@ package com.rk.runner.runners.web.markdown +import android.app.Activity import android.content.Context import android.content.Intent import com.rk.file.BuiltinFileType @@ -7,26 +8,32 @@ import com.rk.file.FileObject import com.rk.icons.Icon import com.rk.resources.getString import com.rk.resources.strings -import com.rk.runner.RunnerImpl +import com.rk.runner.Runner import com.rk.runner.runners.web.html.HtmlRunner import java.lang.ref.WeakReference var mdViewerRef = WeakReference(null) var toPreviewFile: FileObject? = null -class MarkdownRunner : RunnerImpl() { - override suspend fun run(context: Context, fileObject: FileObject) { - val intent = Intent(context, MDViewer::class.java) - toPreviewFile = fileObject - context.startActivity(intent) +object MarkdownRunner : Runner() { + + override val id = "markdown_preview" + override val label = strings.markdown_preview.getString() + override val description = strings.markdown_preview_desc.getString() + + override fun matcher(fileObject: FileObject): Boolean { + val markdownExtensions = BuiltinFileType.MARKDOWN.extensions.joinToString("|") + return Regex(".*\\.($markdownExtensions)$").matches(fileObject.getName()) } - override fun getName(): String { - return strings.markdown_preview.getString() + override suspend fun run(activity: Activity, fileObject: FileObject) { + val intent = Intent(activity, MDViewer::class.java) + toPreviewFile = fileObject + activity.startActivity(intent) } - override fun getIcon(context: Context): Icon { - return Icon.DrawableRes(BuiltinFileType.MARKDOWN.icon!!) + override fun getIcon(context: Context): Icon? { + return BuiltinFileType.MARKDOWN.icon } override suspend fun isRunning(): Boolean { diff --git a/core/main/src/main/java/com/rk/search/CodeSearchDialog.kt b/core/main/src/main/java/com/rk/search/CodeSearchDialog.kt index 7dacf4a4d..406ef957b 100644 --- a/core/main/src/main/java/com/rk/search/CodeSearchDialog.kt +++ b/core/main/src/main/java/com/rk/search/CodeSearchDialog.kt @@ -18,6 +18,8 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.input.TextFieldLineLimits +import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.KeyboardArrowDown import androidx.compose.material.icons.outlined.KeyboardArrowUp @@ -66,6 +68,7 @@ import com.rk.resources.drawables import com.rk.resources.fillPlaceholders import com.rk.resources.strings import com.rk.settings.Settings +import com.rk.tabs.editor.EditorTab import com.rk.utils.rememberNumberFormatter import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.launch @@ -82,6 +85,18 @@ fun CodeSearchDialog( val context = LocalContext.current val viewportHeight = with(LocalDensity.current) { LocalWindowInfo.current.containerSize.height.toDp() } + val editorTab = mainViewModel.currentTab as? EditorTab + val textFieldSearchState = + rememberTextFieldState( + editorTab?.editorState?.editor?.get()?.getSelectedText() ?: searchViewModel.codeSearchQuery + ) + LaunchedEffect(textFieldSearchState.text) { searchViewModel.codeSearchQuery = textFieldSearchState.text.toString() } + + val textFieldReplaceState = rememberTextFieldState(searchViewModel.codeReplaceQuery) + LaunchedEffect(textFieldReplaceState.text) { + searchViewModel.codeReplaceQuery = textFieldReplaceState.text.toString() + } + LaunchedEffect( searchViewModel.isIndexing(projectFile), searchViewModel.codeSearchQuery, @@ -97,19 +112,15 @@ fun CodeSearchDialog( fun replace(codeItem: CodeItem) { searchViewModel.viewModelScope.launch { - searchViewModel.replaceIn(mainViewModel, codeItem) + searchViewModel.replaceIn(context, mainViewModel, projectFile, codeItem) + searchViewModel.syncIndex(projectFile) searchViewModel.launchCodeSearch(context, mainViewModel, projectFile) } } fun replaceAll(codeItems: List) { - val itemsBackwards = - codeItems.toList().sortedWith(compareByDescending { it.line }.thenByDescending { it.column }) - searchViewModel.viewModelScope.launch { - for (codeItem in itemsBackwards) { - searchViewModel.replaceIn(mainViewModel, codeItem) - } + searchViewModel.replaceAllIn(context, mainViewModel, projectFile, codeItems) searchViewModel.launchCodeSearch(context, mainViewModel, projectFile) } } @@ -117,9 +128,8 @@ fun CodeSearchDialog( XedDialog(onDismissRequest = onFinish, modifier = Modifier.imePadding()) { Column(modifier = Modifier.animateContentSize().height(viewportHeight * 0.8f)) { TextField( - value = searchViewModel.codeSearchQuery, - onValueChange = { searchViewModel.codeSearchQuery = it }, - maxLines = 1, + state = textFieldSearchState, + lineLimits = TextFieldLineLimits.SingleLine, leadingIcon = { IconButton(modifier = Modifier, onClick = { searchViewModel.toggleReplaceShown() }) { Icon( @@ -190,9 +200,8 @@ fun CodeSearchDialog( if (searchViewModel.isReplaceShown) { TextField( - value = searchViewModel.codeReplaceQuery, - onValueChange = { searchViewModel.codeReplaceQuery = it }, - maxLines = 1, + state = textFieldReplaceState, + lineLimits = TextFieldLineLimits.SingleLine, keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Search), modifier = Modifier.fillMaxWidth().focusRequester(focusRequester), placeholder = { Text(text = stringResource(strings.replace)) }, diff --git a/core/main/src/main/java/com/rk/search/EditorSearch.kt b/core/main/src/main/java/com/rk/search/EditorSearch.kt index 88f3977e8..b06411964 100644 --- a/core/main/src/main/java/com/rk/search/EditorSearch.kt +++ b/core/main/src/main/java/com/rk/search/EditorSearch.kt @@ -47,6 +47,8 @@ import androidx.compose.ui.unit.dp import com.rk.components.StyledTextField import com.rk.resources.strings import com.rk.tabs.editor.CodeEditorState +import com.rk.utils.logError +import com.rk.utils.toast import io.github.rosemoe.sora.event.PublishSearchResultEvent import io.github.rosemoe.sora.event.SelectionChangeEvent import io.github.rosemoe.sora.widget.EditorSearcher @@ -63,9 +65,15 @@ fun EditorSearchPanel(editorState: CodeEditorState, modifier: Modifier = Modifie var searchMatchesCount by remember { mutableIntStateOf(0) } var searchCurrentIndex by remember { mutableIntStateOf(0) } + fun stopEditorSearch() { + hasSearchError = false + isSearchingInternal = false + editor.get()?.searcher?.stopSearch() + } + // Search execution logic fun tryCommitSearch() { - val query = editorState.searchKeyword + val query = editorState.searchKeyword.text if (query.isNotEmpty()) { try { val searchOptions = @@ -78,9 +86,7 @@ fun EditorSearchPanel(editorState: CodeEditorState, modifier: Modifier = Modifie isSearchingInternal = false } } else { - editor.get()?.searcher?.stopSearch() - hasSearchError = false - isSearchingInternal = false + stopEditorSearch() } } @@ -97,6 +103,7 @@ fun EditorSearchPanel(editorState: CodeEditorState, modifier: Modifier = Modifie // Execute search when keyword changes LaunchedEffect( + editorState.editor.get(), editorState.isSearching, editorState.searchKeyword, editorState.ignoreCase, @@ -147,7 +154,6 @@ fun EditorSearchPanel(editorState: CodeEditorState, modifier: Modifier = Modifie .focusRequester(focusRequester), shape = RoundedCornerShape(8.dp), maxLines = 1, - // Show error state with red text color textStyle = LocalTextStyle.current.copy( color = @@ -244,7 +250,7 @@ fun EditorSearchPanel(editorState: CodeEditorState, modifier: Modifier = Modifie IconButton( onClick = { editorState.isSearching = false - editor.get()?.searcher?.stopSearch() + stopEditorSearch() } ) { Icon(imageVector = Icons.Outlined.Close, null) @@ -279,27 +285,59 @@ fun EditorSearchPanel(editorState: CodeEditorState, modifier: Modifier = Modifie horizontalArrangement = Arrangement.Start, modifier = Modifier.weight(1f).horizontalScroll(rememberScrollState()).padding(horizontal = 8.dp), ) { - TextButton(enabled = isSearchingInternal, onClick = { editor.get()?.searcher?.gotoPrevious() }) { + TextButton( + enabled = isSearchingInternal, + onClick = { + runCatching { editor.get()?.searcher?.gotoPrevious() } + .onFailure { + logError(it) + toast(strings.unknown_err) + } + }, + ) { Text(stringResource(strings.go_prev).uppercase()) } Spacer(Modifier.height(10.dp)) - TextButton(enabled = isSearchingInternal, onClick = { editor.get()?.searcher?.gotoNext() }) { + TextButton( + enabled = isSearchingInternal, + onClick = { + runCatching { editor.get()?.searcher?.gotoNext() } + .onFailure { + logError(it) + toast(strings.unknown_err) + } + }, + ) { Text(stringResource(strings.go_next).uppercase()) } if (editorState.isReplaceShown && editorState.editable) { TextButton( enabled = isSearchingInternal, - onClick = { editor.get()?.searcher?.replaceCurrentMatch(editorState.replaceKeyword) }, + onClick = { + runCatching { + editor.get()?.searcher?.replaceCurrentMatch(editorState.replaceKeyword.text) + } + .onFailure { + logError(it) + toast(strings.unknown_err) + } + }, ) { Text(stringResource(strings.replace).uppercase()) } TextButton( enabled = isSearchingInternal, - onClick = { editor.get()?.searcher?.replaceAll(editorState.replaceKeyword) }, + onClick = { + runCatching { editor.get()?.searcher?.replaceAll(editorState.replaceKeyword.text) } + .onFailure { + logError(it) + toast(strings.unknown_err) + } + }, ) { Text(stringResource(strings.replace_all).uppercase()) } @@ -315,7 +353,7 @@ fun EditorSearchPanel(editorState: CodeEditorState, modifier: Modifier = Modifie BackHandler { editorState.isSearching = false - editor.get()?.searcher?.stopSearch() + stopEditorSearch() } } } diff --git a/core/main/src/main/java/com/rk/search/FileSearchDialog.kt b/core/main/src/main/java/com/rk/search/FileSearchDialog.kt index 01dd521be..cdb415dae 100644 --- a/core/main/src/main/java/com/rk/search/FileSearchDialog.kt +++ b/core/main/src/main/java/com/rk/search/FileSearchDialog.kt @@ -14,6 +14,8 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.input.TextFieldLineLimits +import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -37,6 +39,7 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.rk.activities.main.MainViewModel import com.rk.components.XedDialog import com.rk.components.compose.utils.addIf import com.rk.file.FileObject @@ -45,12 +48,14 @@ import com.rk.filetree.FileIcon import com.rk.filetree.getAppropriateName import com.rk.resources.fillPlaceholders import com.rk.resources.strings +import com.rk.tabs.editor.EditorTab import com.rk.utils.getGitColor import com.rk.utils.rememberNumberFormatter import java.io.File @Composable fun FileSearchDialog( + mainViewModel: MainViewModel, searchViewModel: SearchViewModel, projectFile: FileObject, onFinish: () -> Unit, @@ -59,6 +64,14 @@ fun FileSearchDialog( val focusRequester = remember { FocusRequester() } val context = LocalContext.current + val editorTab = mainViewModel.currentTab as? EditorTab + val textFieldState = + rememberTextFieldState( + editorTab?.editorState?.editor?.get()?.getSelectedText() ?: searchViewModel.fileSearchQuery + ) + + LaunchedEffect(textFieldState.text) { searchViewModel.fileSearchQuery = textFieldState.text.toString() } + LaunchedEffect(searchViewModel.isIndexing(projectFile), searchViewModel.fileSearchQuery) { searchViewModel.launchFileSearch(context, projectFile) } @@ -67,9 +80,8 @@ fun FileSearchDialog( XedDialog(onDismissRequest = onFinish, modifier = Modifier.imePadding()) { Column(modifier = Modifier.animateContentSize().height(viewportHeight * 0.8f)) { TextField( - value = searchViewModel.fileSearchQuery, - onValueChange = { searchViewModel.fileSearchQuery = it }, - maxLines = 1, + state = textFieldState, + lineLimits = TextFieldLineLimits.SingleLine, keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Search), modifier = Modifier.fillMaxWidth().focusRequester(focusRequester), placeholder = { Text(text = stringResource(strings.enter_name)) }, diff --git a/core/main/src/main/java/com/rk/search/IndexDatabase.kt b/core/main/src/main/java/com/rk/search/IndexDatabase.kt index 59ce1064e..b7167116c 100644 --- a/core/main/src/main/java/com/rk/search/IndexDatabase.kt +++ b/core/main/src/main/java/com/rk/search/IndexDatabase.kt @@ -68,30 +68,37 @@ abstract class IndexDatabase : RoomDatabase() { lateinit var projectRoot: FileObject companion object { - @Volatile private var INSTANCES = mutableMapOf() + private val INSTANCES = java.util.concurrent.ConcurrentHashMap() fun getDatabase(context: Context, projectRoot: FileObject): IndexDatabase { return INSTANCES[projectRoot] ?: synchronized(this) { - val instance = - Room.databaseBuilder( + INSTANCES[projectRoot] + ?: Room.databaseBuilder( context, IndexDatabase::class.java, "index_database_${projectRoot.hashCode()}", ) - .build() - instance.projectRoot = projectRoot - INSTANCES[projectRoot] = instance - instance + .build().apply { + this.projectRoot = projectRoot + INSTANCES[projectRoot] = this + } } } fun removeDatabase(context: Context, projectRoot: FileObject) { - INSTANCES[projectRoot]?.close() - INSTANCES.remove(projectRoot) + closeInstance(projectRoot) context.deleteDatabase("index_database_${projectRoot.hashCode()}") } + /** Close and remove the in-memory instance for [projectRoot] without deleting the file. */ + fun closeInstance(projectRoot: FileObject) { + synchronized(this) { + INSTANCES[projectRoot]?.close() + INSTANCES.remove(projectRoot) + } + } + suspend fun findDatabasesFor(file: FileObject): List { var startFile: FileObject? = file val databases = mutableListOf() diff --git a/core/main/src/main/java/com/rk/search/SearchViewModel.kt b/core/main/src/main/java/com/rk/search/SearchViewModel.kt index 6fb1be69c..0ef3af98d 100644 --- a/core/main/src/main/java/com/rk/search/SearchViewModel.kt +++ b/core/main/src/main/java/com/rk/search/SearchViewModel.kt @@ -21,257 +21,294 @@ import com.rk.settings.editor.LineEnding import com.rk.tabs.editor.EditorTab import com.rk.utils.hasBinaryChars import com.rk.utils.isBinaryExtension +import com.rk.utils.logDebug +import com.rk.utils.logError +import com.rk.utils.logWarn import com.rk.utils.parseExtensions +import com.rk.utils.toast import java.io.File import java.io.InputStreamReader import java.nio.charset.Charset +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.ensureActive import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.channelFlow -import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -class SearchViewModel : ViewModel() { - private var isIndexing = mutableStateMapOf() - private var indexJob: Job? = null +/** Strategy for searching file names. */ +private interface FileSearchStrategy { + suspend fun search(query: String, projectRoot: FileObject): List +} - // File search dialog - var fileSearchQuery by mutableStateOf("") - var isSearchingFiles by mutableStateOf(false) - var fileSearchResults by mutableStateOf>(emptyList()) - private var fileSearchJob: Job? = null +/** File search using indexed database. Fast, but requires index to be built. */ +private class FileSearchIndexed(private val context: Context) : FileSearchStrategy { + override suspend fun search(query: String, projectRoot: FileObject): List = + withContext(Dispatchers.IO) { + try { + IndexDatabase.getDatabase(context, projectRoot).fileMetaDao().search(query) + } catch (e: Exception) { + logError(e, "Error searching file index") + emptyList() + } + } +} - // Code search dialog - var showFileMaskDialog by mutableStateOf(false) - var fileMaskText by mutableStateOf(Settings.file_mask) - var fileMask = derivedStateOf { parseExtensions(fileMaskText) } - private val excluder by derivedStateOf { GlobExcluder(Settings.excluded_files_search) } +/** File search using filesystem traversal. Slower but doesn't require index. */ +private class FileSearchDirect(private val excluder: GlobExcluder) : FileSearchStrategy { + override suspend fun search(query: String, projectRoot: FileObject): List = + withContext(Dispatchers.IO) { + val results = mutableListOf() - var isSearchingCode by mutableStateOf(false) - var totalCodeSearchResults by mutableIntStateOf(0) - val codeSearchResultsOrder = mutableStateListOf() - val codeSearchResults = mutableStateMapOf>() - private var codeSearchJob: Job? = null + suspend fun searchRecursively(parent: FileObject) { + try { + val childFiles = parent.listFiles() - var codeSearchQuery by mutableStateOf("") - var codeReplaceQuery by mutableStateOf("") - var showOptionsMenu by mutableStateOf(false) - var ignoreCase by mutableStateOf(true) - var isReplaceShown by mutableStateOf(false) - private set + for (file in childFiles) { + currentCoroutineContext().ensureActive() - companion object { - // NOTE: Occurrence that are between the borders of two chunks won't be found, this is a known issue - private const val MAX_CHUNK_SIZE = 1_000_000 // 1 MB limit per column to avoid CursorWindow crash - const val MAX_CODE_RESULTS = 10_000 // Max amount of code search results to show in UI - private const val MAX_FILE_SIZE_SEARCH = 10_000_000 // Max size for code search (10 MB) - private const val CODE_BATCH_SIZE = 5_000 // Insert code mid-traversal in chunks to avoid OOM - } + val path = file.getAbsolutePath() + if (excluder.isExcluded(path)) continue - fun cleanupJobs(projectRoot: FileObject) { - fileSearchJob?.cancel() - fileSearchJob = null + val isHidden = file.getName().startsWith(".") + if (isHidden && !Settings.show_hidden_files_search) continue - codeSearchJob?.cancel() - codeSearchJob = null + if (file.getName().contains(query, ignoreCase = true)) { + results.add( + // Last modified and size do not matter here, as they're only used for indexing + FileMeta(path = path, fileName = file.getName(), lastModified = 0, size = 0) + ) + } - indexJob?.cancel() - indexJob = null - isIndexing.remove(projectRoot) - } + if (file.isDirectory()) { + searchRecursively(file) + } + } + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + logError(e, "Error during file search") + } + } - fun matchesFileMask(fileExt: String): Boolean { - if (fileMask.value.isEmpty()) return true - return fileMask.value.any { it == fileExt } - } + searchRecursively(projectRoot) + results + } +} - fun cancelFileSearch() { - fileSearchJob?.cancel() - fileSearchJob = null - isSearchingFiles = false - } +/** Strategy for searching code content. */ +private interface CodeSearchStrategy { + fun search(query: String): Flow +} - fun launchFileSearch(context: Context, projectRoot: FileObject) { - cancelFileSearch() +/** Code search using indexed database. Returns results as Flow for streaming. */ +private class CodeSearchIndexed( + private val context: Context, + private val projectRoot: FileObject, + private val mainViewModel: MainViewModel, + private val fileMaskFilter: (String) -> Boolean, + private val ignoreCase: Boolean, + private val openPaths: Set, +) : CodeSearchStrategy { + override fun search(query: String): Flow = channelFlow { + withContext(Dispatchers.IO) { + try { + val dao = IndexDatabase.getDatabase(context, projectRoot).codeIndexDao() + var resultLimit = 5 + var offset = 0 - isSearchingFiles = true - fileSearchJob = - viewModelScope.launch { - fileSearchResults = - searchFileName( - context = context, - projectRoot = projectRoot, - query = fileSearchQuery, - useIndex = - Preference.getBoolean( - "enable_indexing_${projectRoot.hashCode()}", - Settings.always_index_projects, - ), - ) - isSearchingFiles = false - } - } + while (true) { + currentCoroutineContext().ensureActive() - /** Cancels any running search */ - fun cancelCodeSearch() { - codeSearchJob?.cancel() - codeSearchJob = null + val results = + if (ignoreCase) { + dao.search(query, resultLimit, offset) + } else { + dao.searchCaseSensitive(query, resultLimit, offset) + } + if (results.isEmpty()) break - totalCodeSearchResults = 0 - codeSearchResults.clear() - codeSearchResultsOrder.clear() - isSearchingCode = false - } + for (result in results) { + try { + if (result.path in openPaths) continue + val file = File(result.path).toFileWrapper() + val fileExt = file.getExtension() - /** Executes a search */ - fun launchCodeSearch(context: Context, mainViewModel: MainViewModel, projectRoot: FileObject) { - cancelCodeSearch() + if (!fileMaskFilter(fileExt)) continue - if (codeSearchQuery.isBlank()) { - totalCodeSearchResults = 0 - codeSearchResults.clear() - return - } + val indices = SearchUtils.findAllIndices(result.content, query, ignoreCase = ignoreCase) + for (index in indices) { + val absoluteCharIndex = result.chunkStart + index - codeSearchJob = - viewModelScope.launch { - isSearchingCode = true - - searchCode( - context = context, - projectRoot = projectRoot, - query = codeSearchQuery, - mainViewModel = mainViewModel, - useIndex = - Preference.getBoolean( - "enable_indexing_${projectRoot.hashCode()}", - Settings.always_index_projects, - ), - ) - .collect { - if (totalCodeSearchResults < MAX_CODE_RESULTS) { - totalCodeSearchResults++ - if (!codeSearchResults.containsKey(it.file)) { - codeSearchResultsOrder.add(it.file) + currentCoroutineContext().ensureActive() + send( + SearchUtils.createCodeItem( + context = context, + mainViewModel = mainViewModel, + text = result.content, + charIndex = absoluteCharIndex, + query = query, + file = file, + projectRoot = projectRoot, + lineIndex = result.lineNumber, + ) + ) } - val fileList = codeSearchResults.getOrPut(it.file) { mutableStateListOf() } - fileList.add(it) - } else { - isSearchingCode = false - codeSearchJob?.cancel() + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + logError(e, "Error processing indexed code result") } } - isSearchingCode = false - } - } - - fun toggleReplaceShown() { - isReplaceShown = !isReplaceShown - } - - suspend fun replaceIn(mainViewModel: MainViewModel, codeItem: CodeItem) { - withContext(Dispatchers.IO) { - val lineIndex = codeItem.line - val startCol = codeItem.column - val diff = codeItem.snippet.highlight.endIndex - codeItem.snippet.highlight.startIndex - val endCol = codeItem.column + diff - - if (codeItem.isOpen) { - val tab = - mainViewModel.tabs.filterIsInstance().find { tab -> tab.file == codeItem.file } - ?: return@withContext - val editor = tab.editorState.editor.get() ?: return@withContext - - withContext(Dispatchers.Main) { - editor.text.replace(lineIndex, startCol, lineIndex, endCol, codeReplaceQuery) + offset += resultLimit + resultLimit = 20 } - } else { - val content = codeItem.file.readText() ?: return@withContext - val lines = content.lines().toMutableList() - - val line = lines.getOrNull(lineIndex) ?: return@withContext - val newLine = line.replaceRange(startCol, endCol, codeReplaceQuery) - lines[lineIndex] = newLine - - val charset = Charset.forName(Settings.encoding) - val lineEnding = LineEnding.detect(content) - - val normalizedContent = lines.joinToString(lineEnding.char) - codeItem.file.writeText(normalizedContent, charset) + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + logError(e, "Error searching code index") } } } +} - fun isIndexing(projectRoot: FileObject): Boolean { - return isIndexing[projectRoot] ?: false - } - - data class IndexingStats(val totalFiles: Int, val databaseSize: Long) - - suspend fun getStats(context: Context, projectRoot: FileObject): IndexingStats { - val totalFiles = getDatabase(context, projectRoot).fileMetaDao().getCount() - val databaseSize = IndexDatabase.getDatabaseSize(context, projectRoot) - return IndexingStats(totalFiles, databaseSize) - } +/** Code search using filesystem traversal. No index required. */ +private class CodeSearchDirect( + private val context: Context, + private val projectRoot: FileObject, + private val mainViewModel: MainViewModel, + private val fileMaskFilter: (String) -> Boolean, + private val excluder: GlobExcluder, + private val ignoreCase: Boolean, + private val openPaths: Set, +) : CodeSearchStrategy { - private fun getDatabase(context: Context, projectRoot: FileObject): IndexDatabase { - return IndexDatabase.getDatabase(context, projectRoot) + companion object { + private const val MAX_CHUNK_SIZE = 1_000_000 // 1 MB limit per column } - suspend fun searchFileName( - context: Context, - projectRoot: FileObject, - query: String, - useIndex: Boolean = true, - ): List { - return if (useIndex) { - searchFileNameWithIndex(context, projectRoot, query) - } else { - searchFileNameWithoutIndex(projectRoot, query) + override fun search(query: String): Flow = channelFlow { + withContext(Dispatchers.IO) { + try { + searchRecursively(parent = projectRoot, isResultHidden = false, query = query, sendFn = { send(it) }) + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + logError(e, "Error during direct code search") + } } } - private suspend fun searchFileNameWithIndex( - context: Context, - projectRoot: FileObject, + private suspend fun searchRecursively( + parent: FileObject, + isResultHidden: Boolean, query: String, - ): List = getDatabase(context, projectRoot).fileMetaDao().search(query) - - private suspend fun searchFileNameWithoutIndex(projectRoot: FileObject, query: String): List { - val results = mutableListOf() - - suspend fun searchRecursively(parent: FileObject, results: MutableList) { + sendFn: suspend (CodeItem) -> Unit, + ) { + try { val childFiles = parent.listFiles() for (file in childFiles) { + currentCoroutineContext().ensureActive() + val path = file.getAbsolutePath() + if (path in openPaths) continue + + val fileExt = file.getExtension() + if (file.isFile() && !fileMaskFilter(fileExt)) continue + if (excluder.isExcluded(path)) continue - val isHidden = file.getName().startsWith(".") + val isHidden = file.getName().startsWith(".") || isResultHidden if (isHidden && !Settings.show_hidden_files_search) continue - if (file.getName().contains(query, ignoreCase = true)) { - results.add( - // Last modified and size do not matter here, as they're only used for indexing - FileMeta(path = path, fileName = file.getName(), lastModified = 0, size = 0) - ) + if (file.isDirectory()) { + searchRecursively(file, isHidden, query, sendFn) + continue } - if (file.isDirectory()) { - searchRecursively(file, results) + if (!SearchUtils.isFileSearchable(file)) continue + val charset = Charset.forName(Settings.encoding) + + file.useInputStream { inputStream -> + inputStream.bufferedReader(charset).useLines { lineSequence -> + lineSequence.forEachIndexed { lineIndex, line -> + val chunks = line.chunked(MAX_CHUNK_SIZE) + chunks.forEachIndexed { chunkIndex, chunk -> + val indices = SearchUtils.findAllIndices(chunk, query, ignoreCase = ignoreCase) + for (index in indices) { + currentCoroutineContext().ensureActive() + val absoluteCharIndex = (chunkIndex * MAX_CHUNK_SIZE) + index + currentCoroutineContext().ensureActive() + sendFn( + SearchUtils.createCodeItem( + context = context, + mainViewModel = mainViewModel, + text = chunk, + charIndex = absoluteCharIndex, + query = query, + file = file, + projectRoot = projectRoot, + lineIndex = lineIndex, + ) + ) + } + } + } + } } } + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + logError(e, "Error during recursive file search") } + } +} + +object SearchUtils { + private const val MAX_FILE_SIZE_SEARCH = 10_000_000 // 10 MB limit + + /** + * Reads the file content, returning null if it's unsuitable for searching (e.g. if it's too large or likely + * binary). + * + * @param file The file to read. + * @return The file content as a [String], or null. + */ + suspend fun isFileSearchable(file: FileObject): Boolean { + // Do not search in file if it's over 10MB + if (file.length() > MAX_FILE_SIZE_SEARCH) return false + + // Do not search in file if it's likely to be binary (file extension based detection) + val ext = file.getExtension() + if (isBinaryExtension(ext)) return false + + val charset = Charset.forName(Settings.encoding) - searchRecursively(projectRoot, results) - return results + // Do not search in file if it's likely to be binary (character based detection) + val isBinary = + withContext(Dispatchers.IO) { + try { + file.useInputStream { stream -> + val buffer = CharArray(1024) + val charsRead = InputStreamReader(stream, charset).read(buffer, 0, buffer.size) + val sample = String(buffer, 0, charsRead) + hasBinaryChars(sample) + } + } catch (e: Exception) { + e.printStackTrace() + true + } + } + return !isBinary } - private fun findAllIndices(text: String, query: String, ignoreCase: Boolean): List { + fun findAllIndices(text: String, query: String, ignoreCase: Boolean): List { val indices = mutableListOf() var currentIndex = 0 @@ -286,464 +323,806 @@ class SearchViewModel : ViewModel() { return indices } - fun searchCode( + suspend fun createCodeItem( context: Context, mainViewModel: MainViewModel, - projectRoot: FileObject, + text: String, + charIndex: Int, query: String, - useIndex: Boolean = true, - ): Flow = - channelFlow { - // Search in opened tabs - val openedEditorTabs = mainViewModel.tabs.mapNotNull { it as? EditorTab } - val openPaths = openedEditorTabs.map { it.file.getAbsolutePath() }.toSet() - - for (tab in openedEditorTabs) { - val fileExt = tab.file.getExtension() - if (!matchesFileMask(fileExt)) continue - - val editor = tab.editorState.editor.get() - val content = editor?.text - if (content != null) { - val lineCount = content.lineCount - for (lineIndex in 0 until lineCount) { - val line = content.getLine(lineIndex).toString() - val indices = findAllIndices(line, query, ignoreCase = ignoreCase) - for (index in indices) { - currentCoroutineContext().ensureActive() - send( - createCodeItem( - context = context, - mainViewModel = mainViewModel, - text = line, - charIndex = index, - query = query, - file = tab.file, - projectRoot = projectRoot, - lineIndex = lineIndex, - isOpen = true, - ) - ) - } - } - } - } - - // Search through other files - if (!useIndex) { - searchCodeWithoutIndex( - context = context, - mainViewModel = mainViewModel, - parent = projectRoot, - projectRoot = projectRoot, - query = query, - openPaths = openPaths, - send = ::send, - ) - } else { - searchCodeWithIndex( - context = context, - mainViewModel = mainViewModel, - projectRoot = projectRoot, - query = query, - openPaths = openPaths, - send = ::send, - ) - } - } - .flowOn(Dispatchers.IO) - - private suspend fun searchCodeWithIndex( - context: Context, - mainViewModel: MainViewModel, + file: FileObject, projectRoot: FileObject, - query: String, - openPaths: Set, - send: suspend (CodeItem) -> Unit, - ) { - var resultLimit = 5 - var offset = 0 - - val dao = getDatabase(context, projectRoot).codeIndexDao() - - while (true) { - val results = - if (ignoreCase) { - dao.search(query, resultLimit, offset) - } else { - dao.searchCaseSensitive(query, resultLimit, offset) - } - if (results.isEmpty()) break - - for (result in results) { - if (result.path in openPaths) continue - - val file = File(result.path).toFileWrapper() - val fileExt = file.getExtension() - if (!matchesFileMask(fileExt)) continue - - val indices = findAllIndices(result.content, query, ignoreCase = ignoreCase) - for (index in indices) { - val absoluteCharIndex = result.chunkStart + index + lineIndex: Int, + isOpen: Boolean = false, + ): CodeItem { + val snippetResult = + SnippetBuilder(context) + .generateSnippet( + text = text, + highlight = Highlight(charIndex, charIndex + query.length), + fileExt = file.getExtension(), + ) - currentCoroutineContext().ensureActive() - send( - createCodeItem( - context = context, - mainViewModel = mainViewModel, - text = result.content, - charIndex = absoluteCharIndex, - query = query, + val codeItem = + CodeItem( + snippet = snippetResult, + file = file, + line = lineIndex, + column = charIndex, + isOpen = isOpen, + onClick = { + mainViewModel.viewModelScope.launch { + mainViewModel.editorManager.jumpToPosition( file = file, projectRoot = projectRoot, - lineIndex = result.lineNumber, + lineStart = lineIndex, + charStart = charIndex, + lineEnd = lineIndex, + charEnd = charIndex + query.length, ) - ) - } - } - offset += resultLimit - resultLimit = 20 - } + } + }, + ) + return codeItem } +} - private suspend fun searchCodeWithoutIndex( - context: Context, - mainViewModel: MainViewModel, +/** Manages indexing for a single project with proper lifecycle management. */ +private class ProjectIndexer( + private val context: Context, + private val projectRoot: FileObject, + private val excluder: GlobExcluder, + private val onIndexingStateChanged: (Boolean) -> Unit, + private val onError: (String) -> Unit, + private val viewModelScope: kotlinx.coroutines.CoroutineScope, +) { + companion object { + private const val CODE_BATCH_SIZE = 5_000 + private const val MAX_CHUNK_SIZE = 1_000_000 + private const val MAX_FILE_SIZE_SEARCH = 10_000_000 + } + + private var indexingJob: Job? = null + + /** + * Starts full indexing of the project. Cancels any previous indexing job first. Index stores ALL files and code (no + * filtering by file_mask or excluder). + */ + suspend fun startIndexing() { + indexingJob?.cancelAndJoin() + + onIndexingStateChanged(true) + + indexingJob = + viewModelScope.launch(Dispatchers.IO) { + try { + val database = + try { + IndexDatabase.getDatabase(context, projectRoot) + } catch (e: Exception) { + logError(e, "Failed to get index database for sync, attempting recovery") + attemptDatabaseRecovery() + IndexDatabase.getDatabase(context, projectRoot) + } + + val codeLineDao = database.codeIndexDao() + val fileMetaDao = database.fileMetaDao() + + val indexedFiles = fileMetaDao.getAll().associateBy { it.path } + val pathsToKeep = mutableSetOf() + val newCodeLines = mutableListOf() + val newFileMetas = mutableListOf() + + indexRecursively(projectRoot, indexedFiles, pathsToKeep, newCodeLines, newFileMetas, codeLineDao) + + finalizeIndex( + database, + indexedFiles, + pathsToKeep, + codeLineDao, + fileMetaDao, + newCodeLines, + newFileMetas, + ) + + logDebug("Indexing completed for $projectRoot") + } catch (e: CancellationException) { + logDebug("Indexing cancelled for $projectRoot") + throw e + } catch (e: Exception) { + logError(e, "Error during indexing") + onError("Indexing failed: ${e.message}") + } finally { + onIndexingStateChanged(false) + } + } + } + + /** Incremental sync of a specific file or directory. Only re-indexes changed files under the given path. */ + suspend fun syncFile(file: FileObject) { + indexingJob?.cancelAndJoin() + + onIndexingStateChanged(true) + + indexingJob = + viewModelScope.launch(Dispatchers.IO) { + try { + val database = + try { + IndexDatabase.getDatabase(context, projectRoot) + } catch (e: Exception) { + logError(e, "Failed to get index database for sync, attempting recovery") + attemptDatabaseRecovery() + IndexDatabase.getDatabase(context, projectRoot) + } + + val codeLineDao = database.codeIndexDao() + val fileMetaDao = database.fileMetaDao() + + val allIndexedFiles = fileMetaDao.getAll().associateBy { it.path } + + // Only consider files under the changed path + val relevantIndexedFiles = + if (file == projectRoot) { + allIndexedFiles + } else { + allIndexedFiles.filter { it.key.startsWith(file.getAbsolutePath()) } + } + + val pathsToKeep = mutableSetOf() + val newCodeLines = mutableListOf() + val newFileMetas = mutableListOf() + + if (file.isDirectory()) { + indexRecursively( + parent = file, + indexedFiles = relevantIndexedFiles, + pathsToKeep = pathsToKeep, + codeLineResults = newCodeLines, + fileMetaResults = newFileMetas, + codeLineDao = codeLineDao, + ) + } else { + indexFile( + file = file, + indexedFiles = relevantIndexedFiles, + pathsToKeep = pathsToKeep, + codeLineResults = newCodeLines, + fileMetaResults = newFileMetas, + codeLineDao = codeLineDao, + ) + } + + finalizeIndex( + database = database, + indexedFiles = relevantIndexedFiles, + pathsToKeep = pathsToKeep, + codeLineDao = codeLineDao, + fileMetaDao = fileMetaDao, + newCodeLines = newCodeLines, + newFileMetas = newFileMetas, + ) + + logDebug("Sync completed for $file") + } catch (e: CancellationException) { + logDebug("Sync cancelled for $file") + throw e + } catch (e: Exception) { + logError(e, "Error during file sync") + onError("Sync failed: ${e.message}") + } finally { + onIndexingStateChanged(false) + } + } + + indexingJob?.join() + } + + /** Cancels any ongoing indexing operation and waits for it to complete. */ + suspend fun cancelIndexing() { + indexingJob?.cancelAndJoin() + indexingJob = null + onIndexingStateChanged(false) + } + + /** Closes the database and cleans up resources. Does NOT delete the database file. */ + fun closeDatabase() { + try { + IndexDatabase.closeInstance(projectRoot) + logDebug("Closed index database for $projectRoot") + } catch (e: Exception) { + logError(e, "Error closing database") + } + } + + private suspend fun attemptDatabaseRecovery() { + return withContext(Dispatchers.IO) { + try { + logWarn("Attempting database recovery by deleting corrupt database") + IndexDatabase.removeDatabase(context, projectRoot) + onError("Index was corrupted and has been rebuilt. Please try your search again.") + } catch (e: Exception) { + logError(e, "Failed to recover database") + } + } + } + + /** Gets current indexing statistics. */ + suspend fun getStats(): SearchViewModel.IndexingStats { + return withContext(Dispatchers.IO) { + try { + val database = IndexDatabase.getDatabase(context, projectRoot) + val totalFiles = database.fileMetaDao().getCount() + val databaseSize = IndexDatabase.getDatabaseSize(context, projectRoot) + SearchViewModel.IndexingStats(totalFiles, databaseSize) + } catch (e: Exception) { + logError(e, "Error getting indexing stats") + SearchViewModel.IndexingStats(0, 0) + } + } + } + + private suspend fun indexRecursively( parent: FileObject, - projectRoot: FileObject, - query: String, - openPaths: Set, - send: suspend (CodeItem) -> Unit, + indexedFiles: Map, + pathsToKeep: MutableSet, + codeLineResults: MutableList, + fileMetaResults: MutableList, + codeLineDao: CodeLineDao, isResultHidden: Boolean = false, ) { - val childFiles = parent.listFiles() + try { + val childFiles = parent.listFiles() + + for (file in childFiles) { + currentCoroutineContext().ensureActive() + indexFile( + file = file, + indexedFiles = indexedFiles, + pathsToKeep = pathsToKeep, + codeLineResults = codeLineResults, + fileMetaResults = fileMetaResults, + codeLineDao = codeLineDao, + isResultHidden = isResultHidden, + ) + } + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + logError(e, "Error during recursive indexing") + } + } + + private suspend fun indexFile( + file: FileObject, + indexedFiles: Map, + pathsToKeep: MutableSet, + codeLineResults: MutableList, + fileMetaResults: MutableList, + codeLineDao: CodeLineDao, + isResultHidden: Boolean = false, + ) { + try { + val isHidden = file.getName().startsWith(".") || isResultHidden + if (isHidden && !Settings.show_hidden_files_search) return - for (file in childFiles) { val path = file.getAbsolutePath() - if (path in openPaths) continue + val lastModified = file.lastModified() - val fileExt = file.getExtension() - if (file.isFile() && !matchesFileMask(fileExt)) continue + if (excluder.isExcluded(path)) return - if (excluder.isExcluded(path)) continue + val indexedFile = indexedFiles[path] + val isFileModified = + indexedFile == null || indexedFile.lastModified != lastModified || indexedFile.size != file.length() - val isHidden = file.getName().startsWith(".") || isResultHidden - if (isHidden && !Settings.show_hidden_files_search) continue + if (!isFileModified) { + pathsToKeep += path + if (!file.isDirectory()) return + } else { + fileMetaResults.add( + FileMeta(path = path, fileName = file.getName(), lastModified = lastModified, size = file.length()) + ) + } if (file.isDirectory()) { - searchCodeWithoutIndex( - context = context, - mainViewModel = mainViewModel, + indexRecursively( parent = file, - projectRoot = projectRoot, - query = query, - openPaths = openPaths, - send = send, - isResultHidden = isResultHidden, + indexedFiles = indexedFiles, + pathsToKeep = pathsToKeep, + codeLineResults = codeLineResults, + fileMetaResults = fileMetaResults, + codeLineDao = codeLineDao, + isResultHidden = isHidden, ) - continue + return } - if (!isFileSearchable(file)) continue - val charset = Charset.forName(Settings.encoding) + if (!SearchUtils.isFileSearchable(file)) return + val charset = Charset.forName(Settings.encoding) file.useInputStream { inputStream -> inputStream.bufferedReader(charset).useLines { lineSequence -> lineSequence.forEachIndexed { lineIndex, line -> + currentCoroutineContext().ensureActive() + val chunks = line.chunked(MAX_CHUNK_SIZE) chunks.forEachIndexed { chunkIndex, chunk -> - val indices = findAllIndices(chunk, query, ignoreCase = ignoreCase) - for (index in indices) { - val absoluteCharIndex = (chunkIndex * MAX_CHUNK_SIZE) + index - currentCoroutineContext().ensureActive() - send( - createCodeItem( - context = context, - mainViewModel = mainViewModel, - text = chunk, - charIndex = absoluteCharIndex, - query = query, - file = file, - projectRoot = projectRoot, - lineIndex = lineIndex, - ) + codeLineResults.add( + CodeLine( + content = chunk, + path = path, + lineNumber = lineIndex, + chunkStart = chunkIndex * MAX_CHUNK_SIZE, ) + ) + + // Flush batch to avoid OOM + if (codeLineResults.size > CODE_BATCH_SIZE) { + codeLineDao.insertAll(codeLineResults) + codeLineResults.clear() } } } } } + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + logError(e, "Error indexing file: ${file.getAbsolutePath()}") } } - private suspend fun createCodeItem( - context: Context, - mainViewModel: MainViewModel, - text: String, - charIndex: Int, - query: String, - file: FileObject, - projectRoot: FileObject, - lineIndex: Int, - isOpen: Boolean = false, - ): CodeItem { - val snippetResult = - SnippetBuilder(context) - .generateSnippet( - text = text, - highlight = Highlight(charIndex, charIndex + query.length), - fileExt = file.getExtension(), - ) + private suspend fun finalizeIndex( + database: IndexDatabase, + indexedFiles: Map, + pathsToKeep: MutableSet, + codeLineDao: CodeLineDao, + fileMetaDao: FileMetaDao, + newCodeLines: MutableList, + newFileMetas: MutableList, + ) { + return withContext(Dispatchers.IO) { + try { + currentCoroutineContext().ensureActive() + + database.withTransaction { + // Delete files that are no longer present or were modified + val deletedPaths = indexedFiles.keys - pathsToKeep + for (path in deletedPaths) { + codeLineDao.deleteByPath(path) + fileMetaDao.deleteByPath(path) + } - val codeItem = - CodeItem( - snippet = snippetResult, - file = file, - line = lineIndex, - column = charIndex, - isOpen = isOpen, - onClick = { - viewModelScope.launch { - mainViewModel.editorManager.jumpToPosition( - file = file, - projectRoot = projectRoot, - lineStart = lineIndex, - charStart = charIndex, - lineEnd = lineIndex, - charEnd = charIndex + query.length, - ) + // Insert new/updated entries + if (newCodeLines.isNotEmpty()) { + codeLineDao.insertAll(newCodeLines) } - }, - ) - return codeItem + if (newFileMetas.isNotEmpty()) { + fileMetaDao.insertAll(newFileMetas) + } + } + } catch (e: Exception) { + logError(e, "Error finalizing index") + throw e + } + } } +} - /** - * Reads the file content, returning null if it's unsuitable for searching (e.g. if it's too large or likely - * binary). - * - * @param file The file to read. - * @return The file content as a [String], or null. - */ - private suspend fun isFileSearchable(file: FileObject): Boolean { - // Do not search in file if it's over 10MB - if (file.length() > MAX_FILE_SIZE_SEARCH) return false +class SearchViewModel : ViewModel() { + private val projectIndexers = mutableMapOf() + private var isIndexing = mutableStateMapOf() - // Do not search in file if it's likely to be binary (file extension based detection) - val ext = file.getExtension() - if (isBinaryExtension(ext)) return false + // File search dialog + var fileSearchQuery by mutableStateOf("") + var isSearchingFiles by mutableStateOf(false) + var fileSearchResults by mutableStateOf>(emptyList()) + private var fileSearchJob: Job? = null - val charset = Charset.forName(Settings.encoding) + // Code search dialog + var showFileMaskDialog by mutableStateOf(false) + var fileMaskText by mutableStateOf(Settings.file_mask) + var fileMask = derivedStateOf { parseExtensions(fileMaskText) } + private val excluder by derivedStateOf { GlobExcluder(Settings.excluded_files_search) } - // Do not search in file if it's likely to be binary (character based detection) - val isBinary = - withContext(Dispatchers.IO) { - try { - file.useInputStream { stream -> - val buffer = CharArray(1024) - val charsRead = InputStreamReader(stream, charset).read(buffer, 0, buffer.size) - val sample = String(buffer, 0, charsRead) - hasBinaryChars(sample) - } - } catch (e: Exception) { - e.printStackTrace() - true - } - } - return !isBinary + var isSearchingCode by mutableStateOf(false) + var totalCodeSearchResults by mutableIntStateOf(0) + val codeSearchResultsOrder = mutableStateListOf() + val codeSearchResults = mutableStateMapOf>() + private var codeSearchJob: Job? = null + + var codeSearchQuery by mutableStateOf("") + var codeReplaceQuery by mutableStateOf("") + var showOptionsMenu by mutableStateOf(false) + var ignoreCase by mutableStateOf(true) + var isReplaceShown by mutableStateOf(false) + private set + + companion object { + // TODO: Occurrence that are between the borders of two chunks won't be found, this is a known issue + const val MAX_CODE_RESULTS = 10_000 // Cap at 10k entries for code search results } - suspend fun index(context: Context, projectRoot: FileObject) { - isIndexing[projectRoot] = true + var isReplacing by mutableStateOf(false) - val database = getDatabase(context, projectRoot) - val codeLineDao = database.codeIndexDao() - val fileMetaDao = database.fileMetaDao() + fun cancelFileSearch() { + fileSearchJob?.cancel() + fileSearchJob = null + isSearchingFiles = false + } - val indexedFiles = fileMetaDao.getAll().associateBy { it.path } - val pathsToKeep = mutableSetOf() + fun matchesFileMask(fileExt: String): Boolean { + if (fileMask.value.isEmpty()) return true + return fileMask.value.any { it == fileExt } + } - val newCodeLines = mutableListOf() - val newFileMetas = mutableListOf() + fun launchFileSearch(context: Context, projectRoot: FileObject) { + cancelFileSearch() - try { - suspend fun flushBatch() = this@SearchViewModel.flushBatch(codeLineDao, newCodeLines) + isSearchingFiles = true + fileSearchJob = + viewModelScope.launch { + try { + val useIndex = + Preference.getBoolean( + "enable_indexing_${projectRoot.hashCode()}", + Settings.always_index_projects, + ) - indexRecursively(projectRoot, indexedFiles, pathsToKeep, newCodeLines, newFileMetas, ::flushBatch) - finalizeIndex(database, indexedFiles, pathsToKeep, codeLineDao, fileMetaDao, newCodeLines, newFileMetas) - } finally { - isIndexing[projectRoot] = false - } + val strategy: FileSearchStrategy = + if (useIndex) { + FileSearchIndexed(context) + } else { + FileSearchDirect(excluder) + } + + fileSearchResults = strategy.search(fileSearchQuery, projectRoot) + } catch (_: CancellationException) { + logDebug("File search cancelled") + } catch (e: Exception) { + logError(e, "Error during file search") + fileSearchResults = emptyList() + } finally { + isSearchingFiles = false + } + } } - fun syncIndex(file: FileObject) { - indexJob = - viewModelScope.launch(Dispatchers.IO) { - val databases = IndexDatabase.findDatabasesFor(file) - for (database in databases) { - isIndexing[database.projectRoot] = true + /** Cancels any running search */ + fun cancelCodeSearch() { + codeSearchJob?.cancel() + codeSearchJob = null - val codeLineDao = database.codeIndexDao() - val fileMetaDao = database.fileMetaDao() + totalCodeSearchResults = 0 + codeSearchResults.clear() + codeSearchResultsOrder.clear() + isSearchingCode = false + } - val indexedFiles = fileMetaDao.getAll().associateBy { it.path } - val filteredIndexedFiles = indexedFiles.filter { it.key.startsWith(file.getAbsolutePath()) } - val pathsToKeep = mutableSetOf() + fun launchCodeSearch(context: Context, mainViewModel: MainViewModel, projectRoot: FileObject) { + cancelCodeSearch() - val newCodeLines = mutableListOf() - val newFileMetas = mutableListOf() + if (codeSearchQuery.isBlank()) { + totalCodeSearchResults = 0 + codeSearchResults.clear() + return + } - try { - suspend fun flushBatch() = this@SearchViewModel.flushBatch(codeLineDao, newCodeLines) + isSearchingCode = true + codeSearchJob = + viewModelScope.launch { + try { + val useIndex = + Preference.getBoolean( + "enable_indexing_${projectRoot.hashCode()}", + Settings.always_index_projects, + ) - if (file == database.projectRoot) { - indexRecursively(file, indexedFiles, pathsToKeep, newCodeLines, newFileMetas, ::flushBatch) + val openedEditorTabs = mainViewModel.tabs.mapNotNull { it as? EditorTab } + val openPaths = openedEditorTabs.map { it.file.getAbsolutePath() }.toSet() + + // Emit results from open editor tabs first + scanOpenTabs(openedEditorTabs, context, mainViewModel, projectRoot) + + // Search in remaining files + val strategy: CodeSearchStrategy = + if (useIndex) { + CodeSearchIndexed( + context = context, + projectRoot = projectRoot, + mainViewModel = mainViewModel, + fileMaskFilter = ::matchesFileMask, + ignoreCase = ignoreCase, + openPaths = openPaths, + ) } else { - indexFile(file, indexedFiles, pathsToKeep, newCodeLines, newFileMetas, ::flushBatch) + CodeSearchDirect( + context = context, + projectRoot = projectRoot, + mainViewModel = mainViewModel, + fileMaskFilter = ::matchesFileMask, + excluder = excluder, + ignoreCase = ignoreCase, + openPaths = openPaths, + ) } - finalizeIndex( - database, - filteredIndexedFiles, - pathsToKeep, - codeLineDao, - fileMetaDao, - newCodeLines, - newFileMetas, - ) - } finally { - isIndexing[database.projectRoot] = false + strategy.search(codeSearchQuery).collect { codeItem -> + if (totalCodeSearchResults < MAX_CODE_RESULTS) { + addCodeResult(codeItem) + totalCodeSearchResults++ + } else { + isSearchingCode = false + codeSearchJob?.cancel() + } } + } catch (_: CancellationException) { + logDebug("Code search cancelled") + } catch (e: Exception) { + logError(e, "Error during code search") + } finally { + isSearchingCode = false } } } - fun deleteIndex(context: Context, projectRoot: FileObject) { - cleanupJobs(projectRoot) - IndexDatabase.removeDatabase(context, projectRoot) + private suspend fun scanOpenTabs( + openedEditorTabs: List, + context: Context, + mainViewModel: MainViewModel, + projectRoot: FileObject, + ) { + for (tab in openedEditorTabs) { + val fileExt = tab.file.getExtension() + if (!matchesFileMask(fileExt)) continue + + val editor = tab.editorState.editor.get() + val content = editor?.text + if (content != null) { + val lineCount = content.lineCount + for (lineIndex in 0 until lineCount) { + + val line = content.getLine(lineIndex).toString() + val indices = SearchUtils.findAllIndices(line, codeSearchQuery, ignoreCase) + for (index in indices) { + currentCoroutineContext().ensureActive() + + val codeItem = + SearchUtils.createCodeItem( + context = context, + mainViewModel = mainViewModel, + text = line, + charIndex = index, + query = codeSearchQuery, + file = tab.file, + projectRoot = projectRoot, + lineIndex = lineIndex, + isOpen = true, + ) + + addCodeResult(codeItem) + totalCodeSearchResults++ + } + } + } + } } - // Called mid-traversal (to reduce memory allocation size of newCodeLines and newFileMetas) - private suspend fun flushBatch(codeLineDao: CodeLineDao, newCodeLines: MutableList) { - if (newCodeLines.size > CODE_BATCH_SIZE) { - codeLineDao.insertAll(newCodeLines) - newCodeLines.clear() + private fun addCodeResult(codeItem: CodeItem) { + if (!codeSearchResults.containsKey(codeItem.file)) { + codeSearchResultsOrder.add(codeItem.file) } + val fileList = codeSearchResults.getOrPut(codeItem.file) { mutableStateListOf() } + fileList.add(codeItem) } - // Only called once at the end of indexing and sync (handles deletions + remaining inserts) - private suspend fun finalizeIndex( - database: IndexDatabase, - indexedFiles: Map, - pathsToKeep: MutableSet, - codeLineDao: CodeLineDao, - fileMetaDao: FileMetaDao, - newCodeLines: MutableList, - newFileMetas: MutableList, - ) { - currentCoroutineContext().ensureActive() + fun toggleReplaceShown() { + isReplaceShown = !isReplaceShown + } + + suspend fun replaceIn(context: Context, mainViewModel: MainViewModel, projectRoot: FileObject, codeItem: CodeItem) { + // Pause searches while replacing + cancelCodeSearch() + isReplacing = true - database.withTransaction { - val deletedPaths = indexedFiles.keys - pathsToKeep - deletedPaths.forEach { path -> - codeLineDao.deleteByPath(path) - fileMetaDao.deleteByPath(path) + try { + withContext(Dispatchers.IO) { + val lineIndex = codeItem.line + val startCol = codeItem.column + val diff = codeItem.snippet.highlight.endIndex - codeItem.snippet.highlight.startIndex + val endCol = codeItem.column + diff + + if (codeItem.isOpen) { + val tab = + mainViewModel.tabs.filterIsInstance().find { tab -> tab.file == codeItem.file } + ?: return@withContext + val editor = tab.editorState.editor.get() ?: return@withContext + + withContext(Dispatchers.Main) { + editor.text.replace(lineIndex, startCol, lineIndex, endCol, codeReplaceQuery) + } + } else { + val content = codeItem.file.readText() ?: return@withContext + val lines = content.lines().toMutableList() + + val line = lines.getOrNull(lineIndex) ?: return@withContext + val newLine = line.replaceRange(startCol, endCol, codeReplaceQuery) + lines[lineIndex] = newLine + + val charset = Charset.forName(Settings.encoding) + val lineEnding = LineEnding.detect(content) + + val normalizedContent = lines.joinToString(lineEnding.char) + codeItem.file.writeText(normalizedContent, charset) + } } - codeLineDao.insertAll(newCodeLines) - fileMetaDao.insertAll(newFileMetas) + // After replacing, update index for this file if indexing enabled for project + try { + val useIndex = + Preference.getBoolean("enable_indexing_${projectRoot.hashCode()}", Settings.always_index_projects) + if (useIndex) { + val indexer = getOrCreateIndexer(context, projectRoot) + indexer.syncFile(codeItem.file) + } + } catch (e: Exception) { + logWarn("Index sync after replace failed: ${e.message}") + } + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + logError(e, "Error replacing text") + } finally { + isReplacing = false } } - private suspend fun indexRecursively( - parent: FileObject, - indexedFiles: Map, - pathsToKeep: MutableSet, - codeLineResults: MutableList, - fileMetaResults: MutableList, - flushBatch: suspend () -> Unit, - isResultHidden: Boolean = false, - ) { - val childFiles = parent.listFiles() - - for (file in childFiles) { - currentCoroutineContext().ensureActive() - indexFile( - file, - indexedFiles, - pathsToKeep, - codeLineResults, - fileMetaResults, - flushBatch = flushBatch, - isResultHidden = isResultHidden, - ) + suspend fun replaceAllIn(context: Context, mainViewModel: MainViewModel, projectRoot: FileObject, codeItems: List) { + if (codeItems.isEmpty()) return + cancelCodeSearch() + isReplacing = true + + try { + val groupedItems = codeItems.groupBy { it.file } + + withContext(Dispatchers.IO) { + for ((file, items) in groupedItems) { + val itemsSorted = items.sortedWith(compareByDescending { it.line }.thenByDescending { it.column }) + val firstItem = itemsSorted.first() + if (firstItem.isOpen) { + val tab = mainViewModel.tabs.filterIsInstance().find { tab -> tab.file == file } + val editor = tab?.editorState?.editor?.get() + if (editor != null) { + withContext(Dispatchers.Main) { + for (codeItem in itemsSorted) { + val lineIndex = codeItem.line + val startCol = codeItem.column + val diff = codeItem.snippet.highlight.endIndex - codeItem.snippet.highlight.startIndex + val endCol = codeItem.column + diff + editor.text.replace(lineIndex, startCol, lineIndex, endCol, codeReplaceQuery) + } + } + } + } else { + val content = file.readText() ?: continue + val lines = content.lines().toMutableList() + + for (codeItem in itemsSorted) { + val lineIndex = codeItem.line + val startCol = codeItem.column + val diff = codeItem.snippet.highlight.endIndex - codeItem.snippet.highlight.startIndex + val endCol = codeItem.column + diff + + val line = lines.getOrNull(lineIndex) ?: continue + val newLine = line.replaceRange(startCol, endCol, codeReplaceQuery) + lines[lineIndex] = newLine + } + + val charset = Charset.forName(Settings.encoding) + val lineEnding = LineEnding.detect(content) + val normalizedContent = lines.joinToString(lineEnding.char) + file.writeText(normalizedContent, charset) + } + + syncIndex(file) + } + } + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + logError(e, "Error replacing all text") + } finally { + isReplacing = false } } - private suspend fun indexFile( - file: FileObject, - indexedFiles: Map, - pathsToKeep: MutableSet, - codeLineResults: MutableList, - fileMetaResults: MutableList, - flushBatch: suspend () -> Unit, - isResultHidden: Boolean = false, - ) { - val isHidden = file.getName().startsWith(".") || isResultHidden - if (isHidden && !Settings.show_hidden_files_search) return - - val path = file.getAbsolutePath() - val lastModified = file.lastModified() - - if (excluder.isExcluded(path)) return - - val indexedFile = indexedFiles[path] - val isFileModified = - indexedFile == null || indexedFile.lastModified != lastModified || indexedFile.size != file.length() - if (!isFileModified) { - pathsToKeep += path - if (!file.isDirectory()) return - } else { - fileMetaResults.add( - FileMeta(path = path, fileName = file.getName(), lastModified = lastModified, size = file.length()) - ) - flushBatch() + fun isIndexing(projectRoot: FileObject): Boolean { + return isIndexing[projectRoot] ?: false + } + + suspend fun index(context: Context, projectRoot: FileObject) { + val indexer = getOrCreateIndexer(context, projectRoot) + indexer.startIndexing() + } + + fun deleteIndex(context: Context, projectRoot: FileObject) { + viewModelScope.launch(Dispatchers.IO) { + try { + val indexer = projectIndexers[projectRoot] + if (indexer != null) { + indexer.cancelIndexing() + projectIndexers.remove(projectRoot) + } + IndexDatabase.removeDatabase(context, projectRoot) + isIndexing.remove(projectRoot) + } catch (e: Exception) { + logError(e, "Error deleting index") + } } + } - if (file.isDirectory()) { - indexRecursively( - parent = file, - indexedFiles = indexedFiles, - pathsToKeep = pathsToKeep, - codeLineResults = codeLineResults, - fileMetaResults = fileMetaResults, - flushBatch = flushBatch, - isResultHidden = isHidden, - ) - return + fun syncIndex(file: FileObject) { + viewModelScope.launch(Dispatchers.IO) { + try { + val databases = IndexDatabase.findDatabasesFor(file) + for (database in databases) { + val indexer = projectIndexers[database.projectRoot] + indexer?.syncFile(file) + } + } catch (e: Exception) { + logError(e, "Error syncing index") + } } + } - if (!isFileSearchable(file)) return - val charset = Charset.forName(Settings.encoding) + data class IndexingStats(val totalFiles: Int, val databaseSize: Long) - file.useInputStream { inputStream -> - inputStream.bufferedReader(charset).useLines { lineSequence -> - lineSequence.forEachIndexed { lineIndex, line -> - val chunks = line.chunked(MAX_CHUNK_SIZE) - chunks.forEachIndexed { chunkIndex, chunk -> - currentCoroutineContext().ensureActive() - codeLineResults.add( - CodeLine( - content = chunk, - path = path, - lineNumber = lineIndex, - chunkStart = chunkIndex * MAX_CHUNK_SIZE, - ) - ) - flushBatch() - } + suspend fun getStats(context: Context, projectRoot: FileObject): IndexingStats { + return withContext(Dispatchers.IO) { + try { + val indexer = getOrCreateIndexer(context, projectRoot) + indexer.getStats() + } catch (e: Exception) { + logError(e, "Error getting stats") + IndexingStats(0, 0) + } + } + } + + override fun onCleared() { + super.onCleared() + + fileSearchJob?.cancel() + fileSearchJob = null + codeSearchJob?.cancel() + codeSearchJob = null + + viewModelScope.launch { + for ((_, indexer) in projectIndexers) { + try { + indexer.cancelIndexing() + indexer.closeDatabase() + } catch (e: Exception) { + logError(e, "Error cleaning up indexer") } } + projectIndexers.clear() + isIndexing.clear() + } + } + + private fun getOrCreateIndexer(context: Context, projectRoot: FileObject): ProjectIndexer { + return projectIndexers.getOrPut(projectRoot) { + ProjectIndexer( + context = context, + projectRoot = projectRoot, + onIndexingStateChanged = { isIndexing -> this.isIndexing[projectRoot] = isIndexing }, + onError = { errorMessage -> + logError("Indexer error: $errorMessage") + toast("Indexer error: $errorMessage") + }, + viewModelScope = viewModelScope, + excluder = excluder, + ) } } } diff --git a/core/main/src/main/java/com/rk/settings/Settings.kt b/core/main/src/main/java/com/rk/settings/Settings.kt index 5df50dc94..13231a7d4 100644 --- a/core/main/src/main/java/com/rk/settings/Settings.kt +++ b/core/main/src/main/java/com/rk/settings/Settings.kt @@ -7,8 +7,8 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.core.content.edit +import com.rk.commands.ToolbarConfiguration import com.rk.filetree.SortMode -import com.rk.settings.editor.DEFAULT_ACTION_ITEMS import com.rk.settings.editor.DEFAULT_EXCLUDED_FILES_DRAWER import com.rk.settings.editor.DEFAULT_EXCLUDED_FILES_SEARCH import com.rk.settings.editor.DEFAULT_EXTRA_KEYS_COMMANDS @@ -19,6 +19,8 @@ import com.rk.utils.application import com.rk.utils.hasHardwareKeyboard import com.rk.xededitor.BuildConfig import com.termux.terminal.TerminalEmulator +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import java.lang.ref.WeakReference import java.nio.charset.Charset import kotlin.properties.ReadWriteProperty @@ -26,8 +28,6 @@ import kotlin.reflect.KClass import kotlin.reflect.KProperty import kotlin.reflect.full.declaredMemberProperties import kotlin.reflect.jvm.isAccessible -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext // NOTE: USE snake_case FOR KEYS! object Settings { @@ -36,6 +36,7 @@ object Settings { var read_only_default by CachedPreference("read_only_default", false) var shown_disclaimer by CachedPreference("shown_disclaimer", false) + var warn_extensions by CachedPreference("warn_extensions", true) var amoled by CachedPreference("amoled", false) var monet by CachedPreference("monet", false) var pin_line_number by CachedPreference("pin_line_number", false) @@ -72,7 +73,7 @@ object Settings { var sandbox by CachedPreference("sandbox", true) var terminal_virus_notice by CachedPreference("terminal_virus_notice", false) var textmate_suggestions by CachedPreference("textmate_suggestions", true) - var seccomp by CachedPreference("seccomp", false) + var seccomp_mode by CachedPreference("seccomp_mode", "unspecified") var desktop_mode by CachedPreference("desktop_mode", false) var theme_flipper by CachedPreference("theme_flipper", false) var format_on_save by CachedPreference("format_on_save", false) @@ -84,9 +85,6 @@ object Settings { var extra_keys_bg by CachedPreference("extra_keys_bg", false) var auto_open_new_files by CachedPreference("auto_open_new_files", true) var complete_on_enter by CachedPreference("complete_on_enter", true) - var enable_html_runner by CachedPreference("enable_html_runner", true) - var enable_md_runner by CachedPreference("enable_md_runner", true) - var enable_universal_runner by CachedPreference("enable_universal_runner", true) var http_server_port by CachedPreference("http_server_port", 8357) var launch_in_browser by CachedPreference("launch_in_browser", false) var inject_eruda by CachedPreference("inject_eruda", true) @@ -104,6 +102,7 @@ object Settings { var show_minimap by CachedPreference("show_minimap", false) var auto_closing_bracket by CachedPreference("auto_closing_bracket", true) var confirm_exit by CachedPreference("confirm_exit", true) + var terminal_clipboard_keybindings by CachedPreference("terminal_clipboard_keybindings", true) // Int settings var tab_size by CachedPreference("tab_size", 4) @@ -124,7 +123,6 @@ object Settings { var sort_mode by CachedPreference("sort_mode", SortMode.SORT_BY_NAME.ordinal) // String settings - var selected_project by CachedPreference("selected_project", "") var font_gson by CachedPreference("selected_font", "") var theme by CachedPreference("theme", blueberry.id) var icon_pack: String by CachedPreference("icon_pack", "") @@ -148,6 +146,7 @@ object Settings { var excluded_files_drawer by CachedPreference("excluded_files_drawer", DEFAULT_EXCLUDED_FILES_DRAWER.joinToString("\n")) var file_mask by CachedPreference("file_mask", "") + var formatters by CachedPreference("formatters", "") // Long settings var last_update_check_timestamp by CachedPreference("last_update", 0L) @@ -157,7 +156,23 @@ object Settings { var line_spacing by CachedPreference("line_spacing", 1f) var last_used_command by CachedPreference("last_used_command", "") - var action_items by CachedPreference("action_items", DEFAULT_ACTION_ITEMS) + var action_items by CachedPreference("action_items", ToolbarConfiguration.DEFAULT_EDITOR_TOOLBAR_COMMANDS) + + init { + if (Preference.getAll().containsKey("seccomp")) { + try { + val legacySeccomp = Preference.getBoolean("seccomp", false) + if (legacySeccomp) { + Preference.setString("seccomp_mode", "yes") + } else { + Preference.setString("seccomp_mode", "unspecified") + } + Preference.removeKey("seccomp") + } catch (e: Exception) { + e.printStackTrace() + } + } + } } object Preference { diff --git a/core/main/src/main/java/com/rk/settings/SettingsScreen.kt b/core/main/src/main/java/com/rk/settings/SettingsScreen.kt index b210ae249..d7968dd44 100644 --- a/core/main/src/main/java/com/rk/settings/SettingsScreen.kt +++ b/core/main/src/main/java/com/rk/settings/SettingsScreen.kt @@ -101,9 +101,9 @@ private fun Categories(navController: NavController) { if (InbuiltFeatures.extensions.state.value) { PreferenceCategory( - label = stringResource(strings.ext), - description = stringResource(strings.ext_desc), - iconResource = drawables.extension, + label = stringResource(strings.store), + description = stringResource(strings.store_desc), + iconResource = drawables.store, onNavigate = { navController.navigate(SettingsRoutes.Extensions.route) }, ) } diff --git a/core/main/src/main/java/com/rk/settings/about/AboutScreen.kt b/core/main/src/main/java/com/rk/settings/about/AboutScreen.kt index cbbecaefc..bfec3ae6d 100644 --- a/core/main/src/main/java/com/rk/settings/about/AboutScreen.kt +++ b/core/main/src/main/java/com/rk/settings/about/AboutScreen.kt @@ -24,7 +24,7 @@ import androidx.compose.ui.unit.dp import androidx.core.content.pm.PackageInfoCompat import androidx.core.net.toUri import coil.compose.AsyncImage -import com.rk.components.SettingsToggle +import com.rk.components.SettingsItem import com.rk.components.compose.preferences.base.PreferenceGroup import com.rk.components.compose.preferences.base.PreferenceLayout import com.rk.components.compose.preferences.base.PreferenceTemplate @@ -35,6 +35,7 @@ import com.rk.utils.copyToClipboard import com.rk.xededitor.BuildConfig import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import com.rk.utils.okHttpClient import okhttp3.OkHttpClient import okhttp3.Request import org.json.JSONObject @@ -54,18 +55,18 @@ fun AboutScreen() { AsyncImage( model = appIcon, contentDescription = null, - modifier = Modifier.size(64.dp).padding(bottom = 8.dp), + modifier = Modifier.size(96.dp).padding(bottom = 8.dp), ) Text( text = stringResource(strings.app_name), - style = MaterialTheme.typography.titleLarge, + style = MaterialTheme.typography.headlineLarge, fontWeight = FontWeight.Bold, ) Text( text = versionName.toString().uppercase(), - style = MaterialTheme.typography.bodyMedium, + style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary, ) } @@ -117,7 +118,7 @@ fun AboutScreen() { val stars = remember { mutableStateOf(strings.loading.getString()) } LaunchedEffect(Unit) { - val client = OkHttpClient() + val client = okHttpClient val url = "https://api.github.com/repos/Xed-Editor/Xed-Editor" val request = Request.Builder().url(url).build() @@ -125,7 +126,7 @@ fun AboutScreen() { try { client.newCall(request).execute().use { response -> if (response.isSuccessful) { - val jsonBody = response.body?.string() ?: throw RuntimeException("Empty response body") + val jsonBody = response.body.string() val json = JSONObject(jsonBody) val count = json.getInt("stargazers_count") @@ -147,7 +148,7 @@ fun AboutScreen() { description = { Text(text = stars.value, style = MaterialTheme.typography.titleSmall) }, ) - SettingsToggle( + SettingsItem( label = stringResource(id = strings.github), description = stringResource(id = strings.github_desc), isEnabled = true, @@ -167,7 +168,7 @@ fun AboutScreen() { }, ) - SettingsToggle( + SettingsItem( label = stringResource(id = strings.telegram), description = stringResource(id = strings.telegram_desc), isEnabled = true, @@ -187,7 +188,7 @@ fun AboutScreen() { }, ) - SettingsToggle( + SettingsItem( label = stringResource(id = strings.discord), description = stringResource(id = strings.telegram_desc), isEnabled = true, diff --git a/core/main/src/main/java/com/rk/settings/app/SettingsAppScreen.kt b/core/main/src/main/java/com/rk/settings/app/SettingsAppScreen.kt index 0b50a06ad..ee6a28d63 100644 --- a/core/main/src/main/java/com/rk/settings/app/SettingsAppScreen.kt +++ b/core/main/src/main/java/com/rk/settings/app/SettingsAppScreen.kt @@ -25,7 +25,7 @@ import com.rk.activities.settings.SettingsActivity import com.rk.activities.settings.SettingsRoutes import com.rk.components.BasicToggle import com.rk.components.NextScreenCard -import com.rk.components.SettingsToggle +import com.rk.components.SettingsItem import com.rk.components.compose.preferences.base.PreferenceGroup import com.rk.components.compose.preferences.base.PreferenceLayout import com.rk.file.toFileObject @@ -38,7 +38,7 @@ import com.rk.settings.editor.refreshEditors import com.rk.theme.amoled import com.rk.theme.currentTheme import com.rk.theme.dynamicTheme -import com.rk.utils.dialog +import com.rk.utils.dialogRes import com.rk.utils.toast import com.rk.xededitor.BuildConfig import kotlinx.coroutines.Dispatchers @@ -77,7 +77,7 @@ fun SettingsAppScreen(activity: SettingsActivity, navController: NavController) val gson = remember { GsonBuilder().setPrettyPrinting().create() } PreferenceGroup { - SettingsToggle( + SettingsItem( label = stringResource(strings.lang), description = stringResource(strings.lang_desc), showSwitch = false, @@ -92,28 +92,28 @@ fun SettingsAppScreen(activity: SettingsActivity, navController: NavController) sideEffect = { navController.navigate(SettingsRoutes.LanguageScreen.route) }, ) - SettingsToggle( + SettingsItem( label = stringResource(strings.check_for_updates), description = stringResource(strings.check_for_updates_desc), default = Settings.check_for_update, sideEffect = { Settings.check_for_update = it }, ) - SettingsToggle( + SettingsItem( label = stringResource(strings.fullscreen), description = stringResource(strings.fullscreen_desc), default = Settings.fullscreen, sideEffect = { Settings.fullscreen = it }, ) - SettingsToggle( + SettingsItem( label = stringResource(strings.smart_toolbar), description = stringResource(strings.smart_toolbar_desc), default = Settings.smart_toolbar, sideEffect = { Settings.smart_toolbar = it }, ) - SettingsToggle( + SettingsItem( label = stringResource(strings.confirm_exit_dialog), description = stringResource(strings.confirm_exit_dialog_desc), default = Settings.confirm_exit, @@ -121,7 +121,7 @@ fun SettingsAppScreen(activity: SettingsActivity, navController: NavController) ) if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) { - SettingsToggle( + SettingsItem( label = stringResource(strings.manage_storage), description = stringResource(strings.manage_storage_desc), showSwitch = false, @@ -156,8 +156,8 @@ fun SettingsAppScreen(activity: SettingsActivity, navController: NavController) checked = InbuiltFeatures.debugMode.state.value, onSwitch = { if (it) { - dialog( - context = activity, + dialogRes( + activity = activity, title = strings.attention.getString(), msg = strings.debug_mode_warn.getString(), onCancel = { InbuiltFeatures.debugMode.setEnable(false) }, @@ -177,7 +177,7 @@ fun SettingsAppScreen(activity: SettingsActivity, navController: NavController) enabled = InbuiltFeatures.debugMode.supported, ) - SettingsToggle( + SettingsItem( label = stringResource(InbuiltFeatures.terminal.nameRes), default = InbuiltFeatures.terminal.state.value, sideEffect = { InbuiltFeatures.terminal.setEnable(it) }, @@ -191,7 +191,7 @@ fun SettingsAppScreen(activity: SettingsActivity, navController: NavController) isEnabled = InbuiltFeatures.terminal.supported, ) - SettingsToggle( + SettingsItem( label = stringResource(InbuiltFeatures.extensions.nameRes), default = InbuiltFeatures.extensions.state.value, sideEffect = { InbuiltFeatures.extensions.setEnable(it) }, @@ -205,7 +205,7 @@ fun SettingsAppScreen(activity: SettingsActivity, navController: NavController) isEnabled = InbuiltFeatures.extensions.supported, ) - SettingsToggle( + SettingsItem( label = stringResource(InbuiltFeatures.git.nameRes), default = InbuiltFeatures.git.state.value, sideEffect = { InbuiltFeatures.git.setEnable(it) }, @@ -221,7 +221,7 @@ fun SettingsAppScreen(activity: SettingsActivity, navController: NavController) } PreferenceGroup(heading = stringResource(strings.backup)) { - SettingsToggle( + SettingsItem( label = stringResource(id = strings.backup), description = stringResource(id = strings.settings_backup_desc), showSwitch = false, @@ -244,7 +244,7 @@ fun SettingsAppScreen(activity: SettingsActivity, navController: NavController) } }, ) - SettingsToggle( + SettingsItem( label = stringResource(id = strings.restore), description = stringResource(id = strings.settings_restore_desc), showSwitch = false, diff --git a/core/main/src/main/java/com/rk/settings/debugOptions/AppLogs.kt b/core/main/src/main/java/com/rk/settings/debugOptions/AppLogs.kt index 48a0cc559..e8938a94c 100644 --- a/core/main/src/main/java/com/rk/settings/debugOptions/AppLogs.kt +++ b/core/main/src/main/java/com/rk/settings/debugOptions/AppLogs.kt @@ -1,11 +1,25 @@ package com.rk.settings.debugOptions +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuAnchorType +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.ExposedDropdownMenuDefaults +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import androidx.core.content.pm.PackageInfoCompat +import com.rk.components.StyledTextField import com.rk.utils.application import com.rk.xededitor.BuildConfig @@ -14,13 +28,13 @@ fun AppLogs() { var logLevel by remember { mutableStateOf(LogLevel.INFO) } val logText = buildLogs(logLevel) - LogScreen(logText, "App Logs Report", "app_logs", logLevel, { logLevel = it }) + LogScreen(logText, "App Logs Report", "app_logs") { LogLevelDropdown(logLevel, { logLevel = it }) } } private fun buildLogs(logLevel: LogLevel): String { val entries = LogCollector.logs - .filter { it.level.ordinal >= logLevel.ordinal } + .filter { it.level.ordinal <= logLevel.ordinal } .joinToString("\n") { "[${it.level.name.uppercase()}] ${it.message}" } val packageInfo = with(application!!) { packageManager.getPackageInfo(packageName, 0) } @@ -28,12 +42,47 @@ private fun buildLogs(logLevel: LogLevel): String { val versionCode = PackageInfoCompat.getLongVersionCode(packageInfo) return buildString { - append("[DEBUG] App version: ").append(versionName).appendLine() - append("[DEBUG] Version code: ").append(versionCode).appendLine() - append("[DEBUG] Commit hash: ").append(BuildConfig.GIT_COMMIT_HASH.substring(0, 8)).appendLine() + append("[REPORT] App version: ").append(versionName).appendLine() + append("[REPORT] Version code: ").append(versionCode).appendLine() + append("[REPORT] Commit hash: ").append(BuildConfig.GIT_COMMIT_HASH.substring(0, 8)).appendLine() appendLine() append(entries) } } + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun RowScope.LogLevelDropdown(logLevel: LogLevel, onLogLevelChange: (LogLevel) -> Unit) { + var dropdownMenuExpanded by remember { mutableStateOf(false) } + + ExposedDropdownMenuBox( + expanded = dropdownMenuExpanded, + onExpandedChange = { dropdownMenuExpanded = !dropdownMenuExpanded }, + modifier = Modifier.weight(1f).padding(horizontal = 8.dp), + ) { + StyledTextField( + value = logLevel.label, + onValueChange = {}, + shape = RoundedCornerShape(8.dp), + maxLines = 1, + readOnly = true, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = dropdownMenuExpanded) }, + modifier = + Modifier.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable).fillMaxWidth().height(42.dp), + ) + + ExposedDropdownMenu(expanded = dropdownMenuExpanded, onDismissRequest = { dropdownMenuExpanded = false }) { + LogLevel.entries.forEach { level -> + DropdownMenuItem( + text = { Text(text = level.label) }, + onClick = { + onLogLevelChange(level) + dropdownMenuExpanded = false + }, + ) + } + } + } +} diff --git a/core/main/src/main/java/com/rk/settings/debugOptions/DeveloperOptions.kt b/core/main/src/main/java/com/rk/settings/debugOptions/DeveloperOptions.kt index 598becc46..a72038434 100644 --- a/core/main/src/main/java/com/rk/settings/debugOptions/DeveloperOptions.kt +++ b/core/main/src/main/java/com/rk/settings/debugOptions/DeveloperOptions.kt @@ -5,20 +5,22 @@ package com.rk.settings.debugOptions import androidx.activity.compose.LocalActivity import androidx.appcompat.app.AppCompatDelegate import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.* import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.navigation.NavController import com.rk.activities.settings.SettingsRoutes -import com.rk.components.SettingsToggle +import com.rk.components.SettingsItem import com.rk.components.compose.preferences.base.PreferenceGroup import com.rk.components.compose.preferences.base.PreferenceLayout +import com.rk.file.child import com.rk.resources.getString import com.rk.resources.strings import com.rk.settings.Settings -import com.rk.utils.dialog +import com.rk.utils.application +import com.rk.utils.dialogRes import com.rk.utils.toast import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers @@ -28,6 +30,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import java.io.File private var flipperJob: Job? = null @@ -52,14 +55,14 @@ fun DeveloperOptions(modifier: Modifier = Modifier, navController: NavController PreferenceLayout(label = stringResource(strings.debug_options)) { PreferenceGroup { - SettingsToggle( + SettingsItem( label = stringResource(strings.force_crash), description = stringResource(strings.force_crash_desc), showSwitch = false, default = false, sideEffect = { - dialog( - context = activity, + dialogRes( + activity = activity, title = strings.force_crash.getString(), msg = strings.force_crash_confirm.getString(), onCancel = {}, @@ -68,14 +71,14 @@ fun DeveloperOptions(modifier: Modifier = Modifier, navController: NavController }, ) - SettingsToggle( + SettingsItem( label = stringResource(strings.memory_usage), description = memoryUsage.value, showSwitch = false, default = false, ) - SettingsToggle( + SettingsItem( label = stringResource(strings.strict_mode), description = stringResource(strings.strict_mode_desc), showSwitch = true, @@ -83,14 +86,14 @@ fun DeveloperOptions(modifier: Modifier = Modifier, navController: NavController sideEffect = { Settings.strict_mode = it }, ) - SettingsToggle( + SettingsItem( label = stringResource(strings.anr_watchdog), description = stringResource(strings.anr_watchdog_desc), default = Settings.anr_watchdog, sideEffect = { Settings.anr_watchdog = it }, ) - SettingsToggle( + SettingsItem( label = stringResource(strings.verbose_errors), description = stringResource(strings.verbose_errors_desc), showSwitch = true, @@ -98,7 +101,7 @@ fun DeveloperOptions(modifier: Modifier = Modifier, navController: NavController sideEffect = { Settings.verbose_error = it }, ) - SettingsToggle( + SettingsItem( label = stringResource(strings.desktop_mode), description = stringResource(strings.desktop_mode_desc), showSwitch = true, @@ -106,7 +109,7 @@ fun DeveloperOptions(modifier: Modifier = Modifier, navController: NavController sideEffect = { Settings.desktop_mode = it }, ) - SettingsToggle( + SettingsItem( label = stringResource(strings.theme_flipper), description = stringResource(strings.theme_flipper_desc), showSwitch = true, @@ -119,7 +122,7 @@ fun DeveloperOptions(modifier: Modifier = Modifier, navController: NavController }, ) - SettingsToggle( + SettingsItem( label = stringResource(strings.reset_consent), description = stringResource(strings.reset_consent_desc), showSwitch = false, @@ -130,7 +133,7 @@ fun DeveloperOptions(modifier: Modifier = Modifier, navController: NavController }, ) - SettingsToggle( + SettingsItem( label = stringResource(strings.view_logs), description = stringResource(strings.view_app_logs), default = false, diff --git a/core/main/src/main/java/com/rk/settings/debugOptions/LogCollector.kt b/core/main/src/main/java/com/rk/settings/debugOptions/LogCollector.kt index 325940c11..4553fad91 100644 --- a/core/main/src/main/java/com/rk/settings/debugOptions/LogCollector.kt +++ b/core/main/src/main/java/com/rk/settings/debugOptions/LogCollector.kt @@ -4,11 +4,11 @@ import androidx.compose.runtime.mutableStateListOf import com.rk.resources.getString import com.rk.resources.strings -enum class LogLevel(val label: String) { - DEBUG(strings.debug.getString()), - INFO(strings.info.getString()), - WARN(strings.warning.getString()), - ERROR(strings.error.getString()), +enum class LogLevel(val label: String, val value: Int) { + ERROR(strings.error.getString(), 1), + WARN(strings.warning.getString(), 2), + INFO(strings.info.getString(), 3), + DEBUG(strings.debug.getString(), 5), } data class LogEntry(val level: LogLevel, val message: String, val timestamp: Long = System.currentTimeMillis()) diff --git a/core/main/src/main/java/com/rk/settings/debugOptions/LogScreen.kt b/core/main/src/main/java/com/rk/settings/debugOptions/LogScreen.kt index 144556a12..6311091b6 100644 --- a/core/main/src/main/java/com/rk/settings/debugOptions/LogScreen.kt +++ b/core/main/src/main/java/com/rk/settings/debugOptions/LogScreen.kt @@ -5,19 +5,14 @@ import androidx.activity.compose.LocalOnBackPressedDispatcherOwner import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.selection.LocalTextSelectionColors import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ExposedDropdownMenuBox -import androidx.compose.material3.ExposedDropdownMenuDefaults import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -28,11 +23,8 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource @@ -41,7 +33,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.core.net.toUri import com.rk.activities.settings.SettingsActivity -import com.rk.components.StyledTextField import com.rk.crashhandler.CrashHandler.logErrorOrExit import com.rk.editor.Editor import com.rk.file.BuiltinFileType @@ -52,7 +43,7 @@ import com.rk.search.EditorSearchPanel import com.rk.tabs.editor.CodeEditorState import com.rk.theme.XedTheme import com.rk.utils.copyToClipboard -import com.rk.utils.dialog +import com.rk.utils.dialogRes import java.lang.ref.WeakReference import java.net.URLEncoder import java.nio.charset.StandardCharsets @@ -60,19 +51,11 @@ import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable -fun LogScreen( - logText: String, - issueTitle: String, - copyLabel: String, - logLevel: LogLevel, - onLogLevelChange: (LogLevel) -> Unit, - actionButtons: @Composable (() -> Unit)? = null, -) { +fun LogScreen(logText: String, issueTitle: String, copyLabel: String, toolbarButtons: @Composable RowScope.() -> Unit) { val scope = rememberCoroutineScope() val backDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher val editorState = remember { CodeEditorState() } - var dropdownMenuExpanded by remember { mutableStateOf(false) } XedTheme { Scaffold( @@ -118,40 +101,7 @@ fun LogScreen( ) } - ExposedDropdownMenuBox( - expanded = dropdownMenuExpanded, - onExpandedChange = { dropdownMenuExpanded = !dropdownMenuExpanded }, - modifier = Modifier.weight(1f).padding(horizontal = 8.dp), - ) { - StyledTextField( - value = logLevel.label, - onValueChange = {}, - shape = RoundedCornerShape(8.dp), - maxLines = 1, - readOnly = true, - trailingIcon = { - ExposedDropdownMenuDefaults.TrailingIcon(expanded = dropdownMenuExpanded) - }, - modifier = Modifier.menuAnchor().fillMaxWidth().height(42.dp), - ) - - ExposedDropdownMenu( - expanded = dropdownMenuExpanded, - onDismissRequest = { dropdownMenuExpanded = false }, - ) { - LogLevel.entries.forEach { level -> - DropdownMenuItem( - text = { Text(text = level.label) }, - onClick = { - onLogLevelChange(level) - dropdownMenuExpanded = false - }, - ) - } - } - } - - actionButtons?.invoke() + toolbarButtons() } HorizontalDivider() @@ -182,7 +132,7 @@ fun LogScreen( colorScheme = colorScheme, ) - scope.launch { setLanguage(BuiltinFileType.LOG.textmateScope!!) } + scope.launch { configureLanguage(BuiltinFileType.LOG.textmateScope!!) } } }, update = { editor -> @@ -210,11 +160,11 @@ private fun reportLogs(logText: String, issueTitle: String, copyLabel: String) { if (url.length > 2048) { val trimmedUrl = urlStart + URLEncoder.encode("```log \nPaste the logs here\n ```", StandardCharsets.UTF_8.toString()) - dialog( - context = context, + dialogRes( + activity = context, title = strings.logs_too_long.getString(), msg = strings.logs_too_long_desc.getString(), - okString = strings.continue_action, + okRes = strings.continue_action, onOk = { copyToClipboard(copyLabel, logText, true) val browserIntent = Intent(Intent.ACTION_VIEW, trimmedUrl.toUri()) diff --git a/core/main/src/main/java/com/rk/settings/editor/CommandSettings.kt b/core/main/src/main/java/com/rk/settings/editor/CommandSettings.kt index 3a4ff5ec7..de85d557c 100644 --- a/core/main/src/main/java/com/rk/settings/editor/CommandSettings.kt +++ b/core/main/src/main/java/com/rk/settings/editor/CommandSettings.kt @@ -42,14 +42,14 @@ fun CommandSelectionDialog( CommandPalette(progress = 1f, commands = dialogCommands, lastUsedCommand = null) { onDismiss() } } -fun buildAddActions( +private fun buildAddActions( command: Command, commandIds: SnapshotStateList, existingCommands: List, saveOrder: (SnapshotStateList) -> Unit, ): List = buildList { add( - object : Command(command.commandContext) { + object : Command() { override val id: String = command.id override fun getLabel(): String = strings.add_parent_command.getString() @@ -63,7 +63,7 @@ fun buildAddActions( override fun isEnabled(): Boolean = !commandIds.contains(command.id) - override fun getIcon(): Icon = Icon.DrawableRes(drawables.arrow_outward) + override fun getIcon(): Icon = Icon.ResourceIcon(drawables.arrow_outward) } ) addAll( diff --git a/core/main/src/main/java/com/rk/settings/editor/EditExtraKeys.kt b/core/main/src/main/java/com/rk/settings/editor/EditExtraKeys.kt index 8bef40313..5c781c278 100644 --- a/core/main/src/main/java/com/rk/settings/editor/EditExtraKeys.kt +++ b/core/main/src/main/java/com/rk/settings/editor/EditExtraKeys.kt @@ -33,7 +33,7 @@ import com.mohamedrejeb.compose.dnd.reorder.ReorderContainer import com.mohamedrejeb.compose.dnd.reorder.ReorderableItem import com.mohamedrejeb.compose.dnd.reorder.rememberReorderState import com.rk.commands.CommandProvider -import com.rk.components.EditorSettingsToggle +import com.rk.components.EditorSettingsItem import com.rk.components.InfoBlock import com.rk.components.ResetButton import com.rk.components.SingleInputDialog @@ -156,7 +156,7 @@ fun EditExtraKeys(modifier: Modifier = Modifier) { item { PreferenceGroup(heading = stringResource(strings.symbols), modifier = Modifier.animateItem()) { - EditorSettingsToggle( + EditorSettingsItem( label = stringResource(id = strings.change_extra_symbols), description = stringResource(id = strings.change_extra_symbols_desc), showSwitch = false, diff --git a/core/main/src/main/java/com/rk/settings/editor/EditToolbarActions.kt b/core/main/src/main/java/com/rk/settings/editor/EditToolbarActions.kt index be4c6f62f..2b7c37cc7 100644 --- a/core/main/src/main/java/com/rk/settings/editor/EditToolbarActions.kt +++ b/core/main/src/main/java/com/rk/settings/editor/EditToolbarActions.kt @@ -33,6 +33,7 @@ import com.mohamedrejeb.compose.dnd.reorder.ReorderContainer import com.mohamedrejeb.compose.dnd.reorder.ReorderableItem import com.mohamedrejeb.compose.dnd.reorder.rememberReorderState import com.rk.commands.CommandProvider +import com.rk.commands.ToolbarConfiguration import com.rk.components.InfoBlock import com.rk.components.ResetButton import com.rk.components.compose.preferences.base.LocalIsExpandedScreen @@ -44,9 +45,6 @@ import com.rk.settings.Settings import com.rk.utils.handleLazyListScroll import kotlinx.coroutines.launch -const val DEFAULT_ACTION_ITEMS = - "editor.undo|editor.redo|editor.save|editor.run|global.new_file|editor.editable|editor.search|editor.refresh|global.terminal|global.settings" - @Composable fun EditToolbarActions(modifier: Modifier = Modifier) { var showCommandSelectionDialog by remember { mutableStateOf(false) } @@ -145,5 +143,5 @@ private fun saveOrder(commandIds: SnapshotStateList) { private fun resetOrder(commandIds: SnapshotStateList) { Preference.removeKey("action_items") commandIds.clear() - commandIds.addAll(DEFAULT_ACTION_ITEMS.split("|")) + commandIds.addAll(ToolbarConfiguration.DEFAULT_EDITOR_TOOLBAR_COMMANDS.split("|")) } diff --git a/core/main/src/main/java/com/rk/settings/editor/ExcludeFiles.kt b/core/main/src/main/java/com/rk/settings/editor/ExcludeFiles.kt index 7728b9c19..3dad4893d 100644 --- a/core/main/src/main/java/com/rk/settings/editor/ExcludeFiles.kt +++ b/core/main/src/main/java/com/rk/settings/editor/ExcludeFiles.kt @@ -174,7 +174,7 @@ fun ExcludeFiles(isDrawer: Boolean) { colorScheme = colorScheme, ) - scope.launch { setLanguage(BuiltinFileType.IGNORE.textmateScope!!) } + scope.launch { configureLanguage(BuiltinFileType.IGNORE.textmateScope!!) } } }, ) diff --git a/core/main/src/main/java/com/rk/settings/editor/FontScreen.kt b/core/main/src/main/java/com/rk/settings/editor/FontScreen.kt index 8c49c2472..ba2df2303 100644 --- a/core/main/src/main/java/com/rk/settings/editor/FontScreen.kt +++ b/core/main/src/main/java/com/rk/settings/editor/FontScreen.kt @@ -181,7 +181,7 @@ fun FontScreen( } .onFailure { if (it.message?.isNotBlank() == true) { - errorDialog(it) + errorDialog(throwable = it) } } }, diff --git a/core/main/src/main/java/com/rk/settings/editor/FormatterSettings.kt b/core/main/src/main/java/com/rk/settings/editor/FormatterSettings.kt new file mode 100644 index 000000000..a240d0137 --- /dev/null +++ b/core/main/src/main/java/com/rk/settings/editor/FormatterSettings.kt @@ -0,0 +1,308 @@ +package com.rk.settings.editor + +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Info +import androidx.compose.material3.ExtendedFloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavController +import com.mohamedrejeb.compose.dnd.reorder.ReorderContainer +import com.mohamedrejeb.compose.dnd.reorder.ReorderableItem +import com.mohamedrejeb.compose.dnd.reorder.rememberReorderState +import com.rk.activities.settings.SettingsRoutes +import com.rk.components.InfoBlock +import com.rk.components.compose.preferences.base.LocalIsExpandedScreen +import com.rk.components.compose.preferences.base.NestedScrollStretch +import com.rk.components.compose.preferences.base.PreferenceScaffold +import com.rk.components.compose.preferences.base.PreferenceTemplate +import com.rk.editor.FormatterProvider +import com.rk.editor.FormatterSource +import com.rk.editor.Formatters +import com.rk.icons.Icon +import com.rk.icons.XedIcon +import com.rk.resources.drawables +import com.rk.resources.strings +import com.rk.settings.Settings +import com.rk.utils.handleLazyListScroll +import kotlinx.coroutines.launch + +@Composable +fun FormatterSettings(navController: NavController, modifier: Modifier = Modifier) { + val scope = rememberCoroutineScope() + val reorderState = rememberReorderState(dragAfterLongPress = true) + val lazyListState = rememberLazyListState() + + val formatterIds = remember { mutableStateListOf(*Settings.formatters.split("|").toTypedArray()) } + val formatterSources by remember { + derivedStateOf { + formatterIds + .mapNotNull { id -> Formatters.getSourceForId(id) } + .toSet() + .let { it + Formatters.providers.map { provider -> FormatterSource.EXTENSION(provider) } } + .let { it + FormatterSource.LSP } + } + } + + PreferenceScaffold( + label = stringResource(strings.manage_formatters), + backArrowVisible = true, + isExpandedScreen = LocalIsExpandedScreen.current, + fab = { + ExtendedFloatingActionButton( + onClick = { navController.navigate(SettingsRoutes.Extensions.route) }, + icon = { Icon(painter = painterResource(drawables.extension), contentDescription = null) }, + text = { Text(stringResource(strings.browse_extensions)) }, + ) + }, + ) { paddingValues -> + ReorderContainer(state = reorderState, modifier = modifier) { + NestedScrollStretch { + LazyColumn( + contentPadding = + PaddingValues( + top = paddingValues.calculateTopPadding(), + bottom = + paddingValues.calculateBottomPadding() + + 88.dp, // Add extra space so that FAB doesn't cover content + start = paddingValues.calculateStartPadding(LayoutDirection.Ltr), + end = paddingValues.calculateEndPadding(LayoutDirection.Ltr), + ), + state = lazyListState, + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxHeight(), + ) { + item { + InfoBlock( + icon = { Icon(imageVector = Icons.Outlined.Info, contentDescription = null) }, + text = stringResource(strings.info_formatters), + modifier = Modifier.padding(bottom = 8.dp), + ) + } + + items(items = formatterSources.toList(), key = { Formatters.getIdOf(it) }) { formatterSource -> + when (formatterSource) { + is FormatterSource.LSP -> { + ReorderableItem( + state = reorderState, + key = Formatters.LSP_FORMATTER_ID, + data = Formatters.LSP_FORMATTER_ID, + onDragEnter = { state -> + val index = formatterIds.indexOf(Formatters.LSP_FORMATTER_ID) + if (index == -1) return@ReorderableItem + + val oldIndex = formatterIds.indexOf(state.data) + + formatterIds.removeAt(oldIndex) + formatterIds.add(index, state.data) + saveOrder(formatterIds) + + scope.launch { + handleLazyListScroll(lazyListState = lazyListState, dropIndex = index) + } + }, + modifier = Modifier.animateItem(), + ) { + DraggableLspFormatter( + modifier = + Modifier.padding(horizontal = 16.dp).graphicsLayer { + alpha = if (isDragging) 0f else 1f + } + ) + } + } + is FormatterSource.EXTENSION -> { + val formatter = formatterSource.formatter + ReorderableItem( + state = reorderState, + key = formatter.id, + data = formatter.id, + onDragEnter = { state -> + val index = formatterIds.indexOf(formatter.id) + if (index == -1) return@ReorderableItem + + val oldIndex = formatterIds.indexOf(state.data) + + formatterIds.removeAt(oldIndex) + formatterIds.add(index, state.data) + saveOrder(formatterIds) + + scope.launch { + handleLazyListScroll(lazyListState = lazyListState, dropIndex = index) + } + }, + modifier = Modifier.animateItem(), + ) { + DraggableFormatter( + modifier = + Modifier.padding(horizontal = 16.dp).graphicsLayer { + alpha = if (isDragging) 0f else 1f + }, + formatter = formatter, + ) + } + } + } + } + } + } + } + } +} + +@Composable +fun DraggableFormatter(modifier: Modifier = Modifier, formatter: FormatterProvider) { + val interactionSource = remember { MutableInteractionSource() } + var checked by remember { mutableStateOf(Formatters.isProviderEnabled(formatter)) } + val onCheckedChange: (Boolean) -> Unit = { + Formatters.setProviderEnabled(formatter, it) + checked = it + } + + Surface(shape = MaterialTheme.shapes.large, tonalElevation = 1.dp, modifier = modifier) { + PreferenceTemplate( + modifier = + Modifier.combinedClickable( + interactionSource = interactionSource, + onClick = { onCheckedChange(!checked) }, + ), + verticalPadding = 8.dp, + title = { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + imageVector = ImageVector.vectorResource(drawables.drag_indicator), + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f), + modifier = Modifier.padding(end = 12.dp).size(20.dp), + ) + + XedIcon( + icon = formatter.icon ?: Icon.ResourceIcon(drawables.auto_fix), + modifier = Modifier.padding(end = 8.dp).size(20.dp), + contentDescription = formatter.label, + ) + + Column { + Row { Text(text = formatter.label, style = MaterialTheme.typography.bodyLarge) } + Text( + text = formatter.supportedExtensions.joinToString(", ") { ".$it" }, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), + ) + } + } + }, + endWidget = { + Switch(interactionSource = interactionSource, checked = checked, onCheckedChange = onCheckedChange) + }, + ) + } +} + +@Composable +fun DraggableLspFormatter(modifier: Modifier = Modifier) { + val interactionSource = remember { MutableInteractionSource() } + var checked by remember { mutableStateOf(Formatters.isLspFormatterEnabled()) } + val onCheckedChange: (Boolean) -> Unit = { + Formatters.setLspFormatterEnabled(it) + checked = it + } + + val enabled = false // -> set to InbuiltFeatures.terminal.state.value + + Surface(shape = MaterialTheme.shapes.large, tonalElevation = 1.dp, modifier = modifier) { + PreferenceTemplate( + modifier = + Modifier.combinedClickable( + enabled = enabled, + interactionSource = interactionSource, + onClick = { onCheckedChange(!checked) }, + ), + verticalPadding = 8.dp, + title = { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + imageVector = ImageVector.vectorResource(drawables.drag_indicator), + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f), + modifier = Modifier.padding(end = 12.dp).size(20.dp), + ) + + XedIcon( + icon = Icon.ResourceIcon(drawables.server), + modifier = Modifier.padding(end = 8.dp).size(20.dp), + contentDescription = stringResource(strings.language_server), + ) + + Column { + Row { + Text( + text = stringResource(strings.language_server), + style = MaterialTheme.typography.bodyLarge, + ) + } + Text( + text = stringResource(strings.formatter_lsp_desc), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), + ) + } + } + }, + endWidget = { + Switch( + enabled = enabled, + interactionSource = interactionSource, + checked = checked, + onCheckedChange = onCheckedChange, + ) + }, + ) + } +} + +/** Save order of formatters in settings */ +private fun saveOrder(formatterIds: SnapshotStateList) { + val formatters = formatterIds.joinToString("|") + Settings.formatters = formatters +} diff --git a/core/main/src/main/java/com/rk/settings/editor/SettingsEditorScreen.kt b/core/main/src/main/java/com/rk/settings/editor/SettingsEditorScreen.kt index c1870f6d1..1cdc6eb9f 100644 --- a/core/main/src/main/java/com/rk/settings/editor/SettingsEditorScreen.kt +++ b/core/main/src/main/java/com/rk/settings/editor/SettingsEditorScreen.kt @@ -24,9 +24,9 @@ import com.rk.activities.main.MainActivity import com.rk.activities.main.fileTreeViewModel import com.rk.activities.settings.SettingsRoutes import com.rk.activities.settings.settingsNavController -import com.rk.components.EditorSettingsToggle +import com.rk.components.EditorSettingsItem import com.rk.components.NextScreenCard -import com.rk.components.SettingsToggle +import com.rk.components.SettingsItem import com.rk.components.SingleInputDialog import com.rk.components.ValueSlider import com.rk.components.compose.preferences.base.PreferenceGroup @@ -38,7 +38,6 @@ import com.rk.resources.strings import com.rk.settings.Settings import com.rk.settings.app.InbuiltFeatures import com.rk.tabs.editor.EditorTab -import io.github.rosemoe.sora.langs.textmate.TextMateLanguage import kotlinx.coroutines.launch @Composable @@ -67,21 +66,14 @@ fun SettingsEditorScreen(navController: NavController) { route = SettingsRoutes.LspSettings, ) - EditorSettingsToggle( - label = stringResource(strings.format_on_save), - description = stringResource(strings.format_on_save_desc), - default = Settings.format_on_save, - sideEffect = { Settings.format_on_save = it }, - ) - - EditorSettingsToggle( + EditorSettingsItem( label = stringResource(strings.insert_final_newline), description = stringResource(strings.insert_final_newline_desc), default = Settings.insert_final_newline, sideEffect = { Settings.insert_final_newline = it }, ) - EditorSettingsToggle( + EditorSettingsItem( label = stringResource(strings.trim_trailing_whitespace), description = stringResource(strings.trim_trailing_whitespace_desc), default = Settings.trim_trailing_whitespace, @@ -90,8 +82,23 @@ fun SettingsEditorScreen(navController: NavController) { } } + PreferenceGroup(heading = stringResource(strings.formatting)) { + NextScreenCard( + label = stringResource(strings.manage_formatters), + description = stringResource(strings.manage_formatters_desc), + onClick = { navController.navigate(SettingsRoutes.Formatters.route) }, + ) + + EditorSettingsItem( + label = stringResource(strings.format_on_save), + description = stringResource(strings.format_on_save_desc), + default = Settings.format_on_save, + sideEffect = { Settings.format_on_save = it }, + ) + } + PreferenceGroup(heading = stringResource(strings.intelligent_features)) { - EditorSettingsToggle( + EditorSettingsItem( label = stringResource(strings.auto_close_tags), description = stringResource(strings.auto_close_tags_desc), default = Settings.auto_close_tags, @@ -101,7 +108,7 @@ fun SettingsEditorScreen(navController: NavController) { }, ) - EditorSettingsToggle( + EditorSettingsItem( label = stringResource(strings.bullet_continuation), description = stringResource(strings.bullet_continuation_desc), default = Settings.bullet_continuation, @@ -116,7 +123,7 @@ fun SettingsEditorScreen(navController: NavController) { val wordWrap = remember { mutableStateOf(Settings.word_wrap) } val wordWrapTxt = remember { mutableStateOf(Settings.word_wrap_text || Settings.word_wrap) } - EditorSettingsToggle( + EditorSettingsItem( label = stringResource(id = strings.word_wrap), description = stringResource(id = strings.word_wrap_desc), state = wordWrap, @@ -129,7 +136,7 @@ fun SettingsEditorScreen(navController: NavController) { }, ) - EditorSettingsToggle( + EditorSettingsItem( label = stringResource(strings.txt_word_wrap), description = stringResource(strings.txt_word_wrap_desc), isEnabled = !wordWrap.value, @@ -140,7 +147,7 @@ fun SettingsEditorScreen(navController: NavController) { }, ) - EditorSettingsToggle( + EditorSettingsItem( label = stringResource(strings.read_mode), description = stringResource(strings.read_mode_desc), default = Settings.read_only_default, @@ -149,14 +156,14 @@ fun SettingsEditorScreen(navController: NavController) { } PreferenceGroup(heading = stringResource(id = strings.editor)) { - EditorSettingsToggle( + EditorSettingsItem( label = stringResource(strings.disable_virtual_kbd), description = stringResource(strings.disable_virtual_kbd_desc), default = Settings.hide_soft_keyboard_if_hardware, sideEffect = { Settings.hide_soft_keyboard_if_hardware = it }, ) - EditorSettingsToggle( + EditorSettingsItem( label = stringResource(id = strings.line_spacing), description = stringResource(id = strings.line_spacing_desc), showSwitch = false, @@ -164,56 +171,56 @@ fun SettingsEditorScreen(navController: NavController) { sideEffect = { showLineSpacingDialog = true }, ) - EditorSettingsToggle( + EditorSettingsItem( label = stringResource(id = strings.cursor_anim), description = stringResource(id = strings.cursor_anim_desc), default = Settings.cursor_animation, sideEffect = { Settings.cursor_animation = it }, ) - EditorSettingsToggle( + EditorSettingsItem( label = stringResource(strings.show_minimap), description = stringResource(strings.show_minimap_desc), default = Settings.show_minimap, sideEffect = { Settings.show_minimap = it }, ) - EditorSettingsToggle( + EditorSettingsItem( label = stringResource(id = strings.show_line_number), description = stringResource(id = strings.show_line_number), default = Settings.show_line_numbers, sideEffect = { Settings.show_line_numbers = it }, ) - EditorSettingsToggle( + EditorSettingsItem( label = stringResource(id = strings.pin_line_number), description = stringResource(id = strings.pin_line_number), default = Settings.pin_line_number, sideEffect = { Settings.pin_line_number = it }, ) - EditorSettingsToggle( + EditorSettingsItem( label = stringResource(id = strings.render_whitespace), description = stringResource(id = strings.render_whitespace_desc), default = Settings.render_whitespace, sideEffect = { Settings.render_whitespace = it }, ) - EditorSettingsToggle( + EditorSettingsItem( label = stringResource(id = strings.show_suggestions), description = stringResource(id = strings.show_suggestions), default = Settings.show_suggestions, sideEffect = { Settings.show_suggestions = it }, ) - EditorSettingsToggle( + EditorSettingsItem( label = stringResource(id = strings.enable_sticky_scroll), description = stringResource(id = strings.enable_sticky_scroll_desc), default = Settings.sticky_scroll, sideEffect = { Settings.sticky_scroll = it }, ) - EditorSettingsToggle( + EditorSettingsItem( label = stringResource(id = strings.enable_quick_deletion), description = stringResource(id = strings.enable_quick_deletion_desc), default = Settings.quick_deletion, @@ -238,21 +245,21 @@ fun SettingsEditorScreen(navController: NavController) { scope.launch { refreshEditorSettings() } } - EditorSettingsToggle( + EditorSettingsItem( label = stringResource(strings.auto_closing_bracket), description = stringResource(strings.auto_closing_bracket_desc), default = Settings.auto_closing_bracket, sideEffect = { Settings.auto_closing_bracket = it }, ) - EditorSettingsToggle( + EditorSettingsItem( label = stringResource(strings.complete_on_enter), description = stringResource(strings.complete_on_enter_desc), default = Settings.complete_on_enter, sideEffect = { Settings.complete_on_enter = it }, ) - SettingsToggle( + SettingsItem( label = stringResource(strings.text_mate_suggestion), description = stringResource(strings.text_mate_suggestion_desc), default = Settings.textmate_suggestions, @@ -263,13 +270,13 @@ fun SettingsEditorScreen(navController: NavController) { MainActivity.instance?.apply { viewModel.tabs.filterIsInstance().forEach { tab -> val scope = tab.editorState.textmateScope ?: return@forEach - val language = tab.editorState.editor.get()?.editorLanguage as? TextMateLanguage + val textMateLanguage = tab.editorState.editor.get()?.getTextMateLanguage() if (newValue) { val keywords = KeywordManager.getKeywords(scope) - keywords?.let { language?.setCompleterKeywords(it.toTypedArray()) } + keywords?.let { textMateLanguage?.setCompleterKeywords(it.toTypedArray()) } } else { - language?.setCompleterKeywords(null) + textMateLanguage?.setCompleterKeywords(null) } } } @@ -285,10 +292,18 @@ fun SettingsEditorScreen(navController: NavController) { max = 16, ) { Settings.tab_size = it + + MainActivity.instance?.apply { + viewModel.tabs.filterIsInstance().forEach { tab -> + val textMateLanguage = tab.editorState.editor.get()?.getTextMateLanguage() + textMateLanguage?.tabSize = it + } + } + scope.launch { refreshEditorSettings() } } - EditorSettingsToggle( + EditorSettingsItem( label = stringResource(strings.use_tabs), description = stringResource(strings.use_tabs_desc), default = Settings.actual_tabs, @@ -297,10 +312,12 @@ fun SettingsEditorScreen(navController: NavController) { MainActivity.instance?.apply { viewModel.tabs.filterIsInstance().forEach { tab -> - val language = tab.editorState.editor.get()?.editorLanguage as? TextMateLanguage - language?.useTab(it) + val textMateLanguage = tab.editorState.editor.get()?.getTextMateLanguage() + textMateLanguage?.useTab(it) } } + + scope.launch { refreshEditorSettings() } }, ) } @@ -312,14 +329,14 @@ fun SettingsEditorScreen(navController: NavController) { route = SettingsRoutes.ToolbarActions, ) - EditorSettingsToggle( + EditorSettingsItem( label = stringResource(id = strings.extra_keys), description = stringResource(id = strings.extra_keys_desc), default = Settings.show_extra_keys, sideEffect = { Settings.show_extra_keys = it }, ) - EditorSettingsToggle( + EditorSettingsItem( label = stringResource(id = strings.extra_key_bg), description = stringResource(id = strings.extra_key_bg_desc), isEnabled = Settings.show_extra_keys, @@ -327,7 +344,7 @@ fun SettingsEditorScreen(navController: NavController) { sideEffect = { Settings.extra_keys_bg = it }, ) - EditorSettingsToggle( + EditorSettingsItem( label = stringResource(id = strings.split_extra_keys), description = stringResource(id = strings.split_extra_keys_desc), isEnabled = Settings.show_extra_keys, @@ -344,21 +361,21 @@ fun SettingsEditorScreen(navController: NavController) { } PreferenceGroup(heading = stringResource(strings.drawer)) { - EditorSettingsToggle( + EditorSettingsItem( label = stringResource(id = strings.keep_drawer_locked), description = stringResource(id = strings.drawer_lock_desc), default = Settings.keep_drawer_locked, sideEffect = { Settings.keep_drawer_locked = it }, ) - EditorSettingsToggle( + EditorSettingsItem( label = stringResource(id = strings.sort_mode), description = stringResource(id = strings.sort_mode_desc), showSwitch = false, sideEffect = { showSortingModeDialog = true }, ) - EditorSettingsToggle( + EditorSettingsItem( label = stringResource(id = strings.show_hidden_files_drawer), description = stringResource(id = strings.show_hidden_files_drawer_desc), default = Settings.show_hidden_files_drawer, @@ -371,21 +388,21 @@ fun SettingsEditorScreen(navController: NavController) { onClick = { settingsNavController.get()!!.navigate("${SettingsRoutes.ExcludeFiles.route}/true") }, ) - EditorSettingsToggle( + EditorSettingsItem( label = stringResource(id = strings.compact_folders_drawer), description = stringResource(id = strings.compact_folders_drawer_desc), default = Settings.compact_folders_drawer, sideEffect = { Settings.compact_folders_drawer = it }, ) - EditorSettingsToggle( + EditorSettingsItem( label = stringResource(id = strings.show_hidden_files_search), description = stringResource(id = strings.show_hidden_files_search_desc), default = Settings.show_hidden_files_search, sideEffect = { Settings.show_hidden_files_search = it }, ) - EditorSettingsToggle( + EditorSettingsItem( label = stringResource(strings.always_index_projects), description = stringResource(strings.always_index_projects_desc), default = Settings.always_index_projects, @@ -398,7 +415,7 @@ fun SettingsEditorScreen(navController: NavController) { onClick = { settingsNavController.get()!!.navigate("${SettingsRoutes.ExcludeFiles.route}/false") }, ) - EditorSettingsToggle( + EditorSettingsItem( label = stringResource(strings.auto_open_new_files), description = stringResource(strings.auto_open_new_files_desc), default = Settings.auto_open_new_files, @@ -407,35 +424,35 @@ fun SettingsEditorScreen(navController: NavController) { } PreferenceGroup(heading = stringResource(strings.other)) { - EditorSettingsToggle( + EditorSettingsItem( label = stringResource(strings.detect_bin_files), description = stringResource(strings.detect_bin_files_desc), default = Settings.detect_bin_files, sideEffect = { Settings.detect_bin_files = it }, ) - EditorSettingsToggle( + EditorSettingsItem( label = stringResource(strings.oom_prediction), description = stringResource(strings.oom_prediction_desc), default = Settings.oom_prediction, sideEffect = { Settings.oom_prediction = it }, ) - EditorSettingsToggle( + EditorSettingsItem( label = stringResource(id = strings.restore_sessions), description = stringResource(id = strings.restore_sessions_desc), default = Settings.restore_sessions, sideEffect = { Settings.restore_sessions = it }, ) - EditorSettingsToggle( + EditorSettingsItem( label = stringResource(id = strings.smooth_tabs), description = stringResource(id = strings.smooth_tab_desc), default = Settings.smooth_tabs, sideEffect = { Settings.smooth_tabs = it }, ) - EditorSettingsToggle( + EditorSettingsItem( label = stringResource(id = strings.show_tab_icons), description = stringResource(id = strings.show_tab_icons_desc), default = Settings.show_tab_icons, @@ -454,21 +471,21 @@ fun SettingsEditorScreen(navController: NavController) { route = SettingsRoutes.DefaultLineEnding, ) - EditorSettingsToggle( + EditorSettingsItem( label = stringResource(id = strings.auto_save), description = stringResource(id = strings.auto_save_desc), default = Settings.auto_save, sideEffect = { Settings.auto_save = it }, ) - EditorSettingsToggle( + EditorSettingsItem( label = stringResource(id = strings.auto_save_delay), description = stringResource(id = strings.auto_save_delay_desc), showSwitch = false, sideEffect = { showAutoSaveDialog = true }, ) - EditorSettingsToggle( + EditorSettingsItem( label = stringResource(id = strings.enable_editorconfig), description = stringResource(id = strings.enable_editorconfig_desc), default = Settings.enable_editorconfig, diff --git a/core/main/src/main/java/com/rk/settings/extension/ExtensionActionButton.kt b/core/main/src/main/java/com/rk/settings/extension/ExtensionActionButton.kt deleted file mode 100644 index ef0fc7b8f..000000000 --- a/core/main/src/main/java/com/rk/settings/extension/ExtensionActionButton.kt +++ /dev/null @@ -1,87 +0,0 @@ -package com.rk.settings.extension - -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Delete -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import com.rk.extension.Extension -import com.rk.icons.Download -import com.rk.icons.XedIcons -import com.rk.resources.strings -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch - -enum class InstallState { - Idle, - Installing, - Installed, -} - -@Composable -fun ExtensionActionButton( - extension: Extension, - installState: InstallState, - scope: CoroutineScope, - onInstallClick: suspend (Extension) -> Unit, - onUninstallClick: suspend (Extension) -> Unit, -) { - when (installState) { - InstallState.Idle -> { - Button( - onClick = { scope.launch { onInstallClick(extension) } }, - shape = RoundedCornerShape(10.dp), - elevation = ButtonDefaults.buttonElevation(defaultElevation = 4.dp), - ) { - Icon(XedIcons.Download, contentDescription = null, Modifier.size(18.dp)) - Spacer(Modifier.width(6.dp)) - Text(stringResource(strings.install)) - } - } - - InstallState.Installing -> { - Button( - onClick = {}, - enabled = false, - shape = androidx.compose.foundation.shape.RoundedCornerShape(10.dp), - colors = ButtonDefaults.buttonColors(disabledContentColor = MaterialTheme.colorScheme.onSurface), - ) { - CircularProgressIndicator( - modifier = Modifier.size(16.dp), - strokeWidth = 2.dp, - color = MaterialTheme.colorScheme.onSurface, - ) - Spacer(Modifier.width(6.dp)) - Text(stringResource(strings.installing)) - } - } - - InstallState.Installed -> { - Button( - onClick = { scope.launch { onUninstallClick(extension) } }, - shape = androidx.compose.foundation.shape.RoundedCornerShape(10.dp), - elevation = ButtonDefaults.buttonElevation(defaultElevation = 4.dp), - colors = - ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.error, - contentColor = MaterialTheme.colorScheme.onError, - ), - ) { - Icon(Icons.Outlined.Delete, contentDescription = stringResource(strings.delete), Modifier.size(18.dp)) - Spacer(Modifier.width(6.dp)) - Text(stringResource(strings.uninstall)) - } - } - } -} diff --git a/core/main/src/main/java/com/rk/settings/extension/ExtensionActionButtons.kt b/core/main/src/main/java/com/rk/settings/extension/ExtensionActionButtons.kt new file mode 100644 index 000000000..11cc2ac5e --- /dev/null +++ b/core/main/src/main/java/com/rk/settings/extension/ExtensionActionButtons.kt @@ -0,0 +1,234 @@ +package com.rk.settings.extension + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.icons.rounded.Warning +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.rk.icons.Download +import com.rk.icons.XedIcons +import com.rk.resources.drawables +import com.rk.resources.strings +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +enum class InstallState { + Idle, + Installing, + Installed, + Updatable, + Updating, +} + +@Composable +fun SmallExtensionActionButton( + installState: InstallState, + scope: CoroutineScope, + onInstallClick: suspend () -> Unit, + onUninstallClick: suspend () -> Unit, + onUpdateClick: suspend () -> Unit, + modifier: Modifier = Modifier, + outdatedWarning: Boolean = false, +) { + if (outdatedWarning) { + Icon( + imageVector = Icons.Rounded.Warning, + contentDescription = stringResource(strings.warning), + tint = MaterialTheme.colorScheme.error, + modifier = Modifier.padding(12.dp), + ) + return + } + + when (installState) { + InstallState.Idle -> { + IconButton(modifier = modifier, onClick = { scope.launch { onInstallClick() } }) { + Icon(XedIcons.Download, contentDescription = null) + } + } + + InstallState.Installing -> { + IconButton(modifier = modifier, onClick = {}, enabled = false) { + Icon(XedIcons.Download, contentDescription = null) + } + } + + InstallState.Installed -> { + IconButton( + modifier = modifier, + onClick = { scope.launch { onUninstallClick() } }, + colors = IconButtonDefaults.iconButtonColors(contentColor = MaterialTheme.colorScheme.error), + ) { + Icon(Icons.Outlined.Delete, contentDescription = stringResource(strings.delete)) + } + } + + InstallState.Updatable -> { + IconButton( + modifier = modifier, + onClick = { scope.launch { onUpdateClick() } }, + colors = IconButtonDefaults.iconButtonColors(contentColor = MaterialTheme.colorScheme.primary), + ) { + Icon(painterResource(drawables.update), contentDescription = stringResource(strings.update)) + } + } + + InstallState.Updating -> { + IconButton(modifier = modifier, onClick = {}, enabled = false) { + Icon(painterResource(drawables.update), contentDescription = stringResource(strings.update)) + } + } + } +} + +@Composable +fun ExtensionActionButtons( + installState: InstallState, + scope: CoroutineScope, + onInstallClick: suspend () -> Unit, + onUninstallClick: suspend () -> Unit, + onUpdateClick: suspend () -> Unit, + modifier: Modifier = Modifier, + outdatedWarning: Boolean = false, + progress: Float? = null, +) { + when (installState) { + InstallState.Idle -> { + InstallButton(scope, onInstallClick, modifier, outdatedWarning) + } + + InstallState.Installing -> { + InstallingButton(modifier, progress) + } + + InstallState.Installed -> { + UninstallButton(scope, onUninstallClick, modifier) + } + + InstallState.Updatable -> { + Row(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(8.dp)) { + UpdateButton(scope, onUpdateClick, Modifier.weight(1f), outdatedWarning) + UninstallButton(scope, onUninstallClick, Modifier.weight(1f)) + } + } + + InstallState.Updating -> { + UpdatingButton(modifier, progress) + } + } +} + +@Composable +private fun InstallButton( + scope: CoroutineScope, + onInstallClick: suspend () -> Unit, + modifier: Modifier = Modifier, + outdatedWarning: Boolean = false, +) { + Button(modifier = modifier, enabled = !outdatedWarning, onClick = { scope.launch { onInstallClick() } }) { + Icon(XedIcons.Download, contentDescription = null, Modifier.size(18.dp)) + Spacer(Modifier.width(6.dp)) + Text(stringResource(strings.install)) + } +} + +@Composable +private fun InstallingButton(modifier: Modifier = Modifier, progress: Float? = null) { + Button(modifier = modifier, onClick = {}, enabled = false) { + CircularProgressIndicator( + modifier = Modifier.size(16.dp), + strokeWidth = 2.dp, + color = MaterialTheme.colorScheme.onSurface, + ) + Spacer(Modifier.width(6.dp)) + val text = if (progress != null && progress >= 0f) { + "${stringResource(strings.installing)} (${(progress * 100).toInt()}%)" + } else { + stringResource(strings.installing) + } + Text(text) + } +} + +@Composable +private fun UninstallButton( + scope: CoroutineScope, + onUninstallClick: suspend () -> Unit, + modifier: Modifier = Modifier, +) { + Button( + modifier = modifier, + onClick = { scope.launch { onUninstallClick() } }, + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.error, + contentColor = MaterialTheme.colorScheme.onError, + ), + ) { + Icon(Icons.Outlined.Delete, contentDescription = stringResource(strings.delete), Modifier.size(18.dp)) + Spacer(Modifier.width(6.dp)) + Text(stringResource(strings.uninstall)) + } +} + +@Composable +private fun UpdateButton( + scope: CoroutineScope, + onUpdateClick: suspend () -> Unit, + modifier: Modifier = Modifier, + outdatedWarning: Boolean = false, +) { + Button( + modifier = modifier, + enabled = !outdatedWarning, + onClick = { scope.launch { onUpdateClick() } }, + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.secondary, + contentColor = MaterialTheme.colorScheme.onSecondary, + ), + ) { + Icon( + painterResource(drawables.update), + contentDescription = stringResource(strings.update), + Modifier.size(18.dp), + ) + Spacer(Modifier.width(6.dp)) + Text(stringResource(strings.update)) + } +} + +@Composable +private fun UpdatingButton(modifier: Modifier = Modifier, progress: Float? = null) { + Button(modifier = modifier, onClick = {}, enabled = false) { + CircularProgressIndicator( + modifier = Modifier.size(16.dp), + strokeWidth = 2.dp, + color = MaterialTheme.colorScheme.onSurface, + ) + Spacer(Modifier.width(6.dp)) + val text = if (progress != null && progress >= 0f) { + "${stringResource(strings.updating)} (${(progress * 100).toInt()}%)" + } else { + stringResource(strings.updating) + } + Text(text) + } +} diff --git a/core/main/src/main/java/com/rk/settings/extension/ExtensionAuthorIcon.kt b/core/main/src/main/java/com/rk/settings/extension/ExtensionAuthorIcon.kt index cd5356dc3..73245f86e 100644 --- a/core/main/src/main/java/com/rk/settings/extension/ExtensionAuthorIcon.kt +++ b/core/main/src/main/java/com/rk/settings/extension/ExtensionAuthorIcon.kt @@ -20,6 +20,8 @@ fun ExtensionAuthorIcon(author: ExtensionAuthor, modifier: Modifier = Modifier) ImageRequest.Builder(context) .data(author.github?.let { "https://github.com/$it.png" }) .fallback(drawables.person) + .placeholder(drawables.person) + .error(drawables.person) .crossfade(true) .diskCachePolicy(CachePolicy.ENABLED) .memoryCachePolicy(CachePolicy.ENABLED) diff --git a/core/main/src/main/java/com/rk/settings/extension/ExtensionCard.kt b/core/main/src/main/java/com/rk/settings/extension/ExtensionCard.kt index 6200d70fc..9114239d6 100644 --- a/core/main/src/main/java/com/rk/settings/extension/ExtensionCard.kt +++ b/core/main/src/main/java/com/rk/settings/extension/ExtensionCard.kt @@ -1,19 +1,15 @@ package com.rk.settings.extension import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.height import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.material3.contentColorFor -import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment @@ -26,7 +22,12 @@ import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import coil.request.CachePolicy import coil.request.ImageRequest +import com.rk.components.compose.preferences.base.PreferenceTemplate import com.rk.extension.Extension +import com.rk.extension.UpdatableExtension +import androidx.compose.ui.res.stringResource +import com.rk.App.Companion.extensionManager +import com.rk.resources.strings import com.rk.resources.drawables import com.rk.theme.Typography @@ -36,44 +37,46 @@ fun ExtensionCard( extension: Extension, modifier: Modifier = Modifier, installState: InstallState = InstallState.Idle, - onInstallClick: suspend (Extension) -> Unit, - onUninstallClick: suspend (Extension) -> Unit, + onInstallClick: suspend () -> Unit, + onUninstallClick: suspend () -> Unit, + onUpdateClick: suspend () -> Unit, onClick: (Extension) -> Unit = {}, ) { val scope = rememberCoroutineScope() - val cardColor = MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp) + val context = LocalContext.current - Card( - modifier = - modifier.fillMaxWidth().clip(RoundedCornerShape(12.dp)).combinedClickable(onClick = { onClick(extension) }), - elevation = CardDefaults.cardElevation(defaultElevation = 2.dp), - colors = CardDefaults.cardColors(containerColor = cardColor, contentColor = contentColorFor(cardColor)), - ) { - Row(modifier = Modifier.fillMaxWidth().padding(16.dp), verticalAlignment = Alignment.CenterVertically) { + val minAppVersion = extension.minAppVersion + val maxAppVersion = extension.maxAppVersion + + val xedVersionCode = com.rk.App.versionCode + + val outdatedClient = minAppVersion != null && xedVersionCode < minAppVersion + val outdatedExtension = maxAppVersion != null && xedVersionCode > maxAppVersion + + PreferenceTemplate( + modifier = modifier.fillMaxWidth().clickable(onClick = { onClick(extension) }), + startWidget = { AsyncImage( model = ImageRequest.Builder(LocalContext.current) .data(extension.iconUrl) - .fallback(drawables.extension) + .placeholder(drawables.extension) + .error(drawables.extension) .crossfade(true) .diskCachePolicy(CachePolicy.ENABLED) .memoryCachePolicy(CachePolicy.ENABLED) .build(), - modifier = Modifier.size(56.dp).clip(RoundedCornerShape(8.dp)).padding(end = 16.dp), + modifier = Modifier.size(48.dp).clip(RoundedCornerShape(8.dp)), contentDescription = null, ) - - Column(modifier = Modifier.weight(1f)) { - Text( - text = extension.name, - style = Typography.titleMedium, - fontWeight = FontWeight.SemiBold, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - + }, + title = { + Text(text = extension.name, fontWeight = FontWeight.Bold, maxLines = 1, overflow = TextOverflow.Ellipsis) + }, + description = { + androidx.compose.foundation.layout.Column { Row(verticalAlignment = Alignment.CenterVertically) { - ExtensionAuthorIcon(extension.author, Modifier.size(16.dp).padding(end = 4.dp)) + ExtensionAuthorIcon(extension.author, Modifier.size(20.dp).padding(end = 4.dp)) Text( text = "${extension.author} • v${extension.version}", maxLines = 1, @@ -81,10 +84,56 @@ fun ExtensionCard( style = Typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, ) + + val isUpdatable = extension is UpdatableExtension && extension.isUpdatable() + if (isUpdatable) { + Text( + text = " → v${extension.newVersion}", + style = Typography.labelMedium, + color = MaterialTheme.colorScheme.primary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + + if (extensionManager.isExtensionDisabled(extension.id)) { + Text( + text = " • ${stringResource(strings.disabled_crashed)}", + style = Typography.labelMedium, + color = MaterialTheme.colorScheme.error, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } } - } - ExtensionActionButton(extension, installState, scope, onInstallClick, onUninstallClick) - } - } + val progress = com.rk.extension.manager.ExtensionRegistry.downloadProgress[extension.id] + if (progress != null) { + androidx.compose.foundation.layout.Spacer(Modifier.height(4.dp)) + if (progress >= 0f) { + androidx.compose.material3.LinearProgressIndicator( + progress = { progress }, + modifier = Modifier.fillMaxWidth().height(4.dp), + color = MaterialTheme.colorScheme.primary, + ) + } else { + androidx.compose.material3.LinearProgressIndicator( + modifier = Modifier.fillMaxWidth().height(4.dp), + color = MaterialTheme.colorScheme.primary, + ) + } + } + } + }, + endWidget = { + SmallExtensionActionButton( + installState = installState, + scope = scope, + onInstallClick = onInstallClick, + onUninstallClick = onUninstallClick, + onUpdateClick = onUpdateClick, + outdatedWarning = outdatedClient || outdatedExtension, + ) + }, + ) } diff --git a/core/main/src/main/java/com/rk/settings/extension/ExtensionDetail.kt b/core/main/src/main/java/com/rk/settings/extension/ExtensionDetail.kt index 145bc6988..3aa0109c3 100644 --- a/core/main/src/main/java/com/rk/settings/extension/ExtensionDetail.kt +++ b/core/main/src/main/java/com/rk/settings/extension/ExtensionDetail.kt @@ -1,7 +1,10 @@ package com.rk.settings.extension +import com.rk.extension.manager.ExtensionRegistry +import android.content.Intent import androidx.activity.compose.LocalActivity import androidx.appcompat.app.AppCompatActivity +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -14,6 +17,7 @@ import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Star +import androidx.compose.material.icons.rounded.Warning import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.Icon @@ -25,10 +29,10 @@ import androidx.compose.material3.Text import androidx.compose.material3.contentColorFor import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue @@ -42,12 +46,16 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.core.net.toUri +import androidx.navigation.NavController import coil.compose.AsyncImage import coil.request.CachePolicy import coil.request.ImageRequest import com.rk.App.Companion.extensionManager +import com.rk.activities.settings.SettingsRoutes import com.rk.components.compose.preferences.base.RefreshablePreferenceLayout import com.rk.extension.Extension +import com.rk.extension.UpdatableExtension import com.rk.icons.Icon import com.rk.icons.XedIcon import com.rk.resources.drawables @@ -60,16 +68,34 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @Composable -fun ExtensionDetail(extension: Extension?) { +fun ExtensionDetail(extension: Extension?, navController: NavController) { val scope = rememberCoroutineScope() var isRefreshing by remember { mutableStateOf(false) } var refreshKey by remember { mutableIntStateOf(0) } + var showSourceCodeSheet by remember { mutableStateOf(false) } RefreshablePreferenceLayout( label = extension?.name ?: stringResource(strings.ext_not_found), backArrowVisible = true, isExpandedScreen = true, + actions = { + IconButton(onClick = { showSourceCodeSheet = true }) { + Icon(painter = painterResource(drawables.xml), contentDescription = null) + } + + if (extension?.hasSettings == true) { + IconButton( + enabled = extensionManager.isInstalled(extension.id), + onClick = { navController.navigate("${SettingsRoutes.ExtensionSettings.route}/{extensionId}") }, + ) { + Icon( + painter = painterResource(drawables.settings), + contentDescription = stringResource(strings.settings), + ) + } + } + }, isRefreshing = isRefreshing, onRefresh = { isRefreshing = true @@ -79,20 +105,48 @@ fun ExtensionDetail(extension: Extension?) { if (extension == null) { Text(stringResource(strings.ext_not_found_desc), modifier = Modifier.padding(horizontal = 16.dp)) } else { - var installState by remember { + var localInstallState by remember { mutableStateOf( if (extensionManager.isInstalled(extension.id)) { - InstallState.Installed + if (extension is UpdatableExtension && extension.isUpdatable()) { + InstallState.Updatable + } else { + InstallState.Installed + } } else { InstallState.Idle } ) } - Column(modifier = Modifier.padding(horizontal = 16.dp)) { - AboutSection(extension, installState, { installState = it }, scope) + val installState = remember(extension, localInstallState, ExtensionRegistry.activeInstalls[extension.id]) { + val active = ExtensionRegistry.activeInstalls[extension.id] + if (active != null) { + active + } else { + localInstallState + } + } + + Column(modifier = Modifier.padding(horizontal = 16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { + AboutSection( + extension = extension, + refreshKey = refreshKey, + installState = installState, + updateInstallState = { + localInstallState = it + if (it == InstallState.Idle && extensionManager.storeExtension[extension.id] == null) { + navController.popBackStack() + } + }, + scope = scope, + ) } TabSection(extension, scope, refreshKey, onLoaded = { isRefreshing = false }) + + if (showSourceCodeSheet) { + SourceCodeSheet(extension) { showSourceCodeSheet = false } + } } } } @@ -100,6 +154,7 @@ fun ExtensionDetail(extension: Extension?) { @Composable private fun AboutSection( extension: Extension, + refreshKey: Int, installState: InstallState, updateInstallState: (InstallState) -> Unit, scope: CoroutineScope, @@ -107,75 +162,160 @@ private fun AboutSection( val context = LocalContext.current val activity = LocalActivity.current as? AppCompatActivity - var showSourceCodeSheet by remember { mutableStateOf(false) } - - Row(verticalAlignment = Alignment.CenterVertically) { + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(16.dp)) { AsyncImage( model = ImageRequest.Builder(LocalContext.current) .data(extension.iconUrl) - .fallback(drawables.extension) + .placeholder(drawables.extension) + .error(drawables.extension) .crossfade(true) .diskCachePolicy(CachePolicy.ENABLED) .memoryCachePolicy(CachePolicy.ENABLED) .build(), - modifier = Modifier.size(64.dp).clip(RoundedCornerShape(8.dp)).padding(end = 16.dp), + modifier = Modifier.size(70.dp).clip(RoundedCornerShape(8.dp)), contentDescription = null, ) - Column(modifier = Modifier.weight(1f)) { + Column { Text( text = extension.name, - style = Typography.titleLarge, + style = Typography.headlineSmall, fontWeight = FontWeight.SemiBold, maxLines = 1, overflow = TextOverflow.Ellipsis, ) Row(verticalAlignment = Alignment.CenterVertically) { - ExtensionAuthorIcon(extension.author, Modifier.size(16.dp).padding(end = 4.dp)) + Row( + modifier = + Modifier.clickable( + enabled = extension.author.github != null, + onClick = { + val githubProfileUrl = extension.author.github.let { "https://github.com/$it" } + val intent = Intent(Intent.ACTION_VIEW, githubProfileUrl.toUri()) + context.startActivity(intent) + }, + ), + verticalAlignment = Alignment.CenterVertically, + ) { + ExtensionAuthorIcon(extension.author, Modifier.size(24.dp).padding(end = 4.dp)) + Text( + text = "${extension.author}", + style = Typography.labelLarge, + color = + if (extension.author.github != null) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + Text( - text = "${extension.author} • v${extension.version}", - style = Typography.labelMedium, + text = " • v${extension.version}", + style = Typography.labelLarge, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 1, overflow = TextOverflow.Ellipsis, ) + + val isUpdatable = extension is UpdatableExtension && extension.isUpdatable() + if (isUpdatable) { + Text( + text = " → v${extension.newVersion}", + style = Typography.labelLarge, + color = MaterialTheme.colorScheme.primary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } } } + } + + var size by remember { mutableStateOf("---") } + var rating by remember { mutableStateOf("---") } + var downloadCount by remember { mutableStateOf("---") } + var showStar by remember { mutableStateOf(false) } - IconButton(onClick = { showSourceCodeSheet = true }) { - Icon(painter = painterResource(drawables.xml), contentDescription = null) + LaunchedEffect(refreshKey) { + val stats = extension.getStats() + stats.size?.let { size = formatFileSize(it) } + stats.rating?.let { + rating = it.toString() + showStar = true } + stats.downloadCount?.let { downloadCount = formatNumberCompact(it) } + } - ExtensionActionButton( - extension = extension, - installState = installState, - scope = scope, - onInstallClick = { runExtensionInstallAction(extension, updateInstallState, scope, context, activity) }, - onUninstallClick = { runExtensionUninstallAction(extension, updateInstallState, activity) }, + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + ExtensionStats(Modifier.weight(1f), stringResource(strings.downloads).uppercase(), downloadCount) + ExtensionStats( + Modifier.weight(1f), + stringResource(strings.rating).uppercase(), + rating, + if (showStar) Icons.Default.Star else null, ) + ExtensionStats(Modifier.weight(1f), stringResource(strings.size).uppercase(), size) } - val size by produceState("---") { value = formatFileSize(extension.calcSize()) } - val rating by - produceState>("---" to null) { - val rating = extension.getRating() ?: return@produceState - value = rating.toString() to Icons.Default.Star - } - val downloadCount by - produceState("---") { - val count = extension.getDownloadCount() ?: return@produceState - value = formatNumberCompact(count) + val minAppVersion = extension.minAppVersion + val maxAppVersion = extension.maxAppVersion + + val xedVersionCode = com.rk.App.versionCode + + val outdatedClient = minAppVersion != null && xedVersionCode < minAppVersion + val outdatedExtension = maxAppVersion != null && xedVersionCode > maxAppVersion + + val progress = ExtensionRegistry.downloadProgress[extension.id] + if (progress != null) { + if (progress >= 0f) { + androidx.compose.material3.LinearProgressIndicator( + progress = { progress }, + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.primary + ) + } else { + androidx.compose.material3.LinearProgressIndicator( + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.primary + ) } - Row(modifier = Modifier.padding(top = 8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) { - ExtensionStats(Modifier.weight(1f), stringResource(strings.downloads).uppercase(), downloadCount) - ExtensionStats(Modifier.weight(1f), stringResource(strings.rating).uppercase(), rating.first, rating.second) - ExtensionStats(Modifier.weight(1f), stringResource(strings.size).uppercase(), size) } - if (showSourceCodeSheet) { - SourceCodeSheet(extension) { showSourceCodeSheet = false } + ExtensionActionButtons( + outdatedWarning = outdatedClient || outdatedExtension, + modifier = Modifier.fillMaxWidth(), + installState = installState, + scope = scope, + progress = progress, + onInstallClick = { + checkExtensionWarningAndRun(activity) { + runExtensionInstallAction(extension, updateInstallState, context, activity) + } + }, + onUninstallClick = { runExtensionUninstallAction(extension, updateInstallState, scope, activity) }, + onUpdateClick = { + if (extension !is UpdatableExtension) return@ExtensionActionButtons + runExtensionUpdateAction(extension, updateInstallState, context, activity) + }, + ) + + if (outdatedClient || outdatedExtension) { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) { + Icon( + imageVector = Icons.Rounded.Warning, + contentDescription = stringResource(strings.warning), + tint = MaterialTheme.colorScheme.error, + ) + Text( + stringResource(if (outdatedClient) strings.outdated_client else strings.outdated_extension), + style = MaterialTheme.typography.labelMedium, + ) + } } } @@ -205,9 +345,9 @@ fun ExtensionStats(modifier: Modifier = Modifier, title: String, value: String, } enum class ExtensionRoutes(val icon: Icon, val label: String, val route: String) { - OVERVIEW(Icon.DrawableRes(drawables.file), strings.overview.getString(), "overview"), - REVIEWS(Icon.DrawableRes(drawables.comment), strings.reviews.getString(), "reviews"), - CHANGELOG(Icon.DrawableRes(drawables.update), strings.changelog.getString(), "changelog"), + OVERVIEW(Icon.ResourceIcon(drawables.file), strings.overview.getString(), "overview"), + REVIEWS(Icon.ResourceIcon(drawables.comment), strings.reviews.getString(), "reviews"), + CHANGELOG(Icon.ResourceIcon(drawables.update), strings.changelog.getString(), "changelog"), } @Composable diff --git a/core/main/src/main/java/com/rk/settings/extension/ExtensionScreen.kt b/core/main/src/main/java/com/rk/settings/extension/ExtensionScreen.kt index 0eb2e3436..c6515500e 100644 --- a/core/main/src/main/java/com/rk/settings/extension/ExtensionScreen.kt +++ b/core/main/src/main/java/com/rk/settings/extension/ExtensionScreen.kt @@ -11,15 +11,17 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.layout.height import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.clearText import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Add import androidx.compose.material.icons.outlined.Info +import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.rounded.Close import androidx.compose.material.icons.rounded.Search +import androidx.compose.material.icons.rounded.Warning import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem @@ -28,19 +30,20 @@ import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.RadioButton import androidx.compose.material3.SearchBarDefaults -import androidx.compose.material3.SegmentedButton -import androidx.compose.material3.SegmentedButtonDefaults -import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.Text +import androidx.compose.material3.Tab +import androidx.compose.material3.PrimaryScrollableTabRow +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.key import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue @@ -50,32 +53,55 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.navigation.NavController import com.rk.App.Companion.extensionManager +import com.rk.App.Companion.iconPackManager import com.rk.activities.settings.SettingsRoutes import com.rk.components.InfoBlock import com.rk.components.compose.preferences.base.PreferenceGroup import com.rk.components.compose.preferences.base.RefreshablePreferenceLayoutLazyColumn +import com.rk.components.compose.preferences.base.PreferenceTemplate import com.rk.extension.Extension import com.rk.extension.StoreExtension +import com.rk.extension.UpdatableExtension import com.rk.resources.drawables import com.rk.resources.strings import com.rk.theme.Typography -import com.rk.utils.openUrl +import com.rk.utils.openDocs import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch - -private enum class ExtensionCategories(val drawableRes: Int, val stringRes: Int) { - ALL(drawables.widgets, strings.all), - LOCAL(drawables.sd_card, strings.local), - STORE(drawables.store, strings.store), -} +import kotlinx.coroutines.withContext +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import androidx.compose.runtime.mutableStateMapOf +import com.rk.extension.ExtensionId +import com.rk.extension.ExtensionStats +import com.rk.extension.manager.ExtensionRegistry +import com.rk.extension.manager.ThemeStoreEntry +import com.rk.extension.manager.IconPackStoreEntry +import com.rk.utils.LoadingPopup +import com.rk.file.toFileObject +import com.rk.file.copyToTempDir +import com.rk.theme.ThemeConfig +import com.rk.theme.installTheme +import com.rk.theme.updateThemes +import com.rk.settings.theme.themes +import com.rk.file.themeDir +import com.rk.file.child +import kotlinx.serialization.json.jsonPrimitive +import com.google.gson.GsonBuilder +import com.rk.icons.XedIcons +import com.rk.icons.Download +import java.io.ObjectOutputStream +import java.io.FileOutputStream +import java.io.File private enum class ExtensionSortOptions(val stringRes: Int) { NAME(strings.name), RATING(strings.rating), - DATE_ADDED(strings.date_added), + DOWNLOAD_COUNT(strings.download_count) } private enum class ExtensionFilterOptions(val stringRes: Int) { @@ -93,15 +119,84 @@ fun ExtensionScreen(navController: NavController) { var isRefreshing by remember { mutableStateOf(false) } var refreshKey by remember { mutableIntStateOf(0) } - var currentSortOption by remember { mutableStateOf(ExtensionSortOptions.NAME) } + var selectedTab by remember { mutableIntStateOf(0) } // 0 = Extensions, 1 = Themes, 2 = Icon Packs + + var currentSortOption by remember { mutableStateOf(ExtensionSortOptions.DOWNLOAD_COUNT) } var currentFilterOption by remember { mutableStateOf(ExtensionFilterOptions.ALL) } val searchQuery = rememberTextFieldState("") - LaunchedEffect(Unit) { - launch(Dispatchers.IO) { - runCatching { - extensionManager.indexLocalExtensions() - extensionManager.indexStoreExtensions() + var isIndexing by remember { mutableStateOf(false) } + var isFetching by remember { mutableStateOf(false) } + + val statsMap = remember { mutableStateMapOf() } + + var storeThemes by remember { mutableStateOf>(emptyList()) } + var storeIconPacks by remember { mutableStateOf>(emptyList()) } + + LaunchedEffect(refreshKey) { + if (refreshKey > 0) { + statsMap.clear() + } + val shouldLoad = refreshKey > 0 || + extensionManager.localExtensions.isEmpty() || + extensionManager.storeExtension.isEmpty() || + storeThemes.isEmpty() || + storeIconPacks.isEmpty() + + if (shouldLoad) { + isIndexing = true + isFetching = true + + val localJob = launch(Dispatchers.IO) { + runCatching { extensionManager.indexLocalExtensions() } + isIndexing = false + } + val storeJob = launch(Dispatchers.IO) { + runCatching { extensionManager.indexStoreExtensions() } + isFetching = false + } + val themesJob = launch(Dispatchers.IO) { + runCatching { + val list = ExtensionRegistry.fetchThemes() + withContext(Dispatchers.Main) { + storeThemes = list + } + } + } + val iconPacksJob = launch(Dispatchers.IO) { + runCatching { + val list = ExtensionRegistry.fetchIconPacks() + withContext(Dispatchers.Main) { + storeIconPacks = list + } + } + } + + localJob.join() + storeJob.join() + themesJob.join() + iconPacksJob.join() + isRefreshing = false + } + } + + LaunchedEffect(refreshKey, isFetching) { + if (!isFetching) { + val rawList = extensionManager.getSyncedExtensions() + launch(Dispatchers.IO) { + val deferreds = rawList.filter { !statsMap.containsKey(it.id) }.map { ext -> + async { + ext.id to runCatching { ext.getStats() }.getOrNull() + } + } + val results = deferreds.awaitAll() + withContext(Dispatchers.Main) { + results.forEach { (id, stats) -> + if (stats != null) { + statsMap[id] = stats + } + } + } } } } @@ -111,51 +206,81 @@ fun ExtensionScreen(navController: NavController) { installExtensionFromUri(scope, uri, activity) } - var selectedCategory by remember { mutableStateOf(ExtensionCategories.ALL) } - val extensions by - remember(selectedCategory) { - derivedStateOf { - when (selectedCategory) { - ExtensionCategories.ALL -> extensionManager.localExtensions + extensionManager.storeExtension - ExtensionCategories.LOCAL -> extensionManager.localExtensions - ExtensionCategories.STORE -> extensionManager.storeExtension - }.map { it.value } + val themeFilePickerLauncher = + rememberLauncherForActivityResult(contract = ActivityResultContracts.OpenDocument()) { uri -> + if (uri != null) { + scope.launch { + val loading = LoadingPopup(activity, null) + loading.show() + runCatching { + com.rk.theme.installFromFile(uri.toFileObject(expectedIsFile = true)) + } + loading.hide() + } } } - val filteredExtensions by - remember(searchQuery.text, extensions, currentFilterOption) { - derivedStateOf { applyFilter(searchQuery, extensions, currentFilterOption) } - } - val sortedExtension by - produceState(filteredExtensions, filteredExtensions, currentSortOption) { - value = applySort(currentSortOption, filteredExtensions) - } - var isIndexing by remember { mutableStateOf(false) } - var isFetching by remember { mutableStateOf(false) } + val iconPackFilePickerLauncher = + rememberLauncherForActivityResult(contract = ActivityResultContracts.OpenDocument()) { uri -> + if (uri != null) { + scope.launch { + val loading = LoadingPopup(activity, null) + loading.show() + runCatching { + val file = uri.toFileObject(expectedIsFile = true).copyToTempDir() + iconPackManager.installIconPack(file) + } + loading.hide() + } + } + } - LaunchedEffect(refreshKey, selectedCategory) { - if (selectedCategory == ExtensionCategories.STORE) return@LaunchedEffect - isIndexing = true - extensionManager.indexLocalExtensions() - isIndexing = false - isRefreshing = false + val sortedExtension by remember { + derivedStateOf { + val rawList = extensionManager.getSyncedExtensions() + val filtered = applyFilter(searchQuery, rawList, currentFilterOption) + when (currentSortOption) { + ExtensionSortOptions.NAME -> filtered.sortedBy { it.name } + ExtensionSortOptions.RATING -> filtered.sortedByDescending { statsMap[it.id]?.rating ?: 0f } + ExtensionSortOptions.DOWNLOAD_COUNT -> filtered.sortedByDescending { statsMap[it.id]?.downloadCount ?: 0 } + } + } } - LaunchedEffect(refreshKey, selectedCategory) { - if (selectedCategory == ExtensionCategories.LOCAL) return@LaunchedEffect - runCatching { - isFetching = true - extensionManager.indexStoreExtensions() + val sortedThemes by remember { + derivedStateOf { + val query = searchQuery.text + val filtered = if (query.isEmpty()) { + storeThemes + } else { + storeThemes.filter { theme -> + theme.manifest.get("name")?.jsonPrimitive?.content?.contains(query, ignoreCase = true) == true || + theme.id.contains(query, ignoreCase = true) + } } - .also { - isFetching = false - isRefreshing = false + filtered.sortedBy { theme -> + theme.manifest.get("name")?.jsonPrimitive?.content ?: theme.id + } + } + } + + val sortedIconPacks by remember { + derivedStateOf { + val query = searchQuery.text + val filtered = if (query.isEmpty()) { + storeIconPacks + } else { + storeIconPacks.filter { pack -> + pack.manifest.name.contains(query, ignoreCase = true) || + pack.manifest.id.contains(query, ignoreCase = true) + } } + filtered.sortedBy { it.manifest.name } + } } RefreshablePreferenceLayoutLazyColumn( - label = stringResource(strings.ext), + label = stringResource(strings.store), isExpandedScreen = false, backArrowVisible = true, isRefreshing = isRefreshing, @@ -165,92 +290,383 @@ fun ExtensionScreen(navController: NavController) { }, fab = { ExtendedFloatingActionButton( - onClick = { filePickerLauncher.launch(arrayOf("application/zip")) }, + onClick = { + when (selectedTab) { + 0 -> { + checkExtensionWarningAndRun(activity) { + filePickerLauncher.launch(arrayOf("application/zip")) + } + } + 1 -> themeFilePickerLauncher.launch(arrayOf("application/json")) + 2 -> iconPackFilePickerLauncher.launch(arrayOf("application/zip")) + } + }, icon = { Icon(imageVector = Icons.Outlined.Add, contentDescription = null) }, text = { Text(stringResource(strings.install_from_storage)) }, ) }, ) { item { - InfoBlock( - modifier = - Modifier.clickable { activity?.openUrl("https://xed-editor.github.io/Xed-Docs/docs/extensions/") } - .padding(bottom = 16.dp), - icon = { Icon(imageVector = Icons.Outlined.Info, contentDescription = null) }, - text = stringResource(strings.info_ext), - ) + PrimaryScrollableTabRow( + selectedTabIndex = selectedTab, + modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp), + edgePadding = 16.dp + ) { + Tab( + selected = selectedTab == 0, + onClick = { selectedTab = 0 }, + text = { Text(stringResource(strings.ext)) } + ) + Tab( + selected = selectedTab == 1, + onClick = { selectedTab = 1 }, + text = { Text(stringResource(strings.themes)) } + ) + Tab( + selected = selectedTab == 2, + onClick = { selectedTab = 2 }, + text = { Text(stringResource(strings.icon_packs)) } + ) + } } - item { - ExtensionSearchBar( - searchQuery = searchQuery, - currentSortOption = currentSortOption, - currentFilterOption = currentFilterOption, - onSortOptionChange = { currentSortOption = it }, - onFilterOptionChange = { currentFilterOption = it }, - ) - } + if (selectedTab == 0) { + item { + InfoBlock( + onClick = { activity?.openDocs("extensions") }, + modifier = Modifier.padding(bottom = 16.dp), + icon = { Icon(imageVector = Icons.Outlined.Info, contentDescription = null) }, + text = stringResource(strings.info_ext), + ) + } - item { - SingleChoiceSegmentedButtonRow(modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 8.dp)) { - ExtensionCategories.entries.forEach { category -> - SegmentedButton( - selected = selectedCategory == category, - onClick = { selectedCategory = category }, - label = { Text(stringResource(category.stringRes)) }, - icon = { Icon(painterResource(category.drawableRes), null) }, - shape = - SegmentedButtonDefaults.itemShape( - index = category.ordinal, - count = ExtensionCategories.entries.size, - ), - ) - } + item { + ExtensionSearchBar( + searchQuery = searchQuery, + currentSortOption = currentSortOption, + currentFilterOption = currentFilterOption, + onSortOptionChange = { currentSortOption = it }, + onFilterOptionChange = { currentFilterOption = it }, + ) } - } - if (sortedExtension.isNotEmpty() || isIndexing || isFetching) { - items(sortedExtension, key = { it.id }) { extension -> - var installState by remember { - mutableStateOf( - if (extensionManager.isInstalled(extension.id)) { - InstallState.Installed - } else { - InstallState.Idle + if (sortedExtension.isNotEmpty() || isIndexing || isFetching) { + item { + PreferenceGroup { + sortedExtension.forEach { extension -> + key(extension.id) { + val installState = remember(extension, ExtensionRegistry.activeInstalls[extension.id]) { + val active = ExtensionRegistry.activeInstalls[extension.id] + if (active != null) { + active + } else if (extensionManager.isInstalled(extension.id)) { + if (extension is UpdatableExtension && extension.isUpdatable()) { + InstallState.Updatable + } else { + InstallState.Installed + } + } else { + InstallState.Idle + } + } + + ExtensionCard( + extension = extension, + installState = installState, + onInstallClick = { + checkExtensionWarningAndRun(activity) { + runExtensionInstallAction(extension, {}, context, activity) + } + }, + onUninstallClick = { + runExtensionUninstallAction(extension, {}, scope, activity) + }, + onUpdateClick = { + if (extension !is UpdatableExtension) return@ExtensionCard + runExtensionUpdateAction(extension, {}, context, activity) + }, + onClick = { navController.navigate("${SettingsRoutes.ExtensionDetail.route}/${it.id}") }, + ) + } } - ) + } } - ExtensionCard( - modifier = Modifier.padding(top = 8.dp, start = 16.dp, end = 16.dp), - extension = extension, - installState = installState, - onInstallClick = { - runExtensionInstallAction(extension, { installState = it }, scope, context, activity) - }, - onUninstallClick = { runExtensionUninstallAction(extension, { installState = it }, activity) }, - onClick = { navController.navigate("${SettingsRoutes.ExtensionDetail.route}/${it.id}") }, + if (isIndexing || isFetching) { + item { + Row( + modifier = Modifier.fillMaxWidth().padding(top = 16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp, alignment = Alignment.CenterHorizontally), + verticalAlignment = Alignment.CenterVertically, + ) { + CircularProgressIndicator(modifier = Modifier.size(24.dp)) + Text(text = stringResource(strings.loading)) + } + } + } + } else { + item { PreferenceGroup { Text(text = stringResource(strings.no_ext), modifier = Modifier.padding(16.dp)) } } + } + } else if (selectedTab == 1) { + item { + StoreSearchBar( + searchQuery = searchQuery, + placeholderText = stringResource(strings.search_themes) ) } - if (isIndexing || isFetching) { + if (sortedThemes.isNotEmpty() || isRefreshing) { item { - Row( - modifier = Modifier.fillMaxWidth().padding(top = 16.dp), - horizontalArrangement = Arrangement.spacedBy(16.dp, alignment = Alignment.CenterHorizontally), - verticalAlignment = Alignment.CenterVertically, - ) { - CircularProgressIndicator(modifier = Modifier.size(24.dp)) - Text(text = stringResource(strings.loading)) + PreferenceGroup { + sortedThemes.forEach { themeEntry -> + key(themeEntry.id) { + val isInstalled = themes.any { it.id == themeEntry.id } + ThemeStoreCard( + themeEntry = themeEntry, + isInstalled = isInstalled, + onInstallClick = { + val manifestJsonString = themeEntry.manifest.toString() + val gson = GsonBuilder().excludeFieldsWithModifiers(java.lang.reflect.Modifier.STATIC).create() + val themeConfig = gson.fromJson(manifestJsonString, ThemeConfig::class.java) + com.rk.DefaultScope.launch(Dispatchers.IO) { + runCatching { + themeConfig.installTheme() + }.onSuccess { + withContext(Dispatchers.Main) { + com.rk.utils.toast(strings.installed) + } + }.onFailure { err -> + withContext(Dispatchers.Main) { + com.rk.utils.errorDialog(activity, err) + } + } + } + }, + onUninstallClick = { + val installedTheme = themes.find { it.id == themeEntry.id } + if (installedTheme != null) { + com.rk.DefaultScope.launch(Dispatchers.IO) { + runCatching { + themeDir().child(installedTheme.name).delete() + withContext(Dispatchers.Main) { + themes.remove(installedTheme) + } + } + } + } + } + ) + } + } } } + } else { + item { PreferenceGroup { Text(text = stringResource(strings.no_themes), modifier = Modifier.padding(16.dp)) } } + } + } else if (selectedTab == 2) { + item { + StoreSearchBar( + searchQuery = searchQuery, + placeholderText = stringResource(strings.search_icon_packs) + ) + } + + if (sortedIconPacks.isNotEmpty() || isRefreshing) { + item { + PreferenceGroup { + sortedIconPacks.forEach { iconPackEntry -> + key(iconPackEntry.id) { + val isInstalled = iconPackManager.iconPacks.containsKey(iconPackEntry.id) + IconPackStoreCard( + iconPackEntry = iconPackEntry, + isInstalled = isInstalled, + onInstallClick = { + runIconPackInstallAction( + iconPackEntry.id, + iconPackEntry.manifest.name, + context, + activity + ) + }, + onUninstallClick = { + com.rk.DefaultScope.launch(Dispatchers.IO) { + runCatching { + iconPackManager.uninstallIconPack(iconPackEntry.id) + } + } + } + ) + } + } + } + } + } else { + item { PreferenceGroup { Text(text = stringResource(strings.no_icon_packs), modifier = Modifier.padding(16.dp)) } } } - } else { - item { PreferenceGroup { Text(text = stringResource(strings.no_ext), modifier = Modifier.padding(16.dp)) } } + } + item { + androidx.compose.foundation.layout.Spacer(modifier = Modifier.height(80.dp)) } } } +@Composable +@OptIn(ExperimentalMaterial3Api::class) +private fun StoreSearchBar( + searchQuery: TextFieldState, + placeholderText: String, +) { + SearchBarDefaults.InputField( + modifier = Modifier.fillMaxWidth().padding(start = 16.dp, end = 16.dp, bottom = 16.dp), + state = searchQuery, + leadingIcon = { Icon(Icons.Rounded.Search, null) }, + trailingIcon = { + IconButton({ searchQuery.clearText() }) { + Icon(imageVector = Icons.Rounded.Close, contentDescription = stringResource(strings.close)) + } + }, + onSearch = {}, + expanded = false, + onExpandedChange = {}, + placeholder = { Text(placeholderText) }, + ) +} + +@Composable +fun ThemeStoreCard( + themeEntry: ThemeStoreEntry, + isInstalled: Boolean, + onInstallClick: () -> Unit, + onUninstallClick: () -> Unit, +) { + val name = themeEntry.manifest.get("name")?.jsonPrimitive?.content ?: themeEntry.id + val progress = ExtensionRegistry.downloadProgress[themeEntry.id] + PreferenceTemplate( + modifier = Modifier.fillMaxWidth(), + startWidget = { + Icon( + painter = painterResource(drawables.palette), + contentDescription = null, + modifier = Modifier.size(48.dp).padding(8.dp), + tint = MaterialTheme.colorScheme.primary + ) + }, + title = { + Text(text = name, fontWeight = FontWeight.Bold, maxLines = 1, overflow = TextOverflow.Ellipsis) + }, + description = { + androidx.compose.foundation.layout.Column { + Text( + text = themeEntry.id, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = Typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + if (progress != null) { + androidx.compose.foundation.layout.Spacer(Modifier.height(4.dp)) + if (progress >= 0f) { + androidx.compose.material3.LinearProgressIndicator( + progress = { progress }, + modifier = Modifier.fillMaxWidth().height(4.dp), + color = MaterialTheme.colorScheme.primary + ) + } else { + androidx.compose.material3.LinearProgressIndicator( + modifier = Modifier.fillMaxWidth().height(4.dp), + color = MaterialTheme.colorScheme.primary + ) + } + } + } + }, + endWidget = { + if (isInstalled) { + IconButton( + onClick = onUninstallClick, + colors = IconButtonDefaults.iconButtonColors(contentColor = MaterialTheme.colorScheme.error), + ) { + Icon(Icons.Outlined.Delete, contentDescription = stringResource(strings.delete)) + } + } else { + IconButton( + onClick = onInstallClick + ) { + Icon(XedIcons.Download, contentDescription = null) + } + } + } + ) +} + +@Composable +fun IconPackStoreCard( + iconPackEntry: IconPackStoreEntry, + isInstalled: Boolean, + onInstallClick: () -> Unit, + onUninstallClick: () -> Unit, +) { + val name = iconPackEntry.manifest.name + val id = iconPackEntry.manifest.id + val progress = ExtensionRegistry.downloadProgress[id] + PreferenceTemplate( + modifier = Modifier.fillMaxWidth(), + startWidget = { + Icon( + painter = painterResource(drawables.widgets), + contentDescription = null, + modifier = Modifier.size(48.dp).padding(8.dp), + tint = MaterialTheme.colorScheme.secondary + ) + }, + title = { + Text(text = name, fontWeight = FontWeight.Bold, maxLines = 1, overflow = TextOverflow.Ellipsis) + }, + description = { + androidx.compose.foundation.layout.Column { + Text( + text = id, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = Typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + if (progress != null) { + androidx.compose.foundation.layout.Spacer(Modifier.height(4.dp)) + if (progress >= 0f) { + androidx.compose.material3.LinearProgressIndicator( + progress = { progress }, + modifier = Modifier.fillMaxWidth().height(4.dp), + color = MaterialTheme.colorScheme.primary + ) + } else { + androidx.compose.material3.LinearProgressIndicator( + modifier = Modifier.fillMaxWidth().height(4.dp), + color = MaterialTheme.colorScheme.primary + ) + } + } + } + }, + endWidget = { + if (isInstalled) { + IconButton( + onClick = onUninstallClick, + colors = IconButtonDefaults.iconButtonColors(contentColor = MaterialTheme.colorScheme.error), + ) { + Icon(Icons.Outlined.Delete, contentDescription = stringResource(strings.delete)) + } + } else { + IconButton( + onClick = onInstallClick + ) { + Icon(XedIcons.Download, contentDescription = null) + } + } + } + ) +} + @Composable @OptIn(ExperimentalMaterial3Api::class) private fun ExtensionSearchBar( @@ -324,17 +740,6 @@ private fun ExtensionSearchBar( ) } -private suspend fun applySort( - currentSortOption: ExtensionSortOptions, - filteredExtensions: List, -): List = - when (currentSortOption) { - ExtensionSortOptions.NAME -> filteredExtensions.sortedBy { it.name } - ExtensionSortOptions.RATING -> filteredExtensions.sortedBy { it.id } // TODO: RATING - - ExtensionSortOptions.DATE_ADDED -> filteredExtensions.sortedBy { it.id } // TODO: DATE_ADDED - } - private fun applyFilter( searchQuery: TextFieldState, extensions: List, diff --git a/core/main/src/main/java/com/rk/settings/extension/ExtensionSettings.kt b/core/main/src/main/java/com/rk/settings/extension/ExtensionSettings.kt new file mode 100644 index 000000000..c129e06c7 --- /dev/null +++ b/core/main/src/main/java/com/rk/settings/extension/ExtensionSettings.kt @@ -0,0 +1,25 @@ +package com.rk.settings.extension + +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.rk.App +import com.rk.components.compose.preferences.base.PreferenceLayout +import com.rk.extension.Extension +import com.rk.resources.strings + +@Composable +fun ExtensionSettings(extension: Extension?) { + val api = App.extensionManager.loadedExtensions[extension]?.api + + PreferenceLayout(label = extension?.name ?: stringResource(strings.ext_not_found)) { + if (extension == null || api == null) { + Text(stringResource(strings.ext_not_found_desc), modifier = Modifier.padding(horizontal = 16.dp)) + } else { + api.SettingsContent() + } + } +} diff --git a/core/main/src/main/java/com/rk/settings/extension/MarkdownViewer.kt b/core/main/src/main/java/com/rk/settings/extension/MarkdownViewer.kt index 4149c02bb..2be93a5ec 100644 --- a/core/main/src/main/java/com/rk/settings/extension/MarkdownViewer.kt +++ b/core/main/src/main/java/com/rk/settings/extension/MarkdownViewer.kt @@ -2,11 +2,13 @@ package com.rk.settings.extension import android.graphics.Typeface import android.text.Spanned +import android.text.method.LinkMovementMethod import android.widget.TextView import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.text.selection.LocalTextSelectionColors import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme @@ -26,9 +28,11 @@ import com.rk.components.StateScreen import com.rk.resources.drawables import com.rk.resources.strings import io.github.rosemoe.sora.lsp.editor.text.SimpleMarkdownRenderer +import com.rk.utils.okHttpClient import java.io.File import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import okhttp3.CacheControl import okhttp3.OkHttpClient import okhttp3.Request @@ -50,11 +54,12 @@ sealed interface MarkdownStatus { fun MarkdownViewer(url: String?, refreshKey: Int, onLoaded: () -> Unit, modifier: Modifier = Modifier) { var state by remember(url) { mutableStateOf(MarkdownStatus.Loading) } val primaryColor = MaterialTheme.colorScheme.primary - val client = remember { OkHttpClient() } + val client = remember { okHttpClient } LaunchedEffect(url, refreshKey) { state = MarkdownStatus.Loading - state = loadMarkdown(url, primaryColor.toArgb(), client) + val forceRefresh = refreshKey > 0 + state = loadMarkdown(url, primaryColor.toArgb(), client, forceRefresh) onLoaded() } AnimatedContent(targetState = state, modifier = modifier.fillMaxWidth()) { state -> @@ -79,9 +84,16 @@ fun MarkdownViewer(url: String?, refreshKey: Int, onLoaded: () -> Unit, modifier } is MarkdownStatus.Success -> { + val selectionColors = LocalTextSelectionColors.current + val selectionBackground = selectionColors.backgroundColor AndroidView( factory = { ctx -> TextView(ctx) }, - update = { it.text = state.spanned }, + update = { + it.text = state.spanned + it.setTextIsSelectable(true) + it.movementMethod = LinkMovementMethod.getInstance() + it.highlightColor = selectionBackground.toArgb() + }, modifier = Modifier.fillMaxWidth(), ) } @@ -89,7 +101,12 @@ fun MarkdownViewer(url: String?, refreshKey: Int, onLoaded: () -> Unit, modifier } } -private suspend fun loadMarkdown(url: String?, primaryColor: Int, client: OkHttpClient): MarkdownStatus { +private suspend fun loadMarkdown( + url: String?, + primaryColor: Int, + client: OkHttpClient, + forceRefresh: Boolean = false +): MarkdownStatus { return withContext(Dispatchers.IO) { if (url == null) { return@withContext MarkdownStatus.Error.Empty @@ -98,7 +115,11 @@ private suspend fun loadMarkdown(url: String?, primaryColor: Int, client: OkHttp runCatching { val markdown = if (url.startsWith("http://") || url.startsWith("https://")) { - val request = Request.Builder().url(url).build() + val requestBuilder = Request.Builder().url(url) + if (forceRefresh) { + requestBuilder.cacheControl(CacheControl.FORCE_NETWORK) + } + val request = requestBuilder.build() client.newCall(request).execute().use { response -> if (!response.isSuccessful) { return@withContext when (response.code) { diff --git a/core/main/src/main/java/com/rk/settings/extension/SourceCodeSheet.kt b/core/main/src/main/java/com/rk/settings/extension/SourceCodeSheet.kt index 1444fe681..5254fb8b7 100644 --- a/core/main/src/main/java/com/rk/settings/extension/SourceCodeSheet.kt +++ b/core/main/src/main/java/com/rk/settings/extension/SourceCodeSheet.kt @@ -20,7 +20,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.core.net.toUri -import com.rk.components.SettingsToggle +import com.rk.components.SettingsItem import com.rk.components.compose.preferences.base.PreferenceGroup import com.rk.extension.Extension import com.rk.resources.drawables @@ -81,7 +81,7 @@ fun SourceCodeSheet(extension: Extension, onDismissRequest: () -> Unit) { Text(sheetDescription, modifier = Modifier.padding(horizontal = 16.dp)) PreferenceGroup { - SettingsToggle( + SettingsItem( label = stringResource(sourceCodeProvider.viewStringRes), description = extension.repository, isEnabled = true, diff --git a/core/main/src/main/java/com/rk/settings/extension/extensionActions.kt b/core/main/src/main/java/com/rk/settings/extension/extensionActions.kt index c2121db22..c4ad5f296 100644 --- a/core/main/src/main/java/com/rk/settings/extension/extensionActions.kt +++ b/core/main/src/main/java/com/rk/settings/extension/extensionActions.kt @@ -1,25 +1,35 @@ package com.rk.settings.extension +import android.app.NotificationChannel +import android.app.NotificationManager +import androidx.core.app.NotificationCompat +import com.rk.DefaultScope +import com.rk.XedConstants +import com.rk.extension.manager.ExtensionRegistry +import java.io.File +import com.rk.resources.drawables import android.app.Activity import android.content.Context import android.net.Uri import androidx.appcompat.app.AppCompatActivity -import com.rk.App +import com.rk.App.Companion.extensionManager import com.rk.activities.settings.SettingsActivity +import com.rk.crashhandler.CrashActivity import com.rk.extension.Extension import com.rk.extension.ExtensionError import com.rk.extension.InstallResult import com.rk.extension.LocalExtension import com.rk.extension.StoreExtension -import com.rk.extension.installExtensionFromZip -import com.rk.extension.load +import com.rk.extension.UpdatableExtension +import com.rk.extension.loader.installExtensionFromZip +import com.rk.extension.loader.load import com.rk.file.toFileObject import com.rk.resources.getFilledString import com.rk.resources.getString import com.rk.resources.strings import com.rk.utils.LoadingPopup import com.rk.utils.application -import com.rk.utils.dialog +import com.rk.utils.dialogRes import com.rk.utils.errorDialog import com.rk.utils.toast import kotlinx.coroutines.CoroutineScope @@ -29,58 +39,363 @@ import kotlinx.coroutines.withContext import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.MissingFieldException -suspend fun runExtensionUninstallAction( +fun checkExtensionWarningAndRun(activity: AppCompatActivity?, onApproved: () -> Unit) { + if (com.rk.settings.Settings.warn_extensions) { + dialogRes( + activity = activity, + title = strings.attention.getString(), + msg = strings.extension_warning_msg.getString(), + okRes = strings.continue_action, + cancelRes = strings.cancel, + onOk = { + com.rk.settings.Settings.warn_extensions = false + onApproved() + }, + onCancel = {} + ) + } else { + onApproved() + } +} + +fun runExtensionUninstallAction( extension: Extension, updateInstallState: (InstallState) -> Unit, + scope: CoroutineScope, activity: AppCompatActivity?, ) { - App.extensionManager.uninstallExtension(extension.id).onFailure { - errorDialog(it, activity) - return + dialogRes( + activity = activity, + title = strings.uninstall_ext_dialog.getString(), + msg = strings.uninstall_ext_dialog_desc.getFilledString(extension.name), + okRes = strings.uninstall, + onOk = { + scope.launch(Dispatchers.IO) { + extensionManager.uninstallExtension(extension.id).onFailure { error -> + withContext(Dispatchers.Main) { + errorDialog(activity, error) + } + return@launch + } + withContext(Dispatchers.Main) { + updateInstallState(InstallState.Idle) + } + } + }, + onCancel = {}, + ) +} + + +private fun showDownloadNotification( + context: Context, + id: String, + title: String, + progress: Float, + isFinished: Boolean = false, + errorMessage: String? = null +) { + val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + val channelId = "store_downloads" + + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { + val channel = NotificationChannel( + channelId, + "Store Downloads", + NotificationManager.IMPORTANCE_LOW + ).apply { + description = "Notifications for store downloads and installations" + } + notificationManager.createNotificationChannel(channel) + } + + val builder = NotificationCompat.Builder(context, channelId) + .setSmallIcon(drawables.extension) + .setOnlyAlertOnce(true) + + if (isFinished) { + if (errorMessage != null) { + builder.setContentTitle(strings.install_failed.getString(context)) + .setContentText(errorMessage) + .setOngoing(false) + .setAutoCancel(true) + } else { + builder.setContentTitle(strings.installed.getString(context)) + .setContentText(title) + .setOngoing(false) + .setAutoCancel(true) + } + } else { + builder.setContentTitle(title) + .setOngoing(true) + if (progress >= 0f) { + val percent = (progress * 100).toInt() + builder.setContentText("$percent%") + .setProgress(100, percent, false) + } else { + builder.setContentText(strings.installing.getString(context)) + .setProgress(100, 0, true) + } + } + + runCatching { + notificationManager.notify(id.hashCode(), builder.build()) } - updateInstallState(InstallState.Idle) } -suspend fun runExtensionInstallAction( +fun runExtensionInstallAction( extension: Extension, updateInstallState: (InstallState) -> Unit, - scope: CoroutineScope, context: Context, activity: AppCompatActivity?, ) { + val storeExt = extension as? StoreExtension ?: return + val id = storeExt.id + val name = storeExt.name + + ExtensionRegistry.activeInstalls[id] = InstallState.Installing + ExtensionRegistry.downloadProgress[id] = 0f updateInstallState(InstallState.Installing) - var loading: LoadingPopup? = null - runCatching { - val extension = extension as? StoreExtension ?: return + showDownloadNotification(context, id, name, 0f) + + DefaultScope.launch(Dispatchers.IO) { + var success = false + var errorMsg: String? = null + val tempFile = File(context.cacheDir, "ext_download_${id}.zip") + + try { + var lastNotificationTime = 0L + + val downloadSuccess = ExtensionRegistry.downloadFileWithProgress( + url = "${XedConstants.EXTENSION_API_BASE}/$id/plugin.zip", + destFile = tempFile, + onProgress = { progress -> + DefaultScope.launch(Dispatchers.Main) { + ExtensionRegistry.downloadProgress[id] = progress + } + val now = System.currentTimeMillis() + if (now - lastNotificationTime > 300) { + lastNotificationTime = now + showDownloadNotification(context, id, name, progress) + } + } + ) - loading = LoadingPopup(activity).show() - loading.setMessage(strings.installing.getString()) + if (downloadSuccess) { + showDownloadNotification(context, id, name, 1f) - val result = - App.extensionManager.installStoreExtension(context, extension).getOrElse { - loading.hide() - errorDialog(it.message ?: strings.unknown_error.getString(), activity) + val result = extensionManager.installExtensionFromZip(tempFile) + if (result is InstallResult.Success) { + extensionManager.setExtensionDisabled(result.extension.id, false) + result.extension.load(application!!, true).onFailure { error -> + extensionManager.setExtensionDisabled(result.extension.id, true) + errorMsg = error.message ?: "Failed to load extension" + withContext(Dispatchers.Main) { + activity?.let { + CrashActivity.start(it, result.extension, error) + } ?: run { + errorDialog(activity, msg = errorMsg) + } + } + } + success = errorMsg == null + } else if (result is InstallResult.Error) { + errorMsg = when (result.error) { + ExtensionError.OUTDATED_CLIENT -> strings.outdated_client.getString(context) + ExtensionError.OUTDATED_EXTENSION -> strings.outdated_extension.getString(context) + } + } else if (result is InstallResult.ValidationFailed) { + errorMsg = result.error?.message ?: "Validation failed" + } + } else { + errorMsg = "Download failed" + } + } catch (e: Exception) { + e.printStackTrace() + errorMsg = e.message ?: "Unknown error" + } finally { + if (tempFile.exists()) { + tempFile.delete() + } + + withContext(Dispatchers.Main) { + ExtensionRegistry.activeInstalls.remove(id) + ExtensionRegistry.downloadProgress.remove(id) + + if (success) { + showDownloadNotification(context, id, name, 1f, isFinished = true) + updateInstallState(InstallState.Installed) + } else { + showDownloadNotification(context, id, name, 0f, isFinished = true, errorMessage = errorMsg) updateInstallState(InstallState.Idle) - return@runCatching + errorDialog(activity, msg = errorMsg ?: "Unknown error") + } + } + } + } +} + +fun runExtensionUpdateAction( + extension: UpdatableExtension, + updateInstallState: (InstallState) -> Unit, + context: Context, + activity: AppCompatActivity?, +) { + val store = extension.store + val id = store.id + val name = store.name + + ExtensionRegistry.activeInstalls[id] = InstallState.Updating + ExtensionRegistry.downloadProgress[id] = 0f + updateInstallState(InstallState.Updating) + + showDownloadNotification(context, id, name, 0f) + + DefaultScope.launch(Dispatchers.IO) { + var success = false + var errorMsg: String? = null + val tempFile = File(context.cacheDir, "ext_download_${id}.zip") + + try { + var lastNotificationTime = 0L + + val downloadSuccess = ExtensionRegistry.downloadFileWithProgress( + url = "${XedConstants.EXTENSION_API_BASE}/$id/plugin.zip", + destFile = tempFile, + onProgress = { progress -> + DefaultScope.launch(Dispatchers.Main) { + ExtensionRegistry.downloadProgress[id] = progress + } + val now = System.currentTimeMillis() + if (now - lastNotificationTime > 300) { + lastNotificationTime = now + showDownloadNotification(context, id, name, progress) + } } + ) - handleInstallResult(result, activity, { updateInstallState(InstallState.Idle) }) { ext -> - updateInstallState(InstallState.Installed) + if (downloadSuccess) { + showDownloadNotification(context, id, name, 1f) - scope.launch(Dispatchers.Default) { - ext.load(application!!).onFailure { - errorDialog(it.message ?: strings.unknown_error.getString(), activity) + val result = extensionManager.installExtensionFromZip(tempFile) + if (result is InstallResult.Success) { + extensionManager.setExtensionDisabled(result.extension.id, false) + result.extension.load(application!!).onFailure { error -> + extensionManager.setExtensionDisabled(result.extension.id, true) + errorMsg = error.message ?: "Failed to load extension" + withContext(Dispatchers.Main) { + activity?.let { + CrashActivity.start(it, result.extension, error) + } ?: run { + errorDialog(activity, msg = errorMsg) + } + } } + success = errorMsg == null + } else if (result is InstallResult.Error) { + errorMsg = when (result.error) { + ExtensionError.OUTDATED_CLIENT -> strings.outdated_client.getString(context) + ExtensionError.OUTDATED_EXTENSION -> strings.outdated_extension.getString(context) + } + } else if (result is InstallResult.ValidationFailed) { + errorMsg = result.error?.message ?: "Validation failed" + } + } else { + errorMsg = "Download failed" + } + } catch (e: Exception) { + e.printStackTrace() + errorMsg = e.message ?: "Unknown error" + } finally { + if (tempFile.exists()) { + tempFile.delete() + } + + withContext(Dispatchers.Main) { + ExtensionRegistry.activeInstalls.remove(id) + ExtensionRegistry.downloadProgress.remove(id) + + if (success) { + showDownloadNotification(context, id, name, 1f, isFinished = true) + updateInstallState(InstallState.Installed) + } else { + showDownloadNotification(context, id, name, 0f, isFinished = true, errorMessage = errorMsg) + updateInstallState(InstallState.Idle) + errorDialog(activity, msg = errorMsg ?: "Unknown error") } } - loading.hide() } - .onFailure { - loading?.hide() - errorDialog(it, activity) - updateInstallState(InstallState.Idle) + } +} + +fun runIconPackInstallAction( + id: String, + name: String, + context: Context, + activity: AppCompatActivity?, +) { + ExtensionRegistry.activeInstalls[id] = InstallState.Installing + ExtensionRegistry.downloadProgress[id] = 0f + + showDownloadNotification(context, id, name, 0f) + + DefaultScope.launch(Dispatchers.IO) { + var success = false + var errorMsg: String? = null + val tempFile = File(context.cacheDir, "iconpack_${id}.zip") + + try { + var lastNotificationTime = 0L + + val downloadSuccess = ExtensionRegistry.downloadFileWithProgress( + url = "${XedConstants.ICONPACKS_API_BASE}/$id/iconpack.zip", + destFile = tempFile, + onProgress = { progress -> + DefaultScope.launch(Dispatchers.Main) { + ExtensionRegistry.downloadProgress[id] = progress + } + val now = System.currentTimeMillis() + if (now - lastNotificationTime > 300) { + lastNotificationTime = now + showDownloadNotification(context, id, name, progress) + } + } + ) + + if (downloadSuccess) { + showDownloadNotification(context, id, name, 1f) + runCatching { + com.rk.App.Companion.iconPackManager.installIconPack(tempFile) + }.onSuccess { + success = true + }.onFailure { + errorMsg = it.message ?: "Failed to install icon pack" + } + } else { + errorMsg = "Download failed" + } + } catch (e: Exception) { + e.printStackTrace() + errorMsg = e.message ?: "Unknown error" + } finally { + if (tempFile.exists()) { + tempFile.delete() + } + + withContext(Dispatchers.Main) { + ExtensionRegistry.activeInstalls.remove(id) + ExtensionRegistry.downloadProgress.remove(id) + + if (success) { + showDownloadNotification(context, id, name, 1f, isFinished = true) + } else { + showDownloadNotification(context, id, name, 0f, isFinished = true, errorMessage = errorMsg) + errorDialog(activity, msg = errorMsg ?: "Unknown error") + } + } } + } } fun installExtensionFromUri(scope: CoroutineScope, uri: Uri?, activity: AppCompatActivity?) { @@ -88,43 +403,54 @@ fun installExtensionFromUri(scope: CoroutineScope, uri: Uri?, activity: AppCompa scope.launch(Dispatchers.IO) { runCatching { - if (uri == null) return@runCatching + if (uri == null) return@runCatching - val fileObject = uri.toFileObject(expectedIsFile = true) - val exists = fileObject.exists() - val canRead = fileObject.canRead() - val isZip = fileObject.getName().endsWith(".zip") + val fileObject = uri.toFileObject(expectedIsFile = true) + val exists = fileObject.exists() + val canRead = fileObject.canRead() + val isZip = fileObject.getName().endsWith(".zip") - if (exists && canRead && isZip) { - withContext(Dispatchers.Main) { - loading = LoadingPopup(activity).show() - loading.setMessage(strings.installing.getString()) - } + if (exists && canRead && isZip) { + withContext(Dispatchers.Main) { + loading = LoadingPopup(activity).show() + loading.setMessage(strings.installing.getString()) + } - val result = App.extensionManager.installExtensionFromZip(fileObject) + val result = extensionManager.installExtensionFromZip(fileObject) - withContext(Dispatchers.Main) { - handleInstallResult(result, activity) { ext -> - scope.launch(Dispatchers.Default) { - ext.load(application!!).onFailure { - errorDialog(it.message ?: strings.unknown_error.getString(), activity) - } + if (result is InstallResult.Success) { + extensionManager.setExtensionDisabled(result.extension.id, false) + val initialInstallation = !result.performedUpdate + result.extension.load(application!!, initialInstallation).onFailure { error -> + extensionManager.setExtensionDisabled(result.extension.id, true) + withContext(Dispatchers.Main) { + activity?.let { + CrashActivity.start(it, result.extension, error) + } ?: run { + errorDialog(activity, msg = error.message ?: strings.unknown_error.getString()) } } - - loading?.hide() } - } else { + } + + withContext(Dispatchers.Main) { + handleInstallResult(result, activity) + loading?.hide() + } + } else { + withContext(Dispatchers.Main) { errorDialog( - "Install criteria failed \nis_zip = $isZip\ncan_read = $canRead\n exists = $exists\nuri = ${fileObject.getAbsolutePath()}", activity, + msg = "Install criteria failed \nis_zip = $isZip\ncan_read = $canRead\n exists = $exists\nuri = ${fileObject.getAbsolutePath()}", ) } } - .onFailure { + }.onFailure { error -> + withContext(Dispatchers.Main) { loading?.hide() - errorDialog(it, activity) + errorDialog(activity, error) } + } } } @@ -136,16 +462,12 @@ private fun handleInstallResult( onSuccess: (LocalExtension) -> Unit = {}, ) = when (result) { - is InstallResult.AlreadyInstalled -> { - // errorDialog("Extension already installed", activity) - } - is InstallResult.Error -> { when (result.error) { ExtensionError.OUTDATED_CLIENT -> - errorDialog(strings.outdated_client.getString(), activity, strings.install_failed.getString()) + errorDialog(activity, strings.install_failed.getString(), strings.outdated_client.getString()) ExtensionError.OUTDATED_EXTENSION -> - errorDialog(strings.outdated_extension.getString(), activity, strings.install_failed.getString()) + errorDialog(activity, strings.install_failed.getString(), strings.outdated_extension.getString()) } onError() } @@ -159,7 +481,7 @@ private fun handleInstallResult( val e = result.error if (e is MissingFieldException) { val fields = e.missingFields.joinToString("\n") { "• $it" } - dialog( + dialogRes( SettingsActivity.instance, strings.extension_validation_failed.getString(), strings.manifest_missing_fields.getFilledString(fields), @@ -168,8 +490,8 @@ private fun handleInstallResult( onError() } else { errorDialog( - e?.localizedMessage ?: strings.unknown_error.getString(), activity, + e?.localizedMessage ?: strings.unknown_error.getString(), strings.extension_validation_failed.getString(), ) onError() diff --git a/core/main/src/main/java/com/rk/settings/git/GitSettings.kt b/core/main/src/main/java/com/rk/settings/git/GitSettings.kt index 47631ee64..6b91b6cef 100644 --- a/core/main/src/main/java/com/rk/settings/git/GitSettings.kt +++ b/core/main/src/main/java/com/rk/settings/git/GitSettings.kt @@ -7,7 +7,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.res.stringResource import com.rk.components.DoubleInputDialog -import com.rk.components.SettingsToggle +import com.rk.components.SettingsItem import com.rk.components.compose.preferences.base.PreferenceGroup import com.rk.components.compose.preferences.base.PreferenceLayout import com.rk.resources.strings @@ -25,7 +25,7 @@ fun GitSettings() { var email by remember { mutableStateOf(Settings.git_email) } PreferenceGroup(heading = stringResource(strings.general)) { - SettingsToggle( + SettingsItem( label = stringResource(strings.git_colorize_files), description = stringResource(strings.git_colorize_files_desc), default = Settings.git_colorize_names, @@ -34,7 +34,7 @@ fun GitSettings() { } PreferenceGroup(heading = stringResource(strings.account)) { - SettingsToggle( + SettingsItem( label = stringResource(strings.credentials), description = stringResource(strings.credentials_desc), showSwitch = false, @@ -42,7 +42,7 @@ fun GitSettings() { sideEffect = { showCredentialsDialog = true }, ) - SettingsToggle( + SettingsItem( label = stringResource(strings.user_data), description = stringResource(strings.user_data_desc), showSwitch = false, @@ -52,14 +52,14 @@ fun GitSettings() { } PreferenceGroup(heading = stringResource(strings.repository)) { - SettingsToggle( + SettingsItem( label = stringResource(strings.submodules), description = stringResource(strings.submodules_desc), default = Settings.git_submodules, sideEffect = { Settings.git_submodules = it }, ) - SettingsToggle( + SettingsItem( label = stringResource(strings.recursive_submodules), description = stringResource(strings.recursive_submodules_desc), default = Settings.git_recursive_submodules, diff --git a/core/main/src/main/java/com/rk/settings/keybinds/KeyUtils.kt b/core/main/src/main/java/com/rk/settings/keybinds/KeyUtils.kt index 1c1041f0d..7683273a3 100644 --- a/core/main/src/main/java/com/rk/settings/keybinds/KeyUtils.kt +++ b/core/main/src/main/java/com/rk/settings/keybinds/KeyUtils.kt @@ -85,7 +85,7 @@ data object KeyUtils { } fun getKeyIcon(keyCode: Int): Icon { - return Icon.DrawableRes( + return Icon.ResourceIcon( when (keyCode) { KeyEvent.KEYCODE_SHIFT_LEFT -> drawables.shift KeyEvent.KEYCODE_DPAD_DOWN -> drawables.chevron_down diff --git a/core/main/src/main/java/com/rk/settings/keybinds/KeybindingsScreen.kt b/core/main/src/main/java/com/rk/settings/keybinds/KeybindingsScreen.kt index 2861676a6..2f07434a4 100644 --- a/core/main/src/main/java/com/rk/settings/keybinds/KeybindingsScreen.kt +++ b/core/main/src/main/java/com/rk/settings/keybinds/KeybindingsScreen.kt @@ -78,21 +78,22 @@ fun KeybindingsScreen() { val commands = CommandProvider.commandList val filteredCommands = if (searchQuery.text.isEmpty()) { - commands - } else { - val query = searchQuery.text + commands + } else { + val query = searchQuery.text - commands.filter { command -> - val labelMatch = command.getLabel().contains(query, ignoreCase = true) - val prefixMatch = command.prefix?.contains(query, ignoreCase = true) == true - val keybindMatch = - KeybindingsManager.getKeyCombinationForCommand(command.id) - ?.getDisplayName() - ?.contains(query, ignoreCase = true) == true + commands.filter { command -> + val labelMatch = command.getLabel().contains(query, ignoreCase = true) + val prefixMatch = command.prefix?.contains(query, ignoreCase = true) == true + val keybindMatch = + KeybindingsManager.getKeyCombinationForCommand(command.id) + ?.getDisplayName() + ?.contains(query, ignoreCase = true) == true - labelMatch || prefixMatch || keybindMatch + labelMatch || prefixMatch || keybindMatch + } } - } + .sortedBy { it.getLabel() } PreferenceLayoutLazyColumn( label = stringResource(id = strings.keybindings), diff --git a/core/main/src/main/java/com/rk/settings/language/Language.kt b/core/main/src/main/java/com/rk/settings/language/Language.kt index 76588cabc..40ac19cb8 100644 --- a/core/main/src/main/java/com/rk/settings/language/Language.kt +++ b/core/main/src/main/java/com/rk/settings/language/Language.kt @@ -27,7 +27,7 @@ import androidx.core.os.LocaleListCompat import com.google.gson.Gson import com.google.gson.reflect.TypeToken import com.rk.components.InfoBlock -import com.rk.components.SettingsToggle +import com.rk.components.SettingsItem import com.rk.components.compose.preferences.base.PreferenceGroup import com.rk.components.compose.preferences.base.PreferenceLayout import com.rk.resources.strings @@ -104,7 +104,7 @@ fun LanguageScreen(modifier: Modifier = Modifier) { if (locales != null) { locales.forEach { localeInfo -> - SettingsToggle( + SettingsItem( modifier = Modifier, label = localeInfo.displayName, default = false, @@ -120,7 +120,7 @@ fun LanguageScreen(modifier: Modifier = Modifier) { ) } } else { - SettingsToggle( + SettingsItem( modifier = Modifier, label = stringResource(strings.loading), default = false, diff --git a/core/main/src/main/java/com/rk/settings/lsp/LspServerDetail.kt b/core/main/src/main/java/com/rk/settings/lsp/LspServerDetail.kt index 4864ba361..1ebf76868 100644 --- a/core/main/src/main/java/com/rk/settings/lsp/LspServerDetail.kt +++ b/core/main/src/main/java/com/rk/settings/lsp/LspServerDetail.kt @@ -1,5 +1,6 @@ package com.rk.settings.lsp +import androidx.activity.compose.LocalActivity import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -44,10 +45,11 @@ import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.currentStateAsState import androidx.navigation.NavHostController import com.rk.activities.settings.snackbarHostStateRef -import com.rk.components.SettingsToggle +import com.rk.components.SettingsItem import com.rk.components.compose.preferences.base.PreferenceGroup import com.rk.components.compose.preferences.base.PreferenceGroupHeading import com.rk.components.compose.preferences.base.PreferenceLayout +import com.rk.icons.XedIcon import com.rk.lsp.DefinitionPrevention import com.rk.lsp.LspConnectionStatus import com.rk.lsp.LspServer @@ -72,6 +74,7 @@ enum class LspInstallationAction { fun LspServerDetail(navController: NavHostController, server: LspServer) { val scope = rememberCoroutineScope() val context = LocalContext.current + val activity = LocalActivity.current var refreshKey by remember { mutableIntStateOf(0) } val lifecycleOwner = LocalLifecycleOwner.current @@ -97,7 +100,7 @@ fun LspServerDetail(navController: NavHostController, server: LspServer) { if (!server.canBeUninstalled) return FilledTonalButton( - onClick = { server.uninstall(context) }, + onClick = { activity?.let { server.uninstall(it) } }, colors = ButtonDefaults.filledTonalButtonColors() .copy( @@ -113,7 +116,7 @@ fun LspServerDetail(navController: NavHostController, server: LspServer) { @Composable fun UpdateButton() { - FilledTonalButton(onClick = { server.update(context) }) { + FilledTonalButton(onClick = { activity?.let { server.update(it) } }) { Icon(painter = painterResource(drawables.update), contentDescription = stringResource(strings.update)) Spacer(Modifier.width(12.dp)) Text(stringResource(strings.update)) @@ -122,7 +125,7 @@ fun LspServerDetail(navController: NavHostController, server: LspServer) { @Composable fun DownloadButton() { - FilledTonalButton(onClick = { server.install(context) }) { + FilledTonalButton(onClick = { activity?.let { server.install(it) } }) { Icon(painter = painterResource(drawables.download), contentDescription = stringResource(strings.download)) Spacer(Modifier.width(12.dp)) Text(stringResource(strings.install)) @@ -131,7 +134,7 @@ fun LspServerDetail(navController: NavHostController, server: LspServer) { @Composable fun LspFeatureToggle(label: String, description: String? = null, preferenceId: String, server: LspServer) { - SettingsToggle( + SettingsItem( label = label, description = description, default = Preference.getBoolean(preferenceId, true), @@ -154,8 +157,8 @@ fun LspServerDetail(navController: NavHostController, server: LspServer) { Column(modifier = Modifier.padding(16.dp)) { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { server.icon?.let { - Icon( - painter = painterResource(it), + XedIcon( + icon = it, contentDescription = null, modifier = Modifier.size(42.dp).padding(end = 8.dp), ) @@ -221,7 +224,7 @@ fun LspServerDetail(navController: NavHostController, server: LspServer) { shape = MaterialTheme.shapes.large, tonalElevation = 1.dp, ) { - SettingsToggle( + SettingsItem( modifier = Modifier, label = stringResource(strings.no_instances), default = false, @@ -234,6 +237,13 @@ fun LspServerDetail(navController: NavHostController, server: LspServer) { } PreferenceGroup(heading = stringResource(strings.features)) { + LspFeatureToggle( + label = stringResource(strings.document_highlight), + description = stringResource(strings.document_highlight_desc), + preferenceId = "lsp_${server.id}_document_highlight", + server = server, + ) + LspFeatureToggle( label = stringResource(strings.hover_information), description = stringResource(strings.hover_information_desc), diff --git a/core/main/src/main/java/com/rk/settings/lsp/LspServerLogs.kt b/core/main/src/main/java/com/rk/settings/lsp/LspServerLogs.kt index 573f952f0..bb9212eef 100644 --- a/core/main/src/main/java/com/rk/settings/lsp/LspServerLogs.kt +++ b/core/main/src/main/java/com/rk/settings/lsp/LspServerLogs.kt @@ -1,40 +1,114 @@ package com.rk.settings.lsp +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Checkbox +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuAnchorType +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.ExposedDropdownMenuDefaults import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.mutableStateSetOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp import androidx.core.content.pm.PackageInfoCompat +import com.rk.components.StyledTextField import com.rk.lsp.LspConnectionStatus import com.rk.lsp.LspRegistry import com.rk.lsp.LspServer import com.rk.lsp.LspServerInstance +import com.rk.lsp.MessageSource import com.rk.resources.drawables import com.rk.resources.getString import com.rk.resources.strings -import com.rk.settings.debugOptions.LogLevel import com.rk.settings.debugOptions.LogScreen import com.rk.utils.application import com.rk.xededitor.BuildConfig import kotlinx.coroutines.launch +import org.eclipse.lsp4j.MessageType @OptIn(ExperimentalMaterial3Api::class) @Composable fun LspServerLogs(server: LspServer, id: String) { val scope = rememberCoroutineScope() - var logLevel by remember { mutableStateOf(LogLevel.INFO) } + var messageType by remember { mutableStateOf(MessageType.Info) } + + val messageSources = remember { mutableStateSetOf(MessageSource.LSP, MessageSource.Runtime, MessageSource.Client, MessageSource.RPC) } + var sourceDropdownExpanded by remember { mutableStateOf(false) } val instance = server.instances.find { it.id == id } - val logText = instance?.let { buildLogs(server, it, logLevel) } ?: strings.instance_not_found.getString() + val logText = + instance?.let { buildLogs(server, it, messageType, messageSources) } ?: strings.instance_not_found.getString() + + LogScreen(logText, "Language Server Error Report", "server_logs") { + var dropdownMenuExpanded by remember { mutableStateOf(false) } + + ExposedDropdownMenuBox( + expanded = dropdownMenuExpanded, + onExpandedChange = { dropdownMenuExpanded = !dropdownMenuExpanded }, + modifier = Modifier.weight(1f).padding(horizontal = 8.dp), + ) { + StyledTextField( + value = messageType.name + " (LSP)", + onValueChange = {}, + shape = RoundedCornerShape(8.dp), + maxLines = 1, + readOnly = true, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = dropdownMenuExpanded) }, + modifier = + Modifier.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable).fillMaxWidth().height(42.dp), + ) + + ExposedDropdownMenu(expanded = dropdownMenuExpanded, onDismissRequest = { dropdownMenuExpanded = false }) { + MessageType.entries.forEach { type -> + DropdownMenuItem( + text = { Text(text = type.name) }, + onClick = { + messageType = type + dropdownMenuExpanded = false + }, + ) + } + } + } + + IconButton(onClick = { sourceDropdownExpanded = !sourceDropdownExpanded }) { + Icon( + painter = painterResource(drawables.filter), + contentDescription = stringResource(strings.filter_options), + ) + + DropdownMenu(expanded = sourceDropdownExpanded, onDismissRequest = { sourceDropdownExpanded = false }) { + MessageSource.entries.forEach { source -> + DropdownMenuItem( + text = { Text(text = source.name) }, + leadingIcon = { Checkbox(checked = messageSources.contains(source), onCheckedChange = null) }, + onClick = { + if (messageSources.contains(source)) { + messageSources.remove(source) + } else { + messageSources.add(source) + } + }, + ) + } + } + } - LogScreen(logText, "Language Server Error Report", "server_logs", logLevel, { logLevel = it }) { val isRunning = instance?.status != LspConnectionStatus.NOT_RUNNING && instance?.status != LspConnectionStatus.CRASHED && @@ -55,29 +129,39 @@ fun LspServerLogs(server: LspServer, id: String) { } } -private fun buildLogs(server: LspServer, instance: LspServerInstance, logLevel: LogLevel): String { +private fun buildLogs( + server: LspServer, + instance: LspServerInstance, + messageType: MessageType, + messageSources: Set, +): String { val entries = instance - .getLogs() - .filter { it.level.ordinal >= logLevel.ordinal } - .joinToString("\n") { "[${it.level.name.uppercase()}] ${it.message}" } + .getLspLogs() + .filter { messageSources.contains(it.source) } + .filter { it.source == MessageSource.RPC || it.type == null || it.type.value <= messageType.value } + .joinToString("\n") { + val sourceString = it.source.name.uppercase().let { source -> "[$source]" } + val levelString = it.type?.name?.uppercase() ?: "" + "$sourceString $levelString ${it.message}" + } val packageInfo = with(application!!) { packageManager.getPackageInfo(packageName, 0) } val versionName = packageInfo.versionName val versionCode = PackageInfoCompat.getLongVersionCode(packageInfo) return buildString { - append("[DEBUG] App version: ").append(versionName).appendLine() - append("[DEBUG] Version code: ").append(versionCode).appendLine() - append("[DEBUG] Commit hash: ").append(BuildConfig.GIT_COMMIT_HASH.substring(0, 8)).appendLine() + append("[REPORT] App version: ").append(versionName).appendLine() + append("[REPORT] Version code: ").append(versionCode).appendLine() + append("[REPORT] Commit hash: ").append(BuildConfig.GIT_COMMIT_HASH.substring(0, 8)).appendLine() appendLine() - append("[DEBUG] Server name: ").append(server.serverName).appendLine() - append("[DEBUG] Server id: ").append(server.id).appendLine() - append("[DEBUG] Server status: ").append(instance.status.name).appendLine() - append("[DEBUG] Supported extensions: ").append(server.supportedExtensions).appendLine() - append("[DEBUG] Is built-in: ").append(LspRegistry.builtInServer.contains(server)).appendLine() + append("[REPORT] Server name: ").append(server.serverName).appendLine() + append("[REPORT] Server id: ").append(server.id).appendLine() + append("[REPORT] Server status: ").append(instance.status.name).appendLine() + append("[REPORT] Supported extensions: ").append(server.supportedExtensions).appendLine() + append("[REPORT] Is built-in: ").append(LspRegistry.builtInServer.contains(server)).appendLine() appendLine() diff --git a/core/main/src/main/java/com/rk/settings/lsp/LspSettings.kt b/core/main/src/main/java/com/rk/settings/lsp/LspSettings.kt index 026a959cd..362c697e9 100644 --- a/core/main/src/main/java/com/rk/settings/lsp/LspSettings.kt +++ b/core/main/src/main/java/com/rk/settings/lsp/LspSettings.kt @@ -1,6 +1,7 @@ package com.rk.settings.lsp import android.content.Context +import androidx.activity.compose.LocalActivity import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -48,9 +49,11 @@ import androidx.lifecycle.compose.currentStateAsState import androidx.navigation.NavController import com.rk.activities.settings.SettingsRoutes import com.rk.components.InfoBlock -import com.rk.components.SettingsToggle +import com.rk.components.SettingsItem import com.rk.components.compose.preferences.base.PreferenceGroup import com.rk.components.compose.preferences.base.PreferenceLayout +import com.rk.icons.Icon +import com.rk.icons.XedIcon import com.rk.lsp.LspPersistence import com.rk.lsp.LspRegistry import com.rk.lsp.LspServer @@ -61,6 +64,7 @@ import com.rk.resources.drawables import com.rk.resources.getString import com.rk.resources.strings import com.rk.settings.Preference +import com.rk.utils.openDocs import com.rk.utils.parseExtensions import com.rk.utils.toast import kotlinx.coroutines.CoroutineScope @@ -108,8 +112,8 @@ fun LspSettings(navController: NavController) { PreferenceGroup(heading = stringResource(strings.external)) { LspRegistry.externalServers.forEachIndexed { index, server -> key(server.id) { - val icon = server.icon ?: drawables.unknown_document - SettingsToggle( + val icon = server.icon ?: Icon.ResourceIcon(drawables.unknown_document) + SettingsItem( label = server.languageName, default = true, description = server.serverName, @@ -159,6 +163,7 @@ fun LspSettings(navController: NavController) { }, ) { InfoBlock( + onClick = { context.openDocs("lsp") }, icon = { Icon(imageVector = Icons.Outlined.Info, contentDescription = null) }, text = stringResource(strings.info_lsp), ) @@ -205,8 +210,8 @@ private fun LspServerItem( navController: NavController, refreshKey: Int, ) { - val icon = server.icon ?: drawables.unknown_document - SettingsToggle( + val icon = server.icon ?: Icon.ResourceIcon(drawables.unknown_document) + SettingsItem( label = server.languageName, default = Preference.getBoolean("lsp_${server.id}", true), description = server.serverName, @@ -224,10 +229,11 @@ private fun LspServerItem( }, endWidget = { val status by rememberLspInstallStatus(context, server, refreshKey) + val activity = LocalActivity.current when (status) { LspInstallationAction.INSTALL -> { - IconButton(onClick = { server.install(context) }) { + IconButton(onClick = { activity?.let { server.install(it) } }) { Icon( painter = painterResource(drawables.download), contentDescription = stringResource(strings.download), @@ -236,7 +242,7 @@ private fun LspServerItem( } LspInstallationAction.UPDATE -> { - IconButton(onClick = { server.update(context) }) { + IconButton(onClick = { activity?.let { server.update(it) } }) { Icon( painter = painterResource(drawables.update), contentDescription = stringResource(strings.update), @@ -274,9 +280,9 @@ fun rememberLspInstallStatus(context: Context, server: LspServer, refreshKey: In } @Composable -private fun LspServerIcon(server: LspServer, i: Int) { +private fun LspServerIcon(server: LspServer, icon: Icon) { BadgedBox(badge = { server.getDominantStatusColor()?.let { color -> Badge(containerColor = color) } }) { - Icon(modifier = Modifier.padding(start = 16.dp), painter = painterResource(i), contentDescription = null) + XedIcon(icon = icon, contentDescription = null, modifier = Modifier.padding(start = 16.dp)) } } diff --git a/core/main/src/main/java/com/rk/settings/runners/HtmlRunnerSettings.kt b/core/main/src/main/java/com/rk/settings/runners/HtmlRunnerSettings.kt index 71f6dbbf5..45a0b8c36 100644 --- a/core/main/src/main/java/com/rk/settings/runners/HtmlRunnerSettings.kt +++ b/core/main/src/main/java/com/rk/settings/runners/HtmlRunnerSettings.kt @@ -10,7 +10,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import com.rk.components.SettingsToggle +import com.rk.components.SettingsItem import com.rk.components.SingleInputDialog import com.rk.components.compose.preferences.base.PreferenceGroup import com.rk.components.compose.preferences.base.PreferenceLayout @@ -26,21 +26,21 @@ fun HtmlRunnerSettings(modifier: Modifier = Modifier) { PreferenceLayout(label = stringResource(strings.html_preview)) { PreferenceGroup { - SettingsToggle( + SettingsItem( label = stringResource(strings.launch_in_browser), description = stringResource(strings.launch_in_browser_desc), default = Settings.launch_in_browser, sideEffect = { Settings.launch_in_browser = it }, ) - SettingsToggle( + SettingsItem( label = stringResource(strings.inject_eruda), description = stringResource(strings.inject_eruda_desc), default = Settings.inject_eruda, sideEffect = { Settings.inject_eruda = it }, ) - SettingsToggle( + SettingsItem( label = stringResource(strings.server_port), description = stringResource(strings.server_port_desc), default = false, diff --git a/core/main/src/main/java/com/rk/settings/runners/RunnerSettings.kt b/core/main/src/main/java/com/rk/settings/runners/RunnerSettings.kt index 2e734f9d4..6263c7ec2 100644 --- a/core/main/src/main/java/com/rk/settings/runners/RunnerSettings.kt +++ b/core/main/src/main/java/com/rk/settings/runners/RunnerSettings.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue @@ -36,19 +37,22 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.lifecycleScope import androidx.navigation.NavController import com.rk.activities.main.MainActivity -import com.rk.activities.settings.SettingsRoutes import com.rk.components.InfoBlock -import com.rk.components.SettingsToggle +import com.rk.components.SettingsItem import com.rk.components.compose.preferences.base.PreferenceGroup import com.rk.components.compose.preferences.base.PreferenceLayout import com.rk.file.FileWrapper import com.rk.icons.Error +import com.rk.icons.Icon +import com.rk.icons.XedIcon import com.rk.icons.XedIcons +import com.rk.resources.drawables import com.rk.resources.getString import com.rk.resources.strings +import com.rk.runner.RunnerManager import com.rk.runner.ShellBasedRunner import com.rk.runner.ShellBasedRunners -import com.rk.settings.Settings +import com.rk.utils.openDocs import com.rk.utils.toast import kotlinx.coroutines.launch @@ -63,7 +67,9 @@ fun RunnerSettings(modifier: Modifier = Modifier, navController: NavController) val nameFocusRequester = remember { FocusRequester() } val regexFocusRequester = remember { FocusRequester() } + val scope = rememberCoroutineScope() + val context = LocalContext.current var regexFieldValue by remember { mutableStateOf(TextFieldValue("", selection = TextRange(runnerName.length))) } @@ -126,38 +132,48 @@ fun RunnerSettings(modifier: Modifier = Modifier, navController: NavController) }, ) { InfoBlock( + onClick = { context.openDocs("runners") }, icon = { Icon(imageVector = Icons.Outlined.Info, contentDescription = null) }, text = stringResource(strings.info_runners), ) PreferenceGroup(heading = stringResource(strings.built_in)) { - SettingsToggle( - label = stringResource(strings.html_preview), - description = stringResource(strings.html_preview_desc), - default = Settings.enable_html_runner, - sideEffect = { Settings.enable_html_runner = it }, - onClick = { navController.navigate(SettingsRoutes.HtmlRunner.route) }, - ) - - SettingsToggle( - label = stringResource(strings.markdown_preview), - description = stringResource(strings.markdown_preview_desc), - default = Settings.enable_md_runner, - sideEffect = { Settings.enable_md_runner = it }, - ) + RunnerManager.builtinRunners.forEach { runner -> + SettingsItem( + label = runner.label, + description = runner.description, + default = runner.isEnabled(), + sideEffect = { runner.setEnabled(it) }, + onClick = runner.onConfigure, + ) + } + } - SettingsToggle( - label = stringResource(strings.universal_runner), - description = stringResource(strings.universal_runner_desc), - default = Settings.enable_universal_runner, - sideEffect = { Settings.enable_universal_runner = it }, - ) + if (RunnerManager.extensionRunners.isNotEmpty()) { + PreferenceGroup(heading = stringResource(strings.ext)) { + RunnerManager.extensionRunners.forEach { runner -> + SettingsItem( + label = runner.label, + description = runner.description, + startWidget = { + XedIcon( + icon = runner.getIcon(context) ?: Icon.ResourceIcon(drawableRes = drawables.run), + contentDescription = null, + modifier = Modifier.padding(start = 16.dp), + ) + }, + default = runner.isEnabled(), + sideEffect = { runner.setEnabled(it) }, + onClick = runner.onConfigure, + ) + } + } } PreferenceGroup(heading = stringResource(strings.external)) { val scope = rememberCoroutineScope() if (isLoading) { - SettingsToggle( + SettingsItem( modifier = Modifier, label = stringResource(strings.loading), default = false, @@ -167,7 +183,7 @@ fun RunnerSettings(modifier: Modifier = Modifier, navController: NavController) ) } else { if (ShellBasedRunners.runners.isEmpty()) { - SettingsToggle( + SettingsItem( modifier = Modifier, label = stringResource(strings.no_runners), default = false, @@ -177,12 +193,12 @@ fun RunnerSettings(modifier: Modifier = Modifier, navController: NavController) ) } else { ShellBasedRunners.runners.forEach { runner -> - SettingsToggle( + SettingsItem( modifier = Modifier, - label = runner.getName(), + label = runner.label, description = null, - default = false, - sideEffect = { _ -> + default = runner.isEnabled(), + onClick = { MainActivity.instance?.let { it.lifecycleScope.launch { it.viewModel.editorManager.openFile(FileWrapper(runner.getScript()), null, true) @@ -190,7 +206,7 @@ fun RunnerSettings(modifier: Modifier = Modifier, navController: NavController) } } }, - showSwitch = false, + sideEffect = { runner.setEnabled(it) }, endWidget = { Row( verticalAlignment = Alignment.CenterVertically, @@ -199,7 +215,7 @@ fun RunnerSettings(modifier: Modifier = Modifier, navController: NavController) IconButton( onClick = { isEditingExisting = runner - runnerName = runner.getName() + runnerName = runner.label regexFieldValue = regexFieldValue.copy( text = runner.regex, @@ -325,7 +341,7 @@ fun RunnerSettings(modifier: Modifier = Modifier, navController: NavController) onClick = { // Check for duplicate names only when creating new runner if (isEditingExisting == null) { - if (ShellBasedRunners.runners.any { it.getName() == runnerName }) { + if (ShellBasedRunners.runners.any { it.label == runnerName }) { nameError = strings.runner_name_exists.getString() return@TextButton } @@ -334,7 +350,7 @@ fun RunnerSettings(modifier: Modifier = Modifier, navController: NavController) scope.launch { if (isEditingExisting == null) { // Create new runner - val runner = ShellBasedRunner(name = runnerName, regex = regexFieldValue.text) + val runner = ShellBasedRunner(label = runnerName, regex = regexFieldValue.text) val created = ShellBasedRunners.newRunner(runner) if (created) { // Refresh list @@ -350,10 +366,10 @@ fun RunnerSettings(modifier: Modifier = Modifier, navController: NavController) // Update existing runner val updatedRunner = ShellBasedRunner( - name = runnerName, // Name remains the same + label = runnerName, // Name remains the same regex = regexFieldValue.text, ) - ShellBasedRunners.deleteRunner(isEditingExisting!!, deleteScript = false) + ShellBasedRunners.deleteRunner(isEditingExisting!!) val updated = ShellBasedRunners.newRunner(updatedRunner) if (updated) { // Refresh list diff --git a/core/main/src/main/java/com/rk/settings/support/Support.kt b/core/main/src/main/java/com/rk/settings/support/Support.kt index eeaa55db1..f83d2ece3 100644 --- a/core/main/src/main/java/com/rk/settings/support/Support.kt +++ b/core/main/src/main/java/com/rk/settings/support/Support.kt @@ -17,7 +17,7 @@ import androidx.compose.ui.unit.dp import androidx.core.net.toUri import com.rk.activities.settings.SettingsActivity import com.rk.activities.settings.SettingsRoutes -import com.rk.components.SettingsToggle +import com.rk.components.SettingsItem import com.rk.components.compose.preferences.base.PreferenceGroup import com.rk.components.compose.preferences.base.PreferenceLayout import com.rk.resources.drawables @@ -25,7 +25,7 @@ import com.rk.resources.getFilledString import com.rk.resources.getString import com.rk.resources.strings import com.rk.settings.Settings -import com.rk.utils.dialog +import com.rk.utils.dialogRes import com.rk.utils.isDialogShowing import com.rk.utils.toast @@ -62,9 +62,9 @@ fun Support(modifier: Modifier = Modifier) { val context = LocalContext.current PreferenceGroup { - SettingsToggle( + SettingsItem( label = "GitHub Sponsors", - description = stringResource(strings.sponsor_desc), + isEnabled = true, showSwitch = false, default = false, @@ -89,9 +89,9 @@ fun Support(modifier: Modifier = Modifier) { Settings.donated = true }, ) - SettingsToggle( + SettingsItem( label = "Buy Me a Coffee", - description = stringResource(strings.coffee_desc), + isEnabled = true, showSwitch = false, default = false, @@ -118,9 +118,9 @@ fun Support(modifier: Modifier = Modifier) { ) val upiAvailable = remember { isUPISupported(context) } if (upiAvailable) { - SettingsToggle( + SettingsItem( label = "UPI", - description = stringResource(strings.upi_desc), + isEnabled = true, showSwitch = false, default = false, @@ -143,7 +143,7 @@ fun Support(modifier: Modifier = Modifier) { "upi://pay" .toUri() .buildUpon() - .appendQueryParameter("pa", "rohitkushvaha01@axl") + .appendQueryParameter("pa", "rohitkushwaha01x@axl") .appendQueryParameter("pn", "Rohit Kushwaha") .appendQueryParameter("tn", "Xed-Editor") .appendQueryParameter("cu", "INR") @@ -204,12 +204,12 @@ fun Activity.handleSupport() { } private fun Activity.showCombinedDonationDialog() { - dialog( - context = this, + dialogRes( + activity = this, title = strings.enjoying_xed.getString(), msg = strings.support_message.getFilledString(Settings.saves.toString(), Settings.runs.toString()), - okString = strings.yes_support, - cancelString = strings.not_for_me, + okRes = strings.yes_support, + cancelRes = strings.not_for_me, cancelable = false, onCancel = { // User doesn't find value - stop asking diff --git a/core/main/src/main/java/com/rk/settings/terminal/ExtraKeys.kt b/core/main/src/main/java/com/rk/settings/terminal/ExtraKeys.kt index d4db8cf52..637d5e888 100644 --- a/core/main/src/main/java/com/rk/settings/terminal/ExtraKeys.kt +++ b/core/main/src/main/java/com/rk/settings/terminal/ExtraKeys.kt @@ -1,6 +1,5 @@ package com.rk.settings.terminal -import android.content.Intent import androidx.activity.compose.LocalOnBackPressedDispatcherOwner import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize @@ -27,7 +26,6 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.viewinterop.AndroidView -import androidx.core.net.toUri import com.rk.components.ResetButton import com.rk.editor.Editor import com.rk.file.BuiltinFileType @@ -37,6 +35,7 @@ import com.rk.settings.Preference import com.rk.settings.Settings import com.rk.tabs.editor.EditorNotice import com.rk.utils.isSystemInDarkTheme +import com.rk.utils.openUrl import io.github.rosemoe.sora.event.ContentChangeEvent import java.lang.ref.WeakReference import kotlinx.coroutines.launch @@ -114,8 +113,7 @@ fun TerminalExtraKeys() { IconButton( onClick = { val url = "https://wiki.termux.com/wiki/Touch_Keyboard#Extra_Keys_Row" - val intent = Intent(Intent.ACTION_VIEW, url.toUri()) - context.startActivity(intent) + context.openUrl(url) } ) { Icon( @@ -146,7 +144,7 @@ fun TerminalExtraKeys() { colorScheme = colorScheme, ) - scope.launch { setLanguage(BuiltinFileType.JSON.textmateScope!!) } + scope.launch { configureLanguage(BuiltinFileType.JSON.textmateScope!!) } } }, ) diff --git a/core/main/src/main/java/com/rk/settings/terminal/SettingsTerminalScreen.kt b/core/main/src/main/java/com/rk/settings/terminal/SettingsTerminalScreen.kt index 06b59ecb2..163b370fd 100644 --- a/core/main/src/main/java/com/rk/settings/terminal/SettingsTerminalScreen.kt +++ b/core/main/src/main/java/com/rk/settings/terminal/SettingsTerminalScreen.kt @@ -5,7 +5,10 @@ import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.AlertDialog import androidx.compose.material3.MaterialTheme import androidx.compose.material3.RadioButton @@ -27,7 +30,7 @@ import com.rk.activities.settings.SettingsActivity import com.rk.activities.settings.SettingsRoutes import com.rk.activities.settings.settingsNavController import com.rk.components.NextScreenCard -import com.rk.components.SettingsToggle +import com.rk.components.SettingsItem import com.rk.components.ValueSlider import com.rk.components.compose.preferences.base.PreferenceGroup import com.rk.components.compose.preferences.base.PreferenceLayout @@ -46,19 +49,19 @@ import com.rk.settings.Settings import com.rk.settings.app.InbuiltFeatures import com.rk.terminal.terminalView import com.rk.utils.LoadingPopup -import com.rk.utils.dialog +import com.rk.utils.dialogRes import com.rk.utils.dpToPx import com.rk.utils.getTempDir import com.rk.utils.toast import com.termux.terminal.TerminalEmulator -import java.io.File -import java.io.FileOutputStream -import java.lang.Runtime.getRuntime import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import java.io.File +import java.io.FileOutputStream +import java.lang.Runtime.getRuntime enum class TerminalCursorStyle(val value: String, val stringRes: Int) { BLOCK("block", strings.block), @@ -79,6 +82,81 @@ fun SettingsTerminalScreen(overrideNavController: NavController? = null) { val context = LocalContext.current val activity = LocalActivity.current as? AppCompatActivity + PreferenceGroup(heading = stringResource(strings.advanced)) { + if (InbuiltFeatures.debugMode.state.value) { + SettingsItem( + label = stringResource(strings.failsafe_mode), + description = stringResource(strings.failsafe_mode_desc), + default = !Settings.sandbox, + sideEffect = { Settings.sandbox = !it }, + ) + } + + var showSeccompDialog by remember { mutableStateOf(false) } + var seccompMode by remember { mutableStateOf(Settings.seccomp_mode) } + + SettingsItem( + label = "SECCOMP", + description = stringResource(strings.seccomp_desc), + default = false, + showSwitch = false, + onClick = { showSeccompDialog = true }, + ) + + if (showSeccompDialog) { + var tempSeccompMode by remember { mutableStateOf(seccompMode) } + AlertDialog( + onDismissRequest = { + showSeccompDialog = false + }, + title = { Text("SECCOMP") }, + text = { + Column { + listOf( + "unspecified" to strings.seccomp_unspecified, + "no" to strings.seccomp_no_seccomp, + "yes" to strings.seccomp_seccomp + ).forEach { (mode, stringRes) -> + PreferenceTemplate( + modifier = + Modifier.clip(MaterialTheme.shapes.large).clickable { tempSeccompMode = mode }, + title = { Text(stringResource(stringRes)) }, + startWidget = { RadioButton(selected = tempSeccompMode == mode, onClick = null) }, + ) + } + } + }, + confirmButton = { + TextButton( + onClick = { + showSeccompDialog = false + Settings.seccomp_mode = tempSeccompMode + seccompMode = tempSeccompMode + } + ) { + Text(stringResource(strings.apply)) + } + }, + dismissButton = { + TextButton( + onClick = { + showSeccompDialog = false + } + ) { + Text(stringResource(strings.cancel)) + } + }, + ) + } + + NextScreenCard( + label = stringResource(strings.terminal_health), + description = stringResource(strings.terminal_health_desc), + navController = overrideNavController ?: settingsNavController.get(), + route = SettingsRoutes.TerminalCheck, + ) + } + var showCursorStyleDialog by remember { mutableStateOf(false) } var cursorStyleValue by remember { mutableStateOf(TerminalCursorStyle.fromString(Settings.terminal_cursor_style)) @@ -103,7 +181,7 @@ fun SettingsTerminalScreen(overrideNavController: NavController? = null) { route = SettingsRoutes.TerminalFontScreen, ) - SettingsToggle( + SettingsItem( label = stringResource(strings.cursor_style), description = stringResource(strings.cursor_style_desc), default = false, @@ -198,7 +276,7 @@ fun SettingsTerminalScreen(overrideNavController: NavController? = null) { } } - SettingsToggle( + SettingsItem( label = stringResource(strings.backup), description = stringResource(strings.terminal_backup), showSwitch = false, @@ -277,7 +355,7 @@ fun SettingsTerminalScreen(overrideNavController: NavController? = null) { }, ) - SettingsToggle( + SettingsItem( label = stringResource(strings.restore), description = stringResource(strings.restore_terminal), showSwitch = false, @@ -285,18 +363,18 @@ fun SettingsTerminalScreen(overrideNavController: NavController? = null) { sideEffect = { restore.launch("application/gzip") }, ) - SettingsToggle( + SettingsItem( label = stringResource(strings.uninstall), default = false, description = stringResource(strings.uninstall_terminal), showSwitch = false, sideEffect = { - dialog( - context = activity, + dialogRes( + activity = activity, title = strings.attention.getString(), msg = strings.uninstall_terminal_warning.getString(), onCancel = {}, - okString = strings.delete, + okRes = strings.delete, onOk = { GlobalScope.launch(Dispatchers.IO) { val loading = LoadingPopup(activity, null) @@ -315,30 +393,6 @@ fun SettingsTerminalScreen(overrideNavController: NavController? = null) { ) } - PreferenceGroup(heading = stringResource(strings.advanced)) { - if (InbuiltFeatures.debugMode.state.value) { - SettingsToggle( - label = stringResource(strings.failsafe_mode), - description = stringResource(strings.failsafe_mode_desc), - default = !Settings.sandbox, - sideEffect = { Settings.sandbox = !it }, - ) - } - - var seccomp by remember { mutableStateOf(Settings.seccomp) } - - SettingsToggle( - label = "SECCOMP", - default = seccomp, - description = stringResource(strings.seccomp_desc), - sideEffect = { - Settings.seccomp = it - seccomp = it - }, - showSwitch = true, - ) - } - PreferenceGroup(heading = stringResource(strings.other)) { NextScreenCard( label = stringResource(strings.change_extra_keys), @@ -347,6 +401,13 @@ fun SettingsTerminalScreen(overrideNavController: NavController? = null) { route = SettingsRoutes.TerminalExtraKeys, ) + SettingsItem( + label = stringResource(strings.clipboard_keybindings), + description = stringResource(strings.clipboard_keybindings_desc), + default = Settings.terminal_clipboard_keybindings, + sideEffect = { Settings.terminal_clipboard_keybindings = it }, + ) + ValueSlider( label = stringResource(strings.scrollback_buffer), description = stringResource(strings.scrollback_buffer_desc), @@ -359,19 +420,18 @@ fun SettingsTerminalScreen(overrideNavController: NavController? = null) { toast(strings.restart_required) } - SettingsToggle( + SettingsItem( label = stringResource(strings.terminate_all_sessions), description = stringResource(strings.terminate_all_sessions_desc), default = Settings.terminate_sessions_on_exit, sideEffect = { Settings.terminate_sessions_on_exit = it }, ) - SettingsToggle( + SettingsItem( label = stringResource(strings.project_as_wk), description = stringResource(strings.project_as_wk_desc), default = Settings.project_as_pwd, sideEffect = { Settings.project_as_pwd = it }, - showSwitch = true, ) var exposeHomeDirState by remember { mutableStateOf(Settings.expose_home_dir) } @@ -379,11 +439,11 @@ fun SettingsTerminalScreen(overrideNavController: NavController? = null) { checked = exposeHomeDirState, onCheckedChange = { if (it) { - dialog( - context = activity, + dialogRes( + activity = activity, title = strings.attention.getString(), msg = strings.saf_expose_warning.getString(), - okString = strings.continue_action, + okRes = strings.continue_action, onCancel = {}, onOk = { Settings.expose_home_dir = true diff --git a/core/main/src/main/java/com/rk/settings/terminal/TerminalCheckScreen.kt b/core/main/src/main/java/com/rk/settings/terminal/TerminalCheckScreen.kt new file mode 100644 index 000000000..43809ed47 --- /dev/null +++ b/core/main/src/main/java/com/rk/settings/terminal/TerminalCheckScreen.kt @@ -0,0 +1,211 @@ +package com.rk.settings.terminal + +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.KeyboardArrowRight +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.outlined.Warning +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.rk.components.InfoBlock +import com.rk.components.compose.preferences.base.PreferenceGroup +import com.rk.components.compose.preferences.base.PreferenceLayout +import com.rk.components.compose.preferences.base.PreferenceTemplate +import com.rk.icons.LucideCircleQuestionMark +import com.rk.resources.strings +import com.rk.theme.greenStatus +import com.rk.utils.openUrl + +enum class CheckStatus { + PENDING, + LOADING, + SUCCESS, + FAILED, +} + +data class Check( + val label: String, + val status: CheckStatus = CheckStatus.PENDING, + val logs: List = emptyList(), + val isExpanded: Boolean = false, + val run: suspend (printLog: (String) -> Unit) -> Boolean, +) + +@Composable +fun TerminalCheckScreen() { + val context = LocalContext.current + val checks = terminalChecks() + + LaunchedEffect(Unit) { + for (i in checks.indices) { + val check = checks[i] + // Update to LOADING and clear logs + checks[i] = check.copy(status = CheckStatus.LOADING, logs = emptyList()) + + val success = + try { + val result = check.run { log -> + // Append log to the current check's logs + checks[i] = checks[i].copy(logs = checks[i].logs + log) + } + + result + } catch (e: Exception) { + checks[i] = checks[i].copy(logs = checks[i].logs + "Crash: ${e.localizedMessage}") + false + } + + checks[i] = + checks[i].copy(status = if (success) CheckStatus.SUCCESS else CheckStatus.FAILED, isExpanded = !success) + } + } + + PreferenceLayout(label = stringResource(strings.terminal_health), backArrowVisible = true) { + if (isAffectedSamsungDevice()) { + InfoBlock( + icon = { Icon(imageVector = Icons.Outlined.Warning, contentDescription = null) }, + text = stringResource(strings.samsung_proot_warning), + warning = true, + onClick = { + context.openUrl("https://github.com/Xed-Editor/Xed-Editor/issues/639") + }, + ) + } + + if (isTerminalDegraded(context)) { + InfoBlock( + icon = { Icon(imageVector = Icons.Outlined.Warning, contentDescription = null) }, + text = stringResource(strings.terminal_degraded_warning), + warning = true, + ) + } + + checks.forEachIndexed { index, check -> + PreferenceGroup { + PreferenceTemplate( + modifier = Modifier.clickable { checks[index] = check.copy(isExpanded = !check.isExpanded) }, + title = { Text(text = check.label, style = MaterialTheme.typography.bodyLarge) }, + startWidget = { StatusIcon(status = check.status) }, + endWidget = { + val rotation by + animateFloatAsState( + targetValue = if (check.isExpanded) 90f else 0f, + label = "ChevronRotation", + ) + + Icon( + imageVector = Icons.AutoMirrored.Outlined.KeyboardArrowRight, + contentDescription = if (check.isExpanded) "Collapse" else "Expand", + modifier = Modifier.rotate(rotation).size(24.dp), + ) + }, + ) + + if (check.isExpanded) { + Box( + modifier = + Modifier.padding(horizontal = 16.dp) + .padding(bottom = 12.dp) + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)) + .padding(12.dp) + ) { + Column { + if (check.logs.isEmpty()) { + Text( + text = stringResource(strings.no_logs), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } else { + check.logs.forEach { log -> + Text( + text = if (log.startsWith(">")) log else "> $log", + style = + MaterialTheme.typography.bodySmall.copy( + fontFamily = FontFamily.Monospace, + fontSize = 11.sp, + lineHeight = 16.sp, + ), + color = + if ( + log.contains("Error", ignoreCase = true) || + log.contains("Fail", ignoreCase = true) || + log.contains("Crash", ignoreCase = true) + ) + MaterialTheme.colorScheme.error + else MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } + } + } + } + } + } +} + +@Composable +private fun StatusIcon(status: CheckStatus) { + Box(modifier = Modifier.size(24.dp), contentAlignment = Alignment.Center) { + when (status) { + CheckStatus.PENDING -> { + Icon( + imageVector = LucideCircleQuestionMark, + contentDescription = stringResource(strings.pending), + tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f), + modifier = Modifier.size(20.dp), + ) + } + + CheckStatus.LOADING -> { + CircularProgressIndicator( + modifier = Modifier.size(16.dp), + strokeWidth = 4.dp, + color = MaterialTheme.colorScheme.primary, + ) + } + + CheckStatus.SUCCESS -> { + Icon( + imageVector = Icons.Default.CheckCircle, + contentDescription = stringResource(strings.success), + tint = MaterialTheme.colorScheme.greenStatus, + modifier = Modifier.size(20.dp), + ) + } + + CheckStatus.FAILED -> { + Icon( + imageVector = Icons.Default.Close, + contentDescription = stringResource(strings.failed), + tint = MaterialTheme.colorScheme.error, + modifier = Modifier.size(20.dp), + ) + } + } + } +} diff --git a/core/main/src/main/java/com/rk/settings/terminal/terminalChecks.kt b/core/main/src/main/java/com/rk/settings/terminal/terminalChecks.kt new file mode 100644 index 000000000..6689549f8 --- /dev/null +++ b/core/main/src/main/java/com/rk/settings/terminal/terminalChecks.kt @@ -0,0 +1,258 @@ +package com.rk.settings.terminal + +import android.content.Context +import android.os.Build +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.compose.ui.res.stringResource +import com.rk.exec.isTerminalInstalled +import com.rk.exec.readStderr +import com.rk.exec.ubuntuProcess +import com.rk.file.child +import com.rk.file.localDir +import com.rk.file.sandboxDir +import com.rk.file.sandboxHomeDir +import com.rk.resources.strings +import com.rk.utils.application +import com.rk.utils.getTempDir +import java.io.File + +fun isAffectedSamsungDevice(): Boolean { + val model = Build.MODEL.uppercase() + + return model.startsWith("SM-S911") || // S23 + model.startsWith("SM-S721") || // S24 FE + model.startsWith("SM-S936") || // S25+ + model.startsWith("SM-F96") || // Fold7 + model.startsWith("SM-A56") || // A56 + model.startsWith("SM-A17") || // A17 + model.startsWith("SM-A16") // A16 +} + +fun isTerminalDegraded(context: Context): Boolean { + return localDir(context).child(".sandbox_degraded").exists() +} + +/** These checks are intended for troubleshooting terminal issues */ +@Composable +inline fun terminalChecks(): SnapshotStateList { + val checkProot = stringResource(strings.check_proot) + val checkSystemShell = stringResource(strings.check_system_shell) + val checkStoragePermissions = stringResource(strings.check_storage_permissions) + val checkUbuntu = stringResource(strings.check_ubuntu) + val checkNetworkAccess = stringResource(strings.check_network_access) + val checkAbnormalities = stringResource(strings.check_abnormalities) + + return remember { + mutableStateListOf( + Check( + label = checkProot, + run = { printLog -> + val libproot = File(application!!.applicationInfo.nativeLibraryDir, "libproot.so") + val prootloader = File(application!!.applicationInfo.nativeLibraryDir, "libloader.so") + val prootloader32 = File(application!!.applicationInfo.nativeLibraryDir, "libloader32.so") + + printLog("PRoot exists: ${libproot.exists()}") + printLog("PRoot readable: ${libproot.canRead()}") + printLog("PRoot executable: ${libproot.canExecute()}") + printLog("PRoot Loader exists: ${prootloader.exists()}") + printLog("32bit PRoot Loader exists: ${prootloader32.exists()}") + + var exitCode = 999 + + try { + printLog("Creating a temporary sandbox environment...") + + val process = + ProcessBuilder(libproot.absolutePath, "-0", "-r", "/", "true") + .apply { + environment()["PROOT_TMP_DIR"] = getTempDir().absolutePath + environment()["PROOT_LOADER"] = prootloader.absolutePath + environment()["PROOT_LOADER_32"] = prootloader32.absolutePath + } + .start() + + exitCode = process.waitFor() + + printLog("Exit code: $exitCode") + + if (exitCode != 0) { + val stderr = process.errorStream.bufferedReader().use { it.readText() } + + if (stderr.isNotBlank()) { + printLog("stderr:") + printLog(stderr) + } + } + } catch (e: Exception) { + printLog("Error while running PRoot: ${e.message}") + } + + libproot.exists() && exitCode == 0 + }, + ), + Check( + label = checkSystemShell, + run = { printLog -> + val shell = File("/system/bin/sh") + printLog("$shell exists: ${shell.exists()}") + + val shell1 = File("/bin/sh") + printLog("$shell1 exists: ${shell1.exists()}") + + printLog("$shell readable: ${shell.canRead()}") + printLog("$shell1 readable: ${shell1.canRead()}") + + printLog("$shell executable: ${shell.canExecute()}") + printLog("$shell1 executable: ${shell1.canExecute()}") + + var exitcode: Int = 999 + try { + exitcode = Runtime.getRuntime().exec(arrayOf("/system/bin/sh", "-c", "true")).waitFor() + printLog("Exit code: $exitcode") + } catch (e: Exception) { + printLog("Error while running shell: ${e.message}") + } + + exitcode == 0 && + shell.exists() && + shell1.exists() && + shell.canRead() && + shell1.canRead() && + shell.canExecute() && + shell1.canExecute() + }, + ), + Check( + label = checkStoragePermissions, + run = { printLog -> + val filesDir = application!!.filesDir + val totalSpace = filesDir.totalSpace / (1024 * 1024) + val freeSpace = filesDir.freeSpace / (1024 * 1024) + printLog("Internal Storage - Total: $totalSpace MB, Free: $freeSpace MB") + + printLog("Files Dir: ${filesDir.absolutePath}") + printLog("Files Dir Writable: ${filesDir.canWrite()}") + + val sandboxHome = sandboxHomeDir() + printLog("Sandbox Home: ${sandboxHome.absolutePath}") + printLog("Sandbox Home Writable: ${sandboxHome.canWrite()}") + + if (freeSpace < 500) { + printLog("Warning: Low storage space (< 500 MB). Ubuntu might fail to install or update.") + } + + freeSpace > 100 && filesDir.canWrite() && sandboxHome.canWrite() + }, + ), + Check( + label = checkUbuntu, + run = { printLog -> + if (!isTerminalInstalled()) { + printLog("Ubuntu not installed, skipping") + return@Check true + } + + val rootfs = sandboxDir() + printLog("RootFS path: ${rootfs.absolutePath}") + + val bash = rootfs.child("bin/bash") + printLog("Bash exists: ${bash.exists()}") + if (bash.exists()) { + printLog("Bash executable: ${bash.canExecute()}") + } + + val apt = rootfs.child("usr/bin/apt") + printLog("Apt exists: ${apt.exists()}") + + val osRelease = rootfs.child("etc/os-release") + if (osRelease.exists()) { + printLog("OS Release info:") + osRelease.readLines().take(5).forEach { printLog(" $it") } + } + + printLog("Testing Ubuntu execution...") + var working = false + try { + val process = ubuntuProcess(command = listOf("true")) + val exitCode = process.waitFor() + val stderr = process.readStderr().trim() + + printLog("Exit code: $exitCode") + if (stderr.isNotEmpty()) printLog("Stderr: $stderr") + + working = (exitCode == 0) + } catch (e: Exception) { + printLog("Execution failed: ${e.message}") + } + + working + }, + ), + Check( + label = checkNetworkAccess, + run = { printLog -> + if (!isTerminalInstalled()) { + printLog("Ubuntu not installed, skipping network check.") + return@Check true + } + + printLog("Checking DNS resolution (google.com)...") + try { + val dnsProcess = ubuntuProcess(command = listOf("getent", "hosts", "google.com")) + val dnsExitCode = dnsProcess.waitFor() + if (dnsExitCode == 0) { + printLog("DNS resolution works.") + } else { + printLog("DNS resolution FAILED.") + val resolvConf = sandboxDir().child("etc/resolv.conf") + if (resolvConf.exists()) { + printLog("/etc/resolv.conf exists, content:") + resolvConf.readLines().forEach { printLog(" $it") } + } else { + printLog("/etc/resolv.conf is MISSING!") + } + printLog("Abnormality: Ubuntu will not have internet access without DNS.") + } + dnsExitCode == 0 + } catch (e: Exception) { + printLog("Network check failed: ${e.message}") + false + } + }, + ), + Check( + label = checkAbnormalities, + run = { printLog -> + if (!isTerminalInstalled()) { + printLog("Ubuntu not installed, skipping") + return@Check true + } + var abnormalities = 0 + + try { + val process = ubuntuProcess(command = listOf("touch", "/tmp/.test_xed")) + if (process.waitFor() == 0) { + ubuntuProcess(command = listOf("rm", "/tmp/.test_xed")).waitFor() + } else { + printLog("Abnormality: /tmp is not writable inside sandbox.") + abnormalities++ + } + } catch (e: Exception) { + printLog("Error checking /tmp: ${e.message}") + } + + if (abnormalities == 0) { + printLog("No major abnormalities detected.") + } else { + printLog("Found $abnormalities abnormality/ies.") + } + + abnormalities == 0 + }, + ), + ) + } +} diff --git a/core/main/src/main/java/com/rk/settings/theme/Theme.kt b/core/main/src/main/java/com/rk/settings/theme/Theme.kt index 3e548fac1..b4ca5d6c2 100644 --- a/core/main/src/main/java/com/rk/settings/theme/Theme.kt +++ b/core/main/src/main/java/com/rk/settings/theme/Theme.kt @@ -35,7 +35,7 @@ import com.rk.App.Companion.iconPackManager import com.rk.DefaultScope import com.rk.activities.settings.SettingsActivity import com.rk.components.BottomSheetContent -import com.rk.components.SettingsToggle +import com.rk.components.SettingsItem import com.rk.components.compose.preferences.base.PreferenceGroup import com.rk.components.compose.preferences.base.PreferenceLayout import com.rk.components.compose.preferences.base.PreferenceTemplate @@ -68,7 +68,7 @@ fun ThemeScreen(modifier: Modifier = Modifier) { PreferenceLayout(label = stringResource(strings.themes)) { PreferenceGroup(heading = stringResource(strings.theme_settings)) { - SettingsToggle( + SettingsItem( label = stringResource(id = strings.theme_mode), description = stringResource(id = strings.theme_mode_desc), showSwitch = false, @@ -76,7 +76,7 @@ fun ThemeScreen(modifier: Modifier = Modifier) { sideEffect = { showDayNightBottomSheet.value = true }, ) - SettingsToggle( + SettingsItem( label = stringResource(id = strings.oled), description = stringResource(id = strings.oled_desc), default = Settings.amoled, @@ -88,7 +88,7 @@ fun ThemeScreen(modifier: Modifier = Modifier) { }, ) - SettingsToggle( + SettingsItem( label = stringResource(id = strings.monet), description = stringResource(id = strings.monet_desc), default = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && Settings.monet, @@ -104,7 +104,7 @@ fun ThemeScreen(modifier: Modifier = Modifier) { PreferenceGroup(heading = stringResource(strings.themes)) { themes.forEach { theme -> - SettingsToggle( + SettingsItem( isEnabled = !dynamicTheme.value, label = theme.name, description = null, @@ -144,7 +144,7 @@ fun ThemeScreen(modifier: Modifier = Modifier) { ) } - SettingsToggle( + SettingsItem( label = stringResource(strings.add_theme), description = null, showSwitch = false, @@ -161,7 +161,7 @@ fun ThemeScreen(modifier: Modifier = Modifier) { } PreferenceGroup(heading = stringResource(strings.icon_packs)) { - SettingsToggle( + SettingsItem( label = "Simple Icons (${stringResource(strings.default_option)})", description = null, showSwitch = false, @@ -180,17 +180,17 @@ fun ThemeScreen(modifier: Modifier = Modifier) { ) iconPackManager.iconPacks.forEach { (id, iconPack) -> - val iconPackInfo = iconPack.info + val iconPackManifest = iconPack.manifest - SettingsToggle( - label = iconPackInfo.name, + SettingsItem( + label = iconPackManifest.name, description = null, showSwitch = false, default = false, startWidget = { RadioButton( modifier = Modifier.padding(start = 16.dp), - selected = currentIconPack.value?.info?.id == id, + selected = currentIconPack.value?.manifest?.id == id, onClick = null, ) }, @@ -201,7 +201,7 @@ fun ThemeScreen(modifier: Modifier = Modifier) { endWidget = { IconButton( onClick = { - if (currentIconPack.value?.info?.id == id) { + if (currentIconPack.value?.manifest?.id == id) { currentIconPack.value = null Settings.icon_pack = "" } @@ -215,7 +215,7 @@ fun ThemeScreen(modifier: Modifier = Modifier) { ) } - SettingsToggle( + SettingsItem( label = stringResource(strings.add_icon_pack), description = null, showSwitch = false, diff --git a/core/main/src/main/java/com/rk/tabs/base/TabRegistry.kt b/core/main/src/main/java/com/rk/tabs/base/TabRegistry.kt index 932224895..473cea4a0 100644 --- a/core/main/src/main/java/com/rk/tabs/base/TabRegistry.kt +++ b/core/main/src/main/java/com/rk/tabs/base/TabRegistry.kt @@ -1,11 +1,13 @@ package com.rk.tabs.base import com.rk.activities.main.MainViewModel +import com.rk.extension.api.XedExtensionPoint import com.rk.file.BuiltinFileType import com.rk.file.FileObject import com.rk.file.FileTypeManager import com.rk.tabs.image.ImageTab +@XedExtensionPoint fun interface TabFactory { fun createTab(file: FileObject, projectRoot: FileObject?, viewModel: MainViewModel): Tab } @@ -13,10 +15,12 @@ fun interface TabFactory { object TabRegistry { private val registeredTabs = mutableMapOf() + @XedExtensionPoint fun registerTab(tabFactory: TabFactory, fileExtensions: List) { fileExtensions.forEach { registeredTabs[it] = tabFactory } } + @XedExtensionPoint fun unregisterTab(tabFactory: TabFactory) { registeredTabs.values.remove(tabFactory) } diff --git a/core/main/src/main/java/com/rk/tabs/editor/CodeEditorCompose.kt b/core/main/src/main/java/com/rk/tabs/editor/CodeEditorCompose.kt index c04a40fdd..b86239483 100644 --- a/core/main/src/main/java/com/rk/tabs/editor/CodeEditorCompose.kt +++ b/core/main/src/main/java/com/rk/tabs/editor/CodeEditorCompose.kt @@ -1,6 +1,6 @@ package com.rk.tabs.editor -import android.content.Context +import android.app.Activity import android.content.Intent import android.view.KeyEvent import android.view.View @@ -42,7 +42,7 @@ import com.rk.resources.strings import com.rk.settings.Preference import com.rk.settings.Settings import com.rk.settings.app.InbuiltFeatures -import com.rk.utils.info +import com.rk.utils.logInfo import com.rk.utils.logWarn import com.rk.utils.toast import io.github.rosemoe.sora.event.ContentChangeEvent @@ -55,13 +55,13 @@ import io.github.rosemoe.sora.lang.styling.inlayHint.ColorInlayHint import io.github.rosemoe.sora.text.CharPosition import io.github.rosemoe.sora.text.TextRange import io.github.rosemoe.sora.widget.component.TextActionItem -import java.lang.ref.WeakReference import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext +import java.lang.ref.WeakReference @OptIn(DelicateCoroutinesApi::class, ExperimentalLayoutApi::class) @Composable @@ -79,10 +79,10 @@ fun EditorTab.CodeEditor( AndroidView( modifier = Modifier.weight(1f), onRelease = { it.release() }, - update = { info("Editor view update") }, + update = { logInfo("Editor view update") }, factory = { ctx -> Editor(ctx).apply { - info("New Editor instance") + logInfo("New Editor instance") editable = editorState.editable val isTxtFile = file.getName().endsWith(".txt") @@ -112,8 +112,6 @@ fun EditorTab.CodeEditor( } } } - - scope.launch { editorState.editorConfigLoaded?.await()?.let { applySettings() } } } }, ) @@ -148,29 +146,30 @@ fun Editor.registerXedEvents( onTextChange: () -> Unit, ) { subscribeAlways(InlayHintClickEvent::class.java) { event -> - val hint = event.inlayHint - if (hint is ColorInlayHint) { - val colorRange = hint.colorRange - val indexedColorRange = - colorRange?.let { - val startIndex = event.editor.text.getCharIndex(colorRange.start.line, colorRange.start.column) - val endIndex = event.editor.text.getCharIndex(colorRange.end.line, colorRange.end.column) - TextRange( - CharPosition(colorRange.start.line, colorRange.start.column, startIndex), - CharPosition(colorRange.end.line, colorRange.end.column, endIndex), - ) + val hint = event.inlayHint as? ColorInlayHint ?: return@subscribeAlways + val range = + hint.colorRange + ?: run { + toast(strings.invalid_color) + return@subscribeAlways } - if (indexedColorRange != null) { - val colorText = event.editor.text.substring(indexedColorRange.startIndex, indexedColorRange.endIndex) - val colorValue = hint.color.resolve(colorScheme).let { Color(it) } - val parsedColor = colorText.parseUnknownColor() ?: (colorValue to ColorFormat.HEX) + val editor = event.editor + val text = editor.text - editorTab.editorState.showColorPicker = parsedColor - editorTab.editorState.colorPickerRange = indexedColorRange - } else { - toast(strings.invalid_color) - } + val start = CharPosition(range.start.line, range.start.column) + val end = CharPosition(range.end.line, range.end.column) + + val indexedRange = TextRange(start, end) + + val colorText = text.subContent(start.line, start.column, end.line, end.column).toString() + + val colorValue = hint.color.resolve(colorScheme).let(::Color) + val parsed = colorText.parseUnknownColor() ?: (colorValue to ColorFormat.HEX) + + editorTab.editorState.apply { + showColorPicker = parsed + colorPickerRange = indexedRange } } @@ -250,87 +249,118 @@ fun Editor.registerXedEvents( fun EditorTab.applyHighlightingAndConnectLSP() { val editor = editorState.editor.get() ?: return - with(editor) { - scope.launch(Dispatchers.IO) { - editorState.textmateScope?.let { setLanguage(it) } + scope.launch(Dispatchers.IO) { + editorState.textmateScope?.let { editor.configureLanguage(it) } - val builtin = getBuiltinServers(context) - val extension = getExtensionServers(context) - val external = getExternalServers() - val servers = builtin + extension + external - if (servers.isEmpty()) return@launch + val editorConfigProps = editorState.editorConfigLoaded?.await() + editorConfigProps?.let { withContext(Dispatchers.Main) { editor.applySettings(it) } } - val wrapperLanguage = - editorState.textmateScope?.let { - LanguageManager.createLanguage(textmateScope = it, createIdentifiers = false) - } - val projectFile = - projectRoot - ?: run { - logWarn( - "File ${file.getName()} has no suitable project root. Skipping language server connection." - ) - return@launch - } + val activity = editor.context as? Activity ?: return@launch + val builtin = getBuiltinServers(activity) + val extension = getExtensionServers(activity) + val external = getExternalServers() + val servers = builtin + extension + external + if (servers.isEmpty()) return@launch - lspConnector = - LspConnector( - projectFile = projectFile, - fileObject = file, - codeEditor = this@with, - editorTab = this@applyHighlightingAndConnectLSP, - servers = servers, - ) - - info("Trying to connect language servers...") - lspConnector?.connect(wrapperLanguage) - info("isConnected : ${lspConnector?.isConnected() ?: false}") + // Language servers fail with content URIs + if (file !is FileWrapper) { + logWarn("File ${file.getName()} is not a file wrapper. Skipping language server connection.") + return@launch } + + if (!InbuiltFeatures.terminal.state.value) { + logWarn("Terminal is not enabled. Skipping language server connection.") + return@launch + } + + // Create another language, as created identifiers cannot be modified retroactively + val wrapperLanguage = + editorState.textmateScope + ?.let { LanguageManager.createLanguage(textmateScope = it, createIdentifiers = false) } + ?.apply { + editor.getTextMateLanguage()?.let { + useTab(it.useTab()) + tabSize = it.tabSize + } + } + + val projectFile = + projectRoot + ?: run { + logWarn("File ${file.getName()} has no suitable project root. Skipping language server connection.") + return@launch + } + + lspConnector = + LspConnector( + projectFile = projectFile, + fileObject = file, + codeEditor = editor, + editorTab = this@applyHighlightingAndConnectLSP, + servers = servers, + ) + + logInfo("Trying to connect language servers...") + lspConnector?.connect(wrapperLanguage) + logInfo("isConnected : ${lspConnector?.isConnected() ?: false}") } } -private suspend fun EditorTab.getBuiltinServers(context: Context): List { +private suspend fun EditorTab.getBuiltinServers(activity: Activity): List { val servers = LspRegistry.builtInServer.filter { it.isSupported(file) } - return findActiveLspServers(servers, context) + return findActiveLspServers(servers, activity) } -private fun EditorTab.promptLspInstall(context: Context, server: LspServer) { +private fun EditorTab.promptLspInstall(activity: Activity, server: LspServer) { scope.launch { val snackbarHost = snackbarHostStateRef.get() ?: return@launch val result = snackbarHost.showSnackbar( - message = strings.ask_lsp_install.getFilledString(server.languageName, context), + message = strings.ask_lsp_install.getFilledString(server.languageName, activity), actionLabel = strings.install.getString(), withDismissAction = true, duration = SnackbarDuration.Short, ) if (result == SnackbarResult.ActionPerformed) { - server.install(context) + val ext = file.getExtension().lowercase() + if (ext.isNotEmpty()) { + Preference.setInt("lsp_install_reject_count_$ext", 0) + } + server.install(activity) + } else if (result == SnackbarResult.Dismissed) { + val ext = file.getExtension().lowercase() + if (ext.isNotEmpty()) { + val rejectCount = Preference.getInt("lsp_install_reject_count_$ext", 0) + Preference.setInt("lsp_install_reject_count_$ext", rejectCount + 1) + } } } } -private fun EditorTab.promptLspUpdate(context: Context, server: LspServer) { +private fun EditorTab.promptLspUpdate(activity: Activity, server: LspServer) { scope.launch { val snackbarHost = snackbarHostStateRef.get() ?: return@launch val result = snackbarHost.showSnackbar( - message = strings.ask_lsp_update.getFilledString(server.languageName, context), + message = strings.ask_lsp_update.getFilledString(server.languageName, activity), actionLabel = strings.update.getString(), duration = SnackbarDuration.Long, ) if (result == SnackbarResult.ActionPerformed) { - server.update(context) + server.update(activity) } } } -private suspend fun EditorTab.getExtensionServers(context: Context): List { +private suspend fun EditorTab.getExtensionServers(activity: Activity): List { val servers = LspRegistry.extensionServers.filter { server -> server.isSupported(file) } - return findActiveLspServers(servers, context) + return findActiveLspServers(servers, activity) } -private suspend fun EditorTab.findActiveLspServers(servers: List, context: Context): MutableList { +private suspend fun EditorTab.findActiveLspServers( + servers: List, + activity: Activity, +): MutableList { val matchedServers = mutableListOf() servers.forEach { server -> @@ -338,16 +368,20 @@ private suspend fun EditorTab.findActiveLspServers(servers: List, con return@forEach } - if (InbuiltFeatures.terminal.state.value && !server.isInstalled(context) && file is FileWrapper) { - info("Server ${server.id} is not installed") - promptLspInstall(context, server) + if (!server.isInstalled(activity)) { + logInfo("Server ${server.id} is not installed") + val ext = file.getExtension().lowercase() + if (ext.isNotEmpty() && Preference.getInt("lsp_install_reject_count_$ext", 0) >= 3) { + return@forEach + } + promptLspInstall(activity, server) return@forEach } scope.launch(Dispatchers.IO) { - if (server.isUpdatable(context)) { - info("Server ${server.id} is updatable") - promptLspUpdate(context, server) + if (server.isUpdatable(activity)) { + logInfo("Server ${server.id} is updatable") + promptLspUpdate(activity, server) } } diff --git a/core/main/src/main/java/com/rk/tabs/editor/CodeEditorState.kt b/core/main/src/main/java/com/rk/tabs/editor/CodeEditorState.kt index 33da3e601..c417c688a 100644 --- a/core/main/src/main/java/com/rk/tabs/editor/CodeEditorState.kt +++ b/core/main/src/main/java/com/rk/tabs/editor/CodeEditorState.kt @@ -6,9 +6,10 @@ import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.input.TextFieldValue import com.rk.color.ColorFormat import com.rk.editor.Editor -import com.rk.runner.RunnerImpl +import com.rk.runner.Runner import com.rk.search.CodeItem import com.rk.settings.Settings import io.github.rosemoe.sora.text.Content @@ -38,8 +39,8 @@ data class CodeEditorState(val initialContent: Content? = null) { var searchRegex by mutableStateOf(false) var searchWholeWord by mutableStateOf(false) var showOptionsMenu by mutableStateOf(false) - var searchKeyword by mutableStateOf("") - var replaceKeyword by mutableStateOf("") + var searchKeyword by mutableStateOf(TextFieldValue("")) + var replaceKeyword by mutableStateOf(TextFieldValue("")) var showFindingsDialog by mutableStateOf(false) var findingsItems by mutableStateOf(listOf()) @@ -57,7 +58,7 @@ data class CodeEditorState(val initialContent: Content? = null) { var textmateScope by mutableStateOf(null) - var runnersToShow by mutableStateOf>(emptyList()) + var runnersToShow by mutableStateOf>(emptyList()) var showRunnerDialog by mutableStateOf(false) var showColorPicker by mutableStateOf?>(null) diff --git a/core/main/src/main/java/com/rk/tabs/editor/EditorTab.kt b/core/main/src/main/java/com/rk/tabs/editor/EditorTab.kt index e6551e155..08fa91a4a 100644 --- a/core/main/src/main/java/com/rk/tabs/editor/EditorTab.kt +++ b/core/main/src/main/java/com/rk/tabs/editor/EditorTab.kt @@ -1,7 +1,10 @@ package com.rk.tabs.editor +import android.content.Context +import androidx.activity.compose.LocalActivity import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.fillMaxWidth @@ -42,6 +45,7 @@ import com.rk.color.ColorPicker import com.rk.components.AddDialogItem import com.rk.components.SingleInputDialog import com.rk.editor.intelligent.IntelligentFeatureRegistry +import com.rk.extension.api.XedExtensionPoint import com.rk.file.FileObject import com.rk.file.FileTypeManager import com.rk.file.child @@ -51,7 +55,6 @@ import com.rk.lsp.formatDocumentSuspend import com.rk.resources.drawables import com.rk.resources.getString import com.rk.resources.strings -import com.rk.runner.currentRunner import com.rk.search.EditorSearchPanel import com.rk.search.FindingsDialog import com.rk.settings.Settings @@ -62,7 +65,6 @@ import com.rk.utils.errorDialog import com.rk.utils.getTempDir import com.rk.utils.hasBinaryChars import io.github.rosemoe.sora.text.ContentIO -import java.lang.ref.WeakReference import java.nio.charset.Charset import java.nio.file.Paths import kotlinx.coroutines.CompletableDeferred @@ -162,15 +164,15 @@ open class EditorTab(override var file: FileObject, var projectRoot: FileObject? showNotice(BINARY_NOTICE_KEY) { id -> BinaryNotice(id) } } } - .onFailure { errorDialog(it) } + .onFailure { errorDialog(throwable = it) } } } } } companion object { - const val BINARY_NOTICE_KEY = "binary_file" - const val EDITORCONFIG_NOTICE_KEY = "editorconfig_changed" + private const val BINARY_NOTICE_KEY = "binary_file" + private const val EDITORCONFIG_NOTICE_KEY = "editorconfig_changed" } @Composable @@ -209,11 +211,13 @@ open class EditorTab(override var file: FileObject, var projectRoot: FileObject? ) } + @XedExtensionPoint fun showNotice(id: String, notice: @Composable (String) -> Unit) { if (editorState.notices.contains(id)) return editorState.notices[id] = notice } + @XedExtensionPoint fun removeNotice(id: String) { editorState.notices.remove(id) } @@ -299,7 +303,7 @@ open class EditorTab(override var file: FileObject, var projectRoot: FileObject? editorState.isDirty = false lspConnector?.notifySave() } - .onFailure { errorDialog(it) } + .onFailure { errorDialog(throwable = it) } } } @@ -352,28 +356,7 @@ open class EditorTab(override var file: FileObject, var projectRoot: FileObject? Column { if (editorState.showRunnerDialog) { - ModalBottomSheet( - onDismissRequest = { - editorState.showRunnerDialog = false - editorState.runnersToShow = emptyList() - } - ) { - Column(modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp, top = 0.dp)) { - editorState.runnersToShow.forEach { runner -> - AddDialogItem( - icon = runner.getIcon(context) ?: Icon.DrawableRes(drawableRes = drawables.run), - title = runner.getName(), - ) { - DefaultScope.launch { - currentRunner = WeakReference(runner) - runner.run(context, file) - editorState.showRunnerDialog = false - editorState.runnersToShow = emptyList() - } - } - } - } - } + RunnerSheet(context) } if (editorState.showFindingsDialog) { @@ -486,7 +469,13 @@ open class EditorTab(override var file: FileObject, var projectRoot: FileObject? onApply = { val textRange = editorState.colorPickerRange ?: return@ColorPicker val editor = editorState.editor.get() ?: return@ColorPicker - editor.text.replace(textRange.startIndex, textRange.endIndex, it) + editor.text.replace( + textRange.start.line, + textRange.start.column, + textRange.end.line, + textRange.end.column, + it, + ) }, ) { editorState.showColorPicker = null @@ -551,3 +540,38 @@ open class EditorTab(override var file: FileObject, var projectRoot: FileObject? return "[EditorTab] ${file.getAbsolutePath()}" } } + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +private fun EditorTab.RunnerSheet(context: Context) { + ModalBottomSheet( + onDismissRequest = { + editorState.showRunnerDialog = false + editorState.runnersToShow = emptyList() + } + ) { + Column( + modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp, top = 0.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Text(text = stringResource(strings.choose_runner), style = MaterialTheme.typography.titleLarge) + + Column { + editorState.runnersToShow.forEach { runner -> + val activity = LocalActivity.current + + AddDialogItem( + icon = runner.getIcon(context) ?: Icon.ResourceIcon(drawableRes = drawables.run), + title = runner.label, + ) { + DefaultScope.launch { + activity?.let { runner.run(it, file) } + editorState.showRunnerDialog = false + editorState.runnersToShow = emptyList() + } + } + } + } + } + } +} diff --git a/core/main/src/main/java/com/rk/tabs/editor/EditorToolbarActions.kt b/core/main/src/main/java/com/rk/tabs/editor/EditorToolbarActions.kt index 14b6ecb3e..3472da83b 100644 --- a/core/main/src/main/java/com/rk/tabs/editor/EditorToolbarActions.kt +++ b/core/main/src/main/java/com/rk/tabs/editor/EditorToolbarActions.kt @@ -20,6 +20,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -32,13 +33,12 @@ import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp import com.rk.activities.main.MainViewModel import com.rk.commands.ActionContext -import com.rk.commands.CommandProvider import com.rk.commands.KeybindingsManager import com.rk.commands.ToggleableCommand +import com.rk.commands.ToolbarConfiguration import com.rk.icons.Icon import com.rk.icons.XedIcon import com.rk.resources.strings -import com.rk.settings.Settings import com.rk.terminal.isV import com.rk.theme.Typography import com.rk.utils.x @@ -49,7 +49,9 @@ fun EditorToolbarActions(modifier: Modifier = Modifier, viewModel: MainViewModel var expanded by remember { mutableStateOf(false) } val activity = LocalActivity.current - val allActions = Settings.action_items.split("|").mapNotNull { CommandProvider.getForId(it) } + val allActions by remember { + derivedStateOf { ToolbarConfiguration.editorCommands } + } BoxWithConstraints(modifier = modifier) { val itemWidth = 64.dp diff --git a/core/main/src/main/java/com/rk/terminal/MkSession.kt b/core/main/src/main/java/com/rk/terminal/MkSession.kt index 5c85561ab..5d7d5041d 100644 --- a/core/main/src/main/java/com/rk/terminal/MkSession.kt +++ b/core/main/src/main/java/com/rk/terminal/MkSession.kt @@ -1,8 +1,8 @@ package com.rk.terminal -import android.os.Build +import android.app.Activity +import android.content.Context import com.rk.activities.main.MainActivity -import com.rk.activities.terminal.Terminal import com.rk.exec.pendingCommand import com.rk.file.FileWrapper import com.rk.file.child @@ -12,9 +12,9 @@ import com.rk.file.localLibDir import com.rk.file.sandboxHomeDir import com.rk.settings.Settings import com.rk.tabs.editor.EditorTab +import com.rk.utils.application import com.rk.utils.getSourceDirOfPackage import com.rk.utils.getTempDir -import com.rk.utils.isFDroid import com.rk.xededitor.BuildConfig import com.termux.terminal.TerminalSession import com.termux.terminal.TerminalSessionClient @@ -24,159 +24,146 @@ import kotlinx.coroutines.runBlocking object MkSession { fun createSession( - activity: Terminal, + context: Context, sessionClient: TerminalSessionClient, sessionId: String, + isExtraction: Boolean = false, ): Pair { - with(activity) { - val envVariables = - mapOf( - "ANDROID_ART_ROOT" to System.getenv("ANDROID_ART_ROOT"), - "ANDROID_DATA" to System.getenv("ANDROID_DATA"), - "ANDROID_I18N_ROOT" to System.getenv("ANDROID_I18N_ROOT"), - "ANDROID_ROOT" to System.getenv("ANDROID_ROOT"), - "ANDROID_RUNTIME_ROOT" to System.getenv("ANDROID_RUNTIME_ROOT"), - "ANDROID_TZDATA_ROOT" to System.getenv("ANDROID_TZDATA_ROOT"), - "BOOTCLASSPATH" to System.getenv("BOOTCLASSPATH"), - "DEX2OATBOOTCLASSPATH" to System.getenv("DEX2OATBOOTCLASSPATH"), - "EXTERNAL_STORAGE" to System.getenv("EXTERNAL_STORAGE"), - "PATH" to "${System.getenv("PATH")}:${localBinDir().absolutePath}", - ) - - val workingDir = runBlocking { getPwd() } - - val tmpDir = File(getTempDir(), "terminal/$sessionId") - - if (tmpDir.exists()) { - tmpDir.deleteRecursively() - } - - tmpDir.mkdirs() - - val env = - mutableListOf( - "PROOT_TMP_DIR=${tmpDir.absolutePath}", - "WKDIR=${workingDir}", - "PUBLIC_HOME=${getExternalFilesDir(null)?.absolutePath}", - "COLORTERM=truecolor", - "TERM=xterm-256color", - "LANG=C.UTF-8", - "DEBUG=${BuildConfig.DEBUG}", - "LOCAL=${localDir().absolutePath}", - "PRIVATE_DIR=${filesDir.parentFile!!.absolutePath}", - "LD_LIBRARY_PATH=${localLibDir().absolutePath}", - "EXT_HOME=${sandboxHomeDir()}", - "HOME=${if (Settings.sandbox){ "/home"} else{ sandboxHomeDir()}}", - "PROMPT_DIRTRIM=2", - "LINKER=${if(File("/system/bin/linker64").exists()){"/system/bin/linker64"}else{"/system/bin/linker"}}", - "NATIVE_LIB_DIR=${applicationInfo.nativeLibraryDir}", - "FDROID=${isFDroid}", - "SANDBOX=${Settings.sandbox}", - "TMP_DIR=${getTempDir()}", - "TMPDIR=${getTempDir()}", - "TZ=UTC", - "DOTNET_GCHeapHardLimit=1C0000000", - "SOURCE_DIR=${applicationInfo.sourceDir}", - "TERMUX_X11_SOURCE_DIR=${getSourceDirOfPackage(application!!,"com.termux.x11")}", - "DISPLAY=:0", - ) - - if (!isFDroid) { - env.add("PROOT_LOADER=${applicationInfo.nativeLibraryDir}/libproot-loader.so") - if ( - Build.SUPPORTED_32_BIT_ABIS.isNotEmpty() && - File(applicationInfo.nativeLibraryDir).child("libproot-loader32.so").exists() - ) { - env.add("PROOT_LOADER32=${applicationInfo.nativeLibraryDir}/libproot-loader32.so") - } - } + val envVariables = + mapOf( + "ANDROID_ART_ROOT" to System.getenv("ANDROID_ART_ROOT"), + "ANDROID_DATA" to System.getenv("ANDROID_DATA"), + "ANDROID_I18N_ROOT" to System.getenv("ANDROID_I18N_ROOT"), + "ANDROID_ROOT" to System.getenv("ANDROID_ROOT"), + "ANDROID_RUNTIME_ROOT" to System.getenv("ANDROID_RUNTIME_ROOT"), + "ANDROID_TZDATA_ROOT" to System.getenv("ANDROID_TZDATA_ROOT"), + "BOOTCLASSPATH" to System.getenv("BOOTCLASSPATH"), + "DEX2OATBOOTCLASSPATH" to System.getenv("DEX2OATBOOTCLASSPATH"), + "EXTERNAL_STORAGE" to System.getenv("EXTERNAL_STORAGE"), + "PATH" to "${System.getenv("PATH")}:${localBinDir(context).absolutePath}", + ) + + val workingDir = runBlocking { getPwd(context) } + + val tmpDir = File(getTempDir(), "terminal/$sessionId") + + if (tmpDir.exists()) { + tmpDir.deleteRecursively() + } - if (Settings.seccomp) { - env.add("SECCOMP=1") - } + tmpDir.mkdirs() + + val env = + mutableListOf( + "PROOT=${application!!.applicationInfo.nativeLibraryDir}/libproot.so", + "PROOT_LOADER=${application!!.applicationInfo.nativeLibraryDir}/libloader.so", + "PROOT_TMP_DIR=${tmpDir.absolutePath}", + "WKDIR=${workingDir}", + "PUBLIC_HOME=${context.getExternalFilesDir(null)?.absolutePath}", + "COLORTERM=truecolor", + "TERM=xterm-256color", + "LANG=C.UTF-8", + "DEBUG=${BuildConfig.DEBUG}", + "LOCAL=${localDir(context).absolutePath}", + "PRIVATE_DIR=${context.filesDir.parentFile!!.absolutePath}", + "LD_LIBRARY_PATH=${localLibDir(context).absolutePath}", + "EXT_HOME=${sandboxHomeDir(context)}", + "HOME=${if (Settings.sandbox){ "/home"} else{ sandboxHomeDir(context)}}", + "PROMPT_DIRTRIM=2", + "LINKER=${if(File("/system/bin/linker64").exists()){"/system/bin/linker64"}else{"/system/bin/linker"}}", + "NATIVE_LIB_DIR=${context.applicationInfo.nativeLibraryDir}", + "SANDBOX=${Settings.sandbox}", + "TMP_DIR=${getTempDir()}", + "TMPDIR=${getTempDir()}", + "TZ=UTC", + "DOTNET_GCHeapHardLimit=1C0000000", + "SOURCE_DIR=${context.applicationInfo.sourceDir}", + "TERMUX_X11_SOURCE_DIR=${getSourceDirOfPackage(application!!,"com.termux.x11")}", + "DISPLAY=:0", + ) + + val loader32 = "${context.applicationInfo.nativeLibraryDir}/libloader32.so" + if (File(loader32).exists()) { + env.add("PROOT_LOADER_32=$loader32") + } - env.addAll(envVariables.map { "${it.key}=${it.value}" }) + when (Settings.seccomp_mode) { + "yes" -> env.add("SECCOMP=1") + "no" -> env.add("PROOT_NO_SECCOMP=1") + } - pendingCommand?.env?.let { env.addAll(it) } + env.addAll(envVariables.map { "${it.key}=${it.value}" }) - setupTerminalFiles() + pendingCommand?.env?.let { env.addAll(it) } - val sandboxSH = localBinDir().child("sandbox") - val setupSH = localBinDir().child("setup") + setupTerminalFiles() - val args: Array + val sandboxSH = localBinDir(context).child("sandbox") + val setupSH = localBinDir(context).child("setup") - val shell = - if (pendingCommand == null) { - args = - if (Settings.sandbox) { - arrayOf(sandboxSH.absolutePath) - } else { - arrayOf() - } - "/system/bin/sh" - } else if (pendingCommand!!.sandbox.not()) { - args = pendingCommand!!.args - pendingCommand!!.exe - } else { - args = - mutableListOf(sandboxSH.absolutePath, pendingCommand!!.exe, *pendingCommand!!.args) - .toTypedArray() + val args: Array - "/system/bin/sh" - } + val shell = + if (pendingCommand == null) { + args = + if (Settings.sandbox) { + arrayOf(sandboxSH.absolutePath) + } else { + arrayOf() + } + "/system/bin/sh" + } else if (pendingCommand!!.sandbox.not()) { + args = pendingCommand!!.args + pendingCommand!!.exe + } else { + args = + mutableListOf(sandboxSH.absolutePath, pendingCommand!!.exe, *pendingCommand!!.args) + .toTypedArray() + + "/system/bin/sh" + } - val actualShell: String - val actualArgs: Array = - if (installNextStage != null && installNextStage == NEXT_STAGE.EXTRACTION) { - actualShell = "/system/bin/sh" - mutableListOf("-c", setupSH.absolutePath, *args).toTypedArray() - } else { - actualShell = shell - arrayOf("-c", *args) - } + val actualShell: String + val actualArgs: Array = + if (isExtraction) { + actualShell = "/system/bin/sh" + mutableListOf("-c", setupSH.absolutePath, *args).toTypedArray() + } else { + actualShell = shell + arrayOf("-c", *args) + } - pendingCommand = null + pendingCommand = null - return TerminalSession( - actualShell, - localDir().absolutePath, - actualArgs, - env.toTypedArray(), - Settings.terminal_scrollback_buffer, - sessionClient, - ) to workingDir - } + return TerminalSession( + actualShell, + localDir(context).absolutePath, + actualArgs, + env.toTypedArray(), + Settings.terminal_scrollback_buffer, + sessionClient, + ) to workingDir } } -suspend fun Terminal.getPwd(): String { +suspend fun getPwd(context: Context): String { val pendingWorkingDir = pendingCommand?.workingDir if (pendingWorkingDir != null) { return pendingWorkingDir } - if (intent.hasExtra("cwd")) { - return intent.getStringExtra("cwd").toString() + if (context is Activity && context.intent.hasExtra("cwd")) { + return context.intent.getStringExtra("cwd").toString() } val currentTab = MainActivity.instance?.viewModel?.tabManager?.currentTab if (Settings.project_as_pwd) { - // if (currentProject != null && currentProject is FileWrapper) { - // val absolutePath = currentProject!!.getAbsolutePath() - // return if (Settings.sandbox) { - // absolutePath.removePrefix(localDir().absolutePath) - // } else { - // absolutePath - // } - // } - currentTab?.let { if (it is EditorTab && it.file is FileWrapper) { val parent = it.file.getParentFile() if (parent != null && parent is FileWrapper) { return if (Settings.sandbox) { - parent.getAbsolutePath().removePrefix(localDir().absolutePath) + parent.getAbsolutePath().removePrefix(localDir(context).absolutePath) } else { parent.getAbsolutePath() } @@ -189,7 +176,7 @@ suspend fun Terminal.getPwd(): String { val parent = it.file.getParentFile() if (parent != null && parent is FileWrapper) { return if (Settings.sandbox) { - parent.getAbsolutePath().removePrefix(localDir().absolutePath) + parent.getAbsolutePath().removePrefix(localDir(context).absolutePath) } else { parent.getAbsolutePath() } @@ -200,6 +187,6 @@ suspend fun Terminal.getPwd(): String { return if (Settings.sandbox) { "/home" } else { - sandboxHomeDir().absolutePath + sandboxHomeDir(context).absolutePath } } diff --git a/core/main/src/main/java/com/rk/terminal/SessionService.kt b/core/main/src/main/java/com/rk/terminal/SessionService.kt index ffb5b4443..410b3f196 100644 --- a/core/main/src/main/java/com/rk/terminal/SessionService.kt +++ b/core/main/src/main/java/com/rk/terminal/SessionService.kt @@ -38,14 +38,20 @@ class SessionService : Service() { } fun createSession(id: SessionId, client: TerminalSessionClient, activity: Terminal): SessionInfo { - return MkSession.createSession(activity, client, id).let { - val (session, pwd) = it - sessions[id] = session - sessionWorkDirs[id] = pwd - sessionList.add(id) - updateNotification() - SessionInfo(id, pwd, session) - } + return MkSession.createSession( + activity, + client, + id, + activity.installNextStage != null && activity.installNextStage == NEXT_STAGE.EXTRACTION, + ) + .let { + val (session, pwd) = it + sessions[id] = session + sessionWorkDirs[id] = pwd + sessionList.add(id) + updateNotification() + SessionInfo(id, pwd, session) + } } fun getSession(id: SessionId): TerminalSession? { @@ -108,7 +114,15 @@ class SessionService : Service() { super.onCreate() createNotificationChannel() val notification = createNotification() - startForeground(1, notification) + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + startForeground( + 1, + notification, + android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE, + ) + } else { + startForeground(1, notification) + } if (deamonRunning.not()) { GlobalScope.launch(Dispatchers.IO) { deamonRunning = true } diff --git a/core/main/src/main/java/com/rk/terminal/TerminalBackEnd.kt b/core/main/src/main/java/com/rk/terminal/TerminalBackEnd.kt index b5d0e2d62..e9f50d8af 100644 --- a/core/main/src/main/java/com/rk/terminal/TerminalBackEnd.kt +++ b/core/main/src/main/java/com/rk/terminal/TerminalBackEnd.kt @@ -29,8 +29,9 @@ class TerminalBackEnd : TerminalViewClient, TerminalSessionClient { override fun onPasteTextFromClipboard(session: TerminalSession?) { val clip = ClipboardUtils.getText().toString() - if (clip.trim { it <= ' ' }.isNotEmpty() && terminalView.get()?.mEmulator != null) { - terminalView.get()?.mEmulator?.paste(clip) + val emulator = terminalView.get()?.mEmulator ?: return + if (clip.isNotBlank()) { + emulator.paste(clip) } } @@ -101,6 +102,10 @@ class TerminalBackEnd : TerminalViewClient, TerminalSessionClient { return true } + override fun shouldSupportClipboardKeybindings(): Boolean { + return Settings.terminal_clipboard_keybindings + } + override fun isTerminalViewSelected(): Boolean { return true } @@ -110,13 +115,12 @@ class TerminalBackEnd : TerminalViewClient, TerminalSessionClient { override fun onKeyDown(keyCode: Int, e: KeyEvent, session: TerminalSession): Boolean { if (keyCode == KeyEvent.KEYCODE_ENTER && !session.isRunning) { val activity = Terminal.instance ?: return false - activity.sessionBinder - ?.get() - ?.terminateSession(activity.sessionBinder?.get()!!.getService().currentSession.value) - if (activity.sessionBinder?.get()!!.getService().sessionList.isEmpty()) { + val sessionBinder = activity.sessionBinder?.get() ?: return false + sessionBinder.terminateSession(sessionBinder.getService().currentSession.value) + if (sessionBinder.getService().sessionList.isEmpty()) { activity.finish() } else { - activity.changeSession(activity.sessionBinder?.get()!!.getService().sessionList.first()) + activity.changeSession(sessionBinder.getService().sessionList.first()) } return true } diff --git a/core/main/src/main/java/com/rk/terminal/TerminalScreen.kt b/core/main/src/main/java/com/rk/terminal/TerminalScreen.kt index 9e5938eaa..13f49d3e3 100644 --- a/core/main/src/main/java/com/rk/terminal/TerminalScreen.kt +++ b/core/main/src/main/java/com/rk/terminal/TerminalScreen.kt @@ -77,7 +77,9 @@ import com.rk.resources.strings import com.rk.settings.Settings import com.rk.settings.editor.DEFAULT_TERMINAL_FONT_PATH import com.rk.settings.editor.TerminalFontScreen +import com.rk.settings.terminal.DEFAULT_TERMINAL_EXTRA_KEYS import com.rk.settings.terminal.SettingsTerminalScreen +import com.rk.settings.terminal.TerminalCheckScreen import com.rk.settings.terminal.TerminalExtraKeys import com.rk.terminal.virtualkeys.VirtualKeysConstants import com.rk.terminal.virtualkeys.VirtualKeysInfo @@ -86,12 +88,13 @@ import com.rk.terminal.virtualkeys.VirtualKeysView import com.rk.theme.LocalThemeHolder import com.rk.theme.ThemeHolder import com.rk.utils.dpToPx +import com.rk.utils.toast import com.termux.terminal.TerminalColors import com.termux.terminal.TextStyle import com.termux.view.TerminalView +import kotlinx.coroutines.launch import java.lang.ref.WeakReference import java.util.Properties -import kotlinx.coroutines.launch var terminalView = WeakReference(null) var virtualKeysView = WeakReference(null) @@ -113,6 +116,7 @@ fun TerminalScreen(modifier: Modifier = Modifier, terminalActivity: Terminal) { composable(SettingsRoutes.TerminalSettings.route) { SettingsTerminalScreen(navController) } composable(SettingsRoutes.TerminalFontScreen.route) { TerminalFontScreen() } composable(SettingsRoutes.TerminalExtraKeys.route) { TerminalExtraKeys() } + composable(SettingsRoutes.TerminalCheck.route) { TerminalCheckScreen() } } } @@ -170,13 +174,25 @@ fun TerminalScreenInternal(modifier: Modifier = Modifier, terminalActivity: Term buttonTextColor = onSurfaceColor - reload( - VirtualKeysInfo( - Settings.terminal_extra_keys, - "", - VirtualKeysConstants.CONTROL_CHARS_ALIASES, - ) - ) + runCatching { + reload( + VirtualKeysInfo( + Settings.terminal_extra_keys, + "", + VirtualKeysConstants.CONTROL_CHARS_ALIASES, + ) + ) + } + .onFailure { + toast(strings.invalid_terminal_extra_keys) + reload( + VirtualKeysInfo( + DEFAULT_TERMINAL_EXTRA_KEYS, + "", + VirtualKeysConstants.CONTROL_CHARS_ALIASES, + ) + ) + } } }, modifier = Modifier.fillMaxWidth().height(75.dp), @@ -380,7 +396,7 @@ private fun TerminalDrawer(drawerWidth: Dp, terminalActivity: Terminal, navContr Icon(imageVector = Icons.Default.Add, contentDescription = stringResource(strings.add_session)) } - IconButton(onClick = { navController.navigate("terminal_settings") }) { + IconButton(onClick = { navController.navigate(SettingsRoutes.TerminalSettings.route) }) { Icon( imageVector = Icons.Outlined.Settings, contentDescription = stringResource(strings.settings), @@ -448,7 +464,7 @@ fun Terminal.changeSession(sessionId: String) { terminalView.apply { post { keepScreenOn = true - setFocusableInTouchMode(true) + isFocusableInTouchMode = true requestFocus() } } diff --git a/core/main/src/main/java/com/rk/theme/ThemeConfig.kt b/core/main/src/main/java/com/rk/theme/ThemeConfig.kt index a595411bb..ef89d92d4 100644 --- a/core/main/src/main/java/com/rk/theme/ThemeConfig.kt +++ b/core/main/src/main/java/com/rk/theme/ThemeConfig.kt @@ -1,5 +1,6 @@ package com.rk.theme +import androidx.annotation.Keep import com.google.gson.JsonElement import com.google.gson.JsonParser import java.io.ObjectInputStream @@ -7,6 +8,7 @@ import java.io.ObjectOutputStream import java.io.Serial import java.io.Serializable +@Keep data class BaseColors( val primary: String? = null, val onPrimary: String? = null, @@ -46,6 +48,7 @@ data class BaseColors( val surfaceContainerHighest: String? = null, ) : Serializable +@Keep data class ThemePalette( val baseColors: BaseColors?, val terminalColors: Map? = null, @@ -101,10 +104,11 @@ data class ThemePalette( } } +@Keep data class ThemeConfig( val id: String?, val name: String?, - val targetVersion: Int?, + val minAppVersion: Int?, val inheritBase: Boolean?, val light: ThemePalette?, val dark: ThemePalette?, diff --git a/core/main/src/main/java/com/rk/theme/ThemeHolder.kt b/core/main/src/main/java/com/rk/theme/ThemeHolder.kt index bb1f6deef..fb6a30fa2 100644 --- a/core/main/src/main/java/com/rk/theme/ThemeHolder.kt +++ b/core/main/src/main/java/com/rk/theme/ThemeHolder.kt @@ -1,9 +1,11 @@ package com.rk.theme +import androidx.annotation.Keep import androidx.compose.material3.ColorScheme import com.google.gson.JsonArray import java.util.Properties +@Keep data class ThemeHolder( val id: String, val name: String, diff --git a/core/main/src/main/java/com/rk/theme/ThemeLoader.kt b/core/main/src/main/java/com/rk/theme/ThemeLoader.kt index e8715c99d..c34f26c22 100644 --- a/core/main/src/main/java/com/rk/theme/ThemeLoader.kt +++ b/core/main/src/main/java/com/rk/theme/ThemeLoader.kt @@ -18,7 +18,7 @@ import com.rk.resources.getString import com.rk.resources.strings import com.rk.settings.theme.themes import com.rk.utils.application -import com.rk.utils.dialog +import com.rk.utils.dialogRes import com.rk.utils.errorDialog import com.rk.utils.toast import java.io.FileInputStream @@ -47,8 +47,8 @@ suspend fun loadConfigFromJson(file: FileObject): ThemeConfig? = suspend fun ThemeConfig.installTheme() = withContext(Dispatchers.IO) { if (id == null) { - dialog( - context = SettingsActivity.instance, + dialogRes( + activity = SettingsActivity.instance, title = strings.theme_install_failed.getString(), msg = strings.theme_id_missing.getString(), cancelable = false, @@ -57,8 +57,8 @@ suspend fun ThemeConfig.installTheme() = } if (name == null) { - dialog( - context = SettingsActivity.instance, + dialogRes( + activity = SettingsActivity.instance, title = strings.theme_install_failed.getString(), msg = strings.theme_name_missing.getString(), cancelable = false, @@ -66,26 +66,16 @@ suspend fun ThemeConfig.installTheme() = return@withContext } - if (targetVersion == null) { - dialog( - context = SettingsActivity.instance, - title = strings.theme_install_failed.getString(), - msg = strings.theme_version_missing.getString(), - cancelable = false, - ) - return@withContext - } - val packageName = application!!.packageName val packageManager = application!!.packageManager val currentVersionCode = PackageInfoCompat.getLongVersionCode(packageManager.getPackageInfo(packageName, 0)) - if (targetVersion.toLong() != currentVersionCode) { - dialog( - context = SettingsActivity.instance, + if (minAppVersion != null && minAppVersion.toLong() > currentVersionCode) { + dialogRes( + activity = SettingsActivity.instance, title = strings.warning.getString(), msg = strings.incompatible_theme_warning.getString(), - cancelString = strings.cancel, - okString = strings.continue_action, + cancelRes = strings.cancel, + okRes = strings.continue_action, onOk = { finishThemeInstall(name) updateThemes() diff --git a/core/main/src/main/java/com/rk/utils/ApplicationContext.kt b/core/main/src/main/java/com/rk/utils/ApplicationContext.kt index 45fd98f6e..f5f08046a 100644 --- a/core/main/src/main/java/com/rk/utils/ApplicationContext.kt +++ b/core/main/src/main/java/com/rk/utils/ApplicationContext.kt @@ -1,6 +1,22 @@ package com.rk.utils import android.app.Application +import okhttp3.Cache +import okhttp3.OkHttpClient +import java.io.File // legacy - this should have been moved to App.instance but its being used everywhere var application: Application? = null + +val okHttpClient: OkHttpClient by lazy { + val context = application ?: throw IllegalStateException("Application is not initialized yet") + OkHttpClient.Builder() + .cache( + Cache( + directory = File(context.cacheDir, "http_cache"), + maxSize = 10L * 1024L * 1024L // 10 MiB + ) + ) + .build() +} + diff --git a/core/main/src/main/java/com/rk/utils/Log.kt b/core/main/src/main/java/com/rk/utils/Log.kt index fcf952283..e4990e760 100644 --- a/core/main/src/main/java/com/rk/utils/Log.kt +++ b/core/main/src/main/java/com/rk/utils/Log.kt @@ -1,14 +1,16 @@ package com.rk.utils import android.util.Log +import com.rk.resources.getString +import com.rk.resources.strings import com.rk.settings.debugOptions.LogCollector -fun Any.debug(msg: String) { +fun Any.logDebug(msg: String) { Log.d(this::class.java.simpleName, msg) LogCollector.reportDebug(msg) } -fun Any.info(msg: String) { +fun Any.logInfo(msg: String) { Log.i(this::class.java.simpleName, msg) LogCollector.reportInfo(msg) } @@ -22,3 +24,8 @@ fun Any.logError(msg: String) { Log.e(this::class.java.simpleName, msg) LogCollector.reportError(msg) } + +fun Any.logError(throwable: Throwable, msg: String = strings.unknown_error.getString()) { + Log.e(this::class.java.simpleName, msg, throwable) + LogCollector.reportError("$msg: \n${throwable.stackTraceToString()}") +} diff --git a/core/main/src/main/java/com/rk/utils/Utils.kt b/core/main/src/main/java/com/rk/utils/Utils.kt index 93d08756e..b2e22398f 100644 --- a/core/main/src/main/java/com/rk/utils/Utils.kt +++ b/core/main/src/main/java/com/rk/utils/Utils.kt @@ -8,6 +8,8 @@ import android.content.Intent import android.content.pm.PackageManager import android.content.res.Configuration import android.graphics.Typeface +import android.graphics.drawable.Drawable +import android.graphics.drawable.PictureDrawable import android.os.Build import android.telephony.TelephonyManager import android.text.Spanned @@ -37,7 +39,10 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextDecoration import androidx.core.net.toUri import com.blankj.utilcode.util.ThreadUtils +import com.caverock.androidsvg.SVG +import com.rk.activities.main.MainActivity import com.rk.activities.main.gitViewModel +import com.rk.extension.ActivityProvider import com.rk.file.BuiltinFileType import com.rk.file.FileObject import com.rk.filetree.FileTreeViewModel @@ -55,6 +60,7 @@ import com.rk.theme.gitDeleted import com.rk.theme.gitModified import io.github.rosemoe.sora.widget.schemes.EditorColorScheme import java.io.File +import java.io.InputStream import java.io.ObjectInputStream import java.io.ObjectOutputStream import java.text.NumberFormat @@ -87,6 +93,7 @@ suspend fun FileObject.readObject(): Any? = } } + fun toast(message: String?) { if (message.isNullOrBlank()) { Log.w("UTILS", "Toast with null or empty message") @@ -96,7 +103,16 @@ fun toast(message: String?) { Log.w("TOAST", message) return } - runOnUiThread { Toast.makeText(application!!, message, Toast.LENGTH_SHORT).show() } + + runOnUiThread { + val context = ActivityProvider.currentActivity as? Context + if (context != null){ + Toast.makeText(context, message, Toast.LENGTH_SHORT).show() + }else{ + Log.w("Utils","no valid ui context available for making toast: $message") + } + + } } /** Returns true if the currently selected user theme is dark. If it's set to system, the system theme is used. */ @@ -111,7 +127,7 @@ fun isDarkTheme(ctx: Context): Boolean { /** Returns true if the system theme is dark. **NOTE:** Prefer [isDarkTheme] to respect user settings. */ fun isSystemInDarkTheme(ctx: Context): Boolean { return ((ctx.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == - Configuration.UI_MODE_NIGHT_YES) + Configuration.UI_MODE_NIGHT_YES) } inline fun dpToPx(dp: Float, ctx: Context): Int { @@ -134,7 +150,11 @@ fun x(m: MutableCollection, c: Int) { } } -fun Activity.openUrl(url: String) { +fun Context.openDocs(page: String) { + openUrl("https://xed-editor.github.io/Xed-Docs/docs/$page") +} + +fun Context.openUrl(url: String) { val intent = Intent(Intent.ACTION_VIEW, url.toUri()) startActivity(intent) } @@ -180,57 +200,6 @@ fun expectOOM(requiredMemBytes: Long): Boolean { return requiredMemory > availableMemory } -// used for warning purposes -fun isChinaDevice(context: Context): Boolean { - val manufacturer = Build.MANUFACTURER.lowercase() - - if ( - manufacturer.contains("huawei") || - manufacturer.contains("xiaomi") || - manufacturer.contains("oppo") || - manufacturer.contains("vivo") || - manufacturer.contains("realme") || - manufacturer.contains("oneplus") - ) { - return true - } - - val localeCountry = Locale.getDefault().country - if (localeCountry.equals("CN", ignoreCase = true)) return true - - val tm = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager - val simCountry = tm.simCountryIso - return simCountry.equals("cn", ignoreCase = true) -} - -fun showTerminalNotice(activity: Activity, onOk: () -> Unit) { - if (isChinaDevice(activity) && !Settings.terminal_virus_notice) { - dialog( - context = activity, - title = strings.attention.getString(), - msg = strings.terminal_virus_notice.getString(), - onOk = { - Settings.terminal_virus_notice = true - it?.dismiss() - onOk() - }, - onCancel = {}, - cancelable = false, - ) - } else { - onOk() - } -} - -fun isAppInstalled(context: Context, packageName: String): Boolean { - return try { - context.packageManager.getPackageInfo(packageName, 0) - true // App is installed - } catch (e: PackageManager.NameNotFoundException) { - false // App not found - } -} - fun getSourceDirOfPackage(context: Context, packageName: String): String? { return try { val info = context.packageManager.getApplicationInfo(packageName, 0) @@ -248,11 +217,6 @@ fun getTempDir(): File { return tmp } -val isFDroid by lazy { - val targetSdkVersion = application!!.applicationInfo.targetSdkVersion - targetSdkVersion == 28 -} - /** Converts a [Spanned] text object to an [AnnotatedString]. */ fun Spanned.toAnnotatedString(): AnnotatedString { val builder = AnnotatedString.Builder(this.toString()) @@ -384,9 +348,16 @@ fun getGitColor(changeType: ChangeType): Color = suspend fun findGitRoot(path: String): String? = withContext(Dispatchers.IO) { - val startDir = File(path).let { if (it.isDirectory) it else it.parentFile } - val repo = FileRepositoryBuilder().findGitDir(startDir).takeIf { it.gitDir != null }?.build() - repo?.workTree?.canonicalPath + runCatching { + val startDir = File(path).let { if (it.isDirectory) it else it.parentFile } + FileRepositoryBuilder().findGitDir(startDir).takeIf { it.gitDir != null }?.build()?.use { repo -> + if (!repo.isBare) { + repo.workTree?.canonicalPath + } else { + null + } + } + }.getOrNull() } fun hasBinaryChars(text: String): Boolean { @@ -476,3 +447,15 @@ fun timeAgo(currentTimeMillis: Long, startTimeMillis: Long): String? { else -> plurals.time_days_ago.getQuantityString(days, days) } } + +fun loadSvg(inputStream: InputStream): Drawable? { + val svg = + try { + SVG.getFromInputStream(inputStream) + } catch (_: Exception) { + return null + } + + val picture = svg.renderToPicture() + return PictureDrawable(picture) +} diff --git a/core/main/src/main/java/com/rk/utils/Dialogs.kt b/core/main/src/main/java/com/rk/utils/dialogs.kt similarity index 57% rename from core/main/src/main/java/com/rk/utils/Dialogs.kt rename to core/main/src/main/java/com/rk/utils/dialogs.kt index 32fe1aecb..ac5c2308d 100644 --- a/core/main/src/main/java/com/rk/utils/Dialogs.kt +++ b/core/main/src/main/java/com/rk/utils/dialogs.kt @@ -21,17 +21,16 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.rk.activities.main.MainActivity -import com.rk.components.compose.preferences.base.DividerColumn +import com.rk.extension.api.XedExtensionPoint import com.rk.resources.getString import com.rk.resources.strings import com.rk.settings.Settings import com.rk.theme.XedTheme -fun errorDialog(msg: String, activity: Activity? = MainActivity.instance, title: String = strings.error.getString()) { +fun errorDialog(activity: Activity? = MainActivity.instance, title: String = strings.error.getString(), msg: String) { Log.e("ERROR_DIALOG", msg) runOnUiThread { @@ -44,7 +43,7 @@ fun errorDialog(msg: String, activity: Activity? = MainActivity.instance, title: return@runOnUiThread } - dialog(context = activity, title = title, msg = msg, onOk = {}) + dialogRes(activity = activity, title = title, msg = msg, onOk = {}) } } @@ -52,8 +51,11 @@ fun errorDialog(@StringRes msgRes: Int) { runOnUiThread { errorDialog(msg = msgRes.getString()) } } -// todo handle multple function call for same throwable -fun errorDialog(throwable: Throwable, activity: Activity? = MainActivity.instance) { +fun errorDialog( + activity: Activity? = MainActivity.instance, + throwable: Throwable, + title: String = strings.error.getString(), +) { runOnUiThread { if (throwable.message.toString().contains("Job was cancelled")) { Log.w("ERROR_DIALOG", throwable.message.toString()) @@ -67,7 +69,7 @@ fun errorDialog(throwable: Throwable, activity: Activity? = MainActivity.instanc } } - errorDialog(msg = message.toString(), activity = activity) + errorDialog(activity = activity, title = title, msg = message.toString()) } } @@ -90,59 +92,75 @@ fun errorDialog(exception: Exception) { var isDialogShowing = false private set +fun dialogRes( + activity: Activity? = MainActivity.instance, + title: String? = null, + msg: String, + @StringRes cancelRes: Int = strings.cancel, + @StringRes okRes: Int = strings.ok, + onOk: (AlertDialog?) -> Unit = {}, + onCancel: ((AlertDialog?) -> Unit)? = null, + cancelable: Boolean = true, +) { + dialog( + activity = activity, + title = title, + msg = msg, + cancelText = cancelRes.getString(), + okText = okRes.getString(), + onOk = onOk, + onCancel = onCancel, + cancelable = cancelable, + ) +} + +@XedExtensionPoint fun dialog( - context: Activity? = MainActivity.instance, + activity: Activity? = MainActivity.instance, title: String? = null, msg: String, - @StringRes cancelString: Int = strings.cancel, - @StringRes okString: Int = strings.ok, - onDialog: (AlertDialog?) -> Unit = {}, + cancelText: String = strings.cancel.getString(), + okText: String = strings.ok.getString(), onOk: (AlertDialog?) -> Unit = {}, onCancel: ((AlertDialog?) -> Unit)? = null, cancelable: Boolean = true, ) { - if (context == null) { + if (activity == null) { toast(strings.unknown_error) return } var alertDialog: AlertDialog? = null runOnUiThread { - MaterialAlertDialogBuilder(context).apply { + MaterialAlertDialogBuilder(activity).apply { setOnCancelListener { isDialogShowing = false } setView( - ComposeView(context).apply { + ComposeView(activity).apply { setContent { XedTheme { - Surface { - Surface { - Surface(shape = MaterialTheme.shapes.large, tonalElevation = 1.dp) { - DividerColumn(startIndent = 0.dp, endIndent = 0.dp, dividersToSkip = 0) { - alertDialog?.setCancelable(cancelable) - DialogContent( - alertDialog = alertDialog, - title = title, - msg = msg, - cancelString = cancelString, - okString = okString, - onOk = { onOk(alertDialog) }, - onCancel = - if (onCancel == null) { - null - } else { - { onCancel.invoke(alertDialog) } - }, - ) - } - } - } + Surface(shape = MaterialTheme.shapes.large, tonalElevation = 1.dp) { + alertDialog?.setCancelable(cancelable) + DialogContent( + alertDialog = alertDialog, + title = title, + msg = msg, + cancelString = cancelText, + okString = okText, + onOk = { onOk(alertDialog) }, + onCancel = + if (onCancel == null) { + null + } else { + { onCancel.invoke(alertDialog) } + }, + ) } } } } ) - if (context.isFinishing || context.isDestroyed) { + if (activity.isFinishing || activity.isDestroyed) { toast(msg) return@runOnUiThread } @@ -158,8 +176,8 @@ private fun DialogContent( alertDialog: AlertDialog?, title: String?, msg: String, - @StringRes cancelString: Int, - @StringRes okString: Int, + cancelString: String, + okString: String, onOk: () -> Unit, onCancel: (() -> Unit)? = null, ) { @@ -188,7 +206,7 @@ private fun DialogContent( onCancel() } ) { - Text(stringResource(cancelString)) + Text(cancelString) } Spacer(modifier = Modifier.width(8.dp)) @@ -200,8 +218,42 @@ private fun DialogContent( onOk() } ) { - Text(stringResource(okString)) + Text(okString) } } } } + +@XedExtensionPoint +fun composableDialog( + activity: Activity? = MainActivity.instance, + cancelable: Boolean = true, + composable: @Composable (AlertDialog?) -> Unit, +) { + if (activity == null) { + toast(strings.unknown_error) + return + } + var alertDialog: AlertDialog? = null + runOnUiThread { + MaterialAlertDialogBuilder(activity).apply { + setOnCancelListener { isDialogShowing = false } + + setView( + ComposeView(activity).apply { + setContent { + XedTheme { + Surface(shape = MaterialTheme.shapes.large, tonalElevation = 1.dp) { + alertDialog?.setCancelable(cancelable) + composable(alertDialog) + } + } + } + } + ) + + alertDialog = show() + isDialogShowing = true + } + } +} diff --git a/core/proot/.gitignore b/core/proot/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/core/proot/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/proot/build.gradle.kts b/core/proot/build.gradle.kts new file mode 100644 index 000000000..5b1a4aee3 --- /dev/null +++ b/core/proot/build.gradle.kts @@ -0,0 +1,52 @@ +plugins { + alias(libs.plugins.android.library) +} + +android { + namespace = "com.rk.proot" + ndkVersion = "29.0.13846066" + compileSdk { + version = release(36) + } + + defaultConfig { + minSdk = 26 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + externalNativeBuild { + cmake { + cppFlags("") + } + } + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", + ) + } + } + externalNativeBuild { + cmake { + path("src/main/cpp/CMakeLists.txt") + version = "3.22.1" + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } +} + +dependencies { + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.appcompat) + implementation(libs.material) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.test.junit) + androidTestImplementation(libs.androidx.test.espresso) +} diff --git a/core/extension/consumer-rules.pro b/core/proot/consumer-rules.pro similarity index 100% rename from core/extension/consumer-rules.pro rename to core/proot/consumer-rules.pro diff --git a/core/extension/proguard-rules.pro b/core/proot/proguard-rules.pro similarity index 100% rename from core/extension/proguard-rules.pro rename to core/proot/proguard-rules.pro diff --git a/core/proot/src/main/AndroidManifest.xml b/core/proot/src/main/AndroidManifest.xml new file mode 100644 index 000000000..a5918e68a --- /dev/null +++ b/core/proot/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/core/proot/src/main/cpp/CMakeLists.txt b/core/proot/src/main/cpp/CMakeLists.txt new file mode 100644 index 000000000..618cf5432 --- /dev/null +++ b/core/proot/src/main/cpp/CMakeLists.txt @@ -0,0 +1,304 @@ +cmake_minimum_required(VERSION 3.10) +project(proot C ASM) + +# Compatibility flags and headers +add_definitions(-D_FILE_OFFSET_BITS=64 -D_GNU_SOURCE -D__STDC_WANT_LIB_EXT1__=1) +include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/talloc) + +# Compiler flags +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra -O2") +# Reproducibility flags +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -ffile-prefix-map=${CMAKE_CURRENT_SOURCE_DIR}=. -fno-ident") +set(CMAKE_ASM_FLAGS "${CMAKE_ASM_FLAGS} -ffile-prefix-map=${CMAKE_CURRENT_SOURCE_DIR}=.") +set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-z,noexecstack -Wl,--build-id=sha1") + +# Tools from environment (NDK) +find_program(OBJCOPY_TOOL NAMES ${CMAKE_OBJCOPY} $ENV{OBJCOPY} llvm-objcopy objcopy) +find_program(OBJDUMP_TOOL NAMES ${CMAKE_OBJDUMP} $ENV{OBJDUMP} llvm-objdump objdump) +find_program(STRIP_TOOL NAMES ${CMAKE_STRIP} $ENV{STRIP} llvm-strip strip) +find_program(READELF_TOOL NAMES ${CMAKE_READELF} llvm-readelf readelf) + +if(NOT OBJCOPY_TOOL OR NOT OBJDUMP_TOOL OR NOT STRIP_TOOL OR NOT READELF_TOOL) + message(FATAL_ERROR "Could not find required binary tools (objcopy, objdump, strip, readelf)") +endif() + +# Feature Detection (build.h) +include(CheckCSourceCompiles) + +add_definitions(-DPROOT_UNBUNDLE_LOADER) + +function(check_feature FEATURE_NAME SOURCE_CODE DEFINE_VAR) + check_c_source_compiles("${SOURCE_CODE}" ${DEFINE_VAR}) + if(${${DEFINE_VAR}}) + set(${DEFINE_VAR}_STR "#define HAVE_${FEATURE_NAME}" PARENT_SCOPE) + else() + set(${DEFINE_VAR}_STR "" PARENT_SCOPE) + endif() +endfunction() + +check_feature(PROCESS_VM " +#define _GNU_SOURCE +#include +int main(void) { + process_vm_readv(0, (const struct iovec *)0, 0, (const struct iovec *)0, 0, 0); + return 0; +}" HAVE_PROCESS_VM) + +check_feature(SECCOMP_FILTER " +#include +#include +#include +#include +#include +int main(void) { + prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, 0); + return 0; +}" HAVE_SECCOMP_FILTER) + +# Version Detection +execute_process( + COMMAND git describe --tags --dirty --abbrev=8 --always + OUTPUT_VARIABLE GIT_VERSION + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET +) +if(NOT GIT_VERSION) + set(GIT_VERSION "unknown") +endif() + +# Generate build.h +set(BUILD_H_CONTENT "/* This file is auto-generated, edit at your own risk. */\n") +set(BUILD_H_CONTENT "${BUILD_H_CONTENT}#ifndef BUILD_H\n#define BUILD_H\n") +set(BUILD_H_CONTENT "${BUILD_H_CONTENT}#undef VERSION\n#define VERSION \"${GIT_VERSION}\"\n") +set(BUILD_H_CONTENT "${BUILD_H_CONTENT}${HAVE_PROCESS_VM_STR}\n") +set(BUILD_H_CONTENT "${BUILD_H_CONTENT}${HAVE_SECCOMP_FILTER_STR}\n") +set(BUILD_H_CONTENT "${BUILD_H_CONTENT}#endif /* BUILD_H */\n") +file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/build.h "${BUILD_H_CONTENT}") +include_directories(${CMAKE_CURRENT_BINARY_DIR}) + +# Architecture-specific info from arch.h +function(get_arch_define DEFINE_NAME VAR_NAME EXTRA_CFLAGS) + set(FLAGS "") + if(CMAKE_C_COMPILER_TARGET) + list(APPEND FLAGS "--target=${CMAKE_C_COMPILER_TARGET}") + endif() + if(CMAKE_SYSROOT) + list(APPEND FLAGS "--sysroot=${CMAKE_SYSROOT}") + endif() + separate_arguments(C_FLAGS_LIST NATIVE_COMMAND "${CMAKE_C_FLAGS}") + list(APPEND FLAGS ${C_FLAGS_LIST}) + if(EXTRA_CFLAGS) + separate_arguments(EXTRA_FLAGS_LIST NATIVE_COMMAND "${EXTRA_CFLAGS}") + list(APPEND FLAGS ${EXTRA_FLAGS_LIST}) + endif() + + execute_process( + COMMAND ${CMAKE_C_COMPILER} ${FLAGS} -E -dM -DNO_LIBC_HEADER ${CMAKE_CURRENT_SOURCE_DIR}/arch.h + OUTPUT_VARIABLE PREPROCESSOR_OUTPUT + ERROR_VARIABLE PREPROCESSOR_ERROR + ) + if(PREPROCESSOR_OUTPUT MATCHES "#define ${DEFINE_NAME} ([^\n]+)") + set(VAL "${CMAKE_MATCH_1}") + string(STRIP "${VAL}" VAL) + set(${VAR_NAME} "${VAL}" PARENT_SCOPE) + else() + set(${VAR_NAME} "" PARENT_SCOPE) + endif() +endfunction() + +# We always set these to TRUE for the sake of setting up build rules. +# The actual activation happens in the C code via #ifdef. +set(HAS_POKEDATA_WORKAROUND TRUE) + +get_arch_define(HAS_LOADER_32BIT HAS_LOADER_32BIT_VAL "") +if(HAS_LOADER_32BIT_VAL) + set(HAS_LOADER_32BIT TRUE) +else() + set(HAS_LOADER_32BIT FALSE) +endif() + +get_arch_define(LOADER_ADDRESS LOADER_ADDRESS_64 "") +get_arch_define(LOADER_ARCH_CFLAGS LOADER_ARCH_CFLAGS_64 "") + +if(ANDROID) + if(ANDROID_ABI STREQUAL "arm64-v8a") + set(M32_FLAG "--target=armv7a-linux-androideabi${ANDROID_PLATFORM_LEVEL}") + elseif(ANDROID_ABI STREQUAL "x86_64") + set(M32_FLAG "--target=i686-linux-android${ANDROID_PLATFORM_LEVEL}") + else() + set(M32_FLAG "-m32") + endif() +else() + set(M32_FLAG "-m32") +endif() + +if(HAS_LOADER_32BIT) + get_arch_define(LOADER_ADDRESS LOADER_ADDRESS_32 "${M32_FLAG}") + get_arch_define(LOADER_ARCH_CFLAGS LOADER_ARCH_CFLAGS_32 "${M32_FLAG}") +endif() + +# Check if 32-bit loader can actually be built +if(HAS_LOADER_32BIT) + if(ANDROID) + set(COMPILER_SUPPORTS_M32 true) + else() + include(CheckCCompilerFlag) + check_c_compiler_flag("${M32_FLAG}" COMPILER_SUPPORTS_M32) + endif() + if(NOT COMPILER_SUPPORTS_M32 OR LOADER_ADDRESS_32 STREQUAL "") + message(STATUS "Disabling 32-bit loader: ${M32_FLAG} not supported or address unknown") + set(HAS_LOADER_32BIT false) + endif() +endif() + +# Talloc sources +set(TALLOC_SOURCES + talloc/talloc.c + talloc/replace.c + talloc/closefrom.c +) + +# PRoot sources +set(PROOT_SOURCES + cli/cli.c + cli/proot.c + cli/note.c + execve/enter.c + execve/exit.c + execve/shebang.c + execve/elf.c + execve/ldso.c + execve/auxv.c + execve/aoxp.c + path/binding.c + path/glue.c + path/canon.c + path/f2fs-bug.c + path/path.c + path/proc.c + path/temp.c + syscall/seccomp.c + syscall/syscall.c + syscall/chain.c + syscall/enter.c + syscall/exit.c + syscall/sysnum.c + syscall/socket.c + syscall/heap.c + syscall/rlimit.c + tracee/tracee.c + tracee/mem.c + tracee/reg.c + tracee/event.c + tracee/seccomp.c + tracee/statx.c + ptrace/ptrace.c + ptrace/user.c + ptrace/wait.c + extension/extension.c + extension/ashmem_memfd/ashmem_memfd.c + extension/kompat/kompat.c + extension/fake_id0/chown.c + extension/fake_id0/chroot.c + extension/fake_id0/getsockopt.c + extension/fake_id0/sendmsg.c + extension/fake_id0/socket.c + extension/fake_id0/open.c + extension/fake_id0/unlink.c + extension/fake_id0/rename.c + extension/fake_id0/chmod.c + extension/fake_id0/utimensat.c + extension/fake_id0/access.c + extension/fake_id0/exec.c + extension/fake_id0/link.c + extension/fake_id0/symlink.c + extension/fake_id0/mk.c + extension/fake_id0/stat.c + extension/fake_id0/helper_functions.c + extension/fake_id0/fake_id0.c + extension/hidden_files/hidden_files.c + extension/mountinfo/mountinfo.c + extension/port_switch/port_switch.c + extension/sysvipc/sysvipc.c + extension/sysvipc/sysvipc_msg.c + extension/sysvipc/sysvipc_sem.c + extension/sysvipc/sysvipc_shm.c + extension/link2symlink/link2symlink.c + extension/fix_symlink_size/fix_symlink_size.c +) + +# We need a probe object to determine objcopy parameters +add_library(cli_obj OBJECT cli/cli.c) +set_target_properties(cli_obj PROPERTIES COMPILE_FLAGS "-I${CMAKE_CURRENT_BINARY_DIR}") + +# Build loaders +function(build_loader SUFFIX EXTRA_CFLAGS LOADER_ADDR) + set(LOADER_TARGET loader_${SUFFIX}) + if(SUFFIX STREQUAL "64") + set(LOADER_NAME loader) + else() + set(LOADER_NAME loader32) + endif() + + add_executable(${LOADER_TARGET} loader/loader.c loader/assembly.S) + set_target_properties(${LOADER_TARGET} PROPERTIES + COMPILE_FLAGS "-fPIC -ffreestanding ${EXTRA_CFLAGS}" + LINK_FLAGS "-static -nostdlib ${EXTRA_CFLAGS} -Wl,-Ttext=${LOADER_ADDR},--rosegment,-z,noexecstack" + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/loader + OUTPUT_NAME ${LOADER_NAME} + PREFIX "lib" + SUFFIX ".so" + ) + +endfunction() + +build_loader("64" "${LOADER_ARCH_CFLAGS_64}" "${LOADER_ADDRESS_64}") + +if(HAS_LOADER_32BIT) + build_loader("-m32" "${LOADER_ARCH_CFLAGS_32}" "${LOADER_ADDRESS_32}") +endif() + +if(HAS_POKEDATA_WORKAROUND) + # Loader info generation + set(LOADER_INFO_C "${CMAKE_CURRENT_BINARY_DIR}/loader/loader-info.c") + add_custom_command( + OUTPUT "${LOADER_INFO_C}" + COMMAND ${READELF_TOOL} -s "$" | awk -f "${CMAKE_CURRENT_SOURCE_DIR}/loader/loader-info.awk" > "${LOADER_INFO_C}" + DEPENDS loader_64 + WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" + VERBATIM + ) + list(APPEND PROOT_SOURCES "${LOADER_INFO_C}") +endif() + +add_executable(proot ${PROOT_SOURCES} ${TALLOC_SOURCES}) +add_dependencies(proot cli_obj loader_64) +if(HAS_LOADER_32BIT) + add_dependencies(proot loader_-m32) +endif() + +target_link_libraries(proot dl) + +# Rename the output to libproot.so for Android packaging +set_target_properties(proot PROPERTIES + OUTPUT_NAME "proot" + PREFIX "lib" + SUFFIX ".so" +) + +add_custom_command( + TARGET proot POST_BUILD + COMMAND ${STRIP_TOOL} --strip-unneeded $ + COMMAND ${STRIP_TOOL} $ + COMMENT "Stripping libproot.so and loader" +) + +if(HAS_LOADER_32BIT) + add_custom_command( + TARGET proot POST_BUILD + COMMAND ${STRIP_TOOL} $ + COMMENT "Stripping loader32" + ) +endif() + +install(TARGETS proot DESTINATION bin) diff --git a/core/proot/src/main/cpp/arch.h b/core/proot/src/main/cpp/arch.h new file mode 100644 index 000000000..06b7f1b03 --- /dev/null +++ b/core/proot/src/main/cpp/arch.h @@ -0,0 +1,196 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#ifndef ARCH_H +#define ARCH_H + +#ifndef NO_LIBC_HEADER +#include /* linux.git:c0a3a20b */ +#include /* AUDIT_ARCH_*, */ +#endif + +typedef unsigned long word_t; +typedef unsigned char byte_t; + +#define SYSCALL_AVOIDER ((word_t) -2) +#define SYSTRAP_NUM SYSARG_NUM + +#if !defined(ARCH_X86_64) && !defined(ARCH_ARM_EABI) && !defined(ARCH_X86) && !defined(ARCH_SH4) +# if defined(__x86_64__) +# define ARCH_X86_64 1 +# elif defined(__ARM_EABI__) +# define ARCH_ARM_EABI 1 +# elif defined(__aarch64__) +# define ARCH_ARM64 1 +# elif defined(__arm__) +# error "Only EABI is currently supported for ARM" +# elif defined(__i386__) +# define ARCH_X86 1 +# elif defined(__SH4__) +# define ARCH_SH4 1 +# else +# error "Unsupported architecture" +# endif +#endif + +/* Architecture specific definitions. */ +#if defined(ARCH_X86_64) + + #define SYSNUMS_HEADER1 "syscall/sysnums-x86_64.h" + #define SYSNUMS_HEADER2 "syscall/sysnums-i386.h" + #define SYSNUMS_HEADER3 "syscall/sysnums-x32.h" + + #define SYSNUMS_ABI1 sysnums_x86_64 + #define SYSNUMS_ABI2 sysnums_i386 + #define SYSNUMS_ABI3 sysnums_x32 + + #undef SYSTRAP_NUM + #define SYSTRAP_NUM SYSARG_RESULT + #define SYSTRAP_SIZE 2 + + #define SECCOMP_ARCHS { \ + { .value = AUDIT_ARCH_X86_64, .nb_abis = 2, .abis = { ABI_DEFAULT, ABI_3 } }, \ + { .value = AUDIT_ARCH_I386, .nb_abis = 1, .abis = { ABI_2 } }, \ + } + + #define HOST_ELF_MACHINE {62, 3, 6, 0} + #define RED_ZONE_SIZE 128 + #define OFFSETOF_STAT_UID_32 24 + #define OFFSETOF_STAT_GID_32 28 + + #define LOADER_ADDRESS 0x600000000000 + #define HAS_LOADER_32BIT true + + #define EXEC_PIC_ADDRESS 0x500000000000 + #define INTERP_PIC_ADDRESS 0x6f0000000000 + #define EXEC_PIC_ADDRESS_32 0x0f000000 + #define INTERP_PIC_ADDRESS_32 0xaf000000 + +#elif defined(ARCH_ARM_EABI) + + #define SYSNUMS_HEADER1 "syscall/sysnums-arm.h" + #define SYSNUMS_ABI1 sysnums_arm + + #define SYSTRAP_SIZE 4 + + #define SECCOMP_ARCHS { { .value = AUDIT_ARCH_ARM, .nb_abis = 1, .abis = { ABI_DEFAULT } } } + + #define user_regs_struct user_regs + #define HOST_ELF_MACHINE {40, 0}; + #define RED_ZONE_SIZE 0 + #define OFFSETOF_STAT_UID_32 0 + #define OFFSETOF_STAT_GID_32 0 + #define EM_ARM 40 + + #define LOADER_ADDRESS 0x20000000 + + #define EXEC_PIC_ADDRESS 0x0f000000 + #define INTERP_PIC_ADDRESS 0x1f000000 + + /* The syscall number has to be valid on ARM, so use tuxcall(2) as + * the "void" syscall since it has no side effects. */ + #undef SYSCALL_AVOIDER + #define SYSCALL_AVOIDER ((word_t) 222) + +#elif defined(ARCH_ARM64) + + #define SYSNUMS_HEADER1 "syscall/sysnums-arm64.h" + #define SYSNUMS_HEADER2 "syscall/sysnums-arm.h" + + #define SYSNUMS_ABI1 sysnums_arm64 + #define SYSNUMS_ABI2 sysnums_arm + + #define SYSTRAP_SIZE 4 + + #ifndef AUDIT_ARCH_AARCH64 + #define AUDIT_ARCH_AARCH64 (EM_AARCH64 | __AUDIT_ARCH_64BIT | __AUDIT_ARCH_LE) + #endif + + #define SECCOMP_ARCHS { \ + { .value = AUDIT_ARCH_AARCH64, .nb_abis = 1, .abis = { ABI_DEFAULT } }, \ + { .value = AUDIT_ARCH_ARM, .nb_abis = 1, .abis = { ABI_2 } }, \ + } + + #define HOST_ELF_MACHINE {183, 0}; + #define RED_ZONE_SIZE 0 + #define OFFSETOF_STAT_UID_32 24 + #define OFFSETOF_STAT_GID_32 28 + + #define LOADER_ADDRESS 0x2000000000 + #define EXEC_PIC_ADDRESS 0x3000000000 + #define INTERP_PIC_ADDRESS 0x3f00000000 + #define HAS_POKEDATA_WORKAROUND true + + #define HAS_LOADER_32BIT true + #define EXEC_PIC_ADDRESS_32 0x0f000000 + #define INTERP_PIC_ADDRESS_32 0x1f000000 + + /* Syscall -2 appears to cause some odd side effects, use -1. */ + /* See https://github.com/termux/termux-packages/pull/390 */ + #undef SYSCALL_AVOIDER + #define SYSCALL_AVOIDER ((word_t) -1) + +#elif defined(ARCH_X86) + + #define SYSNUMS_HEADER1 "syscall/sysnums-i386.h" + #define SYSNUMS_ABI1 sysnums_i386 + + #undef SYSTRAP_NUM + #define SYSTRAP_NUM SYSARG_RESULT + #define SYSTRAP_SIZE 2 + + #define SECCOMP_ARCHS { { .value = AUDIT_ARCH_I386, .nb_abis = 1, .abis = { ABI_DEFAULT } } } + + #define HOST_ELF_MACHINE {3, 6, 0}; + #define RED_ZONE_SIZE 0 + #define OFFSETOF_STAT_UID_32 0 + #define OFFSETOF_STAT_GID_32 0 + + #define LOADER_ADDRESS 0xa0000000 + #define LOADER_ARCH_CFLAGS -mregparm=3 + + #define EXEC_PIC_ADDRESS 0x0f000000 + #define INTERP_PIC_ADDRESS 0xaf000000 + +#elif defined(ARCH_SH4) + + #define SYSNUMS_HEADER1 "syscall/sysnums-sh4.h" + #define SYSNUMS_ABI1 sysnums_sh4 + + #define SYSTRAP_SIZE 2 + + #define SECCOMP_ARCHS { } + + #define user_regs_struct pt_regs + #define HOST_ELF_MACHINE {42, 0}; + #define RED_ZONE_SIZE 0 + #define OFFSETOF_STAT_UID_32 0 + #define OFFSETOF_STAT_GID_32 0 + #define NO_MISALIGNED_ACCESS 1 + +#else + + #error "Unsupported architecture" + +#endif + +#endif /* ARCH_H */ diff --git a/core/proot/src/main/cpp/attribute.h b/core/proot/src/main/cpp/attribute.h new file mode 100644 index 000000000..f77295423 --- /dev/null +++ b/core/proot/src/main/cpp/attribute.h @@ -0,0 +1,32 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#ifndef ATTRIBUTE_H +#define ATTRIBUTE_H + +#define UNUSED __attribute__((unused)) +#define FORMAT(a, b, c) __attribute__ ((format (a, b, c))) +#define DONT_INSTRUMENT __attribute__((no_instrument_function)) +#define PACKED __attribute__((packed)) +#define WEAK __attribute__((weak)) + +#endif /* ATTRIBUTE_H */ diff --git a/core/proot/src/main/cpp/build.h b/core/proot/src/main/cpp/build.h new file mode 100644 index 000000000..6339f3e02 --- /dev/null +++ b/core/proot/src/main/cpp/build.h @@ -0,0 +1,8 @@ +/* This file is auto-generated, edit at your own risk. */ +#ifndef BUILD_H +#define BUILD_H +#undef VERSION +#define VERSION "v5.1.107.77-2-g81b37778-dirty" + +#define HAVE_SECCOMP_FILTER +#endif /* BUILD_H */ diff --git a/core/proot/src/main/cpp/cli/cli.c b/core/proot/src/main/cpp/cli/cli.c new file mode 100644 index 000000000..d48ab0b7d --- /dev/null +++ b/core/proot/src/main/cpp/cli/cli.c @@ -0,0 +1,599 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#include /* printf(3), */ +#include /* bool, true, false, */ +#include /* ARG_MAX, PATH_MAX, */ +#include /* str*(3), basename(3), */ +#include /* talloc*, */ +#include /* exit(3), EXIT_*, strtol(3), {g,s}etenv(3), */ +#include /* assert(3), */ +#include /* getpid(2), */ +#include /* getpid(2), */ +#include /* errno(3), */ +#include /* basename(3), */ +#ifdef __GLIBC__ +#include /* backtrace_symbols(3), */ +#endif +#include /* INT_MAX, */ + +#include "cli/cli.h" +#include "cli/note.h" +#include "extension/extension.h" +#include "tracee/tracee.h" +#include "tracee/event.h" +#include "path/binding.h" +#include "path/canon.h" +#include "path/path.h" +#include +#include + +#include "build.h" + +/** + * Print a (@detailed) usage of PRoot. + */ +void print_usage(Tracee *tracee, const Cli *cli, bool detailed) +{ + const char *current_class = "none"; + const Option *options; + size_t i, j; + +#define DETAIL(a) if (detailed) a + + DETAIL(printf("%s %s: %s.\n\n", cli->name, cli->version, cli->subtitle)); + printf("Usage:\n %s\n", cli->synopsis); + DETAIL(printf("\n")); + + options = cli->options; + for (i = 0; options[i].class != NULL; i++) { + for (j = 0; ; j++) { + const Argument *argument = &(options[i].arguments[j]); + + if (!argument->name || (!detailed && j != 0)) { + DETAIL(printf("\n")); + printf("\t%s\n", options[i].description); + if (detailed) { + if (options[i].detail[0] != '\0') + printf("\n%s\n\n", options[i].detail); + else + printf("\n"); + } + break; + } + + if (strcmp(options[i].class, current_class) != 0) { + current_class = options[i].class; + printf("\n%s:\n", current_class); + } + + if (j == 0) + printf(" %s", argument->name); + else + printf(", %s", argument->name); + + if (argument->separator != '\0') + printf("%c*%s*", argument->separator, argument->value); + else if (!detailed) + printf("\t"); + } + } + + notify_extensions(tracee, PRINT_USAGE, detailed, 0); + + if (detailed) + printf("%s\n", cli->colophon); +} + +/** + * Print the version of PRoot. + */ +void print_version(const Cli *cli) +{ + printf("%s %s\n\n", cli->logo, cli->version); + printf("built-in accelerators: process_vm = %s, seccomp_filter = %s\n", +#if defined(HAVE_PROCESS_VM) + "yes", +#else + "no", +#endif +#if defined(HAVE_SECCOMP_FILTER) + "yes" +#else + "no" +#endif + ); +} + +static void print_execve_help(const Tracee *tracee, const char *argv0, int status) +{ + note(tracee, ERROR, SYSTEM, "execve(\"%s\")", argv0); + + /* termux-exec replaced execve with path with one that doesn't exist inside proot? */ + if (status == -ENOENT && getenv("LD_PRELOAD") != NULL && strstr(getenv("LD_PRELOAD"), "libtermux-exec.so") != NULL) { + note(tracee, INFO, USER, +"It seems that termux-exec is active and is prepending /data/data/com.termux/... to executable paths\n" +"If this is path is not available inside proot, please \"unset LD_PRELOAD\""); + return; + } + + /* Ubuntu kernel bug? */ + if (status == -EPERM && getenv("PROOT_NO_SECCOMP") == NULL) { + note(tracee, INFO, USER, +"It seems your kernel contains this bug: https://bugs.launchpad.net/ubuntu/+source/linux/+bug/1202161\n" +"To workaround it, set the env. variable PROOT_NO_SECCOMP to 1."); + return; + } + + note(tracee, INFO, USER, "possible causes:\n" +" * the program is a script but its interpreter (eg. /bin/sh) was not found;\n" +" * the program is an ELF but its interpreter (eg. ld-linux.so) was not found;\n" +" * the program is a foreign binary but qemu was not specified;\n" +" * qemu does not work correctly (if specified);\n" +" * the loader was not found or doesn't work."); +} + +static void print_error_separator(const Tracee *tracee, const Argument *argument) +{ + if (argument->separator == '\0') + note(tracee, ERROR, USER, "option '%s' expects no value.", argument->name); + else + note(tracee, ERROR, USER, "option '%s' and its value must be separated by '%c'.", + argument->name, argument->separator); +} + +static void print_argv(const Tracee *tracee, const char *prompt, char *const argv[]) +{ + char string[ARG_MAX] = ""; + size_t i; + + if (!argv) + return; + +#define APPEND(post) \ + do { \ + ssize_t length = sizeof(string) - (strlen(string) + strlen(post)); \ + if (length <= 0) \ + return; \ + strncat(string, post, length); \ + } while (0) + + APPEND(prompt); + APPEND(" ="); + for (i = 0; argv[i] != NULL; i++) { + APPEND(" "); + APPEND(argv[i]); + } + string[sizeof(string) - 1] = '\0'; + +#undef APPEND + + note(tracee, INFO, USER, "%s", string); +} + +static void print_config(Tracee *tracee, char *const argv[]) +{ + assert(tracee != NULL); + + if (tracee->verbose <= 0) + return; + + if (tracee->qemu) + note(tracee, INFO, USER, "host rootfs = %s", HOST_ROOTFS); + + if (tracee->glue) + note(tracee, INFO, USER, "glue rootfs = %s", tracee->glue); + + note(tracee, INFO, USER, "exe = %s", tracee->exe); + print_argv(tracee, "argv", argv); + print_argv(tracee, "qemu", tracee->qemu); + note(tracee, INFO, USER, "initial cwd = %s", tracee->fs->cwd); + note(tracee, INFO, USER, "verbose level = %d", tracee->verbose); + + notify_extensions(tracee, PRINT_CONFIG, 0, 0); +} + +/** + * Initialize @tracee's current working directory. This function + * returns -1 if an error occurred, otherwise 0. + */ +static int initialize_cwd(Tracee *tracee) +{ + char path2[PATH_MAX]; + char path[PATH_MAX]; + int status; + + /* Compute the base directory. */ + if (tracee->fs->cwd[0] != '/') { + status = getcwd2(tracee->reconf.tracee, path); + if (status < 0) { + note(tracee, ERROR, INTERNAL, "getcwd: %s", strerror(-status)); + return -1; + } + } + else + strcpy(path, "/"); + + /* The ending "." ensures canonicalize() will report an error + * if tracee->fs->cwd does not exist or if it is not a + * directory. */ + status = join_paths(3, path2, path, tracee->fs->cwd, "."); + if (status < 0) { + note(tracee, ERROR, INTERNAL, "getcwd: %s", strerror(-status)); + return -1; + } + + /* Initiale state for canonicalization. */ + strcpy(path, "/"); + + status = canonicalize(tracee, path2, true, path, 0); + if (status < 0) { + note(tracee, WARNING, USER, "can't chdir(\"%s\") in the guest rootfs: %s", + path2, strerror(-status)); + note(tracee, INFO, USER, "default working directory is now \"/\""); + strcpy(path, "/"); + } + chop_finality(path); + + /* Replace with the canonicalized working directory. */ + TALLOC_FREE(tracee->fs->cwd); + tracee->fs->cwd = talloc_strdup(tracee->fs, path); + if (tracee->fs->cwd == NULL) + return -1; + talloc_set_name_const(tracee->fs->cwd, "$cwd"); + + /* Keep this special environment variable consistent. */ + setenv("PWD", path, 1); + + return 0; +} + +/** + * Initialize @tracee->exe from @exe, i.e. canonicalize it from a + * guest point-of-view. + */ +static int initialize_exe(Tracee *tracee, const char *exe) +{ + char path[PATH_MAX]; + int status; + + status = which(tracee, tracee->reconf.paths, path, exe ?: "/bin/sh"); + if (status < 0) + return -1; + + status = detranslate_path(tracee, path, NULL); + if (status < 0) + return -1; + + tracee->exe = talloc_strdup(tracee, path); + if (tracee->exe == NULL) + return -1; + talloc_set_name_const(tracee->exe, "$exe"); + + return 0; +} + +/** + * Configure @tracee according to the command-line arguments stored in + * @argv[]. This function returns the index in @argv[] of the command + * to launch, otherwise -1 if an error occured. + */ +static int parse_config(Tracee *tracee, size_t argc, char *const argv[]) +{ + option_handler_t handler = NULL; + const Option *options; + const Cli *cli = NULL; + size_t argc_offset; + size_t i, j, k; + int status; + + /* Unknown tool name? Default to PRoot. */ + if (cli == NULL) + cli = get_proot_cli(tracee->ctx); + tracee->tool_name = cli->name; + + if (argc == 1) { + print_usage(tracee, cli, false); + return -1; + } + + for (i = 1; i < argc; i++) { + const char *arg = argv[i]; + + /* The current argument is the value of a short option. */ + if (handler != NULL) { + status = handler(tracee, cli, arg); + if (status < 0) + return -1; + handler = NULL; + continue; + } + + if (arg[0] != '-') + break; /* End of PRoot options. */ + + options = cli->options; + for (j = 0; options[j].class != NULL; j++) { + const Option *option = &options[j]; + + /* A given option has several aliases. */ + for (k = 0; ; k++) { + const Argument *argument; + size_t length; + + argument = &option->arguments[k]; + + /* End of aliases for this option. */ + if (!argument->name) + break; + + length = strlen(argument->name); + if (strncmp(arg, argument->name, length) != 0) + continue; + + /* Avoid ambiguities. */ + if (strlen(arg) > length + && arg[length] != argument->separator) { + print_error_separator(tracee, argument); + return -1; + } + + /* No option value. */ + if (!argument->value) { + status = option->handler(tracee, cli, NULL); + if (status < 0) + return -1; + goto known_option; + } + + /* Value coalesced with to its option. */ + if (argument->separator == arg[length]) { + assert(strlen(arg) >= length); + status = option->handler(tracee, cli, &arg[length + 1]); + if (status < 0) + return -1; + goto known_option; + } + + /* Avoid ambiguities. */ + if (argument->separator != ' ') { + print_error_separator(tracee, argument); + return -1; + } + + /* Short option with a separated value. */ + handler = option->handler; + goto known_option; + } + } + + note(tracee, ERROR, USER, "unknown option '%s'.", arg); + return -1; + + known_option: + if (handler != NULL && i == argc - 1) { + note(tracee, ERROR, USER, "missing value for option '%s'.", arg); + return -1; + } + } + argc_offset = i; + +#define HOOK_CONFIG(callback) \ + do { \ + if (cli->callback != NULL) { \ + status = cli->callback(tracee, cli, argc, argv, i); \ + if (status < 0) \ + return -1; \ + i = status; \ + } \ + } while (0) + + HOOK_CONFIG(pre_initialize_bindings); + + /* The guest rootfs is now known: bindings specified by the + * user (tracee->bindings.user) can be canonicalized. */ + status = initialize_bindings(tracee); + if (status < 0) + return -1; + + HOOK_CONFIG(post_initialize_bindings); + HOOK_CONFIG(pre_initialize_cwd); + + /* Bindings are now installed (tracee->bindings.guest & + * tracee->bindings.host): the current working directory can + * be canonicalized. */ + status = initialize_cwd(tracee); + if (status < 0) + return -1; + + HOOK_CONFIG(post_initialize_cwd); + HOOK_CONFIG(pre_initialize_exe); + + /* Bindings are now installed and the current working + * directory is canonicalized: resolve path to @tracee->exe + * and configure @tracee->cmdline. */ + status = initialize_exe(tracee, argv[argc_offset]); + if (status < 0) + return -1; + + HOOK_CONFIG(post_initialize_exe); +#undef HOOK_CONFIG + + print_config(tracee, &argv[argc_offset]); + + return argc_offset; +} + +bool exit_failure = true; + +int main(int argc, char *const argv[]) +{ + Tracee *tracee; + int status; + + /* Configure the memory allocator. */ + talloc_enable_leak_report(); + +#if defined(TALLOC_VERSION_MAJOR) && TALLOC_VERSION_MAJOR >= 2 + talloc_set_log_stderr(); +#endif + + if (argc == 2 && strcmp(argv[1], "--shm-helper") == 0) { + sysvipc_shm_helper_main(); + } + + /* Pre-create the first tracee (pid == 0). */ + tracee = get_tracee(NULL, 0, true); + if (tracee == NULL) + goto error; + tracee->pid = getpid(); + + /* Set verboseness from env variable, may be overriden by option */ + { + const char *verbose_env = getenv("PROOT_VERBOSE"); + if (verbose_env != NULL) { + tracee->verbose = strtol(verbose_env, NULL, 10); + global_verbose_level = tracee->verbose; + } + } + + /* Pre-configure the first tracee. */ + status = parse_config(tracee, argc, argv); + if (status < 0) + goto error; + + if (NULL == getenv("PROOT_NO_MOUNTINFO")) + initialize_extension(tracee, mountinfo_callback, NULL); + + /* Start the first tracee. */ + status = launch_process(tracee, &argv[status]); + if (status < 0) { + print_execve_help(tracee, tracee->exe, status); + goto error; + } + + /* Start tracing the first tracee and all its children. */ + exit(event_loop()); + +error: + TALLOC_FREE(tracee); + + if (exit_failure) { + fprintf(stderr, "fatal error: see `%s --help`.\n", basename(argv[0])); + exit(EXIT_FAILURE); + } + else + exit(EXIT_SUCCESS); +} + +/** + * Convert @value into an integer, then put the result into + * *@variable. This function prints a warning and returns -1 if a + * conversion error occured, otherwise it returns 0. + */ +int parse_integer_option(const Tracee *tracee, int *variable, const char *value, const char *option) +{ + char *end_ptr = NULL; + + errno = 0; + *variable = strtol(value, &end_ptr, 10); + if (errno != 0 || end_ptr == value) { + note(tracee, ERROR, USER, "option `%s` expects an integer value.", option); + return -1; + } + + return 0; +} + +/** + * Expand the environment variable in front of @string, if any. For + * example, this function can expand "$HOME" or "$HOME/.ICEauthority". + */ +const char *expand_front_variable(TALLOC_CTX *context, const char *string) +{ + const char *suffix; + char *expanded; + ptrdiff_t size; + + if (string[0] != '$') + return string; + + suffix = strchr(string, '/'); + if (suffix == NULL) + return (getenv(&string[1]) ?: string); + + size = suffix - string; + if (size <= 1) + return string; + + expanded = talloc_strndup(context, &string[1], size - 1); + if (expanded == NULL) + return string; + + expanded = getenv(expanded); + if (expanded == NULL) + return string; + + expanded = talloc_asprintf(context, "%s%s", expanded, suffix); + if (expanded == NULL) + return string; + + return expanded; +} + +/* Here follows the support for GCC function instrumentation. Build + * with CFLAGS='-finstrument-functions -O0 -g' and LDFLAGS='-rdynamic' + * to enable this mechanism. */ + +static int indent_level = 0; + +void __cyg_profile_func_enter(void *this_function, void *call_site) DONT_INSTRUMENT; +void __cyg_profile_func_enter(void *this_function, void *call_site) +{ +#ifdef __GLIBC__ + void *const pointers[] = { this_function, call_site }; + char **symbols = NULL; + + symbols = backtrace_symbols(pointers, 2); + if (symbols == NULL) + goto end; + + fprintf(stderr, "%*s from %s\n", (int) strlen(symbols[0]) + indent_level, symbols[0], symbols[1]); + +end: + if (symbols != NULL) + free(symbols); +#else + (void)this_function; + (void)call_site; +#endif + + if (indent_level < INT_MAX) + indent_level++; +} + +void __cyg_profile_func_exit(void *this_function UNUSED, void *call_site UNUSED) DONT_INSTRUMENT; +void __cyg_profile_func_exit(void *this_function UNUSED, void *call_site UNUSED) +{ + if (indent_level > 0) + indent_level--; +} diff --git a/core/proot/src/main/cpp/cli/cli.h b/core/proot/src/main/cpp/cli/cli.h new file mode 100644 index 000000000..ed883c913 --- /dev/null +++ b/core/proot/src/main/cpp/cli/cli.h @@ -0,0 +1,64 @@ +/* This file is automatically generated from the documentation. EDIT AT YOUR OWN RISK. */ + +#ifndef CLI_H +#define CLI_H + +#include +#include "tracee/tracee.h" +#include "attribute.h" + +typedef struct { + const char *name; + char separator; + const char *value; +} Argument; + +struct Cli; +typedef int (*option_handler_t)(Tracee *tracee, const struct Cli *cli, const char *value); + +typedef struct { + const char *class; + option_handler_t handler; + const char *description; + const char *detail; + Argument arguments[5]; +} Option; + +#define END_OF_OPTIONS { .class = NULL, \ + .arguments = {{ .name = NULL, .separator = '\0', .value = NULL }}, \ + .handler = NULL, \ + .description = NULL, \ + .detail = NULL \ + } + +typedef int (*initialization_hook_t)(Tracee *tracee, const struct Cli *cli, + size_t argc, char *const argv[], size_t cursor); +typedef struct Cli { + const char *name; + const char *version; + const char *subtitle; + const char *synopsis; + const char *colophon; + const char *logo; + + initialization_hook_t pre_initialize_bindings; + initialization_hook_t post_initialize_bindings; + initialization_hook_t pre_initialize_cwd; + initialization_hook_t post_initialize_cwd; + initialization_hook_t pre_initialize_exe; + initialization_hook_t post_initialize_exe; + void *private; + + const Option options[]; +} Cli; + +extern const Cli *get_proot_cli(TALLOC_CTX *context); + +extern void print_usage(Tracee *tracee, const Cli *cli, bool detailed); +extern void print_version(const Cli *cli); +extern int parse_integer_option(const Tracee *tracee, int *variable, const char *value, const char *option); +extern const char *expand_front_variable(TALLOC_CTX *context, const char *string); + +extern bool exit_failure; + +#endif /* CLI_H */ diff --git a/core/proot/src/main/cpp/cli/note.c b/core/proot/src/main/cpp/cli/note.c new file mode 100644 index 000000000..a48636d22 --- /dev/null +++ b/core/proot/src/main/cpp/cli/note.c @@ -0,0 +1,97 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#include /* errno, */ +#include /* strerror(3), */ +#include /* va_*, */ +#include /* vfprintf(3), */ +#include /* INT_MAX, */ + +#include "cli/note.h" +#include "tracee/tracee.h" + +int global_verbose_level; +const char *global_tool_name; + +/** + * Print @message to the standard error stream according to its + * @severity and @origin. + */ +void note(const Tracee *tracee, Severity severity, Origin origin, const char *message, ...) +{ + const char *tool_name; + va_list extra_params; + int verbose_level; + + if (tracee == NULL) { + verbose_level = global_verbose_level; + tool_name = global_tool_name ?: ""; + } + else { + verbose_level = tracee->verbose; + tool_name = tracee->tool_name; + } + + if (verbose_level < 0 && severity != ERROR) + return; + + switch (severity) { + case WARNING: + fprintf(stderr, "%s warning: ", tool_name); + break; + + case ERROR: + fprintf(stderr, "%s error: ", tool_name); + break; + + case INFO: + default: + fprintf(stderr, "%s info: ", tool_name); + break; + } + + if (origin == TALLOC) + fprintf(stderr, "talloc: "); + + va_start(extra_params, message); + vfprintf(stderr, message, extra_params); + va_end(extra_params); + + switch (origin) { + case SYSTEM: + fprintf(stderr, ": "); + perror(NULL); + break; + + case TALLOC: + break; + + case INTERNAL: + case USER: + default: + fprintf(stderr, "\n"); + break; + } + + return; +} + diff --git a/core/proot/src/main/cpp/cli/note.h b/core/proot/src/main/cpp/cli/note.h new file mode 100644 index 000000000..8c73f8479 --- /dev/null +++ b/core/proot/src/main/cpp/cli/note.h @@ -0,0 +1,54 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#ifndef NOTE_H +#define NOTE_H + +#include "tracee/tracee.h" +#include "attribute.h" + +/* Specify where a notice is coming from. */ +typedef enum { + SYSTEM, + INTERNAL, + USER, + TALLOC, +} Origin; + +/* Specify the severity of a notice. */ +typedef enum { + ERROR, + WARNING, + INFO, +} Severity; + +#define VERBOSE(tracee, level, message, args...) do { \ + if (tracee == NULL || tracee->verbose >= (level)) \ + note(tracee, INFO, INTERNAL, (message), ## args); \ + } while (0) + +extern void note(const Tracee *tracee, Severity severity, Origin origin, const char *message, ...) FORMAT(printf, 4, 5); + +extern int global_verbose_level; +extern const char *global_tool_name; + +#endif /* NOTE_H */ diff --git a/core/proot/src/main/cpp/cli/proot.c b/core/proot/src/main/cpp/cli/proot.c new file mode 100644 index 000000000..29365e81f --- /dev/null +++ b/core/proot/src/main/cpp/cli/proot.c @@ -0,0 +1,402 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#include /* str*(3), */ +#include /* assert(3), */ +#include /* printf(3), fflush(3), */ +#include /* write(2), */ + +#include "cli/cli.h" +#include "cli/note.h" +#include "extension/extension.h" +#include "extension/sysvipc/sysvipc.h" +#include "path/binding.h" +#include "attribute.h" + +/* These should be included last. */ +#include "build.h" +#include "cli/proot.h" + +static int handle_option_r(Tracee *tracee, const Cli *cli UNUSED, const char *value) +{ + Binding *binding; + + /* ``chroot $PATH`` is semantically equivalent to ``mount + * --bind $PATH /``. */ + binding = new_binding(tracee, value, "/", true); + if (binding == NULL) + return -1; + + return 0; +} + +static int handle_option_b(Tracee *tracee, const Cli *cli UNUSED, const char *value) +{ + char *host; + char *guest; + + host = talloc_strdup(tracee->ctx, value); + if (host == NULL) { + note(tracee, ERROR, INTERNAL, "can't allocate memory"); + return -1; + } + + guest = strchr(host, ':'); + if (guest != NULL) { + *guest = '\0'; + guest++; + } + + new_binding(tracee, host, guest, true); + return 0; +} + +static int handle_option_q(Tracee *tracee, const Cli *cli UNUSED, const char *value) +{ + const char *ptr; + size_t nb_args; + bool last; + size_t i; + + nb_args = 0; + ptr = value; + while (1) { + nb_args++; + + /* Keep consecutive non-space characters. */ + while (*ptr != ' ' && *ptr != '\0') + ptr++; + + /* End-of-string ? */ + if (*ptr == '\0') + break; + + /* Skip consecutive space separators. */ + while (*ptr == ' ' && *ptr != '\0') + ptr++; + + /* End-of-string ? */ + if (*ptr == '\0') + break; + } + + tracee->qemu = talloc_zero_array(tracee, char *, nb_args + 1); + if (tracee->qemu == NULL) + return -1; + talloc_set_name_const(tracee->qemu, "@qemu"); + + i = 0; + ptr = value; + do { + const void *start; + const void *end; + last = true; + + /* Keep consecutive non-space characters. */ + start = ptr; + while (*ptr != ' ' && *ptr != '\0') + ptr++; + end = ptr; + + /* End-of-string ? */ + if (*ptr == '\0') + goto next; + + /* Remove consecutive space separators. */ + while (*ptr == ' ' && *ptr != '\0') + ptr++; + + /* End-of-string ? */ + if (*ptr == '\0') + goto next; + + last = false; + next: + tracee->qemu[i] = talloc_strndup(tracee->qemu, start, end - start); + if (tracee->qemu[i] == NULL) + return -1; + i++; + } while (!last); + assert(i == nb_args); + + new_binding(tracee, "/", HOST_ROOTFS, true); + new_binding(tracee, "/dev/null", "/etc/ld.so.preload", false); + + return 0; +} + +static int handle_option_w(Tracee *tracee, const Cli *cli UNUSED, const char *value) +{ + tracee->fs->cwd = talloc_strdup(tracee->fs, value); + if (tracee->fs->cwd == NULL) + return -1; + talloc_set_name_const(tracee->fs->cwd, "$cwd"); + return 0; +} + +static int handle_option_k(Tracee *tracee, const Cli *cli UNUSED, const char *value) +{ + void *extension; + int status; + + extension = get_extension(tracee, kompat_callback); + if (extension != NULL) { + note(tracee, WARNING, USER, "option -k was already specified"); + note(tracee, INFO, USER, "only the last -k option is enabled"); + TALLOC_FREE(extension); + } + + status = initialize_extension(tracee, kompat_callback, value); + if (status < 0) + note(tracee, WARNING, INTERNAL, "option \"-k %s\" discarded", value); + + return 0; +} + +static int handle_option_i(Tracee *tracee, const Cli *cli UNUSED, const char *value) +{ + void *extension; + + extension = get_extension(tracee, fake_id0_callback); + if (extension != NULL) { + note(tracee, WARNING, USER, "option -i/-0/-S was already specified"); + note(tracee, INFO, USER, "only the last -i/-0/-S option is enabled"); + TALLOC_FREE(extension); + } + + (void) initialize_extension(tracee, fake_id0_callback, value); + return 0; +} + +static int handle_option_0(Tracee *tracee, const Cli *cli, const char *value UNUSED) +{ + return handle_option_i(tracee, cli, "0:0"); +} + +static int handle_option_kill_on_exit(Tracee *tracee, const Cli *cli UNUSED, const char *value UNUSED) +{ + tracee->killall_on_exit = true; + return 0; +} + +static int handle_option_v(Tracee *tracee, const Cli *cli UNUSED, const char *value) +{ + int status; + + status = parse_integer_option(tracee, &tracee->verbose, value, "-v"); + if (status < 0) + return status; + + global_verbose_level = tracee->verbose; + return 0; +} + +extern unsigned char WEAK _binary_licenses_start; +extern unsigned char WEAK _binary_licenses_end; + +static int handle_option_V(Tracee *tracee UNUSED, const Cli *cli, const char *value UNUSED) +{ + size_t size; + + print_version(cli); + printf("\n%s\n", cli->colophon); + fflush(stdout); + + size = &_binary_licenses_end - &_binary_licenses_start; + if (size > 0) + write(1, &_binary_licenses_start, size); + + exit_failure = false; + return -1; +} + +static int handle_option_h(Tracee *tracee, const Cli *cli, const char *value UNUSED) +{ + print_usage(tracee, cli, true); + exit_failure = false; + return -1; +} + +static void new_bindings(Tracee *tracee, const char *bindings[], const char *value) +{ + int i; + + for (i = 0; bindings[i] != NULL; i++) { + const char *path; + + path = (strcmp(bindings[i], "*path*") != 0 + ? expand_front_variable(tracee->ctx, bindings[i]) + : value); + + new_binding(tracee, path, NULL, false); + } +} + +static int handle_option_R(Tracee *tracee, const Cli *cli, const char *value) +{ + int status; + + status = handle_option_r(tracee, cli, value); + if (status < 0) + return status; + + new_bindings(tracee, recommended_bindings, value); + + return 0; +} + +static int handle_option_S(Tracee *tracee, const Cli *cli, const char *value) +{ + int status; + + status = handle_option_0(tracee, cli, value); + if (status < 0) + return status; + + status = handle_option_r(tracee, cli, value); + if (status < 0) + return status; + + new_bindings(tracee, recommended_su_bindings, value); + + return 0; +} + +static int handle_option_link2symlink(Tracee *tracee, const Cli *cli UNUSED, const char *value UNUSED) +{ + int status; + + /* Initialize the link2symlink extension. */ + status = initialize_extension(tracee, link2symlink_callback, NULL); + if (status < 0) + note(tracee, WARNING, INTERNAL, "link2symlink not initialized"); + + return 0; +} + +static int handle_option_ashmem_memfd(Tracee *tracee, const Cli *cli UNUSED, const char *value UNUSED) +{ + int status; + + /* Initialize the ashmem-memfd extension. */ + status = initialize_extension(tracee, ashmem_memfd_callback, NULL); + if (status < 0) + note(tracee, WARNING, INTERNAL, "ashmem-memfd not initialized"); + + return 0; +} + +static int handle_option_sysvipc(Tracee *tracee, const Cli *cli UNUSED, const char *value UNUSED) +{ + int status; + + /* Initialize the sysvipc extension. */ + status = initialize_extension(tracee, sysvipc_callback, NULL); + if (status < 0) + note(tracee, WARNING, INTERNAL, "sysvipc not initialized"); + + return 0; +} + +static int handle_option_L(Tracee *tracee, const Cli *cli UNUSED, const char *value UNUSED) +{ + (void) initialize_extension(tracee, fix_symlink_size_callback, NULL); + return 0; +} + +static int handle_option_H(Tracee *tracee, const Cli *cli UNUSED, const char *value UNUSED) +{ + (void) initialize_extension(tracee, hidden_files_callback, NULL); + return 0; +} + +static int handle_option_p(Tracee *tracee, const Cli *cli UNUSED, const char *value UNUSED) +{ + (void) initialize_extension(tracee, port_switch_callback, NULL); + return 0; +} + +/** + * Initialize @tracee->qemu. + */ +static int post_initialize_exe(Tracee *tracee, const Cli *cli UNUSED, + size_t argc UNUSED, char *const argv[] UNUSED, size_t cursor UNUSED) +{ + char path[PATH_MAX]; + int status; + + /* Nothing else to do ? */ + if (tracee->qemu == NULL) + return 0; + + /* Resolve the full guest path to tracee->qemu[0]. */ + status = which(tracee->reconf.tracee, tracee->reconf.paths, path, tracee->qemu[0]); + if (status < 0) + return -1; + + /* Actually tracee->qemu[0] has to be a host path from the tracee's + * point-of-view, not from the PRoot's point-of-view. See + * translate_execve() for details. */ + if (tracee->reconf.tracee != NULL) { + status = detranslate_path(tracee->reconf.tracee, path, NULL); + if (status < 0) + return -1; + } + + tracee->qemu[0] = talloc_strdup(tracee->qemu, path); + if (tracee->qemu[0] == NULL) + return -1; + + return 0; +} + +/** + * Initialize @tracee's fields that are mandatory for PRoot but that + * are not required on the command line, i.e. "-w" and "-r". + */ +static int pre_initialize_bindings(Tracee *tracee, const Cli *cli, + size_t argc UNUSED, char *const argv[] UNUSED, size_t cursor) +{ + int status; + + /* Default to "." if no CWD were specified. */ + if (tracee->fs->cwd == NULL) { + status = handle_option_w(tracee, cli, "."); + if (status < 0) + return -1; + } + + /* The default guest rootfs is "/" if none was specified. */ + if (get_root(tracee) == NULL) { + status = handle_option_r(tracee, cli, "/"); + if (status < 0) + return -1; + } + + return cursor; +} + +const Cli *get_proot_cli(TALLOC_CTX *context UNUSED) +{ + global_tool_name = proot_cli.name; + return &proot_cli; +} diff --git a/core/proot/src/main/cpp/cli/proot.h b/core/proot/src/main/cpp/cli/proot.h new file mode 100644 index 000000000..3f6193135 --- /dev/null +++ b/core/proot/src/main/cpp/cli/proot.h @@ -0,0 +1,346 @@ +/* This file is automatically generated from the documentation. EDIT AT YOUR OWN RISK. */ + +#ifndef PROOT_CLI_H +#define PROOT_CLI_H + +#include "cli/cli.h" + +#ifndef VERSION +#define VERSION "5.1.0" +#endif + +static const char *recommended_bindings[] = { + "/etc/host.conf", + "/etc/hosts", + "/etc/hosts.equiv", + "/etc/mtab", + "/etc/netgroup", + "/etc/networks", + "/etc/passwd", + "/etc/group", + "/etc/nsswitch.conf", + "/etc/resolv.conf", + "/etc/localtime", + "/dev/", + "/sys/", + "/proc/", + "/tmp/", + "/run/", + "/var/run/dbus/system_bus_socket", +/* "/var/tmp/kdecache-$LOGNAME", */ + "$HOME", + "*path*", + NULL, +}; + +static const char *recommended_su_bindings[] = { + "/etc/host.conf", + "/etc/hosts", + "/etc/nsswitch.conf", + "/etc/resolv.conf", + "/dev/", + "/sys/", + "/proc/", + "/tmp/", + "/run/shm", + "$HOME", + "*path*", + NULL, +}; + +static int handle_option_r(Tracee *tracee, const Cli *cli, const char *value); +static int handle_option_b(Tracee *tracee, const Cli *cli, const char *value); +static int handle_option_q(Tracee *tracee, const Cli *cli, const char *value); +static int handle_option_w(Tracee *tracee, const Cli *cli, const char *value); +static int handle_option_v(Tracee *tracee, const Cli *cli, const char *value); +static int handle_option_V(Tracee *tracee, const Cli *cli, const char *value); +static int handle_option_h(Tracee *tracee, const Cli *cli, const char *value); +static int handle_option_k(Tracee *tracee, const Cli *cli, const char *value); +static int handle_option_0(Tracee *tracee, const Cli *cli, const char *value); +static int handle_option_i(Tracee *tracee, const Cli *cli, const char *value); +static int handle_option_R(Tracee *tracee, const Cli *cli, const char *value); +static int handle_option_S(Tracee *tracee, const Cli *cli, const char *value); +static int handle_option_link2symlink(Tracee *tracee, const Cli *cli, const char *value); +static int handle_option_ashmem_memfd(Tracee *tracee, const Cli *cli, const char *value); +static int handle_option_sysvipc(Tracee *tracee, const Cli *cli, const char *value); +static int handle_option_kill_on_exit(Tracee *tracee, const Cli *cli, const char *value); +static int handle_option_L(Tracee *tracee, const Cli *cli, const char *value); +static int handle_option_H(Tracee *tracee, const Cli *cli, const char *value); +static int handle_option_p(Tracee *tracee, const Cli *cli, const char *value); + +static int pre_initialize_bindings(Tracee *, const Cli *, size_t, char *const *, size_t); +static int post_initialize_exe(Tracee *, const Cli *, size_t, char *const *, size_t); + +static Cli proot_cli = { + .version = VERSION, + .name = "proot", + .subtitle = "chroot, mount --bind, and binfmt_misc without privilege/setup", + .synopsis = "proot [option] ... [command]", + .colophon = "Visit http://proot.me for help, bug reports, suggestions, patchs, ...\n\ +Copyright (C) 2015 STMicroelectronics, licensed under GPL v2 or later.", + .logo = "\ + _____ _____ ___\n\ +| __ \\ __ \\_____ _____| |_\n\ +| __/ / _ \\/ _ \\ _|\n\ +|__| |__|__\\_____/\\_____/\\____|", + + .pre_initialize_bindings = pre_initialize_bindings, + .post_initialize_exe = post_initialize_exe, + + .options = { + { .class = "Regular options", + .arguments = { + { .name = "-r", .separator = ' ', .value = "path" }, + { .name = "--rootfs", .separator = '=', .value = "path" }, + { .name = NULL, .separator = '\0', .value = NULL } }, + .handler = handle_option_r, + .description = "Use *path* as the new guest root file-system, default is /.", + .detail = "\tThe specified path typically contains a Linux distribution where\n\ +\tall new programs will be confined. The default rootfs is /\n\ +\twhen none is specified, this makes sense when the bind mechanism\n\ +\tis used to relocate host files and directories, see the -b\n\ +\toption and the Examples section for details.\n\ +\t\n\ +\tIt is recommended to use the -R or -S options instead.", + }, + { .class = "Regular options", + .arguments = { + { .name = "-b", .separator = ' ', .value = "path" }, + { .name = "--bind", .separator = '=', .value = "path" }, + { .name = "-m", .separator = ' ', .value = "path" }, + { .name = "--mount", .separator = '=', .value = "path" }, + { .name = NULL, .separator = '\0', .value = NULL } }, + .handler = handle_option_b, + .description = "Make the content of *path* accessible in the guest rootfs.", + .detail = "\tThis option makes any file or directory of the host rootfs\n\ +\taccessible in the confined environment just as if it were part of\n\ +\tthe guest rootfs. By default the host path is bound to the same\n\ +\tpath in the guest rootfs but users can specify any other location\n\ +\twith the syntax: -b *host_path*:*guest_location*. If the\n\ +\tguest location is a symbolic link, it is dereferenced to ensure\n\ +\tthe new content is accessible through all the symbolic links that\n\ +\tpoint to the overlaid content. In most cases this default\n\ +\tbehavior shouldn't be a problem, although it is possible to\n\ +\texplicitly not dereference the guest location by appending it the\n\ +\t! character: -b *host_path*:*guest_location!*.", + }, + { .class = "Regular options", + .arguments = { + { .name = "-q", .separator = ' ', .value = "command" }, + { .name = "--qemu", .separator = '=', .value = "command" }, + { .name = NULL, .separator = '\0', .value = NULL } }, + .handler = handle_option_q, + .description = "Execute guest programs through QEMU as specified by *command*.", + .detail = "\tEach time a guest program is going to be executed, PRoot inserts\n\ +\tthe QEMU user-mode command in front of the initial request.\n\ +\tThat way, guest programs actually run on a virtual guest CPU\n\ +\temulated by QEMU user-mode. The native execution of host programs\n\ +\tis still effective and the whole host rootfs is bound to\n\ +\t/host-rootfs in the guest environment.", + }, + { .class = "Regular options", + .arguments = { + { .name = "-w", .separator = ' ', .value = "path" }, + { .name = "--pwd", .separator = '=', .value = "path" }, + { .name = "--cwd", .separator = '=', .value = "path" }, + { .name = NULL, .separator = '\0', .value = NULL } }, + .handler = handle_option_w, + .description = "Set the initial working directory to *path*.", + .detail = "\tSome programs expect to be launched from a given directory but do\n\ +\tnot perform any chdir by themselves. This option avoids the\n\ +\tneed for running a shell and then entering the directory manually.", + }, + { .class = "Regular options", + .arguments = { + { .name = "--kill-on-exit", .separator = '\0', .value = NULL }, + { .name = NULL, .separator = '\0', .value = NULL } }, + .handler = handle_option_kill_on_exit, + .description = "Kill all processes on command exit.", + .detail = "\tWhen the executed command leaves orphean or detached processes\n\ +\taround, proot waits until all processes possibly terminate. This option forces\n\ +\tthe immediate termination of all tracee processes when the main command exits.", + }, + { .class = "Regular options", + .arguments = { + { .name = "-v", .separator = ' ', .value = "value" }, + { .name = "--verbose", .separator = '=', .value = "value" }, + { .name = NULL, .separator = '\0', .value = NULL } }, + .handler = handle_option_v, + .description = "Set the level of debug information to *value*.", + .detail = "\tThe higher the integer value is, the more detailed debug\n\ +\tinformation is printed to the standard error stream. A negative\n\ +\tvalue makes PRoot quiet except on fatal errors.", + }, + { .class = "Regular options", + .arguments = { + { .name = "-V", .separator = '\0', .value = NULL }, + { .name = "--version", .separator = '\0', .value = NULL }, + { .name = "--about", .separator = '\0', .value = NULL }, + { .name = NULL, .separator = '\0', .value = NULL } }, + .handler = handle_option_V, + .description = "Print version, copyright, license and contact, then exit.", + .detail = "", + }, + { .class = "Regular options", + .arguments = { + { .name = "-h", .separator = '\0', .value = NULL }, + { .name = "--help", .separator = '\0', .value = NULL }, + { .name = "--usage", .separator = '\0', .value = NULL }, + { .name = NULL, .separator = '\0', .value = NULL } }, + .handler = handle_option_h, + .description = "Print the version and the command-line usage, then exit.", + .detail = "", + }, + { .class = "Extension options", + .arguments = { + { .name = "-k", .separator = ' ', .value = "string" }, + { .name = "--kernel-release", .separator = '=', .value = "string" }, + { .name = NULL, .separator = '\0', .value = NULL } }, + .handler = handle_option_k, + .description = "Make current kernel appear as kernel release *string*.", + .detail = "\tIf a program is run on a kernel older than the one expected by its\n\ +\tGNU C library, the following error is reported: \"FATAL: kernel too\n\ +\told\". To be able to run such programs, PRoot can emulate some of\n\ +\tthe features that are available in the kernel release specified by\n\ +\t*string* but that are missing in the current kernel.", + }, + { .class = "Extension options", + .arguments = { + { .name = "-0", .separator = '\0', .value = NULL }, + { .name = "--root-id", .separator = '\0', .value = NULL }, + { .name = NULL, .separator = '\0', .value = NULL } }, + .handler = handle_option_0, + .description = "Make current user appear as \"root\" and fake its privileges.", + .detail = "\tSome programs will refuse to work if they are not run with \"root\"\n\ +\tprivileges, even if there is no technical reason for that. This\n\ +\tis typically the case with package managers. This option allows\n\ +\tusers to bypass this kind of limitation by faking the user/group\n\ +\tidentity, and by faking the success of some operations like\n\ +\tchanging the ownership of files, changing the root directory to\n\ +\t/, ... Note that this option is quite limited compared to\n\ +\tfakeroot.", + }, + { .class = "Extension options", + .arguments = { + { .name = "-i", .separator = ' ', .value = "string" }, + { .name = "--change-id", .separator = '=', .value = "string" }, + { .name = NULL, .separator = '\0', .value = NULL } }, + .handler = handle_option_i, + .description = "Make current user and group appear as *string* \"uid:gid\".", + .detail = "\tThis option makes the current user and group appear as uid and\n\ +\tgid. Likewise, files actually owned by the current user and\n\ +\tgroup appear as if they were owned by uid and gid instead.\n\ +\tNote that the -0 option is the same as -i 0:0.", + }, + { .class = "Extension options", + .arguments = { + { .name = "--link2symlink", .separator = '\0', .value = NULL }, + { .name = "-l", .separator = '\0', .value = NULL }, + { .name = NULL, .separator = '\0', .value = NULL } }, + .handler = handle_option_link2symlink, + .description = "Replace hard links with symlinks, pretending they are really hardlinks", + .detail = "\tEmulates hard links with symbolic links when SELinux policies\n\ +\tdo not allow hard links.", + }, + { .class = "Extension options", + .arguments = { + { .name = "--sysvipc", .separator = '\0', .value = NULL }, + { .name = NULL, .separator = '\0', .value = NULL } }, + .handler = handle_option_sysvipc, + .description = "Handle System V IPC syscalls in proot", + .detail = "\tHandles System V IPC syscalls (shmget, semget, msgget, etc.)\n\ +\tsyscalls inside proot. IPC is handled inside proot and launching 2 proot instances\n\ +\twill lead to 2 different IPC Namespaces", + }, + { .class = "Extension options", + .arguments = { + { .name = "--ashmem-memfd", .separator = '\0', .value = NULL }, + { .name = NULL, .separator = '\0', .value = NULL } }, + .handler = handle_option_ashmem_memfd, + .description = "Emulate memfd_create support through ashmem and simulate fstat.st_size for ashmem", + .detail = "", + }, + { .class = "Extension options", + .arguments = { + { .name = "-H", .separator = '\0', .value = NULL }, + { .name = NULL, .separator = '\0', .value = NULL } }, + .handler = handle_option_H, + .description = "Hide files and directories starting with '.proot.' .", + .detail = "", + }, + { .class = "Extension options", + .arguments = { + { .name = "-p", .separator = '\0', .value = NULL }, + { .name = NULL, .separator = '\0', .value = NULL } }, + .handler = handle_option_p, + .description = "Modify bindings to protected ports to use a higher port number.", + .detail = "", + }, + { .class = "Extension options", + .arguments = { + { .name = "-L", .separator = '\0', .value = NULL }, + { .name = NULL, .separator = '\0', .value = NULL } }, + .handler = handle_option_L, + .description = "Correct the size returned from lstat for symbolic links.", + .detail = "", + }, + { .class = "Alias options", + .arguments = { + { .name = "-R", .separator = ' ', .value = "path" }, + { .name = NULL, .separator = '\0', .value = NULL } }, + .handler = handle_option_R, + .description = "Alias: -r *path* + a couple of recommended -b.", + .detail = "\tPrograms isolated in *path*, a guest rootfs, might still need to\n\ +\taccess information about the host system, as it is illustrated in\n\ +\tthe Examples section of the manual. These host information\n\ +\tare typically: user/group definition, network setup, run-time\n\ +\tinformation, users' files, ... On all Linux distributions, they\n\ +\tall lie in a couple of host files and directories that are\n\ +\tautomatically bound by this option:\n\ +\t\n\ +\t * /etc/host.conf\n\ +\t * /etc/hosts\n\ +\t * /etc/hosts.equiv\n\ +\t * /etc/mtab\n\ +\t * /etc/netgroup\n\ +\t * /etc/networks\n\ +\t * /etc/passwd\n\ +\t * /etc/group\n\ +\t * /etc/nsswitch.conf\n\ +\t * /etc/resolv.conf\n\ +\t * /etc/localtime\n\ +\t * /dev/\n\ +\t * /sys/\n\ +\t * /proc/\n\ +\t * /tmp/\n\ +\t * /run/\n\ +\t * /var/run/dbus/system_bus_socket\n\ +\t * $HOME", + }, + { .class = "Alias options", + .arguments = { + { .name = "-S", .separator = ' ', .value = "path" }, + { .name = NULL, .separator = '\0', .value = NULL } }, + .handler = handle_option_S, + .description = "Alias: -0 -r *path* + a couple of recommended -b.", + .detail = "\tThis option is useful to safely create and install packages into\n\ +\tthe guest rootfs. It is similar to the -R option expect it\n\ +\tenables the -0 option and binds only the following minimal set\n\ +\tof paths to avoid unexpected changes on host files:\n\ +\t\n\ +\t * /etc/host.conf\n\ +\t * /etc/hosts\n\ +\t * /etc/nsswitch.conf\n\ +\t * /etc/resolv.conf\n\ +\t * /dev/\n\ +\t * /sys/\n\ +\t * /proc/\n\ +\t * /tmp/\n\ +\t * /run/shm\n\ +\t * $HOME", + }, + END_OF_OPTIONS, + }, +}; + +#endif /* PROOT_CLI_H */ diff --git a/core/proot/src/main/cpp/compat.h b/core/proot/src/main/cpp/compat.h new file mode 100644 index 000000000..bc96251f2 --- /dev/null +++ b/core/proot/src/main/cpp/compat.h @@ -0,0 +1,287 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#ifndef COMPAT_H +#define COMPAT_H + +/* Local definitions for compatibility with old and/or broken distros... */ +# ifndef AT_NULL +# define AT_NULL 0 +# endif +# ifndef AT_PHDR +# define AT_PHDR 3 +# endif +# ifndef AT_PHENT +# define AT_PHENT 4 +# endif +# ifndef AT_PHNUM +# define AT_PHNUM 5 +# endif +# ifndef AT_BASE +# define AT_BASE 7 +# endif +# ifndef AT_ENTRY +# define AT_ENTRY 9 +# endif +# ifndef AT_RANDOM +# define AT_RANDOM 25 +# endif +# ifndef AT_EXECFN +# define AT_EXECFN 31 +# endif +# ifndef AT_SYSINFO +# define AT_SYSINFO 32 +# endif +# ifndef AT_SYSINFO_EHDR +# define AT_SYSINFO_EHDR 33 +# endif +# ifndef AT_FDCWD +# define AT_FDCWD -100 +# endif +# ifndef AT_SYMLINK_FOLLOW +# define AT_SYMLINK_FOLLOW 0x400 +# endif +# ifndef AT_REMOVEDIR +# define AT_REMOVEDIR 0x200 +# endif +# ifndef AT_SYMLINK_NOFOLLOW +# define AT_SYMLINK_NOFOLLOW 0x100 +# endif +# ifndef IN_DONT_FOLLOW +# define IN_DONT_FOLLOW 0x02000000 +# endif +# ifndef WIFCONTINUED +# define WIFCONTINUED(status) ((status) == 0xffff) +# endif +# ifndef PTRACE_GETREGS +# define PTRACE_GETREGS 12 +# endif +# ifndef PTRACE_SETREGS +# define PTRACE_SETREGS 13 +# endif +# ifndef PTRACE_GETFPREGS +# define PTRACE_GETFPREGS 14 +# endif +# ifndef PTRACE_SETFPREGS +# define PTRACE_SETFPREGS 15 +# endif +# ifndef PTRACE_GETFPXREGS +# define PTRACE_GETFPXREGS 18 +# endif +# ifndef PTRACE_SETFPXREGS +# define PTRACE_SETFPXREGS 19 +# endif +# ifndef PTRACE_SETOPTIONS +# define PTRACE_SETOPTIONS 0x4200 +# endif +# ifndef PTRACE_GETEVENTMSG +# define PTRACE_GETEVENTMSG 0x4201 +# endif +# ifndef PTRACE_GETREGSET +# define PTRACE_GETREGSET 0x4204 +# endif +# ifndef PTRACE_SETREGSET +# define PTRACE_SETREGSET 0x4205 +# endif +# ifndef PTRACE_SEIZE +# define PTRACE_SEIZE 0x4206 +# endif +# ifndef PTRACE_INTERRUPT +# define PTRACE_INTERRUPT 0x4207 +# endif +# ifndef PTRACE_LISTEN +# define PTRACE_LISTEN 0x4208 +# endif +# ifndef PTRACE_O_TRACESYSGOOD +# define PTRACE_O_TRACESYSGOOD 0x00000001 +# endif +# ifndef PTRACE_O_TRACEFORK +# define PTRACE_O_TRACEFORK 0x00000002 +# endif +# ifndef PTRACE_O_TRACEVFORK +# define PTRACE_O_TRACEVFORK 0x00000004 +# endif +# ifndef PTRACE_O_TRACECLONE +# define PTRACE_O_TRACECLONE 0x00000008 +# endif +# ifndef PTRACE_O_TRACEEXEC +# define PTRACE_O_TRACEEXEC 0x00000010 +# endif +# ifndef PTRACE_O_TRACEVFORKDONE +# define PTRACE_O_TRACEVFORKDONE 0x00000020 +# endif +# ifndef PTRACE_O_TRACEEXIT +# define PTRACE_O_TRACEEXIT 0x00000040 +# endif +# ifndef PTRACE_O_TRACESECCOMP +# define PTRACE_O_TRACESECCOMP 0x00000080 +# endif +# ifndef PTRACE_EVENT_FORK +# define PTRACE_EVENT_FORK 1 +# endif +# ifndef PTRACE_EVENT_VFORK +# define PTRACE_EVENT_VFORK 2 +# endif +# ifndef PTRACE_EVENT_CLONE +# define PTRACE_EVENT_CLONE 3 +# endif +# ifndef PTRACE_EVENT_EXEC +# define PTRACE_EVENT_EXEC 4 +# endif +# ifndef PTRACE_EVENT_VFORK_DONE +# define PTRACE_EVENT_VFORK_DONE 5 +# endif +# ifndef PTRACE_EVENT_EXIT +# define PTRACE_EVENT_EXIT 6 +# endif +# ifndef PTRACE_EVENT_SECCOMP +# define PTRACE_EVENT_SECCOMP 7 +# endif +# ifndef PTRACE_EVENT_SECCOMP2 +# if PTRACE_EVENT_SECCOMP == 7 +# define PTRACE_EVENT_SECCOMP2 8 +# elif PTRACE_EVENT_SECCOMP == 8 +# define PTRACE_EVENT_SECCOMP2 7 +# else +# error "unknown PTRACE_EVENT_SECCOMP value" +# endif +# endif +# ifndef PTRACE_SET_SYSCALL +# define PTRACE_SET_SYSCALL 23 +# endif +# ifndef PTRACE_GET_THREAD_AREA +# define PTRACE_GET_THREAD_AREA 25 +# endif +# ifndef PTRACE_SET_THREAD_AREA +# define PTRACE_SET_THREAD_AREA 26 +# endif +# ifndef PTRACE_GETVFPREGS +# define PTRACE_GETVFPREGS 27 +# endif +# ifndef PTRACE_ARCH_PRCTL +# define PTRACE_ARCH_PRCTL 30 +# endif +# ifndef ARCH_SET_GS +# define ARCH_SET_GS 0x1001 +# endif +# ifndef ARCH_SET_FS +# define ARCH_SET_FS 0x1002 +# endif +# ifndef ARCH_GET_GS +# define ARCH_GET_FS 0x1003 +# endif +# ifndef ARCH_GET_FS +# define ARCH_GET_GS 0x1004 +# endif +# ifndef PTRACE_SINGLEBLOCK +# define PTRACE_SINGLEBLOCK 33 +# endif +# ifndef ADDR_NO_RANDOMIZE +# define ADDR_NO_RANDOMIZE 0x0040000 +# endif +# ifndef SYS_ACCEPT4 +# define SYS_ACCEPT4 18 +# endif +# ifndef TALLOC_FREE +# define TALLOC_FREE(ctx) do { talloc_free(ctx); ctx = NULL; } while(0) +# endif +# ifndef PR_SET_NAME +# define PR_SET_NAME 15 +# endif +# ifndef PR_SET_NO_NEW_PRIVS +# define PR_SET_NO_NEW_PRIVS 38 +# endif +# ifndef PR_GET_NO_NEW_PRIVS +# define PR_GET_NO_NEW_PRIVS 39 +# endif +# ifndef PR_SET_SECCOMP +# define PR_SET_SECCOMP 22 +# endif +# ifndef SECCOMP_MODE_FILTER +# define SECCOMP_MODE_FILTER 2 +# endif +# ifndef talloc_get_type_abort +# define talloc_get_type_abort talloc_get_type +# endif +# ifndef FUTEX_PRIVATE_FLAG +# define FUTEX_PRIVATE_FLAG 128 +# endif +# ifndef EFD_SEMAPHORE +# define EFD_SEMAPHORE 1 +# endif +# ifndef F_DUPFD_CLOEXEC +# define F_DUPFD_CLOEXEC 1030 +# endif +# ifndef O_RDONLY +# define O_RDONLY 00000000 +# endif +# ifndef O_CLOEXEC +# define O_CLOEXEC 02000000 +# endif +# ifndef MAP_PRIVATE +# define MAP_PRIVATE 0x02 +# endif +# ifndef MAP_FIXED +# define MAP_FIXED 0x10 +# endif +# ifndef MAP_ANONYMOUS +# define MAP_ANONYMOUS 0x20 +# endif +# ifndef PROT_READ +# define PROT_READ 0x1 +# endif +# ifndef PROT_WRITE +# define PROT_WRITE 0x2 +# endif +# ifndef PROT_EXEC +# define PROT_EXEC 0x4 +# endif +# ifndef PROT_GROWSDOWN +# define PROT_GROWSDOWN 0x01000000 +# endif +# ifndef NT_ARM_SYSTEM_CALL +# define NT_ARM_SYSTEM_CALL 0x404 +# endif +# ifndef SYS_SECCOMP +# define SYS_SECCOMP 1 +# endif + +/* openat2(2) "how" argument. Mirrors struct open_how from + * , but uses a private name so we don't depend on (or + * clash with) that kernel header, which is present on some build + * platforms (e.g. Termux) and absent on others. */ +struct proot_open_how { + unsigned long long flags; + unsigned long long mode; + unsigned long long resolve; +}; + +static inline unsigned long untag_pointer(unsigned long addr) +{ +#if defined(__aarch64__) + return addr & 0x00FFFFFFFFFFFFFFULL; +#else + return addr; +#endif +} + +#endif /* COMPAT_H */ diff --git a/core/proot/src/main/cpp/execve/aoxp.c b/core/proot/src/main/cpp/execve/aoxp.c new file mode 100644 index 000000000..b1103a605 --- /dev/null +++ b/core/proot/src/main/cpp/execve/aoxp.c @@ -0,0 +1,439 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#include /* ARG_MAX, */ +#include /* assert(3), */ +#include /* strlen(3), memcmp(3), memcpy(3), */ +#include /* bzero(3), */ +#include /* bool, true, false, */ +#include /* E*, */ +#include /* va_*, */ +#include /* uint32_t, */ +#include /* talloc_*, */ + +#include "arch.h" +#include "tracee/tracee.h" +#include "tracee/mem.h" +#include "tracee/abi.h" +#include "build.h" + +struct mixed_pointer { + /* Pointer -- in tracee's address space -- to the current + * object, if local == NULL. */ + word_t remote; + + /* Pointer -- in tracer's address space -- to the current + * object, if local != NULL. */ + void *local; +}; + +#include "execve/aoxp.h" + +/** + * Read object pointed to by @array[@index] from tracee's memory, then + * make @local_pointer points to the locally *cached* version. This + * function returns -errno when an error occured, otherwise 0. + */ +int read_xpointee_as_object(ArrayOfXPointers *array, size_t index, void **local_pointer) +{ + int status; + int size; + + assert(index < array->length); + + /* Already cached locally? */ + if (array->_xpointers[index].local != NULL) + goto end; + + /* Remote NULL is mapped to local NULL. */ + if (array->_xpointers[index].remote == 0) { + array->_xpointers[index].local = NULL; + goto end; + } + + size = sizeof_xpointee(array, index); + if (size < 0) + return size; + + array->_xpointers[index].local = talloc_size(array, size); + if (array->_xpointers[index].local == NULL) + return -ENOMEM; + + /* Copy locally the remote object. */ + status = read_data(TRACEE(array), array->_xpointers[index].local, + array->_xpointers[index].remote, size); + if (status < 0) { + array->_xpointers[index].local = NULL; + return status; + } + +end: + *local_pointer = array->_xpointers[index].local; + return 0; +} + +/** + * Read string pointed to by @array[@index] from tracee's memory, then + * make @local_pointer points to the locally *cached* version. This + * function returns -errno when an error occured, otherwise 0. + */ +int read_xpointee_as_string(ArrayOfXPointers *array, size_t index, char **local_pointer) +{ + char tmp[ARG_MAX]; + int status; + + assert(index < array->length); + + /* Already cached locally? */ + if (array->_xpointers[index].local != NULL) + goto end; + + /* Remote NULL is mapped to local NULL. */ + if (array->_xpointers[index].remote == 0) { + array->_xpointers[index].local = NULL; + goto end; + } + + /* Copy locally the remote string into a temporary buffer. */ + status = read_string(TRACEE(array), tmp, array->_xpointers[index].remote, ARG_MAX); + if (status < 0) + return status; + if (status >= ARG_MAX) + return -ENOMEM; + + /* Save the local string in a "persistent" buffer. */ + array->_xpointers[index].local = talloc_strdup(array, tmp); + if (array->_xpointers[index].local == NULL) + return -ENOMEM; + +end: + *local_pointer = array->_xpointers[index].local; + return 0; +} + +/** + * This function returns the number of bytes of the string pointed to + * by @array[@index], otherwise -errno if an error occured. + */ +int sizeof_xpointee_as_string(ArrayOfXPointers *array, size_t index) +{ + char *string; + int status; + + assert(index < array->length); + + status = read_xpointee_as_string(array, index, &string); + if (status < 0) + return status; + + if (string == NULL) + return 0; + + return strlen(string) + 1; +} + +/** + * Compare object pointed to by @array[@index] with object pointed to + * by @local_reference. This function returns 1 if they are + * equivalent, 0 otherwise. On error, -errno is returned. + */ +int compare_xpointee_generic(ArrayOfXPointers *array, size_t index, const void *local_reference) +{ + void *object; + int status; + + assert(index < array->length); + + status = read_xpointee(array, index, &object); + if (status < 0) + return status; + + if (object == NULL && local_reference == NULL) + return 1; + + if (object == NULL && local_reference != NULL) + return 0; + + if (object != NULL && local_reference == NULL) + return 0; + + status = sizeof_xpointee(array, index); + if (status < 0) + return status; + + return (int) (memcmp(object, local_reference, status) == 0); +} + +/** + * This function returns the index in @array of the first pointee + * equivalent to the @local_reference pointee, otherwise it returns + * -errno if an error occured. + */ +int find_xpointee(ArrayOfXPointers *array, const void *local_reference) +{ + size_t i; + + for (i = 0; i < array->length; i++) { + int status; + + status = compare_xpointee(array, i, local_reference); + if (status < 0) + return status; + if (status != 0) + break; + } + + return i; +} + +/** + * Make @array[@index] points to a copy of the string pointed to by + * @string. This function returns -errno when an error occured, + * otherwise 0. + */ +int write_xpointee_as_string(ArrayOfXPointers *array, size_t index, const char *string) +{ + assert(index < array->length); + + array->_xpointers[index].local = talloc_strdup(array, string); + if (array->_xpointers[index].local == NULL) + return -ENOMEM; + + return 0; +} + +/** + * Make @array[@index ... @index + @nb_xpointees] points to a copy of + * the variadic arguments. This function returns -errno when an error + * occured, otherwise 0. + */ +int write_xpointees(ArrayOfXPointers *array, size_t index, size_t nb_xpointees, ...) +{ + va_list va_xpointees; + int status; + size_t i; + + va_start(va_xpointees, nb_xpointees); + + for (i = 0; i < nb_xpointees; i++) { + void *object = va_arg(va_xpointees, void *); + + status = write_xpointee(array, index + i, object); + if (status < 0) + goto end; + } + status = 0; + +end: + va_end(va_xpointees); + return status; +} + + +/** + * Resize the @array at the given @index by the @delta_nb_entries. + * This function returns -errno when an error occured, otherwise 0. + */ +int resize_array_of_xpointers(ArrayOfXPointers *array, size_t index, ssize_t delta_nb_entries) +{ + size_t nb_moved_entries; + size_t new_length; + void *tmp; + + assert(index < array->length); + + if (delta_nb_entries == 0) + return 0; + + new_length = array->length + delta_nb_entries; + nb_moved_entries = array->length - index; + + if (delta_nb_entries > 0) { + tmp = talloc_realloc(array, array->_xpointers, XPointer, new_length); + if (tmp == NULL) + return -ENOMEM; + array->_xpointers = tmp; + + memmove(array->_xpointers + index + delta_nb_entries, array->_xpointers + index, + nb_moved_entries * sizeof(XPointer)); + + bzero(array->_xpointers + index, delta_nb_entries * sizeof(XPointer)); + } + else { + assert(delta_nb_entries <= 0); + assert(index >= (size_t) -delta_nb_entries); + + memmove(array->_xpointers + index + delta_nb_entries, array->_xpointers + index, + nb_moved_entries * sizeof(XPointer)); + + tmp = talloc_realloc(array, array->_xpointers, XPointer, new_length); + if (tmp == NULL) + return -ENOMEM; + array->_xpointers = tmp; + } + + array->length = new_length; + return 0; +} + +/** + * Copy into *@array_ the pointer array pointed to by @reg from + * @tracee's memory space. Only the first @nb_entries are copied, + * unless it is 0 then all the entries up to the NULL pointer are + * copied. This function returns -errno when an error occured, + * otherwise 0. + */ +int fetch_array_of_xpointers(Tracee *tracee, ArrayOfXPointers **array_, Reg reg, size_t nb_entries) +{ + word_t pointer = 1; /* ie. != 0 */ + word_t address; + ArrayOfXPointers *array; + size_t i; + + assert(array_ != NULL); + + *array_ = talloc_zero(tracee->ctx, ArrayOfXPointers); + if (*array_ == NULL) + return -ENOMEM; + array = *array_; + + address = peek_reg(tracee, CURRENT, reg); + + for (i = 0; nb_entries != 0 ? i < nb_entries : pointer != 0; i++) { + void *tmp = talloc_realloc(array, array->_xpointers, XPointer, i + 1); + if (tmp == NULL) + return -ENOMEM; + array->_xpointers = tmp; + + pointer = peek_word(tracee, address + i * sizeof_word(tracee)); + if (errno != 0) + return -errno; + + array->_xpointers[i].remote = pointer; + array->_xpointers[i].local = NULL; + } + array->length = i; + + /* By default, assume it is an array of string pointers. */ + array->read_xpointee = (read_xpointee_t) read_xpointee_as_string; + array->sizeof_xpointee = sizeof_xpointee_as_string; + array->write_xpointee = (write_xpointee_t) write_xpointee_as_string; + + /* By default, use generic callbacks: they rely on + * array->read_xpointee() and array->sizeof_xpointee(). */ + array->compare_xpointee = compare_xpointee_generic; + + return 0; +} + +/** + * Copy @array into tracee's memory space, then put in @reg the + * address where it was copied. This function returns -errno if an + * error occured, otherwise 0. + */ +int push_array_of_xpointers(ArrayOfXPointers *array, Reg reg) +{ + Tracee *tracee; + struct iovec *local; + size_t local_count; + size_t total_size; + word_t *pod_array; + word_t tracee_ptr; + int status; + size_t i; + + /* Nothing to do, for sure. */ + if (array == NULL) + return 0; + + tracee = TRACEE(array); + + /* The pointer table is a POD array in the tracee's memory. */ + pod_array = talloc_zero_size(tracee->ctx, array->length * sizeof_word(tracee)); + if (pod_array == NULL) + return -ENOMEM; + + /* There's one vector per modified pointee + one vector for the + * pod array. */ + local = talloc_zero_array(tracee->ctx, struct iovec, array->length + 1); + if (local == NULL) + return -ENOMEM; + + /* The pod array is expected to be at the beginning of the + * allocated memory by the caller. */ + total_size = array->length * sizeof_word(tracee); + local[0].iov_base = pod_array; + local[0].iov_len = total_size; + local_count = 1; + + /* Create one vector for each modified pointee. */ + for (i = 0; i < array->length; i++) { + ssize_t size; + + if (array->_xpointers[i].local == NULL) + continue; + + /* At this moment, we only know the offsets in the + * tracee's memory block. */ + array->_xpointers[i].remote = total_size; + + size = sizeof_xpointee(array, i); + if (size < 0) + return size; + total_size += size; + + local[local_count].iov_base = array->_xpointers[i].local; + local[local_count].iov_len = size; + local_count++; + } + + /* Nothing has changed, don't update anything. */ + if (local_count == 1) + return 0; + assert(local_count < array->length + 1); + + /* Modified pointees and the pod array are stored in a tracee's + * memory block. */ + tracee_ptr = alloc_mem(tracee, total_size); + if (tracee_ptr == 0) + return -E2BIG; + + /* Now, we know the absolute addresses in the tracee's + * memory. */ + for (i = 0; i < array->length; i++) { + if (array->_xpointers[i].local != NULL) + array->_xpointers[i].remote += tracee_ptr; + + if (is_32on64_mode(tracee)) + ((uint32_t *) pod_array)[i] = array->_xpointers[i].remote; + else + pod_array[i] = array->_xpointers[i].remote; + } + + /* Write all the modified pointees and the pod array at once. */ + status = writev_data(tracee, tracee_ptr, local, local_count); + if (status < 0) + return status; + + poke_reg(tracee, reg, tracee_ptr); + return 0; +} diff --git a/core/proot/src/main/cpp/execve/aoxp.h b/core/proot/src/main/cpp/execve/aoxp.h new file mode 100644 index 000000000..7a852aa36 --- /dev/null +++ b/core/proot/src/main/cpp/execve/aoxp.h @@ -0,0 +1,80 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#ifndef AOXP_H +#define AOXP_H + +#include + +#include "tracee/reg.h" +#include "arch.h" + +typedef struct array_of_xpointers ArrayOfXPointers; +typedef int (*read_xpointee_t)(ArrayOfXPointers *array, size_t index, void **object); +typedef int (*write_xpointee_t)(ArrayOfXPointers *array, size_t index, const void *object); +typedef int (*compare_xpointee_t)(ArrayOfXPointers *array, size_t index, const void *reference); +typedef int (*sizeof_xpointee_t)(ArrayOfXPointers *array, size_t index); + +typedef struct mixed_pointer XPointer; +struct array_of_xpointers { + XPointer *_xpointers; + size_t length; + + read_xpointee_t read_xpointee; + write_xpointee_t write_xpointee; + compare_xpointee_t compare_xpointee; + sizeof_xpointee_t sizeof_xpointee; +}; + +static inline int read_xpointee(ArrayOfXPointers *array, size_t index, void **object) +{ + return array->read_xpointee(array, index, object); +} + +static inline int write_xpointee(ArrayOfXPointers *array, size_t index, const void *object) +{ + return array->write_xpointee(array, index, object); +} + +static inline int compare_xpointee(ArrayOfXPointers *array, size_t index, const void *reference) +{ + return array->compare_xpointee(array, index, reference); +} + +static inline int sizeof_xpointee(ArrayOfXPointers *array, size_t index) +{ + return array->sizeof_xpointee(array, index); +} + +extern int find_xpointee(ArrayOfXPointers *array, const void *reference); +extern int resize_array_of_xpointers(ArrayOfXPointers *array, size_t index, ssize_t nb_delta_entries); +extern int fetch_array_of_xpointers(Tracee *tracee, ArrayOfXPointers **array, Reg reg, size_t nb_entries); +extern int push_array_of_xpointers(ArrayOfXPointers *array, Reg reg); + +extern int read_xpointee_as_object(ArrayOfXPointers *array, size_t index, void **object); +extern int read_xpointee_as_string(ArrayOfXPointers *array, size_t index, char **string); +extern int write_xpointee_as_string(ArrayOfXPointers *array, size_t index, const char *string); +extern int write_xpointees(ArrayOfXPointers *array, size_t index, size_t nb_xpointees, ...); +extern int compare_xpointee_generic(ArrayOfXPointers *array, size_t index, const void *reference); +extern int sizeof_xpointee_as_string(ArrayOfXPointers *array, size_t index); + +#endif /* AOXP_H */ diff --git a/core/proot/src/main/cpp/execve/auxv.c b/core/proot/src/main/cpp/execve/auxv.c new file mode 100644 index 000000000..45f9d8477 --- /dev/null +++ b/core/proot/src/main/cpp/execve/auxv.c @@ -0,0 +1,184 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#include /* AT_*, */ +#include /* assert(3), */ +#include /* E*, */ +#include /* write(3), close(3), */ +#include /* open(2), */ +#include /* open(2), */ +#include /* open(2), */ + +#include "execve/auxv.h" +#include "syscall/sysnum.h" +#include "tracee/tracee.h" +#include "tracee/mem.h" +#include "tracee/reg.h" +#include "tracee/abi.h" +#include "arch.h" + + +/** + * Add the given vector [@type, @value] to @vectors. This function + * returns -errno if an error occurred, otherwise 0. + */ +int add_elf_aux_vector(ElfAuxVector **vectors, word_t type, word_t value) +{ + ElfAuxVector *tmp; + size_t nb_vectors; + + assert(*vectors != NULL); + + nb_vectors = talloc_array_length(*vectors); + + /* Sanity checks. */ + assert(nb_vectors > 0); + assert((*vectors)[nb_vectors - 1].type == AT_NULL); + + tmp = talloc_realloc(talloc_parent(*vectors), *vectors, ElfAuxVector, nb_vectors + 1); + if (tmp == NULL) + return -ENOMEM; + *vectors = tmp; + + /* Replace the sentinel with the new vector. */ + (*vectors)[nb_vectors - 1].type = type; + (*vectors)[nb_vectors - 1].value = value; + + /* Restore the sentinel. */ + (*vectors)[nb_vectors].type = AT_NULL; + (*vectors)[nb_vectors].value = 0; + + return 0; +} + +/** + * Get the address of the the ELF auxiliary vectors table for the + * given @tracee. This function returns 0 if an error occurred. + */ +word_t get_elf_aux_vectors_address(const Tracee *tracee) +{ + word_t address; + word_t data; + + /* Sanity check: this works only in execve sysexit. */ + assert(IS_IN_SYSEXIT2(tracee, PR_execve)); + + /* Right after execve, the stack layout is: + * + * argc, argv[0], ..., 0, envp[0], ..., 0, auxv[0].type, auxv[0].value, ..., 0, 0 + */ + address = peek_reg(tracee, CURRENT, STACK_POINTER); + + /* Read: argc */ + data = peek_word(tracee, address); + if (errno != 0) + return 0; + + /* Skip: argc, argv, 0 */ + address += (1 + data + 1) * sizeof_word(tracee); + + /* Skip: envp, 0 */ + do { + data = peek_word(tracee, address); + if (errno != 0) + return 0; + address += sizeof_word(tracee); + } while (data != 0); + + return address; +} + +/** + * Fetch ELF auxiliary vectors stored at the given @address in + * @tracee's memory. This function returns NULL if an error occurred, + * otherwise it returns a pointer to the new vectors, in an ABI + * independent form (the Talloc parent of this pointer is + * @tracee->ctx). + */ +ElfAuxVector *fetch_elf_aux_vectors(const Tracee *tracee, word_t address) +{ + ElfAuxVector *vectors = NULL; + ElfAuxVector vector; + int status; + + /* It is assumed the sentinel always exists. */ + vectors = talloc_array(tracee->ctx, ElfAuxVector, 1); + if (vectors == NULL) + return NULL; + vectors[0].type = AT_NULL; + vectors[0].value = 0; + + while (1) { + vector.type = peek_word(tracee, address); + if (errno != 0) + return NULL; + address += sizeof_word(tracee); + + if (vector.type == AT_NULL) + break; /* Already added. */ + + vector.value = peek_word(tracee, address); + if (errno != 0) + return NULL; + address += sizeof_word(tracee); + + status = add_elf_aux_vector(&vectors, vector.type, vector.value); + if (status < 0) + return NULL; + } + + return vectors; +} + +/** + * Push ELF auxiliary @vectors to the given @address in @tracee's + * memory. This function returns -errno if an error occurred, + * otherwise 0. + */ +int push_elf_aux_vectors(const Tracee* tracee, ElfAuxVector *vectors, word_t address) +{ + size_t i; + + for (i = 0; vectors[i].type != AT_NULL; i++) { + poke_word(tracee, address, vectors[i].type); + if (errno != 0) + return -errno; + address += sizeof_word(tracee); + + poke_word(tracee, address, vectors[i].value); + if (errno != 0) + return -errno; + address += sizeof_word(tracee); + } + + poke_word(tracee, address, AT_NULL); + if (errno != 0) + return -errno; + address += sizeof_word(tracee); + + poke_word(tracee, address, 0); + if (errno != 0) + return -errno; + address += sizeof_word(tracee); + + return 0; +} diff --git a/core/proot/src/main/cpp/execve/auxv.h b/core/proot/src/main/cpp/execve/auxv.h new file mode 100644 index 000000000..cd5871b65 --- /dev/null +++ b/core/proot/src/main/cpp/execve/auxv.h @@ -0,0 +1,39 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#ifndef AUXV +#define AUXV + +#include "tracee/tracee.h" +#include "arch.h" + +typedef struct elf_aux_vector { + word_t type; + word_t value; +} ElfAuxVector; + +extern word_t get_elf_aux_vectors_address(const Tracee *tracee); +extern ElfAuxVector *fetch_elf_aux_vectors(const Tracee *tracee, word_t address); +extern int add_elf_aux_vector(ElfAuxVector **vectors, word_t type, word_t value); +extern int push_elf_aux_vectors(const Tracee* tracee, ElfAuxVector *vectors, word_t address); + +#endif /* AUXV */ diff --git a/core/proot/src/main/cpp/execve/elf.c b/core/proot/src/main/cpp/execve/elf.c new file mode 100644 index 000000000..b1f78e6de --- /dev/null +++ b/core/proot/src/main/cpp/execve/elf.c @@ -0,0 +1,178 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#include /* open(2), */ +#include /* read(2), close(2), */ +#include /* EACCES, ENOTSUP, */ +#include /* UINT64_MAX, */ +#include /* PATH_MAX, */ +#include /* str*(3), memcpy(3), */ +#include /* assert(3), */ +#include /* talloc_*, */ +#include /* bool, true, false, */ + +#include "execve/elf.h" +#include "tracee/tracee.h" +#include "cli/note.h" +#include "arch.h" + +#include "compat.h" + +/** + * Open the ELF file @t_path and extract its header into @elf_header. + * This function returns -errno if an error occured, otherwise the + * file descriptor for @t_path. + */ +int open_elf(const char *t_path, ElfHeader *elf_header) +{ + int fd; + int status; + + /* + * Read the ELF header. + */ + + fd = open(t_path, O_RDONLY); + if (fd < 0) + return -errno; + + /* Check if it is an ELF file. */ + status = read(fd, elf_header, sizeof(ElfHeader)); + if (status < 0) { + status = -errno; + goto end; + } + if ((size_t) status < sizeof(ElfHeader) + || ELF_IDENT(*elf_header, 0) != 0x7f + || ELF_IDENT(*elf_header, 1) != 'E' + || ELF_IDENT(*elf_header, 2) != 'L' + || ELF_IDENT(*elf_header, 3) != 'F') { + status = -ENOEXEC; + goto end; + } + + /* Check if it is a known class (32-bit or 64-bit). */ + if ( !IS_CLASS32(*elf_header) + && !IS_CLASS64(*elf_header)) { + status = -ENOEXEC; + goto end; + } + + status = 0; +end: + /* Delayed error handling. */ + if (status < 0) { + close(fd); + return status; + } + + return fd; +} + +/** + * Invoke @callback(..., @data) for each program headers from the + * specified ELF file (referenced by @fd, with the given @elf_header). + * This function returns -errno if an error occured, or it returns + * immediately the value != 0 returned by @callback, otherwise 0. + */ +int iterate_program_headers(const Tracee *tracee, int fd, const ElfHeader *elf_header, + program_headers_iterator_t callback, void *data) +{ + ProgramHeader program_header; + + uint64_t elf_phoff; + uint16_t elf_phentsize; + uint16_t elf_phnum; + + int status; + int i; + + /* Get class-specific fields. */ + elf_phnum = ELF_FIELD(*elf_header, phnum); + elf_phentsize = ELF_FIELD(*elf_header, phentsize); + elf_phoff = ELF_FIELD(*elf_header, phoff); + + /* + * Some sanity checks regarding the current + * support of the ELF specification in PRoot. + */ + + if (elf_phnum >= 0xffff) { + note(tracee, WARNING, INTERNAL, "%d: big PH tables are not yet supported.", fd); + return -ENOTSUP; + } + + if (!KNOWN_PHENTSIZE(*elf_header, elf_phentsize)) { + note(tracee, WARNING, INTERNAL, "%d: unsupported size of program header.", fd); + return -ENOTSUP; + } + + status = (int) lseek(fd, elf_phoff, SEEK_SET); + if (status < 0) + return -errno; + + for (i = 0; i < elf_phnum; i++) { + status = read(fd, &program_header, elf_phentsize); + if (status != elf_phentsize) + return (status < 0 ? -errno : -ENOTSUP); + + status = callback(elf_header, &program_header, data); + if (status != 0) + return status; + } + + return 0; +} + +/** + * Check if @host_path is an ELF file for the host architecture. + */ +bool is_host_elf(const Tracee *tracee, const char *host_path) +{ + int host_elf_machine[] = HOST_ELF_MACHINE; + static int force_foreign = -1; + ElfHeader elf_header; + uint16_t elf_machine; + int fd; + int i; + + if (force_foreign < 0) + force_foreign = (getenv("PROOT_FORCE_FOREIGN_BINARY") != NULL); + + if (force_foreign > 0 || !tracee->qemu) + return false; + + fd = open_elf(host_path, &elf_header); + if (fd < 0) + return false; + close(fd); + + elf_machine = ELF_FIELD(elf_header, machine); + for (i = 0; host_elf_machine[i] != 0; i++) { + if (host_elf_machine[i] == elf_machine) { + VERBOSE(tracee, 1, "'%s' is a host ELF", host_path); + return true; + } + } + + return false; +} diff --git a/core/proot/src/main/cpp/execve/elf.h b/core/proot/src/main/cpp/execve/elf.h new file mode 100644 index 000000000..a5b367b71 --- /dev/null +++ b/core/proot/src/main/cpp/execve/elf.h @@ -0,0 +1,179 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#ifndef ELF_H +#define ELF_H + +#define EI_NIDENT 16 + +#include +#include + +typedef struct { + unsigned char e_ident[EI_NIDENT]; + uint16_t e_type; + uint16_t e_machine; + uint32_t e_version; + uint32_t e_entry; + uint32_t e_phoff; + uint32_t e_shoff; + uint32_t e_flags; + uint16_t e_ehsize; + uint16_t e_phentsize; + uint16_t e_phnum; + uint16_t e_shentsize; + uint16_t e_shnum; + uint16_t e_shstrndx; +} ElfHeader32; + +typedef struct { + unsigned char e_ident[EI_NIDENT]; + uint16_t e_type; + uint16_t e_machine; + uint32_t e_version; + uint64_t e_entry; + uint64_t e_phoff; + uint64_t e_shoff; + uint32_t e_flags; + uint16_t e_ehsize; + uint16_t e_phentsize; + uint16_t e_phnum; + uint16_t e_shentsize; + uint16_t e_shnum; + uint16_t e_shstrndx; +} ElfHeader64; + +typedef union { + ElfHeader32 class32; + ElfHeader64 class64; +} ElfHeader; + +typedef struct { + uint32_t p_type; + uint32_t p_offset; + uint32_t p_vaddr; + uint32_t p_paddr; + uint32_t p_filesz; + uint32_t p_memsz; + uint32_t p_flags; + uint32_t p_align; +} ProgramHeader32; + +typedef struct { + uint32_t p_type; + uint32_t p_flags; + uint64_t p_offset; + uint64_t p_vaddr; + uint64_t p_paddr; + uint64_t p_filesz; + uint64_t p_memsz; + uint64_t p_align; +} ProgramHeader64; + +typedef union { + ProgramHeader32 class32; + ProgramHeader64 class64; +} ProgramHeader; + +/* Object type: */ +#define ET_REL 1 +#define ET_EXEC 2 +#define ET_DYN 3 +#define ET_CORE 4 + +/* Segment flags: */ +#define PF_X 1 +#define PF_W 2 +#define PF_R 4 + +typedef enum { + PT_LOAD = 1, + PT_DYNAMIC = 2, + PT_INTERP = 3, + PT_GNU_STACK = 0x6474e551, +} SegmentType; + +typedef struct { + int32_t d_tag; + uint32_t d_val; +} DynamicEntry32; + +typedef struct { + int64_t d_tag; + uint64_t d_val; +} DynamicEntry64; + +typedef union { + DynamicEntry32 class32; + DynamicEntry64 class64; +} DynamicEntry; + +typedef enum { + DT_STRTAB = 5, + DT_RPATH = 15, + DT_RUNPATH = 29 +} DynamicType; + +/* The following macros are also compatible with ELF 64-bit. */ +#define ELF_IDENT(header, index) (header).class32.e_ident[(index)] +#define ELF_CLASS(header) ELF_IDENT(header, 4) +#define IS_CLASS32(header) (ELF_CLASS(header) == 1) +#define IS_CLASS64(header) (ELF_CLASS(header) == 2) + +/* Helper to access a @field of the structure ElfHeaderXX. */ +#define ELF_FIELD(header, field) \ + (IS_CLASS64(header) \ + ? (header).class64. e_ ## field \ + : (header).class32. e_ ## field) + +/* Helper to access a @field of the structure ProgramHeaderXX */ +#define PROGRAM_FIELD(ehdr, phdr, field) \ + (IS_CLASS64(ehdr) \ + ? (phdr).class64. p_ ## field \ + : (phdr).class32. p_ ## field) + +/* Helper to access a @field of the structure DynamicEntryXX */ +#define DYNAMIC_FIELD(ehdr, dynent, field) \ + (IS_CLASS64(ehdr) \ + ? (dynent).class64. d_ ## field \ + : (dynent).class32. d_ ## field) + +#define KNOWN_PHENTSIZE(header, size) \ + ( (IS_CLASS32(header) && (size) == sizeof(ProgramHeader32)) \ + || (IS_CLASS64(header) && (size) == sizeof(ProgramHeader64))) + +#define IS_POSITION_INDENPENDANT(elf_header) \ + (ELF_FIELD((elf_header), type) == ET_DYN) + +#include "tracee/tracee.h" + +extern int open_elf(const char *t_path, ElfHeader *elf_header); + +extern bool is_host_elf(const Tracee *tracee, const char *t_path); + +typedef int (* program_headers_iterator_t)(const ElfHeader *elf_header, + const ProgramHeader *program_header, void *data); + +extern int iterate_program_headers(const Tracee *tracee, int fd, const ElfHeader *elf_header, + program_headers_iterator_t callback, void *data); + +#endif /* ELF_H */ diff --git a/core/proot/src/main/cpp/execve/enter.c b/core/proot/src/main/cpp/execve/enter.c new file mode 100644 index 000000000..06e2db5f7 --- /dev/null +++ b/core/proot/src/main/cpp/execve/enter.c @@ -0,0 +1,721 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#include /* lstat(2), lseek(2), */ +#include /* lstat(2), lseek(2), fchmod(2), */ +#include /* access(2), lstat(2), close(2), read(2), */ +#include /* E*, */ +#include /* assert(3), */ +#include /* talloc*, */ +#include /* PROT_*, */ +#include /* strlen(3), strcpy(3), */ +#include /* getenv(3), */ +#include /* fwrite(3), */ +#include /* assert(3), */ + +#include "execve/execve.h" +#include "execve/shebang.h" +#include "execve/aoxp.h" +#include "execve/ldso.h" +#include "execve/elf.h" +#include "path/path.h" +#include "path/temp.h" +#include "path/binding.h" +#include "tracee/tracee.h" +#include "syscall/syscall.h" +#include "syscall/sysnum.h" +#include "arch.h" +#include "cli/note.h" + +#define P(a) PROGRAM_FIELD(load_info->elf_header, *program_header, a) + +/** + * Add @program_header (type PT_LOAD) to @load_info->mappings. This + * function returns -errno if an error occured, otherwise it returns + * 0. + */ +static int add_mapping(const Tracee *tracee UNUSED, LoadInfo *load_info, + const ProgramHeader *program_header) +{ + size_t index; + word_t start_address; + word_t end_address; + static word_t page_size = 0; + static word_t page_mask = 0; + + if (page_size == 0) { + page_size = sysconf(_SC_PAGE_SIZE); + if ((int) page_size <= 0) + page_size = 0x1000; + page_mask = ~(page_size - 1); + } + + if (load_info->mappings == NULL) + index = 0; + else + index = talloc_array_length(load_info->mappings); + + load_info->mappings = talloc_realloc(load_info, load_info->mappings, Mapping, index + 1); + if (load_info->mappings == NULL) + return -ENOMEM; + + start_address = P(vaddr) & page_mask; + end_address = (P(vaddr) + P(filesz) + page_size) & page_mask; + + load_info->mappings[index].fd = -1; /* Unknown yet. */ + load_info->mappings[index].offset = P(offset) & page_mask; + load_info->mappings[index].addr = start_address; + load_info->mappings[index].length = end_address - start_address; + load_info->mappings[index].flags = MAP_PRIVATE | MAP_FIXED; + load_info->mappings[index].prot = ( (P(flags) & PF_R ? PROT_READ : 0) + | (P(flags) & PF_W ? PROT_WRITE : 0) + | (P(flags) & PF_X ? PROT_EXEC : 0)); + + /* "If the segment's memory size p_memsz is larger than the + * file size p_filesz, the "extra" bytes are defined to hold + * the value 0 and to follow the segment's initialized area." + * -- man 7 elf. */ + if (P(memsz) > P(filesz)) { + /* How many extra bytes in the current page? */ + load_info->mappings[index].clear_length = end_address - P(vaddr) - P(filesz); + + /* Create new pages for the remaining extra bytes. */ + start_address = end_address; + end_address = (P(vaddr) + P(memsz) + page_size) & page_mask; + if (end_address > start_address) { + index++; + load_info->mappings = talloc_realloc(load_info, load_info->mappings, + Mapping, index + 1); + if (load_info->mappings == NULL) + return -ENOMEM; + + load_info->mappings[index].fd = -1; /* Anonymous. */ + load_info->mappings[index].offset = 0; + load_info->mappings[index].addr = start_address; + load_info->mappings[index].length = end_address - start_address; + load_info->mappings[index].clear_length = 0; + load_info->mappings[index].flags = MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED; + load_info->mappings[index].prot = load_info->mappings[index - 1].prot; + } + } + else + load_info->mappings[index].clear_length = 0; + + return 0; +} + +/** + * Translate @user_path into @host_path and check if this latter exists, is + * executable and is a regular file. This function returns -errno if + * an error occured, 0 otherwise. + */ +int translate_and_check_exec(Tracee *tracee, char host_path[PATH_MAX], const char *user_path) +{ + struct stat statl; + int status; + + if (user_path[0] == '\0') + return -ENOEXEC; + + status = translate_path(tracee, host_path, AT_FDCWD, user_path, true); + if (status < 0) + return status; + + status = access(host_path, F_OK); + if (status < 0) + return -ENOENT; + + status = access(host_path, X_OK); + if (status < 0) + return -EACCES; + + status = lstat(host_path, &statl); + if (status < 0) + return -EPERM; + + return 0; +} + +/** + * Add @program_header (type PT_INTERP) to @load_info->interp. This + * function returns -errno if an error occured, otherwise it returns + * 0. + */ +static int add_interp(Tracee *tracee, int fd, LoadInfo *load_info, + const ProgramHeader *program_header) +{ + char host_path[PATH_MAX]; + char *user_path; + int status; + + /* Only one PT_INTERP segment is allowed. */ + if (load_info->interp != NULL) + return -EINVAL; + + load_info->interp = talloc_zero(load_info, LoadInfo); + if (load_info->interp == NULL) + return -ENOMEM; + + user_path = talloc_size(tracee->ctx, P(filesz) + 1); + if (user_path == NULL) + return -ENOMEM; + + /* Remember pread(2) doesn't change the + * current position in the file. */ + status = pread(fd, user_path, P(filesz), P(offset)); + if ((size_t) status != P(filesz)) /* Unexpected size. */ + status = -EACCES; + if (status < 0) + return status; + + user_path[P(filesz)] = '\0'; + + /* When a QEMU command was specified: + * + * - if it's a foreign binary we are reading the ELF + * interpreter of QEMU instead. + * + * - if it's a host binary, we are reading its ELF + * interpreter. + * + * In both case, it lies in "/host-rootfs" from a guest + * point-of-view. */ + if (tracee->qemu != NULL && user_path[0] == '/') { + user_path = talloc_asprintf(tracee->ctx, "%s%s", HOST_ROOTFS, user_path); + if (user_path == NULL) + return -ENOMEM; + } + + status = translate_and_check_exec(tracee, host_path, user_path); + if (status < 0) + return status; + + load_info->interp->host_path = talloc_strdup(load_info->interp, host_path); + if (load_info->interp->host_path == NULL) + return -ENOMEM; + + load_info->interp->user_path = talloc_strdup(load_info->interp, user_path); + if (load_info->interp->user_path == NULL) + return -ENOMEM; + + return 0; +} + +#undef P + +struct add_load_info_data { + LoadInfo *load_info; + Tracee *tracee; + int fd; +}; + +/** + * This function is a program header iterator. It invokes + * add_mapping() or add_interp(), according to the type of + * @program_header. This function returns -errno if an error + * occurred, otherwise 0. + */ +static int add_load_info(const ElfHeader *elf_header, + const ProgramHeader *program_header, void *data_) +{ + struct add_load_info_data *data = data_; + int status; + + switch (PROGRAM_FIELD(*elf_header, *program_header, type)) { + case PT_LOAD: + status = add_mapping(data->tracee, data->load_info, program_header); + if (status < 0) + return status; + break; + + case PT_INTERP: + status = add_interp(data->tracee, data->fd, data->load_info, program_header); + if (status < 0) + return status; + break; + + case PT_GNU_STACK: + data->load_info->needs_executable_stack |= + ((PROGRAM_FIELD(*elf_header, *program_header, flags) & PF_X) != 0); + break; + + default: + break; + } + + return 0; +} + +/** + * Extract the load info from @load->host_path. This function returns + * -errno if an error occured, otherwise it returns 0. + */ +static int extract_load_info(Tracee *tracee, LoadInfo *load_info) +{ + struct add_load_info_data data; + int fd = -1; + int status; + + assert(load_info != NULL); + assert(load_info->host_path != NULL); + + fd = open_elf(load_info->host_path, &load_info->elf_header); + if (fd < 0) + return fd; + + /* Sanity check. */ + switch (ELF_FIELD(load_info->elf_header, type)) { + case ET_EXEC: + case ET_DYN: + break; + + default: + status = -EINVAL; + goto end; + } + + data.load_info = load_info; + data.tracee = tracee; + data.fd = fd; + + status = iterate_program_headers(tracee, fd, &load_info->elf_header, add_load_info, &data); +end: + if (fd >= 0) + close(fd); + + return status; +} + +/** + * Add @load_base to each adresses of @load_info. + */ +static void add_load_base(LoadInfo *load_info, word_t load_base) +{ + size_t nb_mappings; + size_t i; + + nb_mappings = talloc_array_length(load_info->mappings); + for (i = 0; i < nb_mappings; i++) + load_info->mappings[i].addr += load_base; + + if (IS_CLASS64(load_info->elf_header)) + load_info->elf_header.class64.e_entry += load_base; + else + load_info->elf_header.class32.e_entry += load_base; +} + +/** + * Compute the final load address for each position independant + * objects of @tracee. + * + * TODO: support for ASLR. + */ +static void compute_load_addresses(Tracee *tracee) +{ + if (IS_POSITION_INDENPENDANT(tracee->load_info->elf_header) + && tracee->load_info->mappings[0].addr == 0) { +#if defined(HAS_LOADER_32BIT) + if (IS_CLASS32(tracee->load_info->elf_header)) + add_load_base(tracee->load_info, EXEC_PIC_ADDRESS_32); + else +#endif + add_load_base(tracee->load_info, EXEC_PIC_ADDRESS); + } + + /* Nothing more to do? */ + if (tracee->load_info->interp == NULL) + return; + + if (IS_POSITION_INDENPENDANT(tracee->load_info->interp->elf_header) + && tracee->load_info->interp->mappings[0].addr == 0) { +#if defined(HAS_LOADER_32BIT) + if (IS_CLASS32(tracee->load_info->elf_header)) + add_load_base(tracee->load_info->interp, INTERP_PIC_ADDRESS_32); + else +#endif + add_load_base(tracee->load_info->interp, INTERP_PIC_ADDRESS); + } +} + +/** + * Expand in argv[] and envp[] the runner for @user_path, if needed. + * This function returns -errno if an error occurred, otherwise 0. On + * success, both @host_path and @user_path point to the program to + * execute (respectively from host and guest point-of-views), and both + * @tracee's argv[] (pointed to by SYSARG_2) @tracee's envp[] (pointed + * to by SYSARG_3) are correctly updated. + */ +static int expand_runner(Tracee* tracee, char host_path[PATH_MAX], char user_path[PATH_MAX]) +{ + ArrayOfXPointers *envp; + char *argv0; + int status; + + /* Execution of host programs when QEMU is in use relies on + * LD_ environment variables. */ + status = fetch_array_of_xpointers(tracee, &envp, SYSARG_3, 0); + if (status < 0) + return status; + + /* Environment variables should be compared with the "name" + * part of the "name=value" string format. */ + envp->compare_xpointee = (compare_xpointee_t) compare_xpointee_env; + + /* No need to adjust argv[] if it's a host binary (a.k.a + * mixed-mode). */ + if (!is_host_elf(tracee, host_path)) { + ArrayOfXPointers *argv; + size_t nb_qemu_args; + size_t i; + + if (getenv("PROOT_USE_LOADER_FOR_QEMU") == NULL) { + tracee->skip_proot_loader = true; + } + + status = fetch_array_of_xpointers(tracee, &argv, SYSARG_2, 0); + if (status < 0) + return status; + + status = read_xpointee_as_string(argv, 0, &argv0); + if (status < 0) + return status; + + /* Assuming PRoot was invoked this way: + * + * proot -q 'qemu-arm -cpu cortex-a9' ... + * + * a call to: + * + * execve("/bin/true", { "true", NULL }, ...) + * + * becomes: + * + * execve("/usr/bin/qemu", + * { "qemu", "-cpu", "cortex-a9", "-0", "true", "/bin/true", NULL }, ...) + */ + + nb_qemu_args = talloc_array_length(tracee->qemu) - 1; + status = resize_array_of_xpointers(argv, 1, nb_qemu_args + 2); + if (status < 0) + return status; + + for (i = 0; i < nb_qemu_args; i++) { + status = write_xpointee(argv, i, tracee->qemu[i]); + if (status < 0) + return status; + } + + status = write_xpointees(argv, i, 3, "-0", argv0, user_path); + if (status < 0) + return status; + + /* Ensure LD_ features should not be applied to QEMU + * iteself. */ + status = ldso_env_passthru(tracee, envp, argv, "-E", "-U", i); + if (status < 0) + return status; + + status = push_array_of_xpointers(argv, SYSARG_2); + if (status < 0) + return status; + + /* Launch the runner in lieu of the initial + * program. */ + assert(strlen(tracee->qemu[0]) + strlen(HOST_ROOTFS) < PATH_MAX); + assert(tracee->qemu[0][0] == '/'); + + strcpy(host_path, tracee->qemu[0]); + + if (tracee->skip_proot_loader) { + strcpy(user_path, host_path); + } else { + strcpy(user_path, HOST_ROOTFS); + strcat(user_path, host_path); + } + } + + /* Provide information to the host dynamic linker to find host + * libraries (remember the guest root file-system contains + * libraries for the guest architecture only). */ + status = rebuild_host_ldso_paths(tracee, host_path, envp); + if (status < 0) + return status; + + status = push_array_of_xpointers(envp, SYSARG_3); + if (status < 0) + return status; + + return 0; +} + +#if !defined(PROOT_UNBUNDLE_LOADER) +extern unsigned char _binary_loader_exe_start; +extern unsigned char _binary_loader_exe_end; + +extern unsigned char WEAK _binary_loader_m32_exe_start; +extern unsigned char WEAK _binary_loader_m32_exe_end; + +/** + * Extract the built-in loader. This function returns NULL if an + * error occurred, otherwise it returns the path to the extracted + * loader. Note: @tracee is only used for notification purpose. + */ +static char *extract_loader(const Tracee *tracee, bool wants_32bit_version) +{ + char path[PATH_MAX]; + size_t status2; + void *start; + size_t size; + int status; + int fd; + + char *loader_path = NULL; + FILE *file = NULL; + + file = open_temp_file(NULL, "prooted"); + if (file == NULL) + goto end; + fd = fileno(file); + + if (wants_32bit_version) { + start = (void *) &_binary_loader_m32_exe_start; + size = (size_t) (&_binary_loader_m32_exe_end - &_binary_loader_m32_exe_start); + } + else { + start = (void *) &_binary_loader_exe_start; + size = (size_t) (&_binary_loader_exe_end - &_binary_loader_exe_start); + } + + status2 = write(fd, start, size); + if (status2 != size) { + note(tracee, ERROR, SYSTEM, "can't write the loader"); + goto end; + } + + status = fchmod(fd, S_IRUSR|S_IXUSR); + if (status < 0) { + note(tracee, ERROR, SYSTEM, "can't change loader permissions (u+rx)"); + goto end; + } + + status = readlink_proc_pid_fd(getpid(), fd, path); + if (status < 0) { + note(tracee, ERROR, INTERNAL, "can't retrieve loader path (/proc/self/fd/)"); + goto end; + } + + status = access(path, X_OK); + if (status < 0) { + note(tracee, ERROR, INTERNAL, + "it seems the current temporary directory (%s) " + "is mounted with no execution permission.", + get_temp_directory()); + note(tracee, INFO, USER, + "Please set PROOT_TMP_DIR env. variable to an alternate " + "location ('%s/tmp' for example).", get_root(tracee)); + goto end; + } + + loader_path = talloc_strdup(talloc_autofree_context(), path); + if (loader_path == NULL) { + note(tracee, ERROR, INTERNAL, "can't allocate memory"); + goto end; + } + + if (tracee->verbose >= 2) + note(tracee, INFO, INTERNAL, "loader: %s", loader_path); + +end: + if (file != NULL) { + status = fclose(file); + if (status < 0) + note(tracee, WARNING, SYSTEM, "can't close loader file"); + } + + return loader_path; +} +#endif + +/** + * Get the path to the loader for the given @tracee. This function + * returns NULL if an error occurred. + */ +static inline const char *get_loader_path(const Tracee *tracee) +{ +#if defined(PROOT_UNBUNDLE_LOADER) +#if defined(HAS_LOADER_32BIT) + if (IS_CLASS32(tracee->load_info->elf_header)) { + return getenv("PROOT_LOADER_32") ?: "libloader32.so"; + } +#endif + return getenv("PROOT_LOADER") ?: "libloader.so"; +#else + static char *loader_path = NULL; + +#if defined(HAS_LOADER_32BIT) + static char *loader32_path = NULL; + + if (IS_CLASS32(tracee->load_info->elf_header)) { + loader32_path = loader32_path ?: getenv("PROOT_LOADER_32") ?: extract_loader(tracee, true); + return loader32_path; + } + else +#endif + { + loader_path = loader_path ?: getenv("PROOT_LOADER") ?: extract_loader(tracee, false); + return loader_path; + } +#endif +} + +/** + * Extract all the information that will be required by + * translate_load_*(). This function returns -errno if an error + * occured, otherwise 0. + */ +int translate_execve_enter(Tracee *tracee) +{ + char user_path[PATH_MAX]; + char host_path[PATH_MAX]; + char new_exe[PATH_MAX]; + char *raw_path; + const char *loader_path; + int status; + + if (IS_NOTIFICATION_PTRACED_LOAD_DONE(tracee)) { + /* Syscalls can now be reported to its ptracer. */ + tracee->as_ptracee.ignore_loader_syscalls = false; + + /* Cancel this spurious execve, it was only used as a + * notification. */ + set_sysnum(tracee, PR_void); + return 0; + } + + status = get_sysarg_path(tracee, user_path, SYSARG_1); + if (status < 0) + return status; + + /* Remember the user path before it is overwritten by + * expand_shebang(). This "raw" path is useful to fix the + * value of AT_EXECFN and /proc/{@tracee->pid}/comm. */ + raw_path = talloc_strdup(tracee->ctx, user_path); + if (raw_path == NULL) + return -ENOMEM; + + status = expand_shebang(tracee, host_path, user_path); + if (status < 0) + /* The Linux kernel actually returns -EACCES when + * trying to execute a directory. */ + return status == -EISDIR ? -EACCES : status; + + /* user_path is modified only if there's an interpreter + * (ie. for a script or with qemu). */ + if (status == 0 && tracee->qemu == NULL) + TALLOC_FREE(raw_path); + + /* Remember the new value for "/proc/self/exe". It points to + * a canonicalized guest path, hence detranslate_path() + * instead of using user_path directly. */ + talloc_unlink(tracee, tracee->host_exe); + tracee->host_exe = talloc_strdup(tracee, host_path); + + strcpy(new_exe, host_path); + status = detranslate_path(tracee, new_exe, NULL); + if (status >= 0) { + talloc_unlink(tracee, tracee->new_exe); + tracee->new_exe = talloc_strdup(tracee, new_exe); + } + else + tracee->new_exe = NULL; + + tracee->skip_proot_loader = false; + if (tracee->qemu != NULL) { + status = expand_runner(tracee, host_path, user_path); + if (status < 0) + return status; + } + + talloc_unlink(tracee, tracee->load_info); + + if (tracee->skip_proot_loader) { + tracee->load_info = NULL; + tracee->heap->disabled = true; + + status = set_sysarg_path(tracee, host_path, SYSARG_1); + if (status < 0) + return status; + + return 0; + } + + tracee->load_info = talloc_zero(tracee, LoadInfo); + if (tracee->load_info == NULL) + return -ENOMEM; + + tracee->load_info->host_path = talloc_strdup(tracee->load_info, host_path); + if (tracee->load_info->host_path == NULL) + return -ENOMEM; + + tracee->load_info->user_path = talloc_strdup(tracee->load_info, user_path); + if (tracee->load_info->user_path == NULL) + return -ENOMEM; + + tracee->load_info->raw_path = (raw_path != NULL + ? talloc_reparent(tracee->ctx, tracee->load_info, raw_path) + : talloc_reference(tracee->load_info, tracee->load_info->user_path)); + if (tracee->load_info->raw_path == NULL) + return -ENOMEM; + + status = extract_load_info(tracee, tracee->load_info); + if (status < 0) + return status; + + if (tracee->load_info->interp != NULL) { + status = extract_load_info(tracee, tracee->load_info->interp); + if (status < 0) + return status; + + /* An ELF interpreter is supposed to be + * standalone. */ + if (tracee->load_info->interp->interp != NULL) { + TALLOC_FREE(tracee->load_info->interp->interp); + // TODO: Print warning? + } + } + + compute_load_addresses(tracee); + + /* Execute the loader instead of the program. */ + loader_path = get_loader_path(tracee); + if (loader_path == NULL) + return -ENOENT; + + status = set_sysarg_path(tracee, loader_path, SYSARG_1); + if (status < 0) + return status; + + /* Mask to its ptracer syscalls performed by the loader. */ + tracee->as_ptracee.ignore_loader_syscalls = true; + + return 0; +} diff --git a/core/proot/src/main/cpp/execve/execve.h b/core/proot/src/main/cpp/execve/execve.h new file mode 100644 index 000000000..4a1a4094c --- /dev/null +++ b/core/proot/src/main/cpp/execve/execve.h @@ -0,0 +1,64 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#ifndef EXECVE_H +#define EXECVE_H + +#include /* PATH_MAX, */ + +#include "tracee/tracee.h" +#include "execve/elf.h" +#include "arch.h" + +extern int translate_execve_enter(Tracee *tracee); +extern void translate_execve_exit(Tracee *tracee); +extern int translate_and_check_exec(Tracee *tracee, char host_path[PATH_MAX], const char *user_path); + +typedef struct mapping { + word_t addr; + word_t length; + word_t clear_length; + word_t prot; + word_t flags; + word_t fd; + word_t offset; +} Mapping; + +typedef struct load_info { + char *host_path; + char *user_path; + char *raw_path; + Mapping *mappings; + ElfHeader elf_header; + bool needs_executable_stack; + + struct load_info *interp; +} LoadInfo; + +#define IS_NOTIFICATION_PTRACED_LOAD_DONE(tracee) ( \ + (tracee)->as_ptracee.ptracer != NULL \ + && peek_reg((tracee), ORIGINAL, SYSARG_1) == (word_t) 1 \ + && peek_reg((tracee), ORIGINAL, SYSARG_4) == (word_t) 2 \ + && peek_reg((tracee), ORIGINAL, SYSARG_5) == (word_t) 3 \ + && peek_reg((tracee), ORIGINAL, SYSARG_6) == (word_t) 4) + +#endif /* EXECVE_H */ diff --git a/core/proot/src/main/cpp/execve/exit.c b/core/proot/src/main/cpp/execve/exit.c new file mode 100644 index 000000000..348a1fd81 --- /dev/null +++ b/core/proot/src/main/cpp/execve/exit.c @@ -0,0 +1,513 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#include /* AT_*, */ +#include /* talloc*, */ +#include /* MAP_*, */ +#include /* assert(3), */ +#include /* strlen(3), strerror(3), */ +#include /* bzero(3), */ +#include /* kill(2), SIG*, */ +#include /* write(2), */ +#include /* E*, */ + +#include "execve/execve.h" +#include "execve/elf.h" +#include "loader/script.h" +#include "tracee/reg.h" +#include "tracee/abi.h" +#include "tracee/mem.h" +#include "syscall/sysnum.h" +#include "execve/auxv.h" +#include "path/binding.h" +#include "path/temp.h" +#include "cli/note.h" + + +/** + * Fill @path with the content of @vectors, formatted according to + * @ptracee's current ABI. + */ +static int fill_file_with_auxv(const Tracee *ptracee, const char *path, + const ElfAuxVector *vectors) +{ + const ssize_t current_sizeof_word = sizeof_word(ptracee); + ssize_t status; + int fd = -1; + int i; + + fd = open(path, O_WRONLY); + if (fd < 0) + return -1; + + i = 0; + do { + status = write(fd, &vectors[i].type, current_sizeof_word); + if (status < current_sizeof_word) { + status = -1; + goto end; + } + + status = write(fd, &vectors[i].value, current_sizeof_word); + if (status < current_sizeof_word) { + status = -1; + goto end; + } + } while (vectors[i++].type != AT_NULL); + + status = 0; +end: + if (fd >= 0) + (void) close(fd); + + return status; +} + +/** + * Bind content of @vectors over /proc/{@ptracee->pid}/auxv. This + * function returns -1 if an error occurred, otherwise 0. + */ +static int bind_proc_pid_auxv(const Tracee *ptracee) +{ + word_t vectors_address; + ElfAuxVector *vectors; + + const char *guest_path; + const char *host_path; + Binding *binding; + int status; + + vectors_address = get_elf_aux_vectors_address(ptracee); + if (vectors_address == 0) + return -1; + + vectors = fetch_elf_aux_vectors(ptracee, vectors_address); + if (vectors == NULL) + return -1; + + /* Path to these ELF auxiliary vectors. */ + guest_path = talloc_asprintf(ptracee->ctx, "/proc/%d/auxv", ptracee->pid); + if (guest_path == NULL) + return -1; + + /* Remove binding to this path, if any. It contains ELF + * auxiliary vectors of the previous execve(2). */ + binding = get_binding(ptracee, GUEST, guest_path); + if (binding != NULL && compare_paths(binding->guest.path, guest_path) == PATHS_ARE_EQUAL) { + remove_binding_from_all_lists(ptracee, binding); + TALLOC_FREE(binding); + } + + host_path = create_temp_file(ptracee->ctx, "auxv"); + if (host_path == NULL) + return -1; + + status = fill_file_with_auxv(ptracee, host_path, vectors); + if (status < 0) + return -1; + + /* Note: this binding will be removed once ptracee gets freed. */ + binding = insort_binding3(ptracee, ptracee->life_context, host_path, guest_path); + if (binding == NULL) + return -1; + + /* This temporary file (host_path) will be removed once the + * binding is freed. */ + talloc_reparent(ptracee->ctx, binding, host_path); + + return 0; +} + +/** + * Convert @mappings into load @script statements at the given @cursor + * position. This function returns the new cursor position. + */ +static void *transcript_mappings(void *cursor, const Mapping *mappings) +{ + size_t nb_mappings; + size_t i; + + nb_mappings = talloc_array_length(mappings); + for (i = 0; i < nb_mappings; i++) { + LoadStatement *statement = cursor; + + if ((mappings[i].flags & MAP_ANONYMOUS) != 0) + statement->action = LOAD_ACTION_MMAP_ANON; + else + statement->action = LOAD_ACTION_MMAP_FILE; + + statement->mmap.addr = mappings[i].addr; + statement->mmap.length = mappings[i].length; + statement->mmap.prot = mappings[i].prot; + statement->mmap.offset = mappings[i].offset; + statement->mmap.clear_length = mappings[i].clear_length; + + cursor += LOAD_STATEMENT_SIZE(*statement, mmap); + } + + return cursor; +} + +/** + * Convert @tracee->load_info into a load script, then transfer this + * latter into @tracee's memory. + */ +static int transfer_load_script(Tracee *tracee) +{ + const word_t stack_pointer = peek_reg(tracee, CURRENT, STACK_POINTER); + static word_t page_size = 0; + static word_t page_mask = 0; + + word_t entry_point; + + size_t script_size; + size_t strings_size; + size_t string1_size; + size_t string2_size; + size_t string3_size; + size_t padding_size; + + word_t string1_address; + word_t string2_address; + word_t string3_address; + + void *buffer; + size_t buffer_size; + + bool needs_executable_stack; + LoadStatement *statement; + void *cursor; + int status; + + if (page_size == 0) { + page_size = sysconf(_SC_PAGE_SIZE); + if ((int) page_size <= 0) + page_size = 0x1000; + page_mask = ~(page_size - 1); + } + + /* Capture argv[0]'s address from the initial stack as the correct + * AT_EXECFN value. The kernel sets AT_EXECFN to the loader temp file + * path; argv[0] holds the actual program name. This is stored so that + * prctl(PR_GET_AUXV) can be fixed up later in the syscall exit handler, + * since PR_GET_AUXV reads from kernel memory and bypasses the loader's + * in-memory auxv patch. */ + tracee->execfn_addr = peek_word(tracee, stack_pointer + sizeof_word(tracee)); + + needs_executable_stack = (tracee->load_info->needs_executable_stack + || ( tracee->load_info->interp != NULL + && tracee->load_info->interp->needs_executable_stack)); + + /* Strings addresses are required to generate the load script, + * for "open" actions. Since I want to generate it in one + * pass, these strings will be put right below the current + * stack pointer -- the only known adresses so far -- in the + * "strings area". */ + string1_size = strlen(tracee->load_info->user_path) + 1; + + string2_size = (tracee->load_info->interp == NULL ? 0 + : strlen(tracee->load_info->interp->user_path) + 1); + + string3_size = (tracee->load_info->raw_path == tracee->load_info->user_path ? 0 + : strlen(tracee->load_info->raw_path) + 1); + + /* A padding will be appended at the end of the load script + * (a.k.a "strings area") to ensure this latter is aligned to + * a word boundary, for the sake of performance + * (or 16 bytes, since AArch64 needs SP 16-bytes-aligned). */ + padding_size = (stack_pointer - string1_size - string2_size - string3_size) +#ifdef ARCH_ARM64 + % 16; +#else + % sizeof_word(tracee); +#endif + + strings_size = string1_size + string2_size + string3_size + padding_size; + string1_address = stack_pointer - strings_size; + string2_address = stack_pointer - strings_size + string1_size; + string3_address = (string3_size == 0 + ? string1_address + : stack_pointer - strings_size + string1_size + string2_size); + + /* Compute the size of the load script. */ + script_size = + LOAD_STATEMENT_SIZE(*statement, open) + + (LOAD_STATEMENT_SIZE(*statement, mmap) + * talloc_array_length(tracee->load_info->mappings)) + + (tracee->load_info->interp == NULL ? 0 + : LOAD_STATEMENT_SIZE(*statement, open) + + (LOAD_STATEMENT_SIZE(*statement, mmap) + * talloc_array_length(tracee->load_info->interp->mappings))) + + (needs_executable_stack ? LOAD_STATEMENT_SIZE(*statement, make_stack_exec) : 0) + + LOAD_STATEMENT_SIZE(*statement, start); + + /* Allocate enough room for both the load script and the + * strings area. */ + buffer_size = script_size + strings_size; + buffer = talloc_zero_size(tracee->ctx, buffer_size); + if (buffer == NULL) + return -ENOMEM; + + cursor = buffer; + + /* Load script statement: open. */ + statement = cursor; + statement->action = LOAD_ACTION_OPEN; + statement->open.string_address = string1_address; + + cursor += LOAD_STATEMENT_SIZE(*statement, open); + + /* Load script statements: mmap. */ + cursor = transcript_mappings(cursor, tracee->load_info->mappings); + + if (tracee->load_info->interp != NULL) { + /* Load script statement: open. */ + statement = cursor; + statement->action = LOAD_ACTION_OPEN_NEXT; + statement->open.string_address = string2_address; + + cursor += LOAD_STATEMENT_SIZE(*statement, open); + + /* Load script statements: mmap. */ + cursor = transcript_mappings(cursor, tracee->load_info->interp->mappings); + + entry_point = ELF_FIELD(tracee->load_info->interp->elf_header, entry); + } + else + entry_point = ELF_FIELD(tracee->load_info->elf_header, entry); + + if (needs_executable_stack) { + /* Load script statement: stack_exec. */ + statement = cursor; + + statement->action = LOAD_ACTION_MAKE_STACK_EXEC; + statement->make_stack_exec.start = stack_pointer & page_mask; + + cursor += LOAD_STATEMENT_SIZE(*statement, make_stack_exec); + } + + /* Load script statement: start. */ + statement = cursor; + + /* Start of the program slightly differs when ptraced. */ + if (tracee->as_ptracee.ptracer != NULL) + statement->action = LOAD_ACTION_START_TRACED; + else + statement->action = LOAD_ACTION_START; + + statement->start.stack_pointer = stack_pointer; + statement->start.entry_point = entry_point; + statement->start.at_phent = ELF_FIELD(tracee->load_info->elf_header, phentsize); + statement->start.at_phnum = ELF_FIELD(tracee->load_info->elf_header, phnum); + statement->start.at_entry = ELF_FIELD(tracee->load_info->elf_header, entry); + statement->start.at_phdr = ELF_FIELD(tracee->load_info->elf_header, phoff) + + tracee->load_info->mappings[0].addr; + statement->start.at_execfn = string3_address; + + cursor += LOAD_STATEMENT_SIZE(*statement, start); + + /* Sanity check. */ + assert((uintptr_t) cursor - (uintptr_t) buffer == script_size); + + /* Convert the load script to the expected format. */ + if (is_32on64_mode(tracee)) { + int i; + for (i = 0; buffer + i * sizeof(uint64_t) < cursor; i++) + ((uint32_t *) buffer)[i] = ((uint64_t *) buffer)[i]; + } + + /* Concatenate the load script and the strings. */ + memcpy(cursor, tracee->load_info->user_path, string1_size); + cursor += string1_size; + + if (string2_size != 0) { + memcpy(cursor, tracee->load_info->interp->user_path, string2_size); + cursor += string2_size; + } + + if (string3_size != 0) { + memcpy(cursor, tracee->load_info->raw_path, string3_size); + cursor += string3_size; + } + + /* Sanity check. */ + cursor += padding_size; + assert((uintptr_t) cursor - (uintptr_t) buffer == buffer_size); + + /* Allocate enough room in tracee's memory for the load + * script, and make the first user argument points to this + * location. Note that it is safe to update the stack pointer + * manually since we are in execve sysexit. However it should + * be done before transfering data since the kernel might not + * allow page faults below the stack pointer. */ + poke_reg(tracee, STACK_POINTER, stack_pointer - buffer_size); + poke_reg(tracee, USERARG_1, stack_pointer - buffer_size); + + /* Copy everything in the tracee's memory at once. */ + status = write_data(tracee, stack_pointer - buffer_size, buffer, buffer_size); + if (status < 0) + return status; + + /* Tracee's stack content is now as follow: + * + * +------------+ <- initial stack pointer (higher address) + * | padding | + * +------------+ + * | string3 | + * +------------+ + * | string2 | + * +------------+ + * | string1 | + * +------------+ + * | start | + * +------------+ + * | mmap anon | + * +------------+ + * | mmap file | + * +------------+ + * | open next | + * +------------+ + * | mmap anon. | + * +------------+ + * | mmap file | + * +------------+ + * | open | + * +------------+ <- stack pointer, userarg1 (word aligned) + */ + + /* Remember we are in the sysexit stage, so be sure the + * current register values will be used as-is at the end. */ + save_current_regs(tracee, ORIGINAL); + tracee->_regs_were_changed = true; + + return 0; +} + +/** + * Start the loading of @tracee. This function returns no error since + * it's either too late to do anything useful (the calling process is + * already replaced) or the error reported by the kernel + * (syscall_result < 0) will be propagated as-is. + */ +void translate_execve_exit(Tracee *tracee) +{ + word_t syscall_result; + int status; + + tracee->auxv_fd = -1; + + if (tracee->skip_proot_loader) { + tracee->restore_original_regs = false; + tracee->seen_execve = true; + return; + } + + if (IS_NOTIFICATION_PTRACED_LOAD_DONE(tracee)) { + /* Be sure not to confuse the ptracer with an + * unexpected syscall/returned value. */ + poke_reg(tracee, SYSARG_RESULT, 0); + set_sysnum(tracee, PR_execve); + + /* According to most ABIs, all registers have + * undefined values at program startup except: + * + * - the stack pointer + * - the instruction pointer + * - the rtld_fini pointer + * - the state flags + */ + poke_reg(tracee, STACK_POINTER, peek_reg(tracee, ORIGINAL, SYSARG_2)); + poke_reg(tracee, INSTR_POINTER, peek_reg(tracee, ORIGINAL, SYSARG_3)); + poke_reg(tracee, RTLD_FINI, 0); + poke_reg(tracee, STATE_FLAGS, 0); + +#if defined(ARCH_ARM_EABI) && defined(__thumb__) + /* Leave ARM thumb mode */ + tracee->_regs[CURRENT].ARM_cpsr &= ~PSR_T_BIT; +#endif + + /* Restore registers to their current values. */ + save_current_regs(tracee, ORIGINAL); + tracee->_regs_were_changed = true; + + /* This is is required to make GDB work correctly + * under PRoot, however it deserves to be used + * unconditionally. */ + (void) bind_proc_pid_auxv(tracee); + + /* If the PTRACE_O_TRACEEXEC option is *not* in effect + * for the execing tracee, the kernel delivers an + * extra SIGTRAP to the tracee after execve(2) + * *returns*. This is an ordinary signal (similar to + * one which can be generated by "kill -TRAP"), not a + * special kind of ptrace-stop. Employing + * PTRACE_GETSIGINFO for this signal returns si_code + * set to 0 (SI_USER). This signal may be blocked by + * signal mask, and thus may be delivered (much) + * later. -- man 2 ptrace + * + * This signal is delayed so far since the program was + * not fully loaded yet; GDB would get "invalid + * adress" errors otherwise. */ + if ((tracee->as_ptracee.options & PTRACE_O_TRACEEXEC) == 0) + kill(tracee->pid, SIGTRAP); + + return; + } + +#ifdef ARCH_ARM64 + tracee->is_aarch32 = IS_CLASS32(tracee->load_info->elf_header); +#endif + + syscall_result = peek_reg(tracee, CURRENT, SYSARG_RESULT); + if ((int) syscall_result < 0) + return; + + /* The guest program is now running; PR_SET_NO_NEW_PRIVS calls from + * here on belong to the guest, not to PRoot's own pre-execve setup. */ + tracee->seen_execve = true; + + /* Execve happened; commit the new "/proc/self/exe". */ + if (tracee->new_exe != NULL) { + (void) talloc_unlink(tracee, tracee->exe); + tracee->exe = talloc_reference(tracee, tracee->new_exe); + talloc_set_name_const(tracee->exe, "$exe"); + } + + /* New processes have no heap. */ + if (talloc_reference_count(tracee->heap) >= 1) { + talloc_unlink(tracee, tracee->heap); + tracee->heap = talloc_zero(tracee, Heap); + if (tracee->heap == NULL) + note(tracee, ERROR, INTERNAL, "can't alloc heap after execve"); + } else { + bzero(tracee->heap, sizeof(Heap)); + } + + /* Transfer the load script to the loader. */ + mem_prepare_after_execve(tracee); + status = transfer_load_script(tracee); + if (status < 0) + note(tracee, ERROR, INTERNAL, "can't transfer load script: %s", strerror(-status)); + + return; +} diff --git a/core/proot/src/main/cpp/execve/ldso.c b/core/proot/src/main/cpp/execve/ldso.c new file mode 100644 index 000000000..bcce6adb0 --- /dev/null +++ b/core/proot/src/main/cpp/execve/ldso.c @@ -0,0 +1,579 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#include /* bool, true, false, */ +#include /* strlen(3), strcpy(3), strcat(3), strcmp(3), */ +#include /* getenv(3), */ +#include /* assert(3), */ +#include /* ENOMEM, */ +#include /* close(2), */ +#include /* PATH_MAX, ARG_MAX, */ + +#include "execve/ldso.h" +#include "execve/elf.h" +#include "execve/aoxp.h" +#include "tracee/tracee.h" +#include "cli/note.h" + +/** + * Check if the environment @variable has the given @name. + */ +bool is_env_name(const char *variable, const char *name) +{ + size_t length = strlen(name); + + return (variable[0] == name[0] + && length < strlen(variable) + && variable[length] == '=' + && strncmp(variable, name, length) == 0); +} + +/** + * This function returns 1 or 0 depending on the equivalence of the + * @reference environment variable and the one pointed to by the entry + * in @envp at the given @index, otherwise it returns -errno when an + * error occured. + */ +int compare_xpointee_env(ArrayOfXPointers *envp, size_t index, const char *reference) +{ + char *value; + int status; + + assert(index < envp->length); + + status = read_xpointee_as_string(envp, index, &value); + if (status < 0) + return status; + + if (value == NULL) + return 0; + + return (int)is_env_name(value, reference); +} + +/** + * This function ensures that environment variables related to the + * dynamic linker are applied to the emulated program, not to QEMU + * itself. For instance, let's say the user has entered the + * command-line below: + * + * env LD_TRACE_LOADED_OBJECTS=1 /bin/ls + * + * It should be converted to: + * + * qemu -E LD_TRACE_LOADED_OBJECTS=1 /bin/ls + * + * instead of: + * + * env LD_TRACE_LOADED_OBJECTS=1 qemu /bin/ls + * + * Note that the LD_LIBRARY_PATH variable is always required to run + * QEMU (a host binary): + * + * env LD_LIBRARY_PATH=... qemu -U LD_LIBRARY_PATH /bin/ls + * + * or when LD_LIBRARY_PATH was also specified by the user: + * + * env LD_LIBRARY_PATH=... qemu -E LD_LIBRARY_PATH=... /bin/ls + * + * This funtion returns -errno if an error occured, otherwise 0. + */ +int ldso_env_passthru(const Tracee *tracee, ArrayOfXPointers *envp, ArrayOfXPointers *argv, + const char *define, const char *undefine, size_t offset) +{ + bool has_seen_library_path = false; + int status; + size_t i; + + for (i = 0; i < envp->length; i++) { + bool is_known = false; + char *env; + + status = read_xpointee_as_string(envp, i, &env); + if (status < 0) + return status; + + /* Skip variables that do not start with "LD_". */ + if (env == NULL || strncmp(env, "LD_", sizeof("LD_") - 1) != 0) + continue; + + /* When a host program executes a guest program, use + * the value of LD_LIBRARY_PATH as it was before being + * swapped by the mixed-mode support. */ + if ( tracee->host_ldso_paths != NULL + && tracee->guest_ldso_paths != NULL + && is_env_name(env, "LD_LIBRARY_PATH") + && strcmp(env, tracee->host_ldso_paths) == 0) + env = (char *) tracee->guest_ldso_paths; + +#define PASSTHRU(check, name) \ + if (is_env_name(env, name)) { \ + check |= true; \ + /* Errors are not fatal here. */ \ + status = resize_array_of_xpointers(argv, offset, 2); \ + if (status >= 0) { \ + status = write_xpointees(argv, offset, 2, define, env); \ + if (status < 0) \ + return status; \ + } \ + write_xpointee(envp, i, ""); \ + continue; \ + } \ + + PASSTHRU(has_seen_library_path, "LD_LIBRARY_PATH"); + PASSTHRU(is_known, "LD_PRELOAD"); + PASSTHRU(is_known, "LD_BIND_NOW"); + PASSTHRU(is_known, "LD_TRACE_LOADED_OBJECTS"); + PASSTHRU(is_known, "LD_AOUT_LIBRARY_PATH"); + PASSTHRU(is_known, "LD_AOUT_PRELOAD"); + PASSTHRU(is_known, "LD_AUDIT"); + PASSTHRU(is_known, "LD_BIND_NOT"); + PASSTHRU(is_known, "LD_DEBUG"); + PASSTHRU(is_known, "LD_DEBUG_OUTPUT"); + PASSTHRU(is_known, "LD_DYNAMIC_WEAK"); + PASSTHRU(is_known, "LD_HWCAP_MASK"); + PASSTHRU(is_known, "LD_KEEPDIR"); + PASSTHRU(is_known, "LD_NOWARN"); + PASSTHRU(is_known, "LD_ORIGIN_PATH"); + PASSTHRU(is_known, "LD_POINTER_GUARD"); + PASSTHRU(is_known, "LD_PROFILE"); + PASSTHRU(is_known, "LD_PROFILE_OUTPUT"); + PASSTHRU(is_known, "LD_SHOW_AUXV"); + PASSTHRU(is_known, "LD_USE_LOAD_BIAS"); + PASSTHRU(is_known, "LD_VERBOSE"); + PASSTHRU(is_known, "LD_WARN"); + } + + if (!has_seen_library_path) { + /* Errors are not fatal here. */ + status = resize_array_of_xpointers(argv, offset, 2); + if (status >= 0) { + status = write_xpointees(argv, offset, 2, undefine, "LD_LIBRARY_PATH"); + if (status < 0) + return status; + } + } + + return 0; +} + +/** + * Add to @host_ldso_paths the list of @paths prefixed with the path + * to the host rootfs. + */ +static int add_host_ldso_paths(char host_ldso_paths[ARG_MAX], const char *paths) +{ + char *cursor1; + const char *cursor2; + + cursor1 = host_ldso_paths + strlen(host_ldso_paths); + cursor2 = paths; + + do { + bool is_absolute; + size_t length1; + size_t length2 = strcspn(cursor2, ":"); + + is_absolute = (*cursor2 == '/'); + + length1 = 1 + length2; + if (is_absolute) + length1 += strlen(HOST_ROOTFS); + + /* Check there's enough room. */ + if (cursor1 + length1 >= host_ldso_paths + ARG_MAX) + return -ENOEXEC; + + if (cursor1 != host_ldso_paths) { + strcpy(cursor1, ":"); + cursor1++; + } + + /* Since we are executing a host binary under a + * QEMUlated environment, we have to access its + * library paths through the "host-rootfs" binding. + * Technically it means a path like "/lib" is accessed + * as "${HOST_ROOTFS}/lib" to avoid conflict with the + * guest "/lib". */ + if (is_absolute) { + strcpy(cursor1, HOST_ROOTFS); + cursor1 += strlen(HOST_ROOTFS); + } + + strncpy(cursor1, cursor2, length2); + cursor1 += length2; + + cursor2 += length2 + 1; + } while (*(cursor2 - 1) != '\0'); + + *cursor1 = '\0'; + + return 0; +} + +struct find_program_header_data { + ProgramHeader *program_header; + SegmentType type; + uint64_t address; +}; + +/** + * This function is a program header iterator. It stops the iteration + * (by returning 1) once it has found a program header that matches + * @data. This function returns -errno if an error occurred, + * otherwise 0 or 1. + */ +static int find_program_header(const ElfHeader *elf_header, + const ProgramHeader *program_header, void *data_) +{ + struct find_program_header_data *data = data_; + + if (PROGRAM_FIELD(*elf_header, *program_header, type) == data->type) { + uint64_t start; + uint64_t end; + + memcpy(data->program_header, program_header, sizeof(ProgramHeader)); + + if (data->address == (uint64_t) -1) + return 1; + + start = PROGRAM_FIELD(*elf_header, *program_header, vaddr); + end = start + PROGRAM_FIELD(*elf_header, *program_header, memsz); + + if (start < end + && data->address >= start + && data->address <= end) + return 1; + } + + return 0; +} + +/** + * Add to @xpaths the paths (':'-separated list) from the file + * referenced by @fd at the given @offset. This function returns + * -errno if an error occured, otherwise 0. + */ +static int add_xpaths(const Tracee *tracee, int fd, uint64_t offset, char **xpaths) +{ + char *paths = NULL; + char *tmp; + + size_t length; + size_t size; + int status; + + status = (int) lseek(fd, offset, SEEK_SET); + if (status < 0) + return -errno; + + /* Read the complete list of paths. */ + length = 0; + paths = NULL; + do { + size = length + 1024; + + tmp = talloc_realloc(tracee->ctx, paths, char, size); + if (!tmp) + return -ENOMEM; + paths = tmp; + + status = read(fd, paths + length, 1024); + if (status < 0) + return status; + + length += strnlen(paths + length, 1024); + } while (length == size); + + /* Concatene this list of paths to xpaths. */ + if (!*xpaths) { + *xpaths = talloc_array(tracee->ctx, char, length + 1); + if (!*xpaths) + return -ENOMEM; + + strcpy(*xpaths, paths); + } + else { + length += strlen(*xpaths); + length++; /* ":" separator */ + + tmp = talloc_realloc(tracee->ctx, *xpaths, char, length + 1); + if (!tmp) + return -ENOMEM; + *xpaths = tmp; + + strcat(*xpaths, ":"); + strcat(*xpaths, paths); + } + + /* I don't know if DT_R*PATH entries are unique. In + * doubt I support multiple entries. */ + return 0; +} + +/** + * Put the RPATH and RUNPATH dynamic entries from the file referenced + * by @fd -- which has the provided @elf_header -- in @rpaths and + * @runpaths respectively. This function returns -errno if an error + * occured, otherwise 0. + */ +static int read_ldso_rpaths(const Tracee* tracee, int fd, const ElfHeader *elf_header, + char **rpaths, char **runpaths) +{ + ProgramHeader dynamic_segment; + ProgramHeader strtab_segment; + struct find_program_header_data data; + uint64_t strtab_address = (uint64_t) -1; + off_t strtab_offset; + int status; + size_t i; + + uint64_t offsetof_dynamic_segment; + uint64_t sizeof_dynamic_segment; + size_t sizeof_dynamic_entry; + + data.program_header = &dynamic_segment; + data.type = PT_DYNAMIC; + data.address = (uint64_t) -1; + + status = iterate_program_headers(tracee, fd, elf_header, find_program_header, &data); + if (status <= 0) + return status; + + offsetof_dynamic_segment = PROGRAM_FIELD(*elf_header, dynamic_segment, offset); + sizeof_dynamic_segment = PROGRAM_FIELD(*elf_header, dynamic_segment, filesz); + + if (IS_CLASS32(*elf_header)) + sizeof_dynamic_entry = sizeof(DynamicEntry32); + else + sizeof_dynamic_entry = sizeof(DynamicEntry64); + + if (sizeof_dynamic_segment % sizeof_dynamic_entry != 0) + return -ENOEXEC; + +/** + * Invoke @embedded_code on each dynamic entry of the given @type. + */ +#define FOREACH_DYNAMIC_ENTRY(type, embedded_code) \ + for (i = 0; i < sizeof_dynamic_segment / sizeof_dynamic_entry; i++) { \ + DynamicEntry dynamic_entry; \ + uint64_t value; \ + \ + /* embedded_code may change the file offset. */ \ + status = (int) lseek(fd, offsetof_dynamic_segment + i * sizeof_dynamic_entry, \ + SEEK_SET); \ + if (status < 0) \ + return -errno; \ + \ + status = read(fd, &dynamic_entry, sizeof_dynamic_entry); \ + if (status < 0) \ + return status; \ + \ + if (DYNAMIC_FIELD(*elf_header, dynamic_entry, tag) != type) \ + continue; \ + \ + value = DYNAMIC_FIELD(*elf_header, dynamic_entry, val); \ + \ + embedded_code \ + } + + /* Get the address of the *first* string table. The ELF + * specification doesn't mention if it may have several string + * table references. */ + FOREACH_DYNAMIC_ENTRY(DT_STRTAB, { + strtab_address = value; + break; + }) + + if (strtab_address == (uint64_t) -1) + return 0; + + data.program_header = &strtab_segment; + data.type = PT_LOAD; + data.address = strtab_address; + + /* Search the program header that contains the given string table. */ + status = iterate_program_headers(tracee, fd, elf_header, find_program_header, &data); + if (status < 0) + return status; + + strtab_offset = PROGRAM_FIELD(*elf_header, strtab_segment, offset) + + (strtab_address - PROGRAM_FIELD(*elf_header, strtab_segment, vaddr)); + + FOREACH_DYNAMIC_ENTRY(DT_RPATH, { + if (strtab_offset < 0 || (uint64_t) strtab_offset > UINT64_MAX - value) + return -ENOEXEC; + + status = add_xpaths(tracee, fd, strtab_offset + value, rpaths); + if (status < 0) + return status; + }) + + FOREACH_DYNAMIC_ENTRY(DT_RUNPATH, { + if (strtab_offset < 0 || (uint64_t) strtab_offset > UINT64_MAX - value) + return -ENOEXEC; + + status = add_xpaths(tracee, fd, strtab_offset + value, runpaths); + if (status < 0) + return status; + }) + +#undef FOREACH_DYNAMIC_ENTRY + + return 0; +} + +/** + * Rebuild the variable LD_LIBRARY_PATH in @envp for the program + * @host_path according to its RPATH, RUNPATH, and the initial + * LD_LIBRARY_PATH. This function returns -errno if an error occured, + * 1 if RPATH/RUNPATH entries were found, 0 otherwise. + */ +int rebuild_host_ldso_paths(Tracee *tracee, const char host_path[PATH_MAX], ArrayOfXPointers *envp) +{ + static char *initial_ldso_paths = NULL; + ElfHeader elf_header; + + char host_ldso_paths[ARG_MAX] = ""; + bool rpath_found = false; + + char *rpaths = NULL; + char *runpaths = NULL; + + size_t length1; + size_t length2; + + size_t index; + int status; + int fd; + + fd = open_elf(host_path, &elf_header); + if (fd < 0) + return fd; + + status = read_ldso_rpaths(tracee, fd, &elf_header, &rpaths, &runpaths); + close(fd); + if (status < 0) + return status; + + /* 1. DT_RPATH */ + if (rpaths && !runpaths) { + status = add_host_ldso_paths(host_ldso_paths, rpaths); + if (status < 0) + return 0; /* Not fatal. */ + rpath_found = true; + } + + /* 2. LD_LIBRARY_PATH */ + if (initial_ldso_paths == NULL) + initial_ldso_paths = strdup(getenv("LD_LIBRARY_PATH") ?: "/"); + if (initial_ldso_paths != NULL && initial_ldso_paths[0] != '\0') { + status = add_host_ldso_paths(host_ldso_paths, initial_ldso_paths); + if (status < 0) + return 0; /* Not fatal. */ + } + + /* 3. DT_RUNPATH */ + if (runpaths) { + status = add_host_ldso_paths(host_ldso_paths, runpaths); + if (status < 0) + return 0; /* Not fatal. */ + rpath_found = true; + } + + /* 4. /etc/ld.so.cache NYI. */ + + /* 5. /lib[32|64], /usr/lib[32|64] + /usr/local/lib[32|64] */ + /* 6. /lib, /usr/lib + /usr/local/lib */ + if (IS_CLASS32(elf_header)) + status = add_host_ldso_paths(host_ldso_paths, +#if defined(ARCH_X86) || defined(ARCH_X86_64) + "/lib/i386-linux-gnu:/usr/lib/i386-linux-gnu:" +#endif + "/lib32:/usr/lib32:/usr/local/lib32" + ":/lib:/usr/lib:/usr/local/lib" +#ifdef __ANDROID__ + ":/system/lib" +#endif + ); + else + status = add_host_ldso_paths(host_ldso_paths, +#if defined(ARCH_X86_64) + "/lib/x86_64-linux-gnu:/usr/lib/x86_64-linux-gnu:" +#elif defined(ARCH_ARM64) + "/lib/aarch64-linux-gnu:/usr/lib/aarch64-linux-gnu:" +#endif + "/lib64:/usr/lib64:/usr/local/lib64" + ":/lib:/usr/lib:/usr/local/lib" +#ifdef __ANDROID__ + ":/system/lib64" +#endif + ); + if (status < 0) + return 0; /* Not fatal. */ + + status = find_xpointee(envp, "LD_LIBRARY_PATH"); + if (status < 0) + return 0; /* Not fatal. */ + index = (size_t) status; + + if (index == envp->length) { + /* Allocate a new entry at the end of envp[] when + * LD_LIBRARY_PATH was not found. */ + + index = (envp->length > 0 ? envp->length - 1 : 0); + status = resize_array_of_xpointers(envp, index, 1); + if (status < 0) + return 0; /* Not fatal. */ + } + else if (tracee->guest_ldso_paths == NULL) { + /* Remember guest LD_LIBRARY_PATH in order to restore + * it when a host program will execute a guest + * program. */ + char *env; + + /* Errors are not fatal here. */ + status = read_xpointee_as_string(envp, index, &env); + if (status >= 0) + tracee->guest_ldso_paths = talloc_strdup(tracee, env); + } + + /* Forge the new LD_LIBRARY_PATH variable from + * host_ldso_paths. */ + length1 = strlen("LD_LIBRARY_PATH="); + length2 = strlen(host_ldso_paths); + if (ARG_MAX - length2 - 1 < length1) + return 0; /* Not fatal. */ + + memmove(host_ldso_paths + length1, host_ldso_paths, length2 + 1); + memcpy(host_ldso_paths, "LD_LIBRARY_PATH=" , length1); + + write_xpointee(envp, index, host_ldso_paths); + + /* The guest LD_LIBRARY_PATH will be restored only if the host + * program didn't change it explicitly, so remember its + * initial value. */ + if (tracee->host_ldso_paths == NULL) + tracee->host_ldso_paths = talloc_strdup(tracee, host_ldso_paths); + + return (int) rpath_found; +} diff --git a/core/proot/src/main/cpp/execve/ldso.h b/core/proot/src/main/cpp/execve/ldso.h new file mode 100644 index 000000000..43782c17c --- /dev/null +++ b/core/proot/src/main/cpp/execve/ldso.h @@ -0,0 +1,42 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#ifndef LDSO_H +#define LDSO_H + +#include +#include + +#include "execve/aoxp.h" +#include "execve/elf.h" + +extern int ldso_env_passthru(const Tracee *tracee, ArrayOfXPointers *envp, ArrayOfXPointers *argv, + const char *define, const char *undefine, size_t offset); + +extern int rebuild_host_ldso_paths(Tracee *tracee, const char t_program[PATH_MAX], + ArrayOfXPointers *envp); + +extern int compare_xpointee_env(ArrayOfXPointers *envp, size_t index, const char *name); + +extern bool is_env_name(const char *variable, const char *name); + +#endif /* LDSO_H */ diff --git a/core/proot/src/main/cpp/execve/shebang.c b/core/proot/src/main/cpp/execve/shebang.c new file mode 100644 index 000000000..83ceeda1c --- /dev/null +++ b/core/proot/src/main/cpp/execve/shebang.c @@ -0,0 +1,307 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#include /* open(2), */ +#include /* open(2), */ +#include /* open(2), */ +#include /* PATH_MAX, */ +#include /* BINPRM_BUF_SIZE, */ +#include /* read(2), close(2), */ +#include /* -E*, */ +#include /* MAXSYMLINKS, */ +#include /* bool, */ +#include /* assert(3), */ + +#include "execve/shebang.h" +#include "execve/execve.h" +#include "execve/aoxp.h" +#include "tracee/tracee.h" +#include "attribute.h" + +/** + * Extract into @user_path and @argument the shebang from @host_path. + * This function returns -errno if an error occured, 1 if a shebang + * was found and extracted, otherwise 0. + * + * Extract from "man 2 execve": + * + * On Linux, the entire string following the interpreter name is + * passed as a *single* argument to the interpreter, and this + * string can include white space. + */ +static int extract_shebang(const Tracee *tracee UNUSED, const char *host_path, + char user_path[PATH_MAX], char argument[BINPRM_BUF_SIZE]) +{ + char tmp2[2]; + char tmp; + + size_t current_length; + size_t i; + + int status; + int fd; + + /* Assumption. */ + assert(BINPRM_BUF_SIZE < PATH_MAX); + + argument[0] = '\0'; + + /* Inspect the executable. */ + fd = open(host_path, O_RDONLY); + if (fd < 0) + return -errno; + + status = read(fd, tmp2, 2 * sizeof(char)); + if (status < 0) { + status = -errno; + goto end; + } + if ((size_t) status < 2 * sizeof(char)) { /* EOF */ + status = 0; + goto end; + } + + /* Check if it really is a script text. */ + if (tmp2[0] != '#' || tmp2[1] != '!') { + status = 0; + goto end; + } + current_length = 2; + user_path[0] = '\0'; + + /* Skip leading spaces. */ + do { + status = read(fd, &tmp, sizeof(char)); + if (status < 0) { + status = -errno; + goto end; + } + if ((size_t) status < sizeof(char)) { /* EOF */ + status = -ENOEXEC; + goto end; + } + + current_length++; + } while ((tmp == ' ' || tmp == '\t') && current_length < BINPRM_BUF_SIZE); + + /* Slurp the interpreter path until the first space or end-of-line. */ + for (i = 0; current_length < BINPRM_BUF_SIZE; current_length++, i++) { + switch (tmp) { + case ' ': + case '\t': + /* Remove spaces in between the interpreter + * and the hypothetical argument. */ + user_path[i] = '\0'; + break; + + case '\n': + case '\r': + /* There is no argument. */ + user_path[i] = '\0'; + argument[0] = '\0'; + status = 1; + goto end; + + default: + /* There is an argument if the previous + * character in user_path[] is '\0'. */ + if (i > 1 && user_path[i - 1] == '\0') + goto argument; + else + user_path[i] = tmp; + break; + } + + status = read(fd, &tmp, sizeof(char)); + if (status < 0) { + status = -errno; + goto end; + } + if ((size_t) status < sizeof(char)) { /* EOF */ + user_path[i] = '\0'; + argument[0] = '\0'; + status = 1; + goto end; + } + } + + /* The interpreter path is too long, truncate it. */ + user_path[i] = '\0'; + argument[0] = '\0'; + status = 1; + goto end; + +argument: + + /* Slurp the argument until the end-of-line. */ + for (i = 0; current_length < BINPRM_BUF_SIZE; current_length++, i++) { + switch (tmp) { + case '\n': + case '\r': + argument[i] = '\0'; + + /* Remove trailing spaces. */ + for (i--; i > 0 && (argument[i] == ' ' || argument[i] == '\t'); i--) + argument[i] = '\0'; + + status = 1; + goto end; + + default: + argument[i] = tmp; + break; + } + + status = read(fd, &tmp, sizeof(char)); + if (status < 0) { + status = -errno; + goto end; + } + if ((size_t) status < sizeof(char)) { /* EOF */ + argument[0] = '\0'; + status = 1; + goto end; + } + } + + /* The argument is too long, truncate it. */ + argument[i] = '\0'; + status = 1; + +end: + close(fd); + + /* Did an error occur or isn't a script? */ + if (status <= 0) + return status; + + return 1; +} + +/** + * Expand in argv[] the shebang of @user_path, if any. This function + * returns -errno if an error occurred, 1 if a shebang was found and + * extracted, otherwise 0. On success, both @host_path and @user_path + * point to the program to execute (respectively from host + * point-of-view and as-is), and @tracee's argv[] (pointed to by + * SYSARG_2) is correctly updated. + */ +int expand_shebang(Tracee *tracee, char host_path[PATH_MAX], char user_path[PATH_MAX]) +{ + ArrayOfXPointers *argv = NULL; + bool has_shebang = false; + + char argument[BINPRM_BUF_SIZE]; + int status; + size_t i; + + /* "The interpreter must be a valid pathname for an executable + * which is not itself a script [1]. If the filename + * argument of execve() specifies an interpreter script, then + * interpreter will be invoked with the following arguments: + * + * interpreter [optional-arg] filename arg... + * + * where arg... is the series of words pointed to by the argv + * argument of execve()." -- man 2 execve + * + * [1]: as of this writing (3.10.17) this is true only for the + * ELF interpreter; ie. a script can use a script as + * interpreter. + */ + for (i = 0; i < MAXSYMLINKS; i++) { + char *old_user_path; + + /* Translate this path (user -> host), then check it is executable. */ + status = translate_and_check_exec(tracee, host_path, user_path); + if (status < 0) + return status; + + /* Remember the initial user path. */ + old_user_path = talloc_strdup(tracee->ctx, user_path); + if (old_user_path == NULL) + return -ENOMEM; + + /* Extract into user_path and argument the shebang from host_path. */ + status = extract_shebang(tracee, host_path, user_path, argument); + if (status < 0) + return status; + + /* No more shebang. */ + if (status == 0) + break; + has_shebang = true; + + /* Translate new path (user -> host), then check it is executable. */ + status = translate_and_check_exec(tracee, host_path, user_path); + if (status < 0) + return status; + + /* Fetch argv[] only on demand. */ + if (argv == NULL) { + status = fetch_array_of_xpointers(tracee, &argv, SYSARG_2, 0); + if (status < 0) + return status; + } + + /* Assuming the shebang of "script" is "#!/bin/sh -x", + * a call to: + * + * execve("./script", { "script.sh", NULL }, ...) + * + * becomes: + * + * execve("/bin/sh", { "/bin/sh", "-x", "./script", NULL }, ...) + * + * See commit 8c8fbe85 about "argv->length == 1". */ + if (argument[0] != '\0') { + status = resize_array_of_xpointers(argv, 0, 2 + (argv->length == 1)); + if (status < 0) + return status; + + status = write_xpointees(argv, 0, 3, user_path, argument, old_user_path); + if (status < 0) + return status; + } + else { + status = resize_array_of_xpointers(argv, 0, 1 + (argv->length == 1)); + if (status < 0) + return status; + + status = write_xpointees(argv, 0, 2, user_path, old_user_path); + if (status < 0) + return status; + } + } + + if (i == MAXSYMLINKS) + return -ELOOP; + + /* Push argv[] only on demand. */ + if (argv != NULL) { + status = push_array_of_xpointers(argv, SYSARG_2); + if (status < 0) + return status; + } + + return (has_shebang ? 1 : 0); +} diff --git a/core/proot/src/main/cpp/execve/shebang.h b/core/proot/src/main/cpp/execve/shebang.h new file mode 100644 index 000000000..1ae63be1b --- /dev/null +++ b/core/proot/src/main/cpp/execve/shebang.h @@ -0,0 +1,32 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#ifndef SHEBANG_H +#define SHEBANG_H + +#include /* PATH_MAX, ARG_MAX, */ + +#include "tracee/tracee.h" + +extern int expand_shebang(Tracee *tracee, char host_path[PATH_MAX], char user_path[PATH_MAX]); + +#endif /* SHEBANG_H */ diff --git a/core/proot/src/main/cpp/extension/ashmem_memfd/ashmem_memfd.c b/core/proot/src/main/cpp/extension/ashmem_memfd/ashmem_memfd.c new file mode 100644 index 000000000..632e5e6d2 --- /dev/null +++ b/core/proot/src/main/cpp/extension/ashmem_memfd/ashmem_memfd.c @@ -0,0 +1,239 @@ +#include +#include +#include +#include +#include /* __NR_memfd_create, */ +#include /* ASHMEM_GET_SIZE, */ +#include /* MFD_CLOEXEC */ + +#include + +#include "extension/extension.h" +#include "path/path.h" +#include "tracee/mem.h" +#include "tracee/seccomp.h" +#include "syscall/chain.h" +#include "syscall/syscall.h" /* set_sysarg_data, */ + +enum AshmemMemfdChainState { + CS_IDLE, + CS_STAT_ENTERED, + CS_STAT_CHAINED_IOCTL +}; + +typedef struct { + bool memfd_supported; + enum AshmemMemfdChainState chain_state; + int fd; + word_t addr; +} AshmemMemfdState; + +static FilteredSysnum filtered_sysnums[] = { + { PR_memfd_create, 0 }, + { PR_ftruncate, 0 }, + { PR_fstat, 0 }, + FILTERED_SYSNUM_END, +}; + +static int detect_memfd_support() { + const char *assume_unsupported = getenv("PROOT_ASSUME_MEMFD_UNSUPPORTED"); + if (assume_unsupported != NULL) { + if (0 == strcmp(assume_unsupported, "1")) { + return 0; + } + if (0 == strcmp(assume_unsupported, "0")) { + return 1; + } + } + + int reply_pipe[2]; + int status = pipe(reply_pipe); + if (status < 0) { + return -1; + } + + status = fork(); + if (status < 0) { + close(reply_pipe[0]); + close(reply_pipe[1]); + return -1; + } + + if (status == 0) { + /** Child process. Close readable end of pipe. */ + close(reply_pipe[0]); + + /** Attempt creating memfd. */ + signal(SIGSYS, SIG_DFL); + int memfd = syscall(__NR_memfd_create, "support_probe", 0); + + /** Send message to parent on success. */ + if (memfd >= 0) { + write(reply_pipe[1], "\x01", 1); + close(memfd); + } + close(reply_pipe[1]); + _exit(0); + } + + /** Parent process. */ + close(reply_pipe[1]); + char reply_value = 0; + read(reply_pipe[0], &reply_value, 1); + close(reply_pipe[0]); + return reply_value == 1; +} + +static bool is_ashmem_fd(Tracee *tracee, int fd) { + char path[PATH_MAX] = {}; + if (readlink_proc_pid_fd(tracee->pid, fd, path) < 0) { + return false; + } + return 0 == strcmp(path, "/dev/ashmem"); +} + +static void ashmem_memfd_handle_stat(Extension *extension, Tracee *tracee, int fd, Reg stat_reg) { + if (is_ashmem_fd(tracee, fd)) { + AshmemMemfdState *state = talloc_get_type_abort(extension->config, AshmemMemfdState); + state->chain_state = CS_STAT_ENTERED; + state->fd = fd; + state->addr = peek_reg(tracee, CURRENT, stat_reg) + offsetof(struct stat, st_size); + tracee->restart_how = PTRACE_SYSCALL; + } +} + +static int ashmem_memfd_handle_memfd_create(Extension *extension, Tracee *tracee, bool from_sigsys) { + AshmemMemfdState *state = talloc_get_type_abort(extension->config, AshmemMemfdState); + if (!state->memfd_supported) { + word_t flags = peek_reg(tracee, CURRENT, SYSARG_2); + set_sysnum(tracee, PR_openat); + set_sysarg_data(tracee, "/dev/ashmem", 12, SYSARG_2); + poke_reg(tracee, SYSARG_1, AT_FDCWD); + poke_reg(tracee, SYSARG_3, O_RDWR | ((flags & MFD_CLOEXEC) ? O_CLOEXEC : 0)); + poke_reg(tracee, SYSARG_4, 0); + if (from_sigsys) { + restart_syscall_after_seccomp(tracee); + /* Skip further processing (such as forcing syscall result) from SIGSYS handler. */ + return 2; + } + } + return 0; +} + +static void ashmem_memfd_handle_syscall(Extension *extension) { + Tracee *tracee = TRACEE(extension); + switch (get_sysnum(tracee, CURRENT)) { + case PR_memfd_create: + { + ashmem_memfd_handle_memfd_create(extension, tracee, false); + break; + } + case PR_ftruncate: + case PR_ftruncate64: + { + int fd = peek_reg(tracee, CURRENT, SYSARG_1); + if (is_ashmem_fd(tracee, fd)) { + set_sysnum(tracee, PR_ioctl); + poke_reg(tracee, SYSARG_3, peek_reg(tracee, CURRENT, SYSARG_2)); + poke_reg(tracee, SYSARG_2, ASHMEM_SET_SIZE); + } + } + case PR_fstat: + ashmem_memfd_handle_stat(extension, tracee, peek_reg(tracee, CURRENT, SYSARG_1), SYSARG_2); + break; + case PR_fstatat64: + { + int fd = peek_reg(tracee, CURRENT, SYSARG_1); + if ( + fd >= 0 && + /** Is path argument an empty string? */ + !(peek_word(tracee, peek_reg(tracee, CURRENT, SYSARG_2)) & 0xFF) + ) { + ashmem_memfd_handle_stat(extension, tracee, fd, SYSARG_3); + } + break; + } + default: + break; + } +} + +static void ashmem_memfd_handle_stat_exit(Tracee *tracee, AshmemMemfdState *state) { + if (peek_reg(tracee, CURRENT, SYSARG_RESULT) || peek_word(tracee, state->addr)) { + state->chain_state = CS_IDLE; + return; + } + + register_chained_syscall(tracee, PR_ioctl, state->fd, ASHMEM_GET_SIZE, 0, 0, 0, 0); + state->chain_state = CS_STAT_CHAINED_IOCTL; +} + +int ashmem_memfd_callback(Extension *extension, ExtensionEvent event, intptr_t data1, intptr_t data2) +{ + switch (event) { + case INITIALIZATION: { + extension->config = talloc_zero(extension, AshmemMemfdState); + AshmemMemfdState *state = talloc_get_type_abort(extension->config, AshmemMemfdState); + + memset(state, 0, sizeof(*state)); + state->memfd_supported = detect_memfd_support(); + + extension->filtered_sysnums = filtered_sysnums; + + return 0; + } + case INHERIT_PARENT: /* Inheritable for sub reconfiguration ... */ + return 1; + + case INHERIT_CHILD: { + /* Create configuration in child. */ + Extension *parent = (Extension *) data1; + extension->config = talloc_zero(extension, AshmemMemfdState); + if (extension->config == NULL) + return -1; + + AshmemMemfdState *old_state = talloc_get_type_abort(parent->config, AshmemMemfdState); + AshmemMemfdState *state = talloc_get_type_abort(extension->config, AshmemMemfdState); + state->memfd_supported = old_state->memfd_supported; + } + + case SYSCALL_ENTER_END: + ashmem_memfd_handle_syscall(extension); + return 0; + + case SYSCALL_EXIT_START: + { + AshmemMemfdState *state = talloc_get_type_abort(extension->config, AshmemMemfdState); + switch (state->chain_state) { + case CS_IDLE: + case CS_STAT_CHAINED_IOCTL: + break; + case CS_STAT_ENTERED: + ashmem_memfd_handle_stat_exit(TRACEE(extension), state); + break; + } + return 0; + } + case SIGSYS_OCC: + { + Tracee *tracee = TRACEE(extension); + if (get_sysnum(tracee, CURRENT) == PR_memfd_create) { + return ashmem_memfd_handle_memfd_create(extension, tracee, true); + } + return 0; + } + case SYSCALL_CHAINED_EXIT: + { + AshmemMemfdState *state = talloc_get_type_abort(extension->config, AshmemMemfdState); + if (state->chain_state == CS_STAT_CHAINED_IOCTL) { + state->chain_state = CS_IDLE; + Tracee *tracee = TRACEE(extension); + poke_uint32(tracee, state->addr, peek_reg(tracee, CURRENT, SYSARG_RESULT)); + poke_reg(tracee, SYSARG_RESULT, 0); + } + return 0; + } + default: + return 0; + } +} diff --git a/core/proot/src/main/cpp/extension/extension.c b/core/proot/src/main/cpp/extension/extension.c new file mode 100644 index 000000000..1e52ee2e0 --- /dev/null +++ b/core/proot/src/main/cpp/extension/extension.c @@ -0,0 +1,169 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#include /* assert(3), */ +#include /* talloc_*, */ +#include /* LIST_*, */ +#include /* bzero(3), */ + +#include "extension/extension.h" +#include "cli/note.h" +#include "build.h" + +#include "compat.h" + +/** + * Remove an @extension from its tracee's list, then send it the + * "REMOVED" event. + * + * Note: this is a Talloc destructor. + */ +static int remove_extension(Extension *extension) +{ + LIST_REMOVE(extension, link); + extension->callback(extension, REMOVED, 0, 0); + + bzero(extension, sizeof(Extension)); + return 0; +} + +/** + * Allocate a new extension for the given @callback then attach it to + * its @tracee. This function returns NULL on error, otherwise the + * new extension. + */ +static Extension *new_extension(Tracee *tracee, extension_callback_t callback) +{ + Extension *extension; + + /* Lazy allocation of the list head. */ + if (tracee->extensions == NULL) { + tracee->extensions = talloc_zero(tracee, Extensions); + if (tracee->extensions == NULL) + return NULL; + } + + /* Allocate a new extension. */ + extension = talloc_zero(tracee->extensions, Extension); + if (extension == NULL) + return NULL; + extension->callback = callback; + + /* Attach it to its tracee. */ + LIST_INSERT_HEAD(tracee->extensions, extension, link); + talloc_set_destructor(extension, remove_extension); + + return extension; +} + +/** + * Retrieve from @tracee->extensions the extension for the given + * @callback. + */ +Extension *get_extension(Tracee *tracee, extension_callback_t callback) +{ + Extension *extension; + + if (tracee->extensions == NULL) + return NULL; + + LIST_FOREACH(extension, tracee->extensions, link) { + if (extension->callback == callback) + return extension; + } + + return NULL; +} + +/** + * Initialize a new extension for the given @callback then attach it + * to its @tracee. The parameter @cli is its argument that was passed + * to the command-line interface. This function return -1 if an error + * occurred, otherwise 0. + */ +int initialize_extension(Tracee *tracee, extension_callback_t callback, const char *cli) +{ + Extension *extension; + int status; + + extension = new_extension(tracee, callback); + if (extension == NULL) { + note(tracee, WARNING, INTERNAL, "can't create a new extension"); + return -1; + } + + /* Remove the new extension if its initialized has failed. */ + status = extension->callback(extension, INITIALIZATION, (intptr_t) cli, 0); + if (status < 0) { + TALLOC_FREE(extension); + return status; + } + + return 0; +} + +/** + * Rebuild a new list of extensions for this @child from its @parent. + * The inheritance model is controlled by the @parent. + */ +void inherit_extensions(Tracee *child, Tracee *parent, word_t clone_flags) +{ + Extension *parent_extension; + Extension *child_extension; + int status; + + if (parent->extensions == NULL) + return; + + /* Sanity check. */ + assert(child->extensions == NULL || clone_flags == CLONE_RECONF); + + LIST_FOREACH(parent_extension, parent->extensions, link) { + /* Ask the parent how this extension is + * inheritable. */ + status = parent_extension->callback(parent_extension, INHERIT_PARENT, + (intptr_t)child, clone_flags); + + /* Not inheritable. */ + if (status < 0) + continue; + + /* Inheritable... */ + child_extension = new_extension(child, parent_extension->callback); + if (child_extension == NULL) { + note(parent, WARNING, INTERNAL, + "can't create a new extension for pid %d", child->pid); + continue; + } + + if (status == 0) { + /* ... with a shared config or ... */ + child_extension->config = + talloc_reference(child_extension, parent_extension->config); + } + else { + /* ... with another inheritance model. */ + child_extension->callback(child_extension, INHERIT_CHILD, + (intptr_t)parent_extension, clone_flags); + } + } +} diff --git a/core/proot/src/main/cpp/extension/extension.h b/core/proot/src/main/cpp/extension/extension.h new file mode 100644 index 000000000..24409b9e5 --- /dev/null +++ b/core/proot/src/main/cpp/extension/extension.h @@ -0,0 +1,211 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#ifndef EXTENSION_H +#define EXTENSION_H + +#include /* LIST_, */ +#include /* intptr_t, */ + +#include "tracee/tracee.h" +#include "syscall/seccomp.h" + +/* List of possible events. */ +typedef enum { + /* A guest path passed as an argument of the current syscall + * is about to be translated: "(char *) data1" is the base for + * "(char *) data2" -- the guest path -- if this latter is + * relative. If the extension returns > 0, then PRoot skips + * its own handling. If the extension returns < 0, then PRoot + * reports this errno as-is. */ + GUEST_PATH, + + /* A canonicalized host path is being accessed during the + * translation of a guest path: "(char *) data1" is the + * canonicalized host path and "(bool) data2" is true if it is + * the last iteration. Note that several host paths are accessed + * for a given guest path since PRoot has to walk along all + * parent directories and symlinks in order to translate it. + * If the extension returns < 0, then PRoot reports this errno + * as-is. */ + HOST_PATH, + + /* The canonicalization succeed: "(char *) data1" is the + * translated path from the host point-of-view. It can be + * substituted by the extension. If the extension returns < + * 0, then PRoot reports this errno as-is. */ + TRANSLATED_PATH, + + /* The tracee enters a syscall, and PRoot hasn't do anything + * yet. If the extension returns > 0, then PRoot skips its + * own handling. If the extension returns < 0, then PRoot + * cancels the syscall and reports this errno to the + * tracee. */ + SYSCALL_ENTER_START, + + /* The tracee enters a syscall, and PRoot has already handled + * it: "(int) data1" is the current status, it is < 0 when + * something went wrong. If the extension returns < 0, then + * PRoot cancels the syscall and reports this errno to the + * tracee. */ + SYSCALL_ENTER_END, + + /* The tracee exits a syscall, and PRoot hasn't do anything + * yet. If the extension returns > 0, then PRoot skips its + * own handling. If the extension returns < 0, then PRoot + * reports this errno to the tracee. */ + SYSCALL_EXIT_START, + + /* The tracee exits a syscall, and PRoot has already handled + * it. If the extension returns < 0, then PRoot reports this + * errno to the tracee. */ + SYSCALL_EXIT_END, + + /* The tracee is stopped either because of a syscall or a + * signal: "(int) data1" is its new status as reported by + * waitpid(2). If the extension returns != 0, then PRoot + * skips its own handling. */ + NEW_STATUS, + + /* Ask how this extension is inheritable: "(Tracee *) data1" + * is the child tracee and "(bool) data2" is the clone(2) + * flags (CLONE_RECONF for sub-reconfiguration). The meaning + * of the returned value is: + * + * < 0 : not inheritable + * == 0 : inheritable + shared configuration. + * > 0 : inheritable + call INHERIT_CHILD. */ + INHERIT_PARENT, + + /* Control the inheritance: "(Extension *) data1" is the + * extension of the parent and "(word_t) data2" is the clone(2) + * flags (CLONE_RECONF for sub-reconfiguration). For instance + * the extension for the child could use a configuration + * different from the parent's configuration. */ + INHERIT_CHILD, + + /* The tracee enters a "chained" syscall, that is, an + * unrequested syscall inserted by PRoot after an actual + * syscall. If the extension returns < 0, then PRoot cancels + * the syscall and reports this errno to the tracee. */ + SYSCALL_CHAINED_ENTER, + + /* The tracee exists a "chained" syscall, that is, an + * unrequested syscall inserted by PRoot after an actual + * syscall. */ + SYSCALL_CHAINED_EXIT, + + /* Initialize the extension: "(const char *) data1" is its + * argument that was passed to the command-line interface. If + * the extension returns < 0, then PRoot removed it. */ + INITIALIZATION, + + /* The extension is not attached to its tracee anymore + * (destructor). */ + REMOVED, + + /* Print the current configuration of the extension. See + * print_config() as an example. */ + PRINT_CONFIG, + + /* Print the usage of the extension: "(bool) data1" is true + * for a detailed usage. See print_usage() as an example. */ + PRINT_USAGE, + + /* A SIGSYS has occurred and we are going to see if any of the extensions wants to handle it for us*/ + SIGSYS_OCC, + + /* link2symlink notifies other extensions when it is moving + * a file */ + LINK2SYMLINK_RENAME, + + /* link2symlink notifies other extensions when it is unlinking + * a file */ + LINK2SYMLINK_UNLINK, + + /* statx() syscall was used by tracee and is being replaced by proot + * data1 argument contains pointer to statx_syscall_state struct + * defined in tracee/statx.h + * */ + STATX_SYSCALL, +} ExtensionEvent; + +#define CLONE_RECONF ((word_t) -1) + +struct extension; +typedef int (*extension_callback_t)(struct extension *extension, ExtensionEvent event, + intptr_t data1, intptr_t data2); + +typedef struct extension { + /* Function to be called when any event occured. */ + extension_callback_t callback; + + /* A chunk of memory allocated by any talloc functions. + * Mainly useful to store a configuration. */ + TALLOC_CTX *config; + + /* List of sysnum handled by this extension. */ + const FilteredSysnum *filtered_sysnums; + + /* Link to the next and previous extensions. Note the order + * is *never* garantee. */ + LIST_ENTRY(extension) link; +} Extension; + +typedef LIST_HEAD(extensions, extension) Extensions; + +extern int initialize_extension(Tracee *tracee, extension_callback_t callback, const char *cli); +extern void inherit_extensions(Tracee *child, Tracee *parent, word_t clone_flags); +extern Extension *get_extension(Tracee *tracee, extension_callback_t callback); + +/** + * Notify all extensions of @tracee that the given @event occured. + * See ExtensionEvent for the meaning of @data1 and @data2. + */ +static inline int notify_extensions(Tracee *tracee, ExtensionEvent event, + intptr_t data1, intptr_t data2) +{ + Extension *extension; + + if (tracee->extensions == NULL) + return 0; + + LIST_FOREACH(extension, tracee->extensions, link) { + int status = extension->callback(extension, event, data1, data2); + if (status != 0) + return status; + } + + return 0; +} + +/* Built-in extensions. */ +extern int kompat_callback(Extension *extension, ExtensionEvent event, intptr_t d1, intptr_t d2); +extern int fake_id0_callback(Extension *extension, ExtensionEvent event, intptr_t d1, intptr_t d2); +extern int hidden_files_callback(Extension *extension, ExtensionEvent event, intptr_t d1, intptr_t d2); +extern int port_switch_callback(Extension *extension, ExtensionEvent event, intptr_t d1, intptr_t d2); +extern int link2symlink_callback(Extension *extension, ExtensionEvent event, intptr_t d1, intptr_t d2); +extern int fix_symlink_size_callback(Extension *extension, ExtensionEvent event, intptr_t d1, intptr_t d2); +extern int ashmem_memfd_callback(Extension *extension, ExtensionEvent event, intptr_t d1, intptr_t d2); +extern int mountinfo_callback(Extension *extension, ExtensionEvent event, intptr_t d1, intptr_t d2); + +#endif /* EXTENSION_H */ diff --git a/core/proot/src/main/cpp/extension/fake_id0/access.c b/core/proot/src/main/cpp/extension/fake_id0/access.c new file mode 100644 index 000000000..d9197843e --- /dev/null +++ b/core/proot/src/main/cpp/extension/fake_id0/access.c @@ -0,0 +1,55 @@ +#include +#include +#include + +#include "extension/fake_id0/access.h" +#include "extension/fake_id0/helper_functions.h" + +/** Handles the access and faccessat syscalls. Checks permissions according to + * a meta file if it exists. See access(2) for returned errors. + */ +int handle_access_enter_end(Tracee *tracee, Reg path_sysarg, + Reg mode_sysarg, Reg dirfd_sysarg, Config *config) +{ + int status, mode, perms, mask; + char path[PATH_MAX]; + char rel_path[PATH_MAX]; + char meta_path[PATH_MAX]; + + status = read_sysarg_path(tracee, path, path_sysarg, CURRENT); + if(status < 0) + return status; + if(status == 1) + return 0; + + status = get_fd_path(tracee, rel_path, dirfd_sysarg, CURRENT); + if(status < 0) + return status; + + status = check_dir_perms(tracee, 'r', path, rel_path, config); + if(status < 0) + return status; + + // Only care about calls checking permissions. + mode = peek_reg(tracee, ORIGINAL, mode_sysarg); + if(mode & F_OK) + return 0; + + status = get_meta_path(path, meta_path); + if(status < 0) + return status; + + mask = 0; + if((mode & R_OK) == R_OK) + mask += 4; + if((mode & W_OK) == W_OK) + mask += 2; + if((mode & X_OK) == X_OK) + mask += 1; + + perms = get_permissions(meta_path, config, 1); + if((perms & mask) != mask) + return -EACCES; + + return 0; +} diff --git a/core/proot/src/main/cpp/extension/fake_id0/access.h b/core/proot/src/main/cpp/extension/fake_id0/access.h new file mode 100644 index 000000000..ac5a42955 --- /dev/null +++ b/core/proot/src/main/cpp/extension/fake_id0/access.h @@ -0,0 +1,10 @@ +#ifndef FAKE_ID0_ACCESS_H +#define FAKE_ID0_ACCESS_H + +#include "tracee/tracee.h" +#include "tracee/reg.h" +#include "extension/fake_id0/config.h" + +int handle_access_enter_end(Tracee *tracee, Reg path_sysarg, Reg mode_sysarg, Reg dirfd_sysarg, Config *config); + +#endif /* FAKE_ID0_ACCESS_H */ diff --git a/core/proot/src/main/cpp/extension/fake_id0/chmod.c b/core/proot/src/main/cpp/extension/fake_id0/chmod.c new file mode 100644 index 000000000..e1accbc56 --- /dev/null +++ b/core/proot/src/main/cpp/extension/fake_id0/chmod.c @@ -0,0 +1,56 @@ +#include +#include + +#include "syscall/sysnum.h" +#include "extension/fake_id0/chmod.h" + +#include "extension/fake_id0/helper_functions.h" + +/** Handles chmod, fchmod, and fchmodat syscalls. Changes meta files to the new + * permissions if the meta file exists. See chmod(2) for returned permission + * errors. + */ +int handle_chmod_enter_end(Tracee *tracee, Reg path_sysarg, Reg mode_sysarg, + Reg fd_sysarg, Reg dirfd_sysarg, Config *config) +{ + int status; + mode_t call_mode, read_mode; + uid_t owner; + gid_t group; + char path[PATH_MAX]; + char rel_path[PATH_MAX]; + char meta_path[PATH_MAX]; + + // When path_sysarg is set to IGNORE, the call being handled is fchmod. + if(path_sysarg == IGNORE_SYSARG) + status = get_fd_path(tracee, path, fd_sysarg, CURRENT); + else + status = read_sysarg_path(tracee, path, path_sysarg, CURRENT); + if(status < 0) + return status; + // If the file exists outside the guestfs, drop the syscall. + else if(status == 1) { + set_sysnum(tracee, PR_getuid); + return 0; + } + + status = get_meta_path(path, meta_path); + if(path_exists(meta_path) < 0) + return 0; + + status = get_fd_path(tracee, rel_path, dirfd_sysarg, CURRENT); + if(status < 0) + return status; + + status = check_dir_perms(tracee, 'r', path, rel_path, config); + if(status < 0) + return status; + + read_meta_file(meta_path, &read_mode, &owner, &group, config); + if(config->euid != owner && config->euid != 0) + return -EPERM; + + call_mode = peek_reg(tracee, ORIGINAL, mode_sysarg); + set_sysnum(tracee, PR_getuid); + return write_meta_file(meta_path, call_mode, owner, group, 0, config); +} diff --git a/core/proot/src/main/cpp/extension/fake_id0/chmod.h b/core/proot/src/main/cpp/extension/fake_id0/chmod.h new file mode 100644 index 000000000..bd591ef20 --- /dev/null +++ b/core/proot/src/main/cpp/extension/fake_id0/chmod.h @@ -0,0 +1,10 @@ +#ifndef FAKE_ID0_CHMOD_H +#define FAKE_ID0_CHMOD_H + +#include "tracee/tracee.h" +#include "tracee/reg.h" +#include "extension/fake_id0/config.h" + +int handle_chmod_enter_end(Tracee *tracee, Reg path_sysarg, Reg mode_sysarg, Reg fd_sysarg, Reg dirfd_sysarg, Config *config); + +#endif /* FAKE_ID0_CHMOD_H */ diff --git a/core/proot/src/main/cpp/extension/fake_id0/chown.c b/core/proot/src/main/cpp/extension/fake_id0/chown.c new file mode 100644 index 000000000..bca332bfe --- /dev/null +++ b/core/proot/src/main/cpp/extension/fake_id0/chown.c @@ -0,0 +1,98 @@ +#include /* get*id(2), */ +#include +#include + +#include "syscall/sysnum.h" +#include "extension/fake_id0/chown.h" +#include "extension/fake_id0/helper_functions.h" + +#ifndef USERLAND +int handle_chown_enter_end(Tracee *tracee, Config *config, Reg uid_sysarg, Reg gid_sysarg) { + uid_t uid; + gid_t gid; + + uid = peek_reg(tracee, ORIGINAL, uid_sysarg); + gid = peek_reg(tracee, ORIGINAL, gid_sysarg); + + /* Swap actual and emulated ids to get a chance of + * success. */ + if (uid == config->ruid) + poke_reg(tracee, uid_sysarg, getuid()); + if (gid == config->rgid) + poke_reg(tracee, gid_sysarg, getgid()); + + return 0; +} +#endif /* ifndef USERLAND */ + +#ifdef USERLAND +/** Handles chown, lchown, fchown, and fchownat syscalls. Changes the meta file + * to reflect arguments sent to the syscall if the meta file exists. See + * chown(2) for returned permission errors. + */ +int handle_chown_enter_end(Tracee *tracee, Reg path_sysarg, Reg owner_sysarg, + Reg group_sysarg, Reg fd_sysarg, Reg dirfd_sysarg, Config *config) +{ + int status; + mode_t mode; + uid_t owner, read_owner; + gid_t group, read_group; + char path[PATH_MAX]; + char rel_path[PATH_MAX]; + char meta_path[PATH_MAX]; + + if(path_sysarg == IGNORE_SYSARG) + status = get_fd_path(tracee, path, fd_sysarg, CURRENT); + else + status = read_sysarg_path(tracee, path, path_sysarg, CURRENT); + if(status < 0) + return status; + // If the path exists outside the guestfs, drop the syscall. + else if(status == 1) { + set_sysnum(tracee, PR_getuid); + return 0; + } + + status = get_meta_path(path, meta_path); + if(status < 0) + return status; + + if(path_exists(meta_path) != 0) + return 0; + + status = get_fd_path(tracee, rel_path, dirfd_sysarg, CURRENT); + if(status < 0) + return status; + + status = check_dir_perms(tracee, 'r', path, rel_path, config); + if(status < 0) + return status; + + read_meta_file(meta_path, &mode, &read_owner, &read_group, config); + owner = peek_reg(tracee, ORIGINAL, owner_sysarg); + /** When chown is called without an owner specified, eg + * chown :1000 'file', the owner argument to the system call is implicitly + * set to -1. To avoid this, the owner argument is replaced with the owner + * according to the meta file if it exists, or the current euid. + */ + if((int) owner == -1) + owner = read_owner; + group = peek_reg(tracee, ORIGINAL, group_sysarg); + if(config->euid == 0) + write_meta_file(meta_path, mode, owner, group, 0, config); + + //TODO Handle chown properly: owner can only change the group of + // a file to another group they belong to. + else if(config->euid == read_owner) { + write_meta_file(meta_path, mode, read_owner, group, 0, config); + poke_reg(tracee, owner_sysarg, read_owner); + } + + else if(config->euid != read_owner) + return -EPERM; + + set_sysnum(tracee, PR_getuid); + + return 0; +} +#endif /* ifdef USERLAND */ diff --git a/core/proot/src/main/cpp/extension/fake_id0/chown.h b/core/proot/src/main/cpp/extension/fake_id0/chown.h new file mode 100644 index 000000000..44ec109da --- /dev/null +++ b/core/proot/src/main/cpp/extension/fake_id0/chown.h @@ -0,0 +1,16 @@ +#ifndef FAKE_ID0_CHOWN_H +#define FAKE_ID0_CHOWN_H + +#include "tracee/tracee.h" +#include "tracee/reg.h" +#include "extension/fake_id0/config.h" + +#ifndef USERLAND +int handle_chown_enter_end(Tracee *tracee, Config *config, Reg uid_sysarg, Reg gid_sysarg); +#endif /* ifndef USERLAND */ + +#ifdef USERLAND +int handle_chown_enter_end(Tracee *tracee, Reg path_sysarg, Reg owner_sysarg, Reg group_sysarg, Reg fd_sysarg, Reg dirfd_sysarg, Config *config); +#endif /* ifdef USERLAND */ + +#endif /* FAKE_ID0_CHOWN_H */ diff --git a/core/proot/src/main/cpp/extension/fake_id0/chroot.c b/core/proot/src/main/cpp/extension/fake_id0/chroot.c new file mode 100644 index 000000000..2b065cfe0 --- /dev/null +++ b/core/proot/src/main/cpp/extension/fake_id0/chroot.c @@ -0,0 +1,125 @@ +#include /* E*, */ +#include /* stat, */ + +#include "tracee/reg.h" +#include "tracee/mem.h" +#include "path/path.h" +#include "path/binding.h" +#include "extension/fake_id0/chroot.h" + +int handle_chroot_exit_end(Tracee *tracee, Config *config, bool from_sigsys) { + char path[PATH_MAX]; + char path_guest[PATH_MAX]; + char path_host_absolute[PATH_MAX]; + word_t input; + int status; + word_t result; + struct stat statbuf; + bool seen_bind_under_new_root = false; + + if (config->euid != 0) /* TODO: && !HAS_CAP(SYS_CHROOT) */ + return from_sigsys ? -EPERM : 0; + + if (from_sigsys) { + /* Fetch reg early and from CURRENT RegVersion + * if this call is from SIGSYS handler. */ + input = peek_reg(tracee, CURRENT, SYSARG_1); + + /* Set default error if we're here due to SIGSYS. */ + poke_reg(tracee, SYSARG_RESULT, -EPERM); + } else { + /* Override only permission errors. + * (If we're here due to SIGSYS we don't check that + * we know syscall couldn't even be made. */ + result = peek_reg(tracee, CURRENT, SYSARG_RESULT); + if ((int) result != -EPERM) + return 0; + } + + /* Get chroot target path translated to host. */ + if (!from_sigsys) { + input = peek_reg(tracee, MODIFIED, SYSARG_1); + + status = read_path(tracee, path, input); + } else { + status = read_path(tracee, path_guest, input); + if (status < 0) + return status; + status = translate_path(tracee, path, AT_FDCWD, path_guest, true); + } + + if (status < 0) + return status; + + realpath(path, path_host_absolute); + + /* Handle "new rootfs == current rootfs" case. */ + status = compare_paths(get_root(tracee), path_host_absolute); + if (status == PATHS_ARE_EQUAL) { + /* Force success. */ + if (from_sigsys) return 1; + poke_reg(tracee, SYSARG_RESULT, 0); + return 0; + } + + /* Validate chroot target. */ + status = stat(path_host_absolute, &statbuf); + if (status < 0) + return -errno; + + if (!S_ISDIR(statbuf.st_mode)) + return -ENOTDIR; + + /* Fetch guest path if we didn't already. */ + if (!from_sigsys) { + input = peek_reg(tracee, ORIGINAL, SYSARG_1); + status = read_path(tracee, path, input); + if (status < 0) + return -errno; + } + + /* Check if chroot target has bind mounts inside. + * (Those are not supported currently). */ + Binding *binding; + for (binding = CIRCLEQ_FIRST(tracee->fs->bindings.guest); + binding != (void *) tracee->fs->bindings.guest; + binding = CIRCLEQ_NEXT(binding, link.guest)) { + + bool is_guest_root = binding == CIRCLEQ_LAST(tracee->fs->bindings.guest); + + if (!is_guest_root && compare_paths(path_guest, binding->guest.path) == PATH1_IS_PREFIX) { + seen_bind_under_new_root = true; + break; + } + } + + /* Change tracee root binding if supported. */ + if (!seen_bind_under_new_root) { + /* Save current dir. */ + status = translate_path(tracee, path, AT_FDCWD, tracee->fs->cwd, true); + if (status < 0) + return status; + + /* Replace tracee bindings */ + talloc_unlink(tracee, tracee->fs); + tracee->fs = talloc_zero(tracee, FileSystemNameSpace); + binding = new_binding(tracee, path_host_absolute, "/", true); + initialize_bindings(tracee); + + /* Restore current dir. */ + status = detranslate_path(tracee, path, NULL); + if (status <= 0) { + tracee->fs->cwd = talloc_strdup(tracee->fs, "/"); + } else { + tracee->fs->cwd = talloc_strdup(tracee->fs, path); + } + + /* Force success. */ + if (from_sigsys) return 1; + poke_reg(tracee, SYSARG_RESULT, 0); + return 0; + } + + /* Unsupported chroot() variant. */ + return from_sigsys ? -ENOSYS : 0; +} diff --git a/core/proot/src/main/cpp/extension/fake_id0/chroot.h b/core/proot/src/main/cpp/extension/fake_id0/chroot.h new file mode 100644 index 000000000..ee0bf5bf9 --- /dev/null +++ b/core/proot/src/main/cpp/extension/fake_id0/chroot.h @@ -0,0 +1,9 @@ +#ifndef FAKE_ID0_CHROOT_H +#define FAKE_ID0_CHROOT_H + +#include "tracee/tracee.h" +#include "extension/fake_id0/config.h" + +int handle_chroot_exit_end(Tracee *tracee, Config *config, bool from_sigsys); + +#endif /* FAKE_ID0_CHROOT_H */ diff --git a/core/proot/src/main/cpp/extension/fake_id0/config.h b/core/proot/src/main/cpp/extension/fake_id0/config.h new file mode 100644 index 000000000..be0132d07 --- /dev/null +++ b/core/proot/src/main/cpp/extension/fake_id0/config.h @@ -0,0 +1,36 @@ +#ifndef FAKE_ID0_CONFIG_H +#define FAKE_ID0_CONFIG_H + +#include /* bool */ +#include /* uid_t, gid_t */ + +typedef struct { + uid_t ruid; + uid_t euid; + uid_t suid; + uid_t fsuid; + + gid_t rgid; + gid_t egid; + gid_t sgid; + gid_t fsgid; + + mode_t umask; + + /* Whether the process effectively holds CAP_SETUID/CAP_SETGID under + * proot's fake-root model. Initialized to true when proot was + * launched as fake root (uid == 0). Cleared when a setuid-family + * syscall makes none of r/e/s uid be 0 while at least one was 0 + * before, mirroring the kernel rule that clears capabilities on a + * permanent UID drop -- unless keep_caps is set. Re-asserted on + * execve of a setuid-root binary. */ + bool caps_active; + + /* Mirror of the tracee's prctl(PR_SET_KEEPCAPS) flag. When set, + * caps_active survives a UID drop, matching how PR_SET_KEEPCAPS plus + * a follow-up capset() lets a process retain CAP_SETUID/CAP_SETGID + * across setresuid(). Cleared by execve, per Linux semantics. */ + bool keep_caps; +} Config; + +#endif /* FAKE_ID0_CONFIG_H */ diff --git a/core/proot/src/main/cpp/extension/fake_id0/exec.c b/core/proot/src/main/cpp/extension/fake_id0/exec.c new file mode 100644 index 000000000..e3534f437 --- /dev/null +++ b/core/proot/src/main/cpp/extension/fake_id0/exec.c @@ -0,0 +1,64 @@ +#include +#include +#include + +#include "extension/fake_id0/exec.h" + +#include "extension/fake_id0/helper_functions.h" + +/** Handles execve system calls. Checks permissions in a meta file if it exists + * and returns errors matching those in execve(2). + */ +int handle_exec_enter_end(Tracee *tracee, Reg filename_sysarg, Config *config) +{ + int status, perms; + char path[PATH_MAX]; + char meta_path[PATH_MAX]; + uid_t uid; + gid_t gid; + mode_t mode; + + status = read_sysarg_path(tracee, path, filename_sysarg, ORIGINAL); + if(status < 0) + return status; + if(status == 1) + return 0; + + status = get_meta_path(path, meta_path); + if(status < 0) + return status; + + /* If metafile doesn't exist, get out, but don't error. */ + if(path_exists(meta_path) != 0) + return 0; + + /* Check perms relative to / since there is no dirfd argument to execve */ + status = check_dir_perms(tracee, 'r', meta_path, "/", config); + if(status < 0) + return status; + + /* Check whether the file has execute permission. */ + perms = get_permissions(meta_path, config, 0); + if((perms & 1) != 1) + return -EACCES; + + /* If the setuid or setgid bits are on, change config accordingly. */ + read_meta_file(meta_path, &mode, &uid, &gid, config); + if ((mode & S_ISUID) != 0) { + config->ruid = 0; + config->euid = 0; + config->suid = 0; + } + + if ((mode & S_ISGID) != 0) { + config->rgid = 0; + config->egid = 0; + config->sgid = 0; + } + + /** TODO Add logic to determine interpreter being used, and check + * permissions for it. + */ + + return 0; +} diff --git a/core/proot/src/main/cpp/extension/fake_id0/exec.h b/core/proot/src/main/cpp/extension/fake_id0/exec.h new file mode 100644 index 000000000..57cc1bbac --- /dev/null +++ b/core/proot/src/main/cpp/extension/fake_id0/exec.h @@ -0,0 +1,10 @@ +#ifndef FAKE_ID0_EXEC_H +#define FAKE_ID0_EXEC_H + +#include "tracee/tracee.h" +#include "tracee/reg.h" +#include "extension/fake_id0/config.h" + +extern int handle_exec_enter_end(Tracee *tracee, Reg filename_sysarg, Config *config); + +#endif /* FAKE_ID0_EXEC_H */ diff --git a/core/proot/src/main/cpp/extension/fake_id0/fake_id0.c b/core/proot/src/main/cpp/extension/fake_id0/fake_id0.c new file mode 100644 index 000000000..9bd8dadc4 --- /dev/null +++ b/core/proot/src/main/cpp/extension/fake_id0/fake_id0.c @@ -0,0 +1,1414 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#include /* assert(3), */ +#include /* intptr_t, */ +#include /* E*, */ +#include /* FILE, fopen(3), fgets(3), sscanf(3), fclose(3), */ +#include /* O_PATH, */ +#include /* chmod(2), stat(2) */ +#include /* uid_t, gid_t, get*id(2), */ +#include /* get*id(2), */ +#include /* linux.git:c0a3a20b */ +#include /* PR_SET_KEEPCAPS, PR_GET_KEEPCAPS, */ +#include /* AUDIT_ARCH_*, */ +#include /* memcpy(3), */ +#include /* strtol(3), */ +#include /* AT_, */ +#include /* cmsghdr, */ +#include /* SYS_SENDMSG, */ + +#include "extension/extension.h" +#include "syscall/syscall.h" +#include "syscall/sysnum.h" +#include "syscall/seccomp.h" +#include "syscall/chain.h" +#include "execve/execve.h" +#include "tracee/tracee.h" +#include "tracee/abi.h" +#include "tracee/mem.h" +#include "execve/auxv.h" +#include "path/binding.h" +#include "path/f2fs-bug.h" +#include "arch.h" + +#include "extension/fake_id0/chown.h" +#include "extension/fake_id0/chroot.h" +#include "extension/fake_id0/getsockopt.h" +#include "extension/fake_id0/sendmsg.h" +#include "extension/fake_id0/socket.h" +#include "extension/fake_id0/stat.h" +#ifdef USERLAND +#include "extension/fake_id0/open.h" +#include "extension/fake_id0/unlink.h" +#include "extension/fake_id0/rename.h" +#include "extension/fake_id0/chmod.h" +#include "extension/fake_id0/utimensat.h" +#include "extension/fake_id0/access.h" +#include "extension/fake_id0/exec.h" +#include "extension/fake_id0/link.h" +#include "extension/fake_id0/symlink.h" +#include "extension/fake_id0/mk.h" +#include "extension/fake_id0/stat.h" +#include "extension/fake_id0/helper_functions.h" +#endif + +/** + * Copy config->@field to the tracee's memory location pointed to by @sysarg. + */ +#define POKE_MEM_ID(sysarg, field) do { \ + poke_uint16(tracee, peek_reg(tracee, ORIGINAL, sysarg), config->field); \ + if (errno != 0) \ + return -errno; \ +} while (0) + +/** + * Clear the fake CAP_SETUID/CAP_SETGID flag (caps_active) when a setuid- + * family syscall has just performed a "permanent" privilege drop: at + * least one of r/e/s uid was 0 before the call and none is 0 after. + * This mirrors the Linux rule that clears capabilities on such a + * transition, unless prctl(PR_SET_KEEPCAPS, 1) was used. Operates on + * the current Config in scope. + */ +#define MAYBE_DROP_CAPS(prev_root) do { \ + if ((prev_root) && !config->keep_caps \ + && config->ruid != 0 && config->euid != 0 \ + && config->suid != 0) \ + config->caps_active = false; \ +} while (0) + +/** + * Emulate setuid(2) and setgid(2). + */ +#define SETXID(id, version) do { \ + id ## _t id = peek_reg(tracee, version, SYSARG_1); \ + bool prev_root = (config->ruid == 0 || config->euid == 0 \ + || config->suid == 0); \ + bool allowed; \ + \ + /* "EPERM: The user is not privileged (does not have the \ + * CAP_SETUID capability) and uid does not match the real UID \ + * or saved set-user-ID of the calling process." -- man \ + * setuid */ \ + allowed = (config->euid == 0 || config->caps_active \ + || id == config->r ## id \ + || id == config->e ## id \ + || id == config->s ## id); \ + if (!allowed) \ + return -EPERM; \ + \ + /* "If the effective UID of the caller is root [or it holds \ + * CAP_SETUID], the real UID and saved set-user-ID are also \ + * set." -- man setuid */ \ + if (config->euid == 0 || config->caps_active) { \ + config->r ## id = id; \ + config->s ## id = id; \ + } \ + \ + /* "whenever the effective user ID is changed, fsuid will also \ + * be changed to the new value of the effective user ID." -- \ + * man setfsuid */ \ + config->e ## id = id; \ + config->fs ## id = id; \ + \ + MAYBE_DROP_CAPS(prev_root); \ + \ + poke_reg(tracee, SYSARG_RESULT, 0); \ + return 0; \ +} while (0) + +/** + * Check whether @id is set or not. + */ +#define UNSET_ID(id) (id == (uid_t) -1) + +/** + * Check whether @id is change or not. + */ +#define UNCHANGED_ID(id) (UNSET_ID(id) || id == config->id) + +/** + * Emulate setreuid(2) and setregid(2). + */ +#define SETREXID(id, version) do { \ + id ## _t r ## id = peek_reg(tracee, version, SYSARG_1); \ + id ## _t e ## id = peek_reg(tracee, version, SYSARG_2); \ + bool prev_root = (config->ruid == 0 || config->euid == 0 \ + || config->suid == 0); \ + bool allowed; \ + \ + /* "Unprivileged processes may only set the effective user ID \ + * to the real user ID, the effective user ID, or the saved \ + * set-user-ID. \ + * \ + * Unprivileged users may only set the real user ID to the \ + * real user ID or the effective user ID." \ + * + * "EPERM: The calling process is not privileged (does not \ + * have the CAP_SETUID) and a change other than: \ + * 1. swapping the effective user ID with the real user ID, \ + * or; \ + * 2. setting one to the value of the other, or ; \ + * 3. setting the effective user ID to the value of the saved \ + * set-user-ID \ + * was specified." -- man setreuid \ + * \ + * Is it possible to "ruid <- euid" and "euid <- suid" at the \ + * same time? */ \ + allowed = (config->euid == 0 || config->caps_active \ + || (UNCHANGED_ID(e ## id) && UNCHANGED_ID(r ## id)) \ + || (r ## id == config->e ## id && (e ## id == config->r ## id || UNCHANGED_ID(e ## id))) \ + || (e ## id == config->r ## id && (r ## id == config->e ## id || UNCHANGED_ID(r ## id))) \ + || (e ## id == config->s ## id && UNCHANGED_ID(r ## id))); \ + if (!allowed) \ + return -EPERM; \ + \ + /* "Supplying a value of -1 for either the real or effective \ + * user ID forces the system to leave that ID unchanged. \ + * [...] If the real user ID is set or the effective user ID \ + * is set to a value not equal to the previous real user ID, \ + * the saved set-user-ID will be set to the new effective user \ + * ID." -- man setreuid */ \ + if (!UNSET_ID(e ## id)) { \ + if (e ## id != config->r ## id) \ + config->s ## id = e ## id; \ + \ + config->e ## id = e ## id; \ + config->fs ## id = e ## id; \ + } \ + \ + /* Since it changes the current ruid value, this has to be \ + * done after euid handling. */ \ + if (!UNSET_ID(r ## id)) { \ + if (!UNSET_ID(e ## id)) \ + config->s ## id = e ## id; \ + config->r ## id = r ## id; \ + } \ + \ + MAYBE_DROP_CAPS(prev_root); \ + \ + poke_reg(tracee, SYSARG_RESULT, 0); \ + return 0; \ +} while (0) + +/** + * Check if @var is equal to any config->r{@type}id's. + */ +#define EQUALS_ANY_ID(var, type) (var == config->r ## type ## id \ + || var == config->e ## type ## id \ + || var == config->s ## type ## id) + +/** + * Emulate setresuid(2) and setresgid(2). + */ +#define SETRESXID(type,version) do { \ + type ## id_t r ## type ## id = peek_reg(tracee, version, SYSARG_1); \ + type ## id_t e ## type ## id = peek_reg(tracee, version, SYSARG_2); \ + type ## id_t s ## type ## id = peek_reg(tracee, version, SYSARG_3); \ + bool prev_root = (config->ruid == 0 || config->euid == 0 \ + || config->suid == 0); \ + bool allowed; \ + \ + /* "Unprivileged user processes may change the real UID, \ + * effective UID, and saved set-user-ID, each to one of: the \ + * current real UID, the current effective UID or the current \ + * saved set-user-ID. \ + * \ + * Privileged processes (on Linux, those having the CAP_SETUID \ + * capability) may set the real UID, effective UID, and saved \ + * set-user-ID to arbitrary values." -- man setresuid */ \ + allowed = (config->euid == 0 || config->caps_active \ + || ((UNSET_ID(r ## type ## id) || EQUALS_ANY_ID(r ## type ## id, type)) \ + && (UNSET_ID(e ## type ## id) || EQUALS_ANY_ID(e ## type ## id, type)) \ + && (UNSET_ID(s ## type ## id) || EQUALS_ANY_ID(s ## type ## id, type)))); \ + if (!allowed) \ + return -EPERM; \ + \ + /* "If one of the arguments equals -1, the corresponding value \ + * is not changed." -- man setresuid */ \ + if (!UNSET_ID(r ## type ## id)) \ + config->r ## type ## id = r ## type ## id; \ + \ + if (!UNSET_ID(e ## type ## id)) { \ + /* "the file system UID is always set to the same \ + * value as the (possibly new) effective UID." -- man \ + * setresuid */ \ + config->e ## type ## id = e ## type ## id; \ + config->fs ## type ## id = e ## type ## id; \ + } \ + \ + if (!UNSET_ID(s ## type ## id)) \ + config->s ## type ## id = s ## type ## id; \ + \ + MAYBE_DROP_CAPS(prev_root); \ + \ + poke_reg(tracee, SYSARG_RESULT, 0); \ + return 0; \ +} while (0) + +/** + * Emulate setfsuid(2) and setfsgid(2). + */ +#define SETFSXID(type) do { \ + uid_t fs ## type ## id = peek_reg(tracee, ORIGINAL, SYSARG_1); \ + uid_t old_fs ## type ## id = config->fs ## type ## id; \ + bool allowed; \ + \ + /* "setfsuid() will succeed only if the caller is the \ + * superuser or if fsuid matches either the real user ID, \ + * effective user ID, saved set-user-ID, or the current value \ + * of fsuid." -- man setfsuid */ \ + allowed = (config->euid == 0 || config->caps_active \ + || fs ## type ## id == config->fs ## type ## id \ + || EQUALS_ANY_ID(fs ## type ## id, type)); \ + if (allowed) \ + config->fs ## type ## id = fs ## type ## id; \ + \ + /* "On success, the previous value of fsuid is returned. On \ + * error, the current value of fsuid is returned." -- man \ + * setfsuid */ \ + poke_reg(tracee, SYSARG_RESULT, old_fs ## type ## id); \ + return 0; \ +} while (0) + +typedef struct { + char *path; + mode_t mode; +} ModifiedNode; + +/* List of syscalls handled by this extensions. */ +static FilteredSysnum filtered_sysnums[] = { +#ifdef USERLAND + { PR_access, FILTER_SYSEXIT }, + { PR_creat, FILTER_SYSEXIT }, + { PR_faccessat, FILTER_SYSEXIT }, + { PR_faccessat2, FILTER_SYSEXIT }, + { PR_link, FILTER_SYSEXIT }, + { PR_linkat, FILTER_SYSEXIT }, + { PR_mkdir, FILTER_SYSEXIT }, + { PR_mkdirat, FILTER_SYSEXIT }, + { PR_symlink, FILTER_SYSEXIT }, + { PR_symlinkat, FILTER_SYSEXIT }, + { PR_umask, FILTER_SYSEXIT }, + { PR_unlink, FILTER_SYSEXIT }, + { PR_unlinkat, FILTER_SYSEXIT }, + { PR_utimensat, FILTER_SYSEXIT }, +#endif + { PR_capset, FILTER_SYSEXIT }, + { PR_chmod, FILTER_SYSEXIT }, + { PR_chown, FILTER_SYSEXIT }, + { PR_chown32, FILTER_SYSEXIT }, + { PR_chroot, FILTER_SYSEXIT }, + { PR_execve, FILTER_SYSEXIT }, + { PR_fchmod, FILTER_SYSEXIT }, + { PR_fchmodat, FILTER_SYSEXIT }, + { PR_fchown, FILTER_SYSEXIT }, + { PR_fchown32, FILTER_SYSEXIT }, + { PR_fchownat, FILTER_SYSEXIT }, + { PR_fstat, FILTER_SYSEXIT }, + { PR_fstat64, FILTER_SYSEXIT }, + { PR_fstatat64, FILTER_SYSEXIT }, + { PR_getegid, FILTER_SYSEXIT }, + { PR_getegid32, FILTER_SYSEXIT }, + { PR_geteuid, FILTER_SYSEXIT }, + { PR_geteuid32, FILTER_SYSEXIT }, + { PR_getgid, FILTER_SYSEXIT }, + { PR_getgid32, FILTER_SYSEXIT }, + { PR_getgroups, FILTER_SYSEXIT }, + { PR_getgroups32, FILTER_SYSEXIT }, + { PR_getresgid, FILTER_SYSEXIT }, + { PR_getresgid32, FILTER_SYSEXIT }, + { PR_getresuid, FILTER_SYSEXIT }, + { PR_getresuid32, FILTER_SYSEXIT }, + { PR_getuid, FILTER_SYSEXIT }, + { PR_getuid32, FILTER_SYSEXIT }, + { PR_getsockopt, FILTER_SYSEXIT }, + { PR_lchown, FILTER_SYSEXIT }, + { PR_lchown32, FILTER_SYSEXIT }, + { PR_lstat, FILTER_SYSEXIT }, + { PR_lstat64, FILTER_SYSEXIT }, + { PR_mknod, FILTER_SYSEXIT }, + { PR_mknodat, FILTER_SYSEXIT }, + { PR_newfstatat, FILTER_SYSEXIT }, + { PR_oldlstat, FILTER_SYSEXIT }, + { PR_oldstat, FILTER_SYSEXIT }, + { PR_prctl, FILTER_SYSEXIT }, + { PR_sendmsg, 0 }, + { PR_setfsgid, FILTER_SYSEXIT }, + { PR_setfsgid32, FILTER_SYSEXIT }, + { PR_setfsuid, FILTER_SYSEXIT }, + { PR_setfsuid32, FILTER_SYSEXIT }, + { PR_setgid, FILTER_SYSEXIT }, + { PR_setgid32, FILTER_SYSEXIT }, + { PR_setgroups, FILTER_SYSEXIT }, + { PR_setgroups32, FILTER_SYSEXIT }, + { PR_setregid, FILTER_SYSEXIT }, + { PR_setregid32, FILTER_SYSEXIT }, + { PR_setreuid, FILTER_SYSEXIT }, + { PR_setreuid32, FILTER_SYSEXIT }, + { PR_setresgid, FILTER_SYSEXIT }, + { PR_setresgid32, FILTER_SYSEXIT }, + { PR_setresuid, FILTER_SYSEXIT }, + { PR_setresuid32, FILTER_SYSEXIT }, + { PR_setuid, FILTER_SYSEXIT }, + { PR_setuid32, FILTER_SYSEXIT }, + { PR_setxattr, FILTER_SYSEXIT }, + { PR_setdomainname, FILTER_SYSEXIT }, + { PR_sethostname, FILTER_SYSEXIT }, + { PR_socket, FILTER_SYSEXIT }, + { PR_lsetxattr, FILTER_SYSEXIT }, + { PR_fsetxattr, FILTER_SYSEXIT }, + { PR_stat, FILTER_SYSEXIT }, + { PR_stat64, FILTER_SYSEXIT }, + { PR_statfs, FILTER_SYSEXIT }, + { PR_statfs64, FILTER_SYSEXIT }, + FILTERED_SYSNUM_END, +}; + +/** + * Restore the @node->mode for the given @node->path. + * + * Note: this is a Talloc destructor. + */ +static int restore_mode(ModifiedNode *node) +{ + (void) chmod(node->path, node->mode); + return 0; +} + +/** + * Force permissions of @path to "rwx" during the path translation of + * current @tracee's syscall, in order to simulate CAP_DAC_OVERRIDE. + * The original permissions are restored through talloc destructors. + * See canonicalize() for the meaning of @is_final. + */ +static void override_permissions(const Tracee *tracee, const char *path, bool is_final) +{ + ModifiedNode *node; + struct stat perms; + mode_t new_mode; + int status; + + /* Get the meta-data */ + if (should_skip_file_access_due_to_f2fs_bug(tracee, path)) + return; + + status = stat(path, &perms); + if (status < 0) + return; + + /* Copy the current permissions */ + new_mode = perms.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO); + + /* Add read and write permissions to everything. */ + new_mode |= (S_IRUSR | S_IWUSR); + + /* Always add 'x' bit to directories */ + if (S_ISDIR(perms.st_mode)) + new_mode |= S_IXUSR; + + /* Patch the permissions only if needed. */ + if (new_mode == (perms.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO))) + return; + + node = talloc_zero(tracee->ctx, ModifiedNode); + if (node == NULL) + return; + + if (!is_final) { + /* Restore the previous mode of any non final components. */ + node->mode = perms.st_mode; + } + else { + switch (get_sysnum(tracee, ORIGINAL)) { + /* For chmod syscalls: restore the new mode of the final component. */ + case PR_chmod: + node->mode = peek_reg(tracee, ORIGINAL, SYSARG_2); + break; + + case PR_fchmodat: + node->mode = peek_reg(tracee, ORIGINAL, SYSARG_3); + break; + + /* For stat syscalls: don't touch the mode of the final component. */ + case PR_fstatat64: + case PR_lstat: + case PR_lstat64: + case PR_newfstatat: + case PR_oldlstat: + case PR_oldstat: + case PR_stat: + case PR_stat64: + case PR_statfs: + case PR_statfs64: + return; + + /* Otherwise: restore the previous mode of the final component. */ + default: + node->mode = perms.st_mode; + break; + } + } + + node->path = talloc_strdup(node, path); + if (node->path == NULL) { + /* Keep only consistent nodes. */ + TALLOC_FREE(node); + return; + } + + /* The mode restoration works because Talloc destructors are + * called in reverse order. */ + talloc_set_destructor(node, restore_mode); + + (void) chmod(path, new_mode); + + return; +} + +/** + * Adjust some ELF auxiliary vectors. This function assumes the + * "argv, envp, auxv" stuff is pointed to by @tracee's stack pointer, + * as expected right after a successful call to execve(2). + */ +static int adjust_elf_auxv(Tracee *tracee, Config *config) +{ + ElfAuxVector *vectors; + ElfAuxVector *vector; + word_t vectors_address; + + vectors_address = get_elf_aux_vectors_address(tracee); + if (vectors_address == 0) + return 0; + + vectors = fetch_elf_aux_vectors(tracee, vectors_address); + if (vectors == NULL) + return 0; + + for (vector = vectors; vector->type != AT_NULL; vector++) { + switch (vector->type) { + case AT_UID: + vector->value = config->ruid; + break; + + case AT_EUID: + vector->value = config->euid; + break; + + case AT_GID: + vector->value = config->rgid; + break; + + case AT_EGID: + vector->value = config->egid; + break; + + default: + break; + } + } + + push_elf_aux_vectors(tracee, vectors, vectors_address); + + return 0; +} + +static int handle_perm_err_exit_end(Tracee *tracee, Config *config, bool even_if_not_root) { + word_t result; + + /* Override only permission errors. */ + result = peek_reg(tracee, CURRENT, SYSARG_RESULT); + +#ifdef USERLAND + /** If the call has been set to PR_void, it "succeeded" in + * altering a meta file correctly. + */ + if(get_sysnum(tracee, CURRENT) == PR_getuid && (int) result != 0) + poke_reg(tracee, SYSARG_RESULT, 0); + if(get_sysnum(tracee, CURRENT) == PR_void && (int) result != 0) + poke_reg(tracee, SYSARG_RESULT, 0); +#endif + + if ((int) result != -EPERM && (int) result != -EACCES) + return 0; + + /* Force success if the tracee was supposed to have + * the capability. */ + if (even_if_not_root || config->euid == 0) /* TODO: || HAS_CAP(...) */ + poke_reg(tracee, SYSARG_RESULT, 0); + + return 0; +} + +static int handle_getresuid_exit_end(Tracee *tracee, Config *config) { + POKE_MEM_ID(SYSARG_1, ruid); + POKE_MEM_ID(SYSARG_2, euid); + POKE_MEM_ID(SYSARG_3, suid); + return 0; +} + +static int handle_getresgid_exit_end(Tracee *tracee, Config *config) { + POKE_MEM_ID(SYSARG_1, rgid); + POKE_MEM_ID(SYSARG_2, egid); + POKE_MEM_ID(SYSARG_3, sgid); + return 0; +} + +/** + * Adjust current @tracee's syscall parameters according to @config. + * This function always returns 0. + */ +static int handle_sysenter_end(Tracee *tracee, Config *config) +{ + word_t sysnum; + Reg uid_sysarg = SYSARG_2; + Reg gid_sysarg = SYSARG_3; + + sysnum = get_sysnum(tracee, ORIGINAL); + switch (sysnum) { + +#ifdef USERLAND + /* handle_open(tracee, fd_sysarg, path_sysarg, flags_sysarg, mode_sysarg, config) */ + /* int openat(int dirfd, const char *pathname, int flags, mode_t mode) */ + case PR_openat: + return handle_open_enter_end(tracee, SYSARG_1, SYSARG_2, SYSARG_3, SYSARG_4, config); + /* int open(const char *pathname, int flags, mode_t mode) */ + case PR_open: + return handle_open_enter_end(tracee, IGNORE_SYSARG, SYSARG_1, SYSARG_2, SYSARG_3, config); + /* int creat(const char *pathname, mode_t mode) */ + case PR_creat: + return handle_open_enter_end(tracee, IGNORE_SYSARG, SYSARG_1, IGNORE_SYSARG, SYSARG_2, config); + + /* handle_mk(tracee, fd_sysarg, path_sysarg, mode_sysarg, config) */ + /* int mkdirat(int dirfd, const char *pathname, mode_t mode) */ + case PR_mkdirat: + return handle_mk_enter_end(tracee, SYSARG_1, SYSARG_2, SYSARG_3, config); + /* int mkdir(const char *pathname, mode_t mode) */ + case PR_mkdir: + return handle_mk_enter_end(tracee, IGNORE_SYSARG, SYSARG_1, SYSARG_2, config); + + /* handle_mk(tracee, fd_sysarg, path_sysarg, mode_sysarg, config) */ + /* int mknodat(int dirfd, const char *pathname, mode_t mode, dev_t dev); */ + case PR_mknodat: + return handle_mk_enter_end(tracee, SYSARG_1, SYSARG_2, SYSARG_3, config); + /* int mknod(const char *pathname, mode_t mode, dev_t dev); */ + case PR_mknod: + return handle_mk_enter_end(tracee, IGNORE_SYSARG, SYSARG_1, SYSARG_2, config); + + /* handle_unlink(tracee, fd_sysarg, path_sysarg, config) */ + /* int unlinkat(int dirfd, const char *pathname, int flags) */ + case PR_unlinkat: + return handle_unlink_enter_end(tracee, SYSARG_1, SYSARG_2, config); + /* int rmdir(const char *pathname */ + case PR_rmdir: + /* int unlink(const char *pathname) */ + case PR_unlink: + return handle_unlink_enter_end(tracee, IGNORE_SYSARG, SYSARG_1, config); + + /* handle_rename(tracee, oldfd_sysarg, oldpath_sysarg, newfd_sysarg, newpath_sysarg, config) */ + /* int renameat(int olddirfd, const char *oldpath, int newdirfd, const char *newpath) */ + case PR_renameat: + return handle_rename_enter_end(tracee, SYSARG_1, SYSARG_2, SYSARG_3, SYSARG_4, config); + /* int rename(const char *oldpath, const char *newpath) */ + case PR_rename: + return handle_rename_enter_end(tracee, IGNORE_SYSARG, SYSARG_1, IGNORE_SYSARG, SYSARG_2, config); + + /* handle_chmod(tracee, path_sysarg, mode_sysarg, fd_sysarg, dirfd_sysarg, config) */ + /* int chmod(const char *pathname, mode_t mode) */ + case PR_chmod: + return handle_chmod_enter_end(tracee, SYSARG_1, SYSARG_2, IGNORE_SYSARG, IGNORE_SYSARG, config); + /* int fchmod(int fd, mode_t mode) */ + case PR_fchmod: + return handle_chmod_enter_end(tracee, IGNORE_SYSARG, SYSARG_2, + SYSARG_1, IGNORE_SYSARG, config); + /* int fchmodat(int dirfd, const char *pathname, mode_t mode, int flags (unused)) */ + case PR_fchmodat: + return handle_chmod_enter_end(tracee, SYSARG_2, SYSARG_3, + IGNORE_SYSARG, SYSARG_1, config); + + /* handle_chown(tracee, path_sysarg, owner_sysarg, group_sysarg, fd_sysarg, dirfd_sysarg, config) */ + /* int chown(const char *pathname, uid_t owner, gid_t group) */ + case PR_chown: + case PR_chown32: + return handle_chown_enter_end(tracee, SYSARG_1, SYSARG_2, SYSARG_3, + IGNORE_SYSARG, IGNORE_SYSARG, config); + /* int fchown(int fd, uid_t owner, gid_t group) */ + case PR_fchown: + case PR_fchown32: + return handle_chown_enter_end(tracee, IGNORE_SYSARG, SYSARG_2, SYSARG_3, + SYSARG_1, IGNORE_SYSARG, config); + /* int lchown(const char *pathname, uid_t owner, gid_t group) */ + case PR_lchown: + case PR_lchown32: + return handle_chown_enter_end(tracee, SYSARG_1, SYSARG_2, SYSARG_3, + IGNORE_SYSARG, IGNORE_SYSARG, config); + /* int fchownat(int dirfd, const char *pathname, uid_t owner, gid_t group, int flags (unused)) */ + case PR_fchownat: + return handle_chown_enter_end(tracee, SYSARG_2, SYSARG_3, SYSARG_4, + IGNORE_SYSARG, SYSARG_1, config); + + /* handle_utimensat(tracee, dirfd_sysarg, path_sysarg, times_sysarg, config) */ + /* int utimensat(int dirfd, const char *pathname, const struct timespec times[2], int flags) */ + case PR_utimensat: + return handle_utimensat_enter_end(tracee, SYSARG_1, SYSARG_2, SYSARG_3, config); + + /* handle_access(tracee path_sysarg, mode_sysarg, dirfd_sysarg, config) */ + /* int access(const char *pathname, int mode) */ + case PR_access: + return handle_access_enter_end(tracee, SYSARG_1, SYSARG_2, IGNORE_SYSARG, config); + /* int faccessat(int dirfd, const char *pathname, int mode, int flags) */ + case PR_faccessat: + case PR_faccessat2: + return handle_access_enter_end(tracee, SYSARG_2, SYSARG_3, SYSARG_1, config); + + /* handle_exec(tracee, filename_sysarg, config) */ + case PR_execve: + return handle_exec_enter_end(tracee, SYSARG_1, config); + + /* handle_link(tracee, olddirfd_sysarg, oldpath_sysarg, newdirfd_sysarg, newpath_sysarg, config) */ + /* int link(const char *oldpath, const char *newpath) */ + case PR_link: + return handle_link_enter_end(tracee, IGNORE_SYSARG, SYSARG_1, IGNORE_SYSARG, SYSARG_2, config); + /* int linkat(int olddirfd, const char *oldpath, int newdirfd, const char *newpath, int flags) */ + case PR_linkat: + return handle_link_enter_end(tracee, SYSARG_1, SYSARG_2, SYSARG_3, SYSARG_4, config); + + /* handle_symlink(tracee, oldpath_sysarg, newdirfd_sysarg, newpath_sysarg, config) */ + /* int symlink(const char *target, const char *linkpath); */ + case PR_symlink: + return handle_symlink_enter_end(tracee, SYSARG_1, IGNORE_SYSARG, SYSARG_2, config); + /* int symlinkat(const char *target, int newdirfd, const char *linkpath); */ + case PR_symlinkat: + return handle_symlink_enter_end(tracee, SYSARG_1, SYSARG_2, SYSARG_3, config); + + /* int fstat(int fd, struct stat *buf); */ + case PR_fstat: + case PR_fstat64: + return handle_stat_enter_end(tracee, SYSARG_1); +#endif + case PR_sendmsg: + case PR_socketcall: + return handle_sendmsg_enter_end(tracee, sysnum); + + case PR_setuid: + case PR_setuid32: + case PR_setgid: + case PR_setgid32: + case PR_setreuid: + case PR_setreuid32: + case PR_setregid: + case PR_setregid32: + case PR_setresuid: + case PR_setresuid32: + case PR_setresgid: + case PR_setresgid32: + case PR_setfsuid: + case PR_setfsuid32: + case PR_setfsgid: + case PR_setfsgid32: +#ifdef USERLAND + case PR_umask: +#endif + /* These syscalls are fully emulated. */ + set_sysnum(tracee, PR_void); + return 0; + +#ifndef USERLAND + case PR_fchownat: + uid_sysarg = SYSARG_3; + gid_sysarg = SYSARG_4; + case PR_chown: + case PR_chown32: + case PR_lchown: + case PR_lchown32: + case PR_fchown: + case PR_fchown32: + return handle_chown_enter_end(tracee, config, uid_sysarg, gid_sysarg); +#endif + +#ifndef USERLAND + case PR_openat: { + char path[PATH_MAX]; + char prefix[64]; + char *end; + int fd_num; + + /* Only apply when the tracee has fake root (DAC override). */ + if (config->euid != 0) + return 0; + + /* Read the translated (host) path from the CURRENT registers. + * helper_functions.h is USERLAND-only so use read_string directly. */ + if (read_string(tracee, path, + peek_reg(tracee, CURRENT, SYSARG_2), + PATH_MAX) < 0) + return 0; + + /* Check if the path is /proc/pid>/fd/. This + * arises when the guest opens /dev/stderr, /dev/stdout, or + * /proc/self/fd/N — e.g. when a log file is a symlink to + * /dev/stderr (common in containerised nginx/apache images). + * The kernel rejects opening these paths when the underlying + * fd target (e.g. a pty owned by root) isn't accessible to + * the real non-root uid. Using dup(N) sidesteps that check + * because the fd is already open. */ + snprintf(prefix, sizeof(prefix), "/proc/%d/fd/", tracee->pid); + if (strncmp(path, prefix, strlen(prefix)) != 0) + return 0; + + errno = 0; + fd_num = (int) strtol(path + strlen(prefix), &end, 10); + if (errno != 0 || end == path + strlen(prefix) || *end != '\0' || fd_num < 0) + return 0; + + /* If the existing fd was opened with O_PATH, dup() inherits + * that flag and lseek/read/write on the duplicate return + * EBADF. This breaks the safe_open() pattern used by e.g. + * systemd-machine-id-setup, which opens a file with O_PATH + * first and then reopens it via /proc/self/fd/N with real + * access flags. Skip the substitution in that case — the + * kernel reopens the underlying file with the requested + * flags, and override_permissions() ran during the original + * O_PATH open so the file is still accessible to the real + * uid. */ + { + char fdinfo_path[64]; + char line[64]; + unsigned int existing_flags = 0; + FILE *fdinfo; + + snprintf(fdinfo_path, sizeof(fdinfo_path), + "/proc/%d/fdinfo/%d", tracee->pid, fd_num); + fdinfo = fopen(fdinfo_path, "r"); + if (fdinfo != NULL) { + while (fgets(line, sizeof(line), fdinfo) != NULL) { + if (sscanf(line, "flags: %o", &existing_flags) == 1) + break; + } + fclose(fdinfo); + } + if (existing_flags & O_PATH) + return 0; + } + + set_sysnum(tracee, PR_dup); + poke_reg(tracee, SYSARG_1, (word_t) fd_num); + return 0; + } +#endif /* ifndef USERLAND */ + + case PR_setgroups: + case PR_setgroups32: + case PR_getgroups: + case PR_getgroups32: + /* TODO */ +#ifdef USERLAND + /* TODO: need to actually emulate these */ + //On Android, the system is returning gids that our rootfs knows nothing about + //which is generating errors + set_sysnum(tracee, PR_void); + return 0; +#endif + + default: + return 0; + } + + /* Never reached */ + assert(0); + return 0; + +} + +/** + * Adjust current @tracee's syscall result according to @config. This + * function returns -errno if an error occured, otherwise 0. + */ +static int handle_sysexit_end(Tracee *tracee, Config *config) +{ + word_t sysnum; +#ifdef USERLAND + word_t result; +#endif +#ifndef USERLAND + Reg stat_sysarg = SYSARG_2; +#endif + + sysnum = get_sysnum(tracee, ORIGINAL); + +#ifdef USERLAND + if ((get_sysnum(tracee, CURRENT) == PR_fstat) || (get_sysnum(tracee, CURRENT) == PR_fstat64)) { + word_t address; + Reg sysarg; + uid_t uid; + gid_t gid; + + /* Override only if it succeed. */ + result = peek_reg(tracee, CURRENT, SYSARG_RESULT); + if (result != 0) + return 0; + + /* Get the address of the 'stat' structure. */ + sysarg = SYSARG_2; + + address = peek_reg(tracee, ORIGINAL, sysarg); + + /* Sanity checks. */ + assert(__builtin_types_compatible_p(uid_t, uint32_t)); + assert(__builtin_types_compatible_p(gid_t, uint32_t)); + + /* Get the uid & gid values from the 'stat' structure. */ + uid = peek_uint32(tracee, address + offsetof_stat_uid(tracee)); + if (errno != 0) + uid = 0; /* Not fatal. */ + + gid = peek_uint32(tracee, address + offsetof_stat_gid(tracee)); + if (errno != 0) + gid = 0; /* Not fatal. */ + + /* Override only if the file is owned by the current user. + * * Errors are not fatal here. */ + if (uid == getuid()) + poke_uint32(tracee, address + offsetof_stat_uid(tracee), config->suid); + + if (gid == getgid()) + poke_uint32(tracee, address + offsetof_stat_gid(tracee), config->sgid); + + return 0; + } + + if (((sysnum == PR_fstat) || (sysnum == PR_fstat64)) && (get_sysnum(tracee, CURRENT) == PR_readlinkat)) { + int status; + char path[PATH_MAX]; + result = peek_reg(tracee, CURRENT, SYSARG_RESULT); + poke_reg(tracee, SYSARG_RESULT, 0); + if ((int)result <= 0) + return result; + + status = read_sysarg_path(tracee, path, SYSARG_3, MODIFIED); + if(status < 0) + return status; + + path[result] = '\0'; + + if ((strcmp(path + strlen(path) - strlen(" (deleted)"), " (deleted)") == 0) || (strncmp(path, "pipe", 4) == 0)) { + register_chained_syscall(tracee, sysnum, peek_reg(tracee, ORIGINAL, SYSARG_1), peek_reg(tracee, ORIGINAL, SYSARG_2), 0, 0, 0, 0); + } else { + write_data(tracee, peek_reg(tracee, MODIFIED, SYSARG_3), path, sizeof(path)); +# if defined(__x86_64__) + register_chained_syscall(tracee, PR_newfstatat, AT_FDCWD, peek_reg(tracee, MODIFIED, SYSARG_3), peek_reg(tracee, ORIGINAL, SYSARG_2), 0, 0, 0); +# else + register_chained_syscall(tracee, PR_fstatat64, AT_FDCWD, peek_reg(tracee, MODIFIED, SYSARG_3), peek_reg(tracee, ORIGINAL, SYSARG_2), 0, 0, 0); +# endif + } + + return 0; + } +#endif + + switch (sysnum) { + + case PR_setuid: + case PR_setuid32: + SETXID(uid, ORIGINAL); + + case PR_setgid: + case PR_setgid32: + SETXID(gid, ORIGINAL); + + case PR_setreuid: + case PR_setreuid32: + SETREXID(uid, ORIGINAL); + + case PR_setregid: + case PR_setregid32: + SETREXID(gid, ORIGINAL); + + case PR_setresuid: + case PR_setresuid32: + SETRESXID(u, ORIGINAL); + + case PR_setresgid: + case PR_setresgid32: + SETRESXID(g, ORIGINAL); + + case PR_setfsuid: + case PR_setfsuid32: + SETFSXID(u); + + case PR_setfsgid: + case PR_setfsgid32: + SETFSXID(g); + + case PR_prctl: { + /* Mirror the tracee's PR_SET_KEEPCAPS state. The real prctl + * runs on the kernel; we only observe successful calls so the + * fake_id0 cap model stays in sync with what the kernel does + * for the tracee (PR_GET_KEEPCAPS is satisfied by the kernel + * directly and needs no intercept). */ + int op = (int) peek_reg(tracee, ORIGINAL, SYSARG_1); + int result = (int) peek_reg(tracee, CURRENT, SYSARG_RESULT); + if (result == 0 && op == PR_SET_KEEPCAPS) { + word_t arg = peek_reg(tracee, ORIGINAL, SYSARG_2); + config->keep_caps = (arg != 0); + } + return 0; + } + + case PR_getuid: + case PR_getuid32: + poke_reg(tracee, SYSARG_RESULT, config->ruid); + return 0; + + case PR_getgid: + case PR_getgid32: + poke_reg(tracee, SYSARG_RESULT, config->rgid); + return 0; + + case PR_geteuid: + case PR_geteuid32: + poke_reg(tracee, SYSARG_RESULT, config->euid); + return 0; + + case PR_getegid: + case PR_getegid32: + poke_reg(tracee, SYSARG_RESULT, config->egid); + return 0; + + case PR_getresuid: + case PR_getresuid32: + return handle_getresuid_exit_end(tracee, config); + + case PR_getresgid: + case PR_getresgid32: + return handle_getresgid_exit_end(tracee, config); + +#ifdef USERLAND + case PR_umask: + poke_reg(tracee, SYSARG_RESULT, config->umask); + config->umask = (mode_t) peek_reg(tracee, MODIFIED, SYSARG_1); + return 0; + + case PR_setgroups: + case PR_setgroups32: + case PR_getgroups: + case PR_getgroups32: + /*TODO: need to really emulate*/ + poke_reg(tracee, SYSARG_RESULT, 0); + return 0; +#endif + + case PR_setdomainname: + case PR_sethostname: +#ifndef USERLAND + case PR_setgroups: + case PR_setgroups32: +#endif + case PR_mknod: + case PR_mknodat: + case PR_capset: + case PR_chmod: + case PR_chown: + case PR_fchmod: + case PR_fchown: + case PR_lchown: + case PR_chown32: + case PR_fchown32: + case PR_lchown32: + case PR_fchmodat: + case PR_fchownat: + return handle_perm_err_exit_end(tracee, config, false); + + case PR_setxattr: + case PR_lsetxattr: + case PR_fsetxattr: + return handle_perm_err_exit_end(tracee, config, true); + + case PR_socket: + return handle_socket_exit_end(tracee, config); + +#ifndef USERLAND + case PR_fstatat64: + case PR_newfstatat: + stat_sysarg = SYSARG_3; + case PR_stat64: + case PR_lstat64: + case PR_fstat64: + case PR_stat: + case PR_lstat: + case PR_fstat: + return handle_stat_exit_end(tracee, config, stat_sysarg); +#endif /* ifndef USERLAND */ + +#ifdef USERLAND + case PR_fstatat64: + case PR_newfstatat: + case PR_stat64: + case PR_lstat64: + case PR_fstat64: + case PR_stat: + case PR_lstat: + case PR_fstat: + return handle_stat_exit_end(tracee, config, sysnum); +#endif /* ifdef USERLAND */ + + case PR_chroot: + return handle_chroot_exit_end(tracee, config, false); + + case PR_getsockopt: + return handle_getsockopt_exit_end(tracee); + +#ifdef USERLAND +/** Check to see if a meta was created for a file that no longer exists. + * If so, delete it. + */ + case PR_open: + case PR_openat: + case PR_creat: { + int status; + Reg sysarg; + char path[PATH_MAX]; + char meta_path[PATH_MAX]; + + if(sysnum == PR_open || sysnum == PR_creat) + sysarg = SYSARG_1; + else + sysarg = SYSARG_2; + + status = read_sysarg_path(tracee, path, sysarg, MODIFIED); + if(status < 0) + return status; + if(status == 1) + return 0; + + /* If the file exists, it doesn't matter if a metafile exists. */ + if(path_exists(path) == 0) + return 0; + + status = get_meta_path(path, meta_path); + if(status < 0) + return status; + + /* If the metafile exists and the original file does not, delete it. */ + if(path_exists(meta_path) == 0) + status = unlink(meta_path); + + return 0; + } +#endif + + default: + return 0; + } +} + +static int handle_sigsys(Tracee *tracee, Config *config) +{ + word_t sysnum; + + sysnum = get_sysnum(tracee, CURRENT); + switch (sysnum) { + + case PR_setuid: + case PR_setuid32: + SETXID(uid, CURRENT); + + case PR_setgid: + case PR_setgid32: + SETXID(gid, CURRENT); + + case PR_setreuid: + case PR_setreuid32: + SETREXID(uid, CURRENT); + + case PR_setregid: + case PR_setregid32: + SETREXID(gid, CURRENT); + + case PR_setresuid: + case PR_setresuid32: + SETRESXID(u, CURRENT); + + case PR_setresgid: + case PR_setresgid32: + SETRESXID(g, CURRENT); + + case PR_chroot: + return handle_chroot_exit_end(tracee, config, true); + + default: + return 0; + } +} + +static int handle_sysexit_start(Tracee *tracee, Config *config) { + word_t result = peek_reg(tracee, CURRENT, SYSARG_RESULT); + word_t sysnum = get_sysnum(tracee, ORIGINAL); + struct stat mode; + int status; + + if ((int) result < 0 || tracee->status < 0 || sysnum != PR_execve) + return 0; + + /* This has to be done before PRoot pushes the load + * script into tracee's stack. */ + if (!tracee->skip_proot_loader) + adjust_elf_auxv(tracee, config); + + /* Linux clears PR_SET_KEEPCAPS on execve. */ + config->keep_caps = false; + + status = stat(tracee->host_exe, &mode); + if (status < 0) + return 0; /* Not fatal. */ + + if ((mode.st_mode & S_ISUID) != 0) { + config->euid = 0; + config->suid = 0; + /* Setuid-root binary gives the process fake CAP_SETUID. */ + config->caps_active = true; + } + + if ((mode.st_mode & S_ISGID) != 0) { + config->egid = 0; + config->sgid = 0; + } + + return 0; +} + +/** + * Handler for this @extension. It is triggered each time an @event + * occurred. See ExtensionEvent for the meaning of @data1 and @data2. + */ +int fake_id0_callback(Extension *extension, ExtensionEvent event, intptr_t data1, intptr_t data2) +{ + switch (event) { + case INITIALIZATION: { + const char *uid_string = (const char *) data1; + const char *gid_string; + Config *config; + int uid, gid; + + errno = 0; + uid = strtol(uid_string, NULL, 10); + if (errno != 0) + uid = getuid(); + + gid_string = strchr(uid_string, ':'); + if (gid_string == NULL) { + errno = EINVAL; + } + else { + errno = 0; + gid = strtol(gid_string + 1, NULL, 10); + } + /* Fallback to the current gid if an error occured. */ + if (errno != 0) + gid = getgid(); + + extension->config = talloc(extension, Config); + if (extension->config == NULL) + return -1; + + config = talloc_get_type_abort(extension->config, Config); + config->ruid = uid; + config->euid = uid; + config->suid = uid; + config->fsuid = uid; + config->rgid = gid; + config->egid = gid; + config->sgid = gid; + config->fsgid = gid; + config->caps_active = (uid == 0); + config->keep_caps = false; + /* Set the umask to the typical linux value. */ + config->umask = 022; + + extension->filtered_sysnums = filtered_sysnums; + return 0; + } + + case INHERIT_PARENT: /* Inheritable for sub reconfiguration ... */ + return 1; + + case INHERIT_CHILD: { + /* Copy the parent configuration to the child. The + * structure should not be shared as uid/gid changes + * in one process should not affect other processes. + * This assertion is not true for POSIX threads + * sharing the same group, however Linux threads never + * share uid/gid information. As a consequence, the + * GlibC emulates the POSIX behavior on Linux by + * sending a signal to all group threads to cause them + * to invoke the system call too. Finally, PRoot + * doesn't have to worry about clone flags. + */ + + Extension *parent = (Extension *) data1; + extension->config = talloc_zero(extension, Config); + if (extension->config == NULL) + return -1; + + memcpy(extension->config, parent->config, sizeof(Config)); + return 0; + } + + case HOST_PATH: { + Tracee *tracee = TRACEE(extension); + Config *config = talloc_get_type_abort(extension->config, Config); + + /* Force permissions if the tracee was supposed to + * have the capability. */ + if (config->euid == 0) /* TODO: || HAS_CAP(DAC_OVERRIDE) */ + override_permissions(tracee, (char*) data1, (bool) data2); + return 0; + } + +#ifdef USERLAND + /** LINK2SYMLINK is an extension intended to emulate hard links on + * platforms that do not have the capability to create them. In order to + * retain functionality of metafiles, it's necessary to move the metafile + * associated with the file being linked to the end of a symlink chain. + */ + case LINK2SYMLINK_RENAME: { + int status; + char old_meta[PATH_MAX]; + char new_meta[PATH_MAX]; + + status = get_meta_path((char *) data1, old_meta); + if(status < 0) + return status; + + /* If meta doesn't exist, get out. */ + if(path_exists(old_meta) != 0) + return 0; + + status = get_meta_path((char *) data2, new_meta); + if(status < 0) + return status; + + status = rename(old_meta, new_meta); + if(status < 0) + return status; + + return 0; + } + + case LINK2SYMLINK_UNLINK: { + int status; + char meta_path[PATH_MAX]; + + status = get_meta_path((char *) data1, meta_path); + if(status < 0) + return status; + + /* If metafile doesn't already exist, get out */ + if(path_exists(meta_path) != 0) + return 0; + + status = unlink(meta_path); + if(status < 0) + return status; + + return 0; + } +#endif + + case SYSCALL_ENTER_END: { + Tracee *tracee = TRACEE(extension); + Config *config = talloc_get_type_abort(extension->config, Config); + + return handle_sysenter_end(tracee, config); + } + +#ifdef USERLAND + case SYSCALL_CHAINED_EXIT: +#endif + case SYSCALL_EXIT_END: { + Tracee *tracee = TRACEE(extension); + Config *config = talloc_get_type_abort(extension->config, Config); + + return handle_sysexit_end(tracee, config); + } + + case SIGSYS_OCC: { + Tracee *tracee = TRACEE(extension); + Config *config = talloc_get_type_abort(extension->config, Config); + word_t sysnum = get_sysnum(tracee, CURRENT); + int status; + + switch (sysnum) { + + case PR_setuid: + case PR_setuid32: + case PR_setgid: + case PR_setgid32: + case PR_setreuid: + case PR_setreuid32: + case PR_setregid: + case PR_setregid32: + case PR_setresuid: + case PR_setresuid32: + case PR_setresgid: + case PR_setresgid32: + case PR_chroot: + status = handle_sigsys(tracee, config); + if (status < 0) + return status; + break; + + default: + return 0; + } + + return 1; + + } + + case SYSCALL_EXIT_START: { + Tracee *tracee = TRACEE(extension); + Config *config = talloc_get_type_abort(extension->config, Config); + + return handle_sysexit_start(tracee, config); + } + + case STATX_SYSCALL: { + Tracee *tracee = TRACEE(extension); + Config *config = talloc_get_type_abort(extension->config, Config); + + return fake_id0_handle_statx_syscall(tracee, config, data1); + } + + default: + return 0; + } +} + +#undef POKE_MEM_ID +#undef SETXID +#undef UNSET_ID +#undef UNCHANGED_ID +#undef SETREXID +#undef EQUALS_ANY_ID +#undef SETRESXID +#undef SETFSXID diff --git a/core/proot/src/main/cpp/extension/fake_id0/getsockopt.c b/core/proot/src/main/cpp/extension/fake_id0/getsockopt.c new file mode 100644 index 000000000..e0874b417 --- /dev/null +++ b/core/proot/src/main/cpp/extension/fake_id0/getsockopt.c @@ -0,0 +1,42 @@ +#include /* SOL_SOCKET,SO_PEERCRED */ + +#include "tracee/reg.h" +#include "tracee/mem.h" +#include "extension/extension.h" +#include "extension/fake_id0/config.h" +#include "extension/fake_id0/getsockopt.h" + +/** + * Get fake_id0 Config for given pid + * + * If pid isn't under fake_id0 returns NULL + */ +static Config *get_fake_id_for_pid(pid_t pid) +{ + Tracee *tracee = get_tracee(NULL, pid, false); + if (tracee == NULL) + return NULL; + Extension *extension = get_extension(tracee, fake_id0_callback); + if (extension == NULL) + return NULL; + return talloc_get_type_abort(extension->config, Config); +} + +int handle_getsockopt_exit_end(Tracee *tracee) { + if ( + peek_reg(tracee, ORIGINAL, SYSARG_2) == SOL_SOCKET && + peek_reg(tracee, ORIGINAL, SYSARG_3) == SO_PEERCRED && + peek_reg(tracee, CURRENT, SYSARG_RESULT) == 0) { + + struct ucred cred; + word_t cred_addr = peek_reg(tracee, ORIGINAL, SYSARG_4); + int status = read_data(tracee, &cred, cred_addr, sizeof(struct ucred)); + if (status) return 0; + Config *peer_config = get_fake_id_for_pid(cred.pid); + if (peer_config == NULL) return 0; + cred.uid = peer_config->euid; + cred.gid = peer_config->egid; + write_data(tracee, cred_addr, &cred, sizeof(struct ucred)); + } + return 0; +} diff --git a/core/proot/src/main/cpp/extension/fake_id0/getsockopt.h b/core/proot/src/main/cpp/extension/fake_id0/getsockopt.h new file mode 100644 index 000000000..581ab1dfa --- /dev/null +++ b/core/proot/src/main/cpp/extension/fake_id0/getsockopt.h @@ -0,0 +1,8 @@ +#ifndef FAKE_ID0_GETSOCKOPT_H +#define FAKE_ID0_GETSOCKOPT_H + +#include "tracee/tracee.h" + +int handle_getsockopt_exit_end(Tracee *tracee); + +#endif /* FAKE_ID0_GETSOCKOPT_H */ diff --git a/core/proot/src/main/cpp/extension/fake_id0/helper_functions.c b/core/proot/src/main/cpp/extension/fake_id0/helper_functions.c new file mode 100644 index 000000000..3a16ce576 --- /dev/null +++ b/core/proot/src/main/cpp/extension/fake_id0/helper_functions.c @@ -0,0 +1,353 @@ +#include +#include +#include +#include +#include +#include + +#include "tracee/tracee.h" +#include "tracee/reg.h" +#include "tracee/mem.h" +#include "path/path.h" +#include "extension/fake_id0/config.h" +#include "extension/fake_id0/helper_functions.h" + +#define META_TAG ".proot-meta-file." + +#define OWNER_PERMS 0 +#define GROUP_PERMS 1 +#define OTHER_PERMS 2 + +/** Converts a decimal number to its octal representation. Used to convert + * system returned modes to a more common form for humans. + */ +int dtoo(int n) +{ + int rem, i=1, octal=0; + while (n!=0) + { + rem=n%8; + n/=8; + octal+=rem*i; + i*=10; + } + return octal; +} + +/** Converts an octal number to its decimal representation. Used to return to a + * more machine-usable form of mode from human-readable. + */ +int otod(int n) +{ + int decimal=0, i=0, rem; + while (n!=0) + { + int j; + int pow = 1; + for(j = 0; j < i; j++) + pow = pow * 8; + rem = n%10; + n/=10; + decimal += rem*pow; + ++i; + } + return decimal; +} + +/** Determines whether the file specified by path exists. + */ +int path_exists(char path[PATH_MAX]) +{ + return access(path, F_OK); +} + +/** Gets a path from file descriptor system argument. If that sysarg is + * IGNORE_FLAGS, it returns the root of the guestfs, and if the file + * descriptor refers to the cwd, it returns that. Returning the root + * is used in cases where the function is used to find relative paths + * for __at calls. + */ + +int get_fd_path(Tracee *tracee, char path[PATH_MAX], Reg fd_sysarg, RegVersion version) +{ + int status; + + if(fd_sysarg != IGNORE_SYSARG) { + // AT_CWD translates to -100, so replace it with a canonicalized version + if((signed int) peek_reg(tracee, version, fd_sysarg) == -100) + status = getcwd2(tracee, path); + + else { + // See read_sysarg_path for an explanation of the use of modified. + status = readlink_proc_pid_fd(tracee->pid, peek_reg(tracee, version, fd_sysarg), path); + } + if(status < 0) + return status; + } + + else + translate_path(tracee, path, AT_FDCWD, "/", true); + + /** If a path does not belong to the guestfs, a handler either exits with 0 + * or sets the syscall to void (in the case of chmod and chown. + */ + if(!belongs_to_guestfs(tracee, path)) + return 1; + + return 0; +} + +/** Reads a path from path_sysarg into path. + */ + +int read_sysarg_path(Tracee *tracee, char path[PATH_MAX], Reg path_sysarg, RegVersion version) +{ + int size; + char original[PATH_MAX]; + /** Current is already canonicalized. . Modified is used here + * for exit calls because on ARM architectures, the result to a system + * call is placed in SYSARG_1. Using MODIFIED allows the original path to + * be read. ORIGINAL is necessary in the case of execve(2) because of the + * modifications that PRoot makes to the path of the executable. + */ + switch(version) { + case MODIFIED: + size = read_string(tracee, path, peek_reg(tracee, MODIFIED, path_sysarg), PATH_MAX); + break; + case CURRENT: + size = read_string(tracee, path, peek_reg(tracee, CURRENT, path_sysarg), PATH_MAX); + break; + case ORIGINAL: + size = read_string(tracee, original, peek_reg(tracee, ORIGINAL, path_sysarg), PATH_MAX); + translate_path(tracee, path, AT_FDCWD, original, true); + break; + /* Never hit */ + default: + size = 0; //Shut the compiler up + break; + } + + if(size < 0) + return size; + if(size >= PATH_MAX) + return -ENAMETOOLONG; + + /** If a path does not belong to the guestfs, a handler either exits with 0 + * or sets the syscall to void (in the case of chmod and chown). Checking + * whether or not a path belongs to the guestfs only needs to happen if + * that path actually exists. Removing this check will cause some package + * installations to fail because they try to create symlinks with null + * targets. + */ + if(strlen(path) > 0) + if(!belongs_to_guestfs(tracee, path)) + return 1; + + return 0; +} + +/** Gets the final component of a path. + */ +char * get_name(char path[PATH_MAX]) +{ + char *name; + + name = strrchr(path,'/'); + if (name == NULL) + name = path; + else + name++; + + return name; +} + +/** Returns the mode pertinent to the level of permissions the user has. Eg if + * uid 1000 tries to access a file it owns with mode 751, this returns 7. + */ +int get_permissions(char meta_path[PATH_MAX], Config *config, bool uses_real) +{ + int perms; + int omode; + mode_t mode; + uid_t owner, emulated_uid; + gid_t group, emulated_gid; + + int status = read_meta_file(meta_path, &mode, &owner, &group, config); + if(status < 0) + return status; + + if(uses_real) { + emulated_uid = config->ruid; + emulated_gid = config->rgid; + } + else + emulated_uid = config->euid; + emulated_gid = config->egid; + + if (emulated_uid == owner || emulated_uid == 0) + perms = OWNER_PERMS; + else if(emulated_gid == group) + perms = GROUP_PERMS; + else + perms = OTHER_PERMS; + + omode = dtoo(mode); + switch(perms) { + case OWNER_PERMS: + omode /= 10; + case GROUP_PERMS: + omode /= 10; + case OTHER_PERMS: + omode = omode % 10; + } + + /** Root always has RW permissions for every file. Has weird interactions + * with sudo v su, EG su can echo into a file with perms of 400 but sudo cannot. + */ + if(emulated_uid == 0) + omode |= 6; + return omode; +} + +/** Checks permissions on every component of path. Up to the location specifed + * by rel_path. If type is specified to be "read", it checks only execute + * permissions. If type is specified to be "write", it makes sure that the + * parent directory of the file specified by path also has write permissions. + * The permission check uses guest paths only. + */ +int check_dir_perms(Tracee *tracee, char type, char path[PATH_MAX], char rel_path[PATH_MAX], Config *config) +{ + int status, perms; + char meta_path[PATH_MAX]; + char shorten_path[PATH_MAX]; + int x = 1; + int w = 2; + + get_dir_path(path, shorten_path); + status = get_meta_path(shorten_path, meta_path); + if(status < 0) + return status; + + perms = get_permissions(meta_path, config, 0); + + if(type == 'w' && (perms & w) != w) + return -EACCES; + + if(type == 'r' && (perms & x) != x) + return -EACCES; + + while(strcmp(shorten_path, rel_path) != 0 && strlen(rel_path) < strlen(shorten_path)) { + get_dir_path(shorten_path, shorten_path); + if(!belongs_to_guestfs(tracee, shorten_path)) + break; + + status = get_meta_path(shorten_path, meta_path); + if(status < 0) + return status; + + perms = get_permissions(meta_path, config, 0); + if((perms & x) != x) + return -EACCES; + + } + + return 0; +} + +/** Gets a path without its final component. + */ +int get_dir_path(char path[PATH_MAX], char dir_path[PATH_MAX]) +{ + int offset; + + strcpy(dir_path, path); + offset = strlen(dir_path) - 1; + if (offset > 0) { + /* Skip trailing path separators. */ + while (offset > 1 && dir_path[offset] == '/') + offset--; + + /* Search for the previous path separator. */ + while (offset > 1 && dir_path[offset] != '/') + offset--; + + /* Cut the end of the string before the last component. */ + dir_path[offset] = '\0'; + } + return 0; +} + +/** Stores in meta_path the contents of orig_path with the addition of META_TAG + * to the final component. + */ +int get_meta_path(char orig_path[PATH_MAX], char meta_path[PATH_MAX]) +{ + char *filename; + + /*Separate the final component from the path. */ + get_dir_path(orig_path, meta_path); + filename = get_name(orig_path); + + /* Add a / between the final component and the rest of the path. */ + if(strcmp(meta_path, "/") != 0) + strcat(meta_path, "/"); + + if(strlen(meta_path) + strlen(filename) + strlen(META_TAG) >= PATH_MAX) + return -ENAMETOOLONG; + + /* Insert the meta_tag between the path and its final component. */ + strcat(meta_path, META_TAG); + strcat(meta_path, filename); + return 0; +} + +/** Stores in mode, owner, and group the relative information found in the meta + * meta file. If the meta file doesn't exist, it reverts back to the original + * functionality of PRoot, with the addition of setting the mode to 755. + */ + +int read_meta_file(char path[PATH_MAX], mode_t *mode, uid_t *owner, gid_t *group, Config *config) +{ + FILE *fp; + int lcl_mode; + fp = fopen(path, "r"); + if(!fp) { + /* If the metafile doesn't exist, allow overly permissive behavior. */ + *owner = config->euid; + *group = config->egid; + *mode = otod(755); + return 0; + + } + fscanf(fp, "%d %d %d ", &lcl_mode, owner, group); + lcl_mode = otod(lcl_mode); + *mode = (mode_t)lcl_mode; + fclose(fp); + return 0; +} + +/** Writes mode, owner, and group to the meta file specified by path. If + * is_creat is set to true, the umask needs to be used since it would have + * been by a real system call. + */ + +int write_meta_file(char path[PATH_MAX], mode_t mode, uid_t owner, gid_t group, + bool is_creat, Config *config) +{ + FILE *fp; + fp = fopen(path, "w"); + if(!fp) + //Errno is set + return -1; + + /** In syscalls that don't have the ability to create a file (chmod v open) + * for example, the umask isn't used in determining the permissions of the + * the file. + */ + if(is_creat) + mode = (mode & ~(config->umask) & 0777); + + fprintf(fp, "%d\n%d\n%d\n", dtoo(mode), owner, group); + fclose(fp); + return 0; +} diff --git a/core/proot/src/main/cpp/extension/fake_id0/helper_functions.h b/core/proot/src/main/cpp/extension/fake_id0/helper_functions.h new file mode 100644 index 000000000..6c533fe0e --- /dev/null +++ b/core/proot/src/main/cpp/extension/fake_id0/helper_functions.h @@ -0,0 +1,35 @@ +#ifndef FAKE_ID0_HELPER_FUNCTIONS_H +#define FAKE_ID0_HELPER_FUNCTIONS_H + +#include + +#include "tracee/tracee.h" +#include "tracee/reg.h" +#include "extension/fake_id0/config.h" + +#define IGNORE_SYSARG (Reg)2000 + +int check_dir_perms(Tracee *tracee, char type, char path[PATH_MAX], char rel_path[PATH_MAX], Config *config); + +int get_dir_path(char path[PATH_MAX], char dir_path[PATH_MAX]); + +int dtoo(int n); +int otod(int n); + +int get_meta_path(char orig_path[PATH_MAX], char meta_path[PATH_MAX]); + +int read_meta_file(char path[PATH_MAX], mode_t *mode, uid_t *owner, gid_t *group, Config *config); + +int write_meta_file(char path[PATH_MAX], mode_t mode, uid_t owner, gid_t group, bool is_creat, Config *config); + +char * get_name(char path[PATH_MAX]); + +int get_permissions(char meta_path[PATH_MAX], Config *config, bool uses_real); + +int path_exists(char path[PATH_MAX]); + +int get_fd_path(Tracee *tracee, char path[PATH_MAX], Reg fd_sysarg, RegVersion version); + +int read_sysarg_path(Tracee *tracee, char path[PATH_MAX], Reg path_sysarg, RegVersion version); + +#endif /* FAKE_ID0_HELPER_FUNCTIONS_H */ diff --git a/core/proot/src/main/cpp/extension/fake_id0/link.c b/core/proot/src/main/cpp/extension/fake_id0/link.c new file mode 100644 index 000000000..ef984691f --- /dev/null +++ b/core/proot/src/main/cpp/extension/fake_id0/link.c @@ -0,0 +1,49 @@ +#include + +#include "extension/fake_id0/link.h" + +#include "extension/fake_id0/helper_functions.h" + +/** Handles link and linkat. Returns -EACCES if search permission is not + * given for the entire relative oldpath and the entire relative newpath + * except where write permission is needed (on the final directory component). + */ +int handle_link_enter_end(Tracee *tracee, Reg olddirfd_sysarg, Reg oldpath_sysarg, + Reg newdirfd_sysarg, Reg newpath_sysarg, Config *config) +{ + int status; + char oldpath[PATH_MAX]; + char rel_oldpath[PATH_MAX]; + char newpath[PATH_MAX]; + char rel_newpath[PATH_MAX]; + + status = read_sysarg_path(tracee, oldpath, oldpath_sysarg, ORIGINAL); + if(status < 0) + return status; + if(status == 1) + return 0; + + status = read_sysarg_path(tracee, newpath, newpath_sysarg, ORIGINAL); + if(status < 0) + return status; + if(status == 1) + return 0; + + status = get_fd_path(tracee, rel_oldpath, olddirfd_sysarg, ORIGINAL); + if(status < 0) + return status; + + status = get_fd_path(tracee, rel_newpath, newdirfd_sysarg, ORIGINAL); + if(status < 0) + return status; + + status = check_dir_perms(tracee, 'r', oldpath, rel_oldpath, config); + if(status < 0) + return status; + + status = check_dir_perms(tracee, 'w', newpath, rel_newpath, config); + if(status < 0) + return status; + + return 0; +} diff --git a/core/proot/src/main/cpp/extension/fake_id0/link.h b/core/proot/src/main/cpp/extension/fake_id0/link.h new file mode 100644 index 000000000..5fbd29488 --- /dev/null +++ b/core/proot/src/main/cpp/extension/fake_id0/link.h @@ -0,0 +1,10 @@ +#ifndef FAKE_ID0_LINK_H +#define FAKE_ID0_LINK_H + +#include "tracee/tracee.h" +#include "tracee/reg.h" +#include "extension/fake_id0/config.h" + +int handle_link_enter_end(Tracee *tracee, Reg olddirfd_sysarg, Reg oldpath_sysarg, Reg newdirfd_sysarg, Reg newpath_sysarg, Config *config); + +#endif /* FAKE_ID0_LINK_H */ diff --git a/core/proot/src/main/cpp/extension/fake_id0/mk.c b/core/proot/src/main/cpp/extension/fake_id0/mk.c new file mode 100644 index 000000000..082aa0602 --- /dev/null +++ b/core/proot/src/main/cpp/extension/fake_id0/mk.c @@ -0,0 +1,44 @@ +#include + +#include "extension/fake_id0/mk.h" + +#include "extension/fake_id0/helper_functions.h" + +/** Handles mkdir, mkdirat, mknod, and mknodat syscalls. Creates a matching + * meta file. See mkdir(2) and mknod(2) for returned permission errors. + */ +int handle_mk_enter_end(Tracee *tracee, Reg fd_sysarg, Reg path_sysarg, + Reg mode_sysarg, Config *config) +{ + int status; + mode_t mode; + char orig_path[PATH_MAX]; + char rel_path[PATH_MAX]; + char meta_path[PATH_MAX]; + + status = read_sysarg_path(tracee, orig_path, path_sysarg, CURRENT); + if(status < 0) + return status; + if(status == 1) + return 0; + + /* If the path exists, get out. The syscall itself will return EEXIST. */ + if(path_exists(orig_path) == 0) + return 0; + + status = get_meta_path(orig_path, meta_path); + if(status < 0) + return status; + + status = get_fd_path(tracee, rel_path, fd_sysarg, CURRENT); + if(status < 0) + return status; + + status = check_dir_perms(tracee, 'w', orig_path, rel_path, config); + if(status < 0) + return status; + + mode = peek_reg(tracee, ORIGINAL, mode_sysarg); + poke_reg(tracee, mode_sysarg, (mode|0700)); + return write_meta_file(meta_path, mode, config->euid, config->egid, 1, config); +} diff --git a/core/proot/src/main/cpp/extension/fake_id0/mk.h b/core/proot/src/main/cpp/extension/fake_id0/mk.h new file mode 100644 index 000000000..cd426c0d7 --- /dev/null +++ b/core/proot/src/main/cpp/extension/fake_id0/mk.h @@ -0,0 +1,10 @@ +#ifndef FAKE_ID0_MK_H +#define FAKE_ID0_MK_H + +#include "tracee/tracee.h" +#include "tracee/reg.h" +#include "extension/fake_id0/config.h" + +int handle_mk_enter_end(Tracee *tracee, Reg fd_sysarg, Reg path_sysarg, Reg mode_sysarg, Config *config); + +#endif /* FAKE_ID0_MK_H */ diff --git a/core/proot/src/main/cpp/extension/fake_id0/open.c b/core/proot/src/main/cpp/extension/fake_id0/open.c new file mode 100644 index 000000000..066acf6a3 --- /dev/null +++ b/core/proot/src/main/cpp/extension/fake_id0/open.c @@ -0,0 +1,96 @@ +#include +#include +#include +#include +#include + +#include "tracee/reg.h" + +#include "extension/fake_id0/open.h" +#include "extension/fake_id0/helper_functions.h" + +/** Handles open, openat, and creat syscalls. Creates meta files to match the + * creation of new files, or checks the permissions of files that already + * exist given a matching meta file. See open(2) for returned permission + * errors. + */ +int handle_open_enter_end(Tracee *tracee, Reg fd_sysarg, Reg path_sysarg, + Reg flags_sysarg, Reg mode_sysarg, Config *config) +{ + int status, perms, access_mode; + char orig_path[PATH_MAX]; + char rel_path[PATH_MAX]; + char meta_path[PATH_MAX]; + word_t flags; + mode_t mode; + + status = read_sysarg_path(tracee, orig_path, path_sysarg, CURRENT); + if(status < 0) + return status; + if(status == 1) + return 0; + + status = get_meta_path(orig_path, meta_path); + if(status < 0) + return status; + + if(flags_sysarg != IGNORE_SYSARG) + flags = peek_reg(tracee, ORIGINAL, flags_sysarg); + else + flags = O_CREAT; + + /* If the metafile doesn't exist and we aren't creating a new file, get out. */ + if(path_exists(meta_path) != 0 && (flags & O_CREAT) != O_CREAT) + return 0; + + status = get_fd_path(tracee, rel_path, fd_sysarg, CURRENT); + if(status < 0) + return status; + + /** If the open call is a creat call (flags is set to IGNORE_SYSARG in + * handle_sysenter_end) or an open call intended to create a new file + * then we write a new meta file to match. Note that flags is compared + * only to O_CREAT because some utilities (like touch) do not + * use O_TRUNC and O_WRONLY and instead incorporate other flags. + * A value in flags_sysarg of IGNORE_SYSARG signifies a creat(2) call. + */ + if((flags & O_CREAT) == O_CREAT) { + + /** Many open calls include O_CREAT flags even if the file exists + * already. Probably because many things don't check existence and + * just tell open to create a file if it doesn't exist. In the cases + * a file does exist already, the permissions of it still need to be + * checked. + */ + if(path_exists(orig_path) == 0) + goto check; + + status = check_dir_perms(tracee, 'w', meta_path, rel_path, config); + if(status < 0) + return status; + + mode = peek_reg(tracee, ORIGINAL, mode_sysarg); + poke_reg(tracee, mode_sysarg, (mode|0700)); + status = write_meta_file(meta_path, mode, config->euid, config->egid, 1, config); + return status; + } + +check: + + status = check_dir_perms(tracee, 'r', meta_path, rel_path, config); + if(status < 0) + return status; + + perms = get_permissions(meta_path, config, 0); + access_mode = (flags & O_ACCMODE); + + /* 0 = RDONLY, 1 = WRONLY, 2 = RDWR */ + if((access_mode == O_WRONLY && (perms & 2) != 2) || + (access_mode == O_RDONLY && (perms & 4) != 4) || + (access_mode == O_RDWR && (perms & 6) != 6)) { + return -EACCES; + } + + return 0; +} + diff --git a/core/proot/src/main/cpp/extension/fake_id0/open.h b/core/proot/src/main/cpp/extension/fake_id0/open.h new file mode 100644 index 000000000..abac2e34e --- /dev/null +++ b/core/proot/src/main/cpp/extension/fake_id0/open.h @@ -0,0 +1,10 @@ +#ifndef FAKE_ID0_OPEN_H +#define FAKE_ID0_OPEN_H + +#include "tracee/tracee.h" +#include "tracee/reg.h" +#include "extension/fake_id0/config.h" + +int handle_open_enter_end(Tracee *tracee, Reg fd_sysarg, Reg path_sysarg, Reg flags_sysarg, Reg mode_sysarg, Config *config); + +#endif /* FAKE_ID0_OPEN_H */ diff --git a/core/proot/src/main/cpp/extension/fake_id0/rename.c b/core/proot/src/main/cpp/extension/fake_id0/rename.c new file mode 100644 index 000000000..47b517c7f --- /dev/null +++ b/core/proot/src/main/cpp/extension/fake_id0/rename.c @@ -0,0 +1,70 @@ +#include +#include +#include + +#include "extension/fake_id0/rename.h" +#include "extension/fake_id0/helper_functions.h" + +/** Handles rename and renameat syscalls. If a meta file matching the file to + * to be renamed exists, renames the meta file as well. See rename(2) for + * returned permission errors. + */ +int handle_rename_enter_end(Tracee *tracee, Reg oldfd_sysarg, Reg oldpath_sysarg, + Reg newfd_sysarg, Reg newpath_sysarg, Config *config) +{ + int status; + uid_t uid; + gid_t gid; + mode_t mode; + char oldpath[PATH_MAX]; + char newpath[PATH_MAX]; + char rel_oldpath[PATH_MAX]; + char rel_newpath[PATH_MAX]; + char meta_path[PATH_MAX]; + + status = read_sysarg_path(tracee, oldpath, oldpath_sysarg, CURRENT); + if(status < 0) + return status; + if(status == 1) + return 0; + + status = read_sysarg_path(tracee, newpath, newpath_sysarg, CURRENT); + if(status < 0) + return status; + if(status == 1) + return 0; + + status = get_fd_path(tracee, rel_oldpath, oldfd_sysarg, CURRENT); + if(status < 0) + return status; + + status = get_fd_path(tracee, rel_newpath, newfd_sysarg, CURRENT); + if(status < 0) + return status; + + status = check_dir_perms(tracee, 'w', oldpath, rel_oldpath, config); + if(status < 0) + return status; + + status = check_dir_perms(tracee, 'w', newpath, rel_newpath, config); + if(status < 0) + return status; + + // If a meta file exists, "copy" it to the new path. + status = get_meta_path(oldpath, meta_path); + if(status < 0) + return status; + + if(path_exists(meta_path) != 0) + return 0; + + read_meta_file(meta_path, &mode, &uid, &gid, config); + unlink(meta_path); + + strcpy(meta_path, ""); + status = get_meta_path(newpath, meta_path); + if(status < 0) + return status; + + return write_meta_file(meta_path, mode, uid, gid, 0, config); +} diff --git a/core/proot/src/main/cpp/extension/fake_id0/rename.h b/core/proot/src/main/cpp/extension/fake_id0/rename.h new file mode 100644 index 000000000..13ab81f06 --- /dev/null +++ b/core/proot/src/main/cpp/extension/fake_id0/rename.h @@ -0,0 +1,10 @@ +#ifndef FAKE_ID0_RENAME_H +#define FAKE_ID0_RENAME_H + +#include "tracee/tracee.h" +#include "tracee/reg.h" +#include "extension/fake_id0/config.h" + +int handle_rename_enter_end(Tracee *tracee, Reg oldfd_sysarg, Reg oldpath_sysarg, Reg newfd_sysarg, Reg newpath_sysarg, Config *config); + +#endif /* FAKE_ID0_RENAME_H */ diff --git a/core/proot/src/main/cpp/extension/fake_id0/sendmsg.c b/core/proot/src/main/cpp/extension/fake_id0/sendmsg.c new file mode 100644 index 000000000..73392cbc1 --- /dev/null +++ b/core/proot/src/main/cpp/extension/fake_id0/sendmsg.c @@ -0,0 +1,206 @@ +#include /* get*id(2), */ +#include /* cmsghdr, */ +#include /* uid_t, gid_t, get*id(2), */ +#include /* SYS_SENDMSG, */ + +#include "cli/note.h" +#include "tracee/mem.h" +#include "syscall/sysnum.h" +#include "syscall/syscall.h" +#include "extension/fake_id0/sendmsg.h" + +#define MAX_CONTROLLEN 1024 + +static void sendmsg_unpack_control_and_len(const Tracee *tracee, const struct msghdr *msghdr, word_t *out_control, size_t *out_controllen) { +#if defined(ARCH_X86_64) || defined(ARCH_ARM64) + if (is_32on64_mode(tracee)) { + const char *raw_msghdr = (const char *) msghdr; + *out_control = *(uint32_t*) &raw_msghdr[16]; + *out_controllen = *(uint32_t*) &raw_msghdr[20]; + } else +#else + (void) tracee; +#endif + { + *out_control = (word_t) msghdr->msg_control; + *out_controllen = msghdr->msg_controllen; + } +} + +static void sendmsg_pack_control(const Tracee *tracee, struct msghdr *msghdr, word_t control) { +#if defined(ARCH_X86_64) || defined(ARCH_ARM64) + if (is_32on64_mode(tracee)) { + const char *raw_msghdr = (const char *) msghdr; + *(uint32_t*) &raw_msghdr[16] = (uint32_t) control; + } else +#else + (void) tracee; +#endif + { + msghdr->msg_control = (void *) control; + } +} + +static void sendmsg_unpack_cmsghdr(const Tracee *tracee, const struct cmsghdr *cmsghdr, size_t *out_len, int *out_level, int *out_type) { +#if defined(ARCH_X86_64) || defined(ARCH_ARM64) + if (is_32on64_mode(tracee)) { + const uint32_t *cmsghdr_as_ints = (const uint32_t *) cmsghdr; + *out_len = cmsghdr_as_ints[0]; + *out_level = cmsghdr_as_ints[1]; + *out_type = cmsghdr_as_ints[2]; + } else +#else + (void) tracee; +#endif + { + *out_len = cmsghdr->cmsg_len; + *out_level = cmsghdr->cmsg_level; + *out_type = cmsghdr->cmsg_type; + } +} + +int handle_sendmsg_enter_end(Tracee *tracee, word_t sysnum) +{ + /* Read sendmsg header. */ + int status; + unsigned long socketcall_args[3]; + struct msghdr msg = {}; + bool is_socketcall = sysnum == PR_socketcall; + + size_t tracee_sizeof_msghdr = sizeof(struct msghdr); + size_t tracee_sizeof_cmsghdr = sizeof(struct cmsghdr); + size_t tracee_alignofmask_cmsghdr = sizeof(long) - 1; +#if defined(ARCH_X86_64) || defined(ARCH_ARM64) + if (is_32on64_mode(tracee)) { + tracee_sizeof_msghdr = 28; + tracee_sizeof_cmsghdr = 12; + tracee_alignofmask_cmsghdr = 4 - 1; + } +#endif + + if (!is_socketcall) + { + status = read_data(tracee, &msg, peek_reg(tracee, CURRENT, SYSARG_2), tracee_sizeof_msghdr); + if (status < 0) return status; + } + else + { + word_t call = peek_reg(tracee, CURRENT, SYSARG_1); + + /* On i386 opening audit socket is done through socketcall() + * See extension/fake_id0/socket.c + * for non-socketcall handler. */ + if (call == SYS_SOCKET) { + status = read_data(tracee, socketcall_args, peek_reg(tracee, CURRENT, SYSARG_2), sizeof(socketcall_args)); + /* Emulate audit functionality not compiled into kernel + * * if tracee was supposed to have the capability. */ + if ( + socketcall_args[0] == 16 /* AF_NETLINK */ && + socketcall_args[2] == 9 /* NETLINK_AUDIT */) + return -EPROTONOSUPPORT; + } + + /* Check if socketcall(2) is a sendmsg(2) */ + if (call != SYS_SENDMSG) return 0; + + /* Read socketcall args structure */ + status = read_data(tracee, socketcall_args, peek_reg(tracee, CURRENT, SYSARG_2), sizeof(socketcall_args)); + if (status < 0) return status; + + /* Read sendmsg header */ + status = read_data(tracee, &msg, socketcall_args[1], tracee_sizeof_msghdr); + if (status < 0) return status; + } + + size_t msg_controllen; + word_t msg_control; + sendmsg_unpack_control_and_len(tracee, &msg, &msg_control, &msg_controllen); + if (msg_control != 0 && msg_controllen != 0) + { + bool did_modify = 0; + + if (msg.msg_controllen > MAX_CONTROLLEN) { + VERBOSE(tracee, 1, "sendmsg() with msg_controllen=%zu, is_32on64_mode=%d, not doing fixup", msg_controllen, is_32on64_mode(tracee)); + return 0; + } + + /* Read cmsg header. */ + char cmsg_buf[msg_controllen]; + status = read_data(tracee, cmsg_buf, msg_control, msg_controllen); + if (status < 0) return status; + + /* Iterate over control messages. */ + size_t msg_position = 0; + while (msg_position < msg_controllen) + { + if (msg_controllen - msg_position < tracee_sizeof_cmsghdr) { + /* Malformed cmsg - header didn't fit in last entry. */ + return 0; + } + + size_t cmsg_len; + int cmsg_level, cmsg_type; + sendmsg_unpack_cmsghdr(tracee, (const struct cmsghdr *) &cmsg_buf[msg_position], &cmsg_len, &cmsg_level, &cmsg_type); + + if (!( + cmsg_len >= tracee_sizeof_cmsghdr && + cmsg_len <= msg_controllen - msg_position + )) + { + /* Malformed cmsg - header or body didn't fit in entry. */ + return 0; + } + + /* Look into cmsg data. */ + if (cmsg_level == SOL_SOCKET && cmsg_type == SCM_CREDENTIALS) + { + /* cmsg_len != CMSG_LEN(sizeof(struct ucred)) **/ + if (cmsg_len != tracee_sizeof_cmsghdr + sizeof(struct ucred)) + { + /* Malformed cmsg - struct ucred size mismatch. */ + return 0; + } + struct ucred *ucred = (struct ucred *) &cmsg_buf[msg_position + tracee_sizeof_cmsghdr]; + /* Set uid and gid of SCM_CREDENTIALS to ones that proot really has. + * Pid is not changed as we don't fiddle with getpid() */ + ucred->uid = getuid(); + ucred->gid = getgid(); + did_modify = true; + } + + /* Advance to next message (CMSG_NXTHDR). */ + msg_position += (cmsg_len + tracee_alignofmask_cmsghdr) & ~tracee_alignofmask_cmsghdr; + } + if (did_modify) + { + /* Write cmsg data into tracee. */ + msg_control = alloc_mem(tracee, msg_controllen); + if (msg_control == 0) return -ENOMEM; + + status = write_data(tracee, msg_control, cmsg_buf, msg_controllen); + if (status < 0) return -ENOMEM; + + /* Write sendmsg header. */ + word_t sendmsg_header_addr = alloc_mem(tracee, tracee_sizeof_msghdr); + if (sendmsg_header_addr == 0) return -ENOMEM; + + sendmsg_pack_control(tracee, &msg, msg_control); + status = write_data(tracee, sendmsg_header_addr, &msg, tracee_sizeof_msghdr); + if (status < 0) return -ENOMEM; + + /* Write address of new sendmsg header. */ + if (!is_socketcall) + { + poke_reg(tracee, SYSARG_2, sendmsg_header_addr); + } + else + { + socketcall_args[1] = sendmsg_header_addr; + status = set_sysarg_data(tracee, socketcall_args, sizeof(socketcall_args), SYSARG_2); + if (status < 0) return status; + } + } + } + + return 0; +} diff --git a/core/proot/src/main/cpp/extension/fake_id0/sendmsg.h b/core/proot/src/main/cpp/extension/fake_id0/sendmsg.h new file mode 100644 index 000000000..596b42f7c --- /dev/null +++ b/core/proot/src/main/cpp/extension/fake_id0/sendmsg.h @@ -0,0 +1,9 @@ +#ifndef FAKE_ID0_SENDMSG_H +#define FAKE_ID0_SENDMSG_H + +#include "tracee/tracee.h" +#include "arch.h" + +int handle_sendmsg_enter_end(Tracee *tracee, word_t sysnum); + +#endif /* FAKE_ID0_SENGMSG_H */ diff --git a/core/proot/src/main/cpp/extension/fake_id0/socket.c b/core/proot/src/main/cpp/extension/fake_id0/socket.c new file mode 100644 index 000000000..300cc2b6e --- /dev/null +++ b/core/proot/src/main/cpp/extension/fake_id0/socket.c @@ -0,0 +1,23 @@ +#include /* E*, */ + +#include "tracee/reg.h" +#include "extension/fake_id0/socket.h" + +int handle_socket_exit_end(Tracee *tracee, Config *config) { + word_t result; + + /* Override only permission errors. */ + result = peek_reg(tracee, CURRENT, SYSARG_RESULT); + if ((int) result != -EPERM && (int) result != -EACCES) + return 0; + + /* Emulate audit functionality not compiled into kernel + * * if tracee was supposed to have the capability. */ + if ( + peek_reg(tracee, ORIGINAL, SYSARG_1) == 16 /* AF_NETLINK */ && + peek_reg(tracee, ORIGINAL, SYSARG_3) == 9 /* NETLINK_AUDIT */ && + config->euid == 0) /* TODO: || HAS_CAP(...) */ + return -EPROTONOSUPPORT; + + return 0; +} diff --git a/core/proot/src/main/cpp/extension/fake_id0/socket.h b/core/proot/src/main/cpp/extension/fake_id0/socket.h new file mode 100644 index 000000000..f1732e561 --- /dev/null +++ b/core/proot/src/main/cpp/extension/fake_id0/socket.h @@ -0,0 +1,9 @@ +#ifndef FAKE_ID0_SOCKET_H +#define FAKE_ID0_SOCKET_H + +#include "tracee/tracee.h" +#include "extension/fake_id0/config.h" + +int handle_socket_exit_end(Tracee *tracee, Config *config); + +#endif /* FAKE_ID0_SOCKET_H */ diff --git a/core/proot/src/main/cpp/extension/fake_id0/stat.c b/core/proot/src/main/cpp/extension/fake_id0/stat.c new file mode 100644 index 000000000..f4fb012ea --- /dev/null +++ b/core/proot/src/main/cpp/extension/fake_id0/stat.c @@ -0,0 +1,177 @@ +#include +#include /* uid_t, gid_t, get*id(2), */ +#include /* get*id(2), */ +#include /* assert(3), */ + +#include "tracee/mem.h" +#include "syscall/syscall.h" +#include "syscall/sysnum.h" +#include "syscall/seccomp.h" +#include "extension/fake_id0/stat.h" +#include "extension/fake_id0/helper_functions.h" +#include "tracee/statx.h" + +#ifndef USERLAND +int handle_stat_exit_end(Tracee *tracee, Config *config, Reg stat_sysarg) { + word_t address; + uid_t uid; + gid_t gid; + word_t result; + + /* Override only if it succeed. */ + result = peek_reg(tracee, CURRENT, SYSARG_RESULT); + if (result != 0) + return 0; + + address = peek_reg(tracee, ORIGINAL, stat_sysarg); + + /* Sanity checks. */ + assert(__builtin_types_compatible_p(uid_t, uint32_t)); + assert(__builtin_types_compatible_p(gid_t, uint32_t)); + + /* Get the uid & gid values from the 'stat' structure. */ + uid = peek_uint32(tracee, address + offsetof_stat_uid(tracee)); + if (errno != 0) + uid = 0; /* Not fatal. */ + + gid = peek_uint32(tracee, address + offsetof_stat_gid(tracee)); + if (errno != 0) + gid = 0; /* Not fatal. */ + + /* Override only if the file is owned by the current user. + * Errors are not fatal here. */ + if (uid == getuid()) + poke_uint32(tracee, address + offsetof_stat_uid(tracee), config->suid); + + if (gid == getgid()) + poke_uint32(tracee, address + offsetof_stat_gid(tracee), config->sgid); + + return 0; +} +#endif /* ifndef USERLAND */ + +#ifdef USERLAND +/** Convert fstat and fstat64 to readlink + * this is so we can get the path associated with /proc/pid#/fd/fd# + */ +int handle_stat_enter_end(Tracee *tracee, Reg fd_sysarg) { + char path[PATH_MAX]; + char link_path[64]; + word_t link_address; + word_t path_address; + + set_sysnum(tracee, PR_readlinkat); + snprintf(link_path, sizeof(link_path), "/proc/%d/fd/%d", tracee->pid, (int)peek_reg(tracee, CURRENT, fd_sysarg)); + link_address = alloc_mem(tracee, sizeof(link_path)); + path_address = alloc_mem(tracee, sizeof(path)); + write_data(tracee, link_address, link_path, sizeof(link_path)); + poke_reg(tracee, SYSARG_1, AT_FDCWD); + poke_reg(tracee, SYSARG_2, link_address); + poke_reg(tracee, SYSARG_3, path_address); + poke_reg(tracee, SYSARG_4, sizeof(path)); + return 0; +} + +int handle_stat_exit_end(Tracee *tracee, Config *config, word_t sysnum) { + int status = 0; + word_t address; + Reg sysarg; + uid_t uid; + gid_t gid; + mode_t mode; + struct stat my_stat; + char path[PATH_MAX]; + char meta_path[PATH_MAX]; + word_t result; + + /* Override only if it succeed. */ + result = peek_reg(tracee, CURRENT, SYSARG_RESULT); + if (result != 0) + return 0; + + /* Get the pathname of the file to be 'stat'. */ + if(sysnum == PR_fstat || sysnum == PR_fstat64) { + status = read_sysarg_path(tracee, path, SYSARG_2, CURRENT); + } else if(sysnum == PR_fstatat64 || sysnum == PR_newfstatat) + status = read_sysarg_path(tracee, path, SYSARG_2, MODIFIED); + else + status = read_sysarg_path(tracee, path, SYSARG_1, MODIFIED); + + if(status < 0) + return status; + if(status == 1) + return 0; + + /* Get the address of the 'stat' structure. */ + if (sysnum == PR_fstatat64 || sysnum == PR_newfstatat) + sysarg = SYSARG_3; + else + sysarg = SYSARG_2; + + /** If the meta file exists, read the data from it and replace it the + * relevant data in the stat structure. + */ + + status = get_meta_path(path, meta_path); + if(status == 0) { + status = path_exists(meta_path); + if(status == 0) { + read_meta_file(meta_path, &mode, &uid, &gid, config); + + /** Get the file type and sticky/set-id bits of the original + * file and add them to the mode found in the meta_file. + */ + read_data(tracee, &my_stat, peek_reg(tracee, ORIGINAL, sysarg), sizeof(struct stat)); + my_stat.st_mode = (mode | ((my_stat.st_mode & S_IFMT) | (my_stat.st_mode & 07000))); + my_stat.st_uid = uid; + my_stat.st_gid = gid; + write_data(tracee, peek_reg(tracee, ORIGINAL, sysarg), &my_stat, sizeof(struct stat)); + return 0; + } + } + + address = peek_reg(tracee, ORIGINAL, sysarg); + + /* Sanity checks. */ + assert(__builtin_types_compatible_p(uid_t, uint32_t)); + assert(__builtin_types_compatible_p(gid_t, uint32_t)); + + /* Get the uid & gid values from the 'stat' structure. */ + uid = peek_uint32(tracee, address + offsetof_stat_uid(tracee)); + if (errno != 0) + uid = 0; /* Not fatal. */ + + gid = peek_uint32(tracee, address + offsetof_stat_gid(tracee)); + if (errno != 0) + gid = 0; /* Not fatal. */ + + /* Override only if the file is owned by the current user. + * Errors are not fatal here. */ + if (uid == getuid()) + poke_uint32(tracee, address + offsetof_stat_uid(tracee), config->suid); + + if (gid == getgid()) + poke_uint32(tracee, address + offsetof_stat_gid(tracee), config->sgid); + + return 0; +} +#endif /* ifdef USERLAND */ + +int fake_id0_handle_statx_syscall(Tracee *tracee, Config *config, uintptr_t statx_state_raw) { + (void) tracee; + // TODO: USERLAND + struct statx_syscall_state *state = (struct statx_syscall_state *) statx_state_raw; + if (state->statx_buf.stx_mask & STATX_UID) { + if (state->statx_buf.stx_uid == getuid()) { + state->statx_buf.stx_uid = config->suid; + state->updated_stats = true; + } + } + if (state->statx_buf.stx_mask & STATX_GID) { + if (state->statx_buf.stx_gid == getuid()) { + state->statx_buf.stx_gid = config->sgid; + state->updated_stats = true; + } + } + return 0; +} diff --git a/core/proot/src/main/cpp/extension/fake_id0/stat.h b/core/proot/src/main/cpp/extension/fake_id0/stat.h new file mode 100644 index 000000000..bc3bf31be --- /dev/null +++ b/core/proot/src/main/cpp/extension/fake_id0/stat.h @@ -0,0 +1,17 @@ +#ifndef FAKE_ID0_STAT_H +#define FAKE_ID0_STAT_H + +#include "tracee/tracee.h" +#include "tracee/reg.h" +#include "extension/fake_id0/config.h" + +int handle_stat_enter_end(Tracee *tracee, Reg fd_sysarg); +int fake_id0_handle_statx_syscall(Tracee *tracee, Config *config, uintptr_t statx_state_raw); +#ifndef USERLAND +int handle_stat_exit_end(Tracee *tracee, Config *config, Reg stat_sysarg); +#endif +#ifdef USERLAND +int handle_stat_exit_end(Tracee *tracee, Config *config, word_t sysnum); +#endif + +#endif /* FAKE_ID0_STAT_H */ diff --git a/core/proot/src/main/cpp/extension/fake_id0/symlink.c b/core/proot/src/main/cpp/extension/fake_id0/symlink.c new file mode 100644 index 000000000..08e1e1d0e --- /dev/null +++ b/core/proot/src/main/cpp/extension/fake_id0/symlink.c @@ -0,0 +1,38 @@ +#include + +#include "extension/fake_id0/rename.h" +#include "extension/fake_id0/helper_functions.h" + +/** Handles symlink and symlinkat syscalls. Returns -EACCES if search + * permission is not found for the directories except the final in newpath. + * Write permission is required for that directory. Unlike with link(2), + * symlink does not require permissions on the original path. + */ +int handle_symlink_enter_end(Tracee *tracee, Reg oldpath_sysarg, + Reg newdirfd_sysarg, Reg newpath_sysarg, Config *config) +{ + int status; + char oldpath[PATH_MAX]; + char newpath[PATH_MAX]; + char rel_newpath[PATH_MAX]; + + status = read_sysarg_path(tracee, oldpath, oldpath_sysarg, CURRENT); + if(status < 0) + return status; + + status = read_sysarg_path(tracee, newpath, newpath_sysarg, CURRENT); + if(status < 0) + return status; + if(status == 1) + return 0; + + status = get_fd_path(tracee, rel_newpath, newdirfd_sysarg, CURRENT); + if(status < 0) + return status; + + status = check_dir_perms(tracee, 'w', newpath, rel_newpath, config); + if(status < 0) + return status; + + return 0; +} diff --git a/core/proot/src/main/cpp/extension/fake_id0/symlink.h b/core/proot/src/main/cpp/extension/fake_id0/symlink.h new file mode 100644 index 000000000..6364da32f --- /dev/null +++ b/core/proot/src/main/cpp/extension/fake_id0/symlink.h @@ -0,0 +1,10 @@ +#ifndef FAKE_ID0_SYMLINK_H +#define FAKE_ID0_SYMLINK_H + +#include "tracee/tracee.h" +#include "tracee/reg.h" +#include "extension/fake_id0/config.h" + +int handle_symlink_enter_end(Tracee *tracee, Reg oldpath_sysarg, Reg newdirfd_sysarg, Reg newpath_sysarg, Config *config); + +#endif /* FAKE_ID0_SYMLINK_H */ diff --git a/core/proot/src/main/cpp/extension/fake_id0/unlink.c b/core/proot/src/main/cpp/extension/fake_id0/unlink.c new file mode 100644 index 000000000..5f0d0c8fc --- /dev/null +++ b/core/proot/src/main/cpp/extension/fake_id0/unlink.c @@ -0,0 +1,44 @@ +#include +#include + +#include "extension/fake_id0/unlink.h" +#include "extension/fake_id0/helper_functions.h" + +/** Handles unlink, unlinkat, and rmdir syscalls. Checks permissions in meta + * files matching the file to be unlinked if the meta file exists. Unlinks + * the meta file if the call would be successful. See unlink(2) and rmdir(2) + * for returned errors. + */ +int handle_unlink_enter_end(Tracee *tracee, Reg fd_sysarg, Reg path_sysarg, Config *config) +{ + int status; + char orig_path[PATH_MAX]; + char rel_path[PATH_MAX]; + char meta_path[PATH_MAX]; + + status = read_sysarg_path(tracee, orig_path, path_sysarg, CURRENT); + if(status < 0) + return status; + if(status == 1) + return 0; + + status = get_meta_path(orig_path, meta_path); + if(status < 0) + return status; + + status = get_fd_path(tracee, rel_path, fd_sysarg, CURRENT); + if(status < 0) + return status; + + status = check_dir_perms(tracee, 'w', orig_path, rel_path, config); + if(status < 0) + return status; + + /** If the meta_file relating to the file being unlinked exists, + * unlink that as well. + */ + if(path_exists(meta_path) == 0) + unlink(meta_path); + + return 0; +} diff --git a/core/proot/src/main/cpp/extension/fake_id0/unlink.h b/core/proot/src/main/cpp/extension/fake_id0/unlink.h new file mode 100644 index 000000000..c4773cf2a --- /dev/null +++ b/core/proot/src/main/cpp/extension/fake_id0/unlink.h @@ -0,0 +1,10 @@ +#ifndef FAKE_ID0_UNLINK_H +#define FAKE_ID0_UNLINK_H + +#include "tracee/tracee.h" +#include "tracee/reg.h" +#include "extension/fake_id0/config.h" + +extern int handle_unlink_enter_end(Tracee *tracee, Reg fd_sysarg, Reg path_sysarg, Config *config); + +#endif /* FAKE_ID0_UNLINK_H */ diff --git a/core/proot/src/main/cpp/extension/fake_id0/utimensat.c b/core/proot/src/main/cpp/extension/fake_id0/utimensat.c new file mode 100644 index 000000000..288fe6052 --- /dev/null +++ b/core/proot/src/main/cpp/extension/fake_id0/utimensat.c @@ -0,0 +1,63 @@ +#include +#include +#include +#include +#include + +#include "tracee/mem.h" +#include "extension/fake_id0/utimensat.h" +#include "extension/fake_id0/helper_functions.h" + +/** Handles the utimensat syscall. Checks permissions of the meta file if it + * exists and returns an error if the call would not pass according to the + * errors found in utimensat(2). + */ +int handle_utimensat_enter_end(Tracee *tracee, Reg dirfd_sysarg, + Reg path_sysarg, Reg times_sysarg, Config *config) +{ + int status, perms, fd; + struct timespec times[2]; + mode_t ignore_m; + uid_t owner; + gid_t ignore_g; + char path[PATH_MAX]; + char meta_path[PATH_MAX]; + + // Only care about calls that attempt to change something. + status = peek_reg(tracee, ORIGINAL, times_sysarg); + if(status != 0) { + status = read_data(tracee, times, peek_reg(tracee, ORIGINAL, times_sysarg), sizeof(times)); + if(times[0].tv_nsec != UTIME_NOW && times[1].tv_nsec != UTIME_NOW) + return 0; + } + + fd = peek_reg(tracee, ORIGINAL, dirfd_sysarg); + if(fd == AT_FDCWD) { + status = read_sysarg_path(tracee, path, path_sysarg, CURRENT); + if(status < 0) + return status; + if(status == 1) + return 0; + } + else { + status = get_fd_path(tracee, path, dirfd_sysarg, CURRENT); + if(status < 0) + return status; + } + + status = get_meta_path(path, meta_path); + if(status < 0) + return status; + + // Current user must be owner of file or root. + read_meta_file(meta_path, &ignore_m, &owner, &ignore_g, config); + if(config->euid != owner && config->euid != 0) + return -EACCES; + + // If write permissions are on the file, continue. + perms = get_permissions(meta_path, config, 0); + if((perms & 2) != 2) + return -EACCES; + + return 0; +} diff --git a/core/proot/src/main/cpp/extension/fake_id0/utimensat.h b/core/proot/src/main/cpp/extension/fake_id0/utimensat.h new file mode 100644 index 000000000..2b3e80e4a --- /dev/null +++ b/core/proot/src/main/cpp/extension/fake_id0/utimensat.h @@ -0,0 +1,10 @@ +#ifndef FAKE_ID0_UTIMENSAT_H +#define FAKE_ID0_UTIMENSAT_H + +#include "tracee/tracee.h" +#include "tracee/reg.h" +#include "extension/fake_id0/config.h" + +int handle_utimensat_enter_end(Tracee *tracee, Reg dirfd_sysarg, Reg path_sysarg, Reg times_sysarg, Config *config); + +#endif /* FAKE_ID0_UTIMENSAT_H */ diff --git a/core/proot/src/main/cpp/extension/fix_symlink_size/fix_symlink_size.c b/core/proot/src/main/cpp/extension/fix_symlink_size/fix_symlink_size.c new file mode 100644 index 000000000..eddd331f0 --- /dev/null +++ b/core/proot/src/main/cpp/extension/fix_symlink_size/fix_symlink_size.c @@ -0,0 +1,119 @@ +#include /* DIR, struct dirent, opendir, closedir, readdir) */ +#include /* rename(2), */ +#include /* atoi */ +#include /* symlink(2), symlinkat(2), readlink(2), lstat(2), unlink(2), unlinkat(2)*/ +#include /* str*, strrchr, strcat, strcpy, strncpy, strncmp */ +#include /* lstat(2), */ +#include /* lstat(2), */ +#include /* E*, */ +#include /* PATH_MAX, */ + +#include "extension/extension.h" +#include "tracee/tracee.h" +#include "tracee/mem.h" +#include "syscall/syscall.h" +#include "syscall/sysnum.h" +#include "path/path.h" +#include "arch.h" +#include "attribute.h" + +#define PREFIX ".proot.l2s." +#define DELETED_SUFFIX " (deleted)" + +/** + * Make it so fake hard links look like real hard link with respect to number of links and inode + * This function returns -errno if an error occured, otherwise 0. + */ +static int handle_sysexit_end(Tracee *tracee) +{ + word_t sysnum; + + sysnum = get_sysnum(tracee, ORIGINAL); + + switch (sysnum) { + + case PR_lstat64: //int lstat(const char *path, struct stat *buf); + case PR_lstat: { //int lstat(const char *path, struct stat *buf); + word_t result; + Reg sysarg_stat; + Reg sysarg_path; + int status; + struct stat statl; + ssize_t size; + char original[PATH_MAX]; + char intermediate[PATH_MAX]; + + /* Override only if it succeed. */ + result = peek_reg(tracee, CURRENT, SYSARG_RESULT); + if (result != 0) + return 0; + + /*for lstat, the link2symlink extension should have already drilled down to the final final and past a fake hard link + so if the path returned points to a symbolic link, it should be a normal symbolic link*/ + sysarg_path = SYSARG_1; + size = read_string(tracee, original, peek_reg(tracee, MODIFIED, sysarg_path), PATH_MAX); + if (size < 0) + return size; + if (size >= PATH_MAX) + return -ENAMETOOLONG; + + /* Check if it is a link */ + status = lstat(original, &statl); + if (status < 0) {//shouldn't happen + return status; + } + + /* If it is not a link, get out */ + if (!S_ISLNK(statl.st_mode)) { + return 0; + } + + + size = readlink(original, intermediate, PATH_MAX); + if (size < 0) + return size; + + sysarg_stat = SYSARG_2; + + /* Overwrite the stat struct with the correct size. */ + read_data(tracee, &statl, peek_reg(tracee, ORIGINAL, sysarg_stat), sizeof(statl)); + statl.st_size = (off_t)size; + status = write_data(tracee, peek_reg(tracee, ORIGINAL, sysarg_stat), &statl, sizeof(statl)); + if (status < 0) + return status; + + return 0; + } + + default: + return 0; + } +} + +/** + * Handler for this @extension. It is triggered each time an @event + * occurred. See ExtensionEvent for the meaning of @data1 and @data2. + */ +int fix_symlink_size_callback(Extension *extension, ExtensionEvent event, + intptr_t data1 UNUSED, intptr_t data2 UNUSED) +{ + switch (event) { + case INITIALIZATION: { + /* List of syscalls handled by this extensions. */ + static FilteredSysnum filtered_sysnums[] = { + { PR_lstat, FILTER_SYSEXIT }, + { PR_lstat64, FILTER_SYSEXIT }, + FILTERED_SYSNUM_END, + }; + extension->filtered_sysnums = filtered_sysnums; + return 0; + } + + case SYSCALL_EXIT_END: { + return handle_sysexit_end(TRACEE(extension)); + } + + default: + return 0; + } +} diff --git a/core/proot/src/main/cpp/extension/hidden_files/hidden_files.c b/core/proot/src/main/cpp/extension/hidden_files/hidden_files.c new file mode 100644 index 000000000..54e4abce4 --- /dev/null +++ b/core/proot/src/main/cpp/extension/hidden_files/hidden_files.c @@ -0,0 +1,198 @@ +/* + * Author: Dieter Mueller + * Date: 6/12/2015 + * + * Description: An extension to allow getdents to hide files + * with a given prefix. Useful for keeping files from being + * deleted by wildcard expressions. + */ + +#include "extension/extension.h" +#include "tracee/mem.h" +#include "syscall/chain.h" +#include "path/path.h" + +/* Change the HIDDEN_PREFIX to change which files are hidden */ +#define HIDDEN_PREFIX ".proot" + +struct linux_dirent { + unsigned long d_ino; + unsigned long d_off; + unsigned short d_reclen; + char d_name[]; +}; + +struct linux_dirent64 { + unsigned long long d_ino; + long long d_off; + unsigned short d_reclen; + unsigned char d_type; + char d_name[]; +}; + +/* + * Blind copies the given num of bytes from src to dst + */ +static void mybcopy(char *src, char *dst, unsigned int num) { + while(num--) { *(dst++) = *(src++); } +} + +/* + * Compares the given prefix with the given string. + * If str has the given prefix, return 1. Otherwise + * return 0 + */ + +static int hasprefix(char *prefix, char *str) { + while (*prefix && *str && (*(prefix) == *(str))) { + prefix++; + str++; + } + + /* If there is not any prefix left after stepping + * through the strings, then it matches */ + if (!(*prefix)) { return 1; } + return 0; +} + +/** + * Hide all files with a given PREFIX so they don't exist to + * the user + */ +static int handle_getdents(Tracee *tracee) +{ + switch (get_sysnum(tracee, ORIGINAL)) { + case PR_getdents64: + case PR_getdents: { + /* get the result of the syscall, which is the number of bytes read by getdents */ + unsigned int res = peek_reg(tracee, CURRENT, SYSARG_RESULT); + if (res <= 0) { + return res; + } + + /* get the system call arguments */ + word_t orig_start = peek_reg(tracee, CURRENT, SYSARG_2); + unsigned int count = peek_reg(tracee, CURRENT, SYSARG_3); + char orig[count]; + + char path[PATH_MAX]; + int status = readlink_proc_pid_fd(tracee->pid, peek_reg(tracee, ORIGINAL, SYSARG_1), path); + if (status < 0) { + return 0; + } + if(!belongs_to_guestfs(tracee, path)) + return 0; + + /* retrieve the data from getdents */ + status = read_data(tracee, orig, orig_start, res); + if (status < 0) { + return status; + } + + /* allocate a space for the copy of the data we want */ + char copy[count]; + /* curr will hold the current struct we're examining */ + struct linux_dirent64 *curr64; + struct linux_dirent *curr32; + /* pos keeps track of where in memory the copy is */ + char *pos = copy; + /* ptr keeps track of where in memory the original is */ + char *ptr = orig; + /* nleft keeps track of how many bytes we've saved */ + unsigned int nleft = 0; + + /* while we're still within the memory allowed */ + if (get_sysnum(tracee, ORIGINAL) == PR_getdents64) { + while (ptr < orig + res) { + + /* get the current struct */ + curr64 = (struct linux_dirent64 *)ptr; + + /* if the name does not matche a given prefix */ + if (!hasprefix(HIDDEN_PREFIX, curr64->d_name)) { + + /* copy the information */ + mybcopy(ptr, pos, curr64->d_reclen); + + /* move the pos and nleft */ + pos += curr64->d_reclen; + nleft += curr64->d_reclen; + } + /* move to the next linux_dirent */ + ptr += curr64->d_reclen; + } + } else { + while (ptr < orig + res) { + + /* get the current struct */ + curr32 = (struct linux_dirent *)ptr; + + /* if the name does not matche a given prefix */ + if (!hasprefix(HIDDEN_PREFIX, curr32->d_name)) { + + /* copy the information */ + mybcopy(ptr, pos, curr32->d_reclen); + + /* move the pos and nleft */ + pos += curr32->d_reclen; + nleft += curr32->d_reclen; + } + /* move to the next linux_dirent */ + ptr += curr32->d_reclen; + } + } + /* If there is nothing left */ + if (!nleft) { + /* call getdents again */ + if (get_sysnum(tracee, ORIGINAL) == PR_getdents64) + register_chained_syscall(tracee, PR_getdents64, peek_reg(tracee, ORIGINAL, SYSARG_1), orig_start, count, 0, 0, 0); + else + register_chained_syscall(tracee, PR_getdents, peek_reg(tracee, ORIGINAL, SYSARG_1), orig_start, count, 0, 0, 0); + } + else { + /* copy the data back into the register */ + status = write_data(tracee, orig_start, copy, nleft); + if (status < 0) { + return status; + } + /* update the return value to match the data */ + poke_reg(tracee, SYSARG_RESULT, nleft); + } + + /* return successful */ + return 0; + } + + default: + return 0; + } +} + +/** + * Handler for this @extension. It is triggered each time an @event + * occured. See ExtensionEvent for the meaning of @data1 and @data2. + */ +int hidden_files_callback(Extension *extension, ExtensionEvent event, + intptr_t data1 UNUSED, intptr_t data2 UNUSED) +{ + switch (event) { + case INITIALIZATION: { + /* List of syscalls handled by this extension */ + static FilteredSysnum filtered_sysnums[] = { + { PR_getdents, FILTER_SYSEXIT }, + { PR_getdents64, FILTER_SYSEXIT }, + FILTERED_SYSNUM_END, + }; + extension->filtered_sysnums = filtered_sysnums; + return 0; + } + + case SYSCALL_CHAINED_EXIT: + case SYSCALL_EXIT_END: { + return handle_getdents(TRACEE(extension)); + } + + default: + return 0; + } +} diff --git a/core/proot/src/main/cpp/extension/kompat/kompat.c b/core/proot/src/main/cpp/extension/kompat/kompat.c new file mode 100644 index 000000000..5c39942a9 --- /dev/null +++ b/core/proot/src/main/cpp/extension/kompat/kompat.c @@ -0,0 +1,1049 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#include /* intptr_t, */ +#include /* strtoul(3), */ +#include /* KERNEL_VERSION, */ +#include /* assert(3), */ +#include /* uname(2), utsname, */ +#include /* str*(3), memcpy(3), */ +#include /* talloc_*, */ +#include /* AT_*, */ +#include /* linux.git:c0a3a20b */ +#include /* errno, */ +#include /* AT_, */ +#include /* FUTEX_PRIVATE_FLAG */ +#include /* MIN, */ + +#include "extension/extension.h" +#include "syscall/seccomp.h" +#include "syscall/sysnum.h" +#include "syscall/chain.h" +#include "tracee/tracee.h" +#include "tracee/reg.h" +#include "tracee/abi.h" +#include "tracee/mem.h" +#include "execve/auxv.h" +#include "cli/note.h" +#include "arch.h" + +#include "attribute.h" +#include "compat.h" + +#define MAX_ARG_SHIFT 2 +typedef struct { + int expected_release; + word_t new_sysarg_num; + struct { + Reg sysarg; /* first argument to be moved. */ + size_t nb_args; /* number of arguments to be moved. */ + int offset; /* offset to be applied. */ + } shifts[MAX_ARG_SHIFT]; +} Modif; + +#define NONE {{0, 0, 0}} + +typedef struct { + int actual_release; + int virtual_release; + struct utsname utsname; + word_t hwcap; +} Config; + +/** + * Return whether the @expected_release is newer than + * @config->actual_release and older than @config->virtual_release. + */ +static bool needs_kompat(const Config *config, int expected_release) +{ + return (expected_release > config->actual_release + && expected_release <= config->virtual_release); +} + +/** + * Modify the current syscall of @tracee as described by @modif + * regarding the given @config. This function returns whether the + * syscall was modified or not. + */ +static bool modify_syscall(Tracee *tracee, const Config *config, const Modif *modif) +{ + size_t i, j; + word_t syscall; + + assert(config != NULL); + + if (!needs_kompat(config, modif->expected_release)) + return false; + + /* Check if this syscall is supported on this architecture. */ + syscall = detranslate_sysnum(get_abi(tracee), modif->new_sysarg_num); + if (syscall == SYSCALL_AVOIDER) + return false; + + set_sysnum(tracee, modif->new_sysarg_num); + + /* Shift syscall arguments. */ + for (i = 0; i < MAX_ARG_SHIFT; i++) { + Reg sysarg = modif->shifts[i].sysarg; + size_t nb_args = modif->shifts[i].nb_args; + int offset = modif->shifts[i].offset; + + for (j = 0; j < nb_args; j++) { + word_t arg = peek_reg(tracee, CURRENT, sysarg + j); + poke_reg(tracee, sysarg + j + offset, arg); + } + } + + return true; +} + +/** + * Return the numeric value for the given kernel @release. + */ +static int parse_kernel_release(const char *release) +{ + unsigned long major = 0; + unsigned long minor = 0; + unsigned long revision = 0; + char *cursor = (char *)release; + + major = strtoul(cursor, &cursor, 10); + + if (*cursor == '.') { + cursor++; + minor = strtoul(cursor, &cursor, 10); + } + + if (*cursor == '.') { + cursor++; + revision = strtoul(cursor, &cursor, 10); + } + + return KERNEL_VERSION(major, minor, revision); +} + +/** + * Remove @discarded_flags from the given @tracee's @sysarg register + * if the actual kernel release is not compatible with the + * @expected_release. + */ +static void discard_fd_flags(Tracee *tracee, const Config *config, + int discarded_flags, int expected_release, Reg sysarg) +{ + word_t flags; + + if (!needs_kompat(config, expected_release)) + return; + + flags = peek_reg(tracee, CURRENT, sysarg); + poke_reg(tracee, sysarg, flags & ~discarded_flags); +} + +/** + * Replace current @tracee's syscall with an older and compatible one + * whenever it's required, i.e. when the syscall is supported by the + * kernel as specified by @config->virtual_release but it isn't + * supported by the actual kernel. + */ +static int handle_sysenter_end(Tracee *tracee, Config *config) +{ + /* Note: syscalls like "openat" can be replaced by "open" since PRoot + * has canonicalized "fd + path" into "path". */ + switch (get_sysnum(tracee, ORIGINAL)) { + case PR_accept4: { + Modif modif = { + .expected_release = KERNEL_VERSION(2,6,28), + .new_sysarg_num = PR_accept, + .shifts = NONE + }; + modify_syscall(tracee, config, &modif); + return 0; + } + + case PR_dup3: { + Modif modif = { + .expected_release = KERNEL_VERSION(2,6,27), + .new_sysarg_num = PR_dup2, + .shifts = NONE + }; + + /* "If oldfd equals newfd, then dup3() fails with the + * error EINVAL" -- man dup3 */ + if (peek_reg(tracee, CURRENT, SYSARG_1) == peek_reg(tracee, CURRENT, SYSARG_2)) + return -EINVAL; + + modify_syscall(tracee, config, &modif); + return 0; + } + + case PR_epoll_create1: { + bool modified; + Modif modif = { + .expected_release = KERNEL_VERSION(2,6,27), + .new_sysarg_num = PR_epoll_create, + .shifts = NONE + }; + + /* "the size argument is ignored, but must be greater + * than zero" -- man epoll_create */ + modified = modify_syscall(tracee, config, &modif); + if (modified) + poke_reg(tracee, SYSARG_1, 1); + return 0; + } + + case PR_epoll_pwait: { + Modif modif = { + .expected_release = KERNEL_VERSION(2,6,19), + .new_sysarg_num = PR_epoll_wait, + .shifts = NONE + }; + modify_syscall(tracee, config, &modif); + return 0; + } + + case PR_eventfd2: { + bool modified; + word_t flags; + Modif modif = { + .expected_release = KERNEL_VERSION(2,6,27), + .new_sysarg_num = PR_eventfd, + .shifts = NONE + }; + + modified = modify_syscall(tracee, config, &modif); + if (modified) { + /* EFD_SEMAPHORE can't be emulated with eventfd. */ + flags = peek_reg(tracee, CURRENT, SYSARG_2); + if ((flags & EFD_SEMAPHORE) != 0) + return -EINVAL; + } + return 0; + } + + case PR_faccessat: { + Modif modif = { + .expected_release = KERNEL_VERSION(2,6,16), + .new_sysarg_num = PR_access, + .shifts = { [0] = { + .sysarg = SYSARG_2, + .nb_args = 2, + .offset = -1 } + } + }; + modify_syscall(tracee, config, &modif); + return 0; + } + + case PR_fchmodat: { + Modif modif = { + .expected_release = KERNEL_VERSION(2,6,16), + .new_sysarg_num = PR_chmod, + .shifts = { [0] = { + .sysarg = SYSARG_2, + .nb_args = 2, + .offset = -1 } + } + }; + modify_syscall(tracee, config, &modif); + return 0; + } + + case PR_fchownat: { + word_t flags; + Modif modif = { + .expected_release = KERNEL_VERSION(2,6,16), + .shifts = { [0] = { + .sysarg = SYSARG_2, + .nb_args = 3, + .offset = -1 } + } + }; + + flags = peek_reg(tracee, CURRENT, SYSARG_5); + modif.new_sysarg_num = ((flags & AT_SYMLINK_NOFOLLOW) != 0 + ? PR_lchown + : PR_chown); + + modify_syscall(tracee, config, &modif); + return 0; + } + + case PR_fcntl: { + word_t command; + + if (!needs_kompat(config, KERNEL_VERSION(2,6,24))) + return 0; + + command = peek_reg(tracee, ORIGINAL, SYSARG_2); + if (command == F_DUPFD_CLOEXEC) + poke_reg(tracee, SYSARG_2, F_DUPFD); + + return 0; + } + + case PR_newfstatat: + case PR_fstatat64: { + word_t flags; + Modif modif = { + .expected_release = KERNEL_VERSION(2,6,16), + .shifts = { [0] = { + .sysarg = SYSARG_2, + .nb_args = 2, + .offset = -1 } + } + }; + + flags = peek_reg(tracee, CURRENT, SYSARG_4); + if ((flags & ~( + AT_SYMLINK_NOFOLLOW| + AT_NO_AUTOMOUNT| + AT_EMPTY_PATH| + 0x6000 /* AT_STATX_SYNC_TYPE aka. KSTAT_QUERY_FLAGS */ + )) != 0) + return -EINVAL; /* Exposed by LTP. */ + +#if defined(ARCH_X86_64) + if ((flags & AT_SYMLINK_NOFOLLOW) != 0) + modif.new_sysarg_num = (get_abi(tracee) != ABI_2 ? PR_lstat : PR_lstat64); + else + modif.new_sysarg_num = (get_abi(tracee) != ABI_2 ? PR_stat : PR_stat64); +#else + if ((flags & AT_SYMLINK_NOFOLLOW) != 0) + modif.new_sysarg_num = PR_lstat64; + else + modif.new_sysarg_num = PR_stat64; +#endif + + modify_syscall(tracee, config, &modif); + return 0; + } + + case PR_futex: { + word_t operation; + static bool warned = false; + + if (!needs_kompat(config, KERNEL_VERSION(2,6,22)) || config->actual_release == 0) + return 0; + + operation = peek_reg(tracee, CURRENT, SYSARG_2); + if ((operation & FUTEX_PRIVATE_FLAG) == 0) + return 0; + + if (!warned) { + warned = true; + note(tracee, WARNING, USER, + "kompat: this kernel doesn't support private futexes " + "and PRoot can't emulate them. Expect some troubles..."); + } + + poke_reg(tracee, SYSARG_2, operation & ~FUTEX_PRIVATE_FLAG); + return 0; + } + + case PR_futimesat: { + Modif modif = { + .expected_release = KERNEL_VERSION(2,6,16), + .new_sysarg_num = PR_utimes, + .shifts = { [0] = { + .sysarg = SYSARG_2, + .nb_args = 2, + .offset = -1 } + } + }; + modify_syscall(tracee, config, &modif); + return 0; + } + + case PR_inotify_init1: { + Modif modif = { + .expected_release = KERNEL_VERSION(2,6,27), + .new_sysarg_num = PR_inotify_init, + .shifts = NONE + }; + modify_syscall(tracee, config, &modif); + return 0; + } + + case PR_linkat: { + word_t flags; + Modif modif = { + .expected_release = KERNEL_VERSION(2,6,16), + .new_sysarg_num = PR_link, + .shifts = { [0] = { + .sysarg = SYSARG_2, + .nb_args = 1, + .offset = -1 }, + [1] = { + .sysarg = SYSARG_4, + .nb_args = 1, + .offset = -2 } + } + }; + + flags = peek_reg(tracee, CURRENT, SYSARG_5); + if ((flags & ~AT_SYMLINK_FOLLOW) != 0) + return -EINVAL; /* Exposed by LTP. */ + + modify_syscall(tracee, config, &modif); + return 0; + } + + case PR_mkdirat: { + Modif modif = { + .expected_release = KERNEL_VERSION(2,6,16), + .new_sysarg_num = PR_mkdir, + .shifts = { [0] = { + .sysarg = SYSARG_2, + .nb_args = 2, + .offset = -1 } + } + }; + modify_syscall(tracee, config, &modif); + return 0; + } + + case PR_mknodat: { + Modif modif = { + .expected_release = KERNEL_VERSION(2,6,16), + .new_sysarg_num = PR_mknod, + .shifts = { [0] = { + .sysarg = SYSARG_2, + .nb_args = 3, + .offset = -1 } + } + }; + modify_syscall(tracee, config, &modif); + return 0; + } + + case PR_openat: { + bool modified; + Modif modif = { + .expected_release = KERNEL_VERSION(2,6,16), + .new_sysarg_num = PR_open, + .shifts = { [0] = { + .sysarg = SYSARG_2, + .nb_args = 3, + .offset = -1 } + } + }; + modified = modify_syscall(tracee, config, &modif); + discard_fd_flags(tracee, config, O_CLOEXEC, KERNEL_VERSION(2,6,23), + modified ? SYSARG_2 : SYSARG_3); + return 0; + } + + case PR_open: + discard_fd_flags(tracee, config, O_CLOEXEC, KERNEL_VERSION(2,6,23), SYSARG_2); + return 0; + + case PR_pipe2: { + Modif modif = { + .expected_release = KERNEL_VERSION(2,6,27), + .new_sysarg_num = PR_pipe, + .shifts = NONE + }; + modify_syscall(tracee, config, &modif); + return 0; + } + + case PR_pselect6: { + Modif modif = { + .expected_release = KERNEL_VERSION(2,6,16), + .shifts = NONE + }; +#if defined(ARCH_X86_64) + modif.new_sysarg_num = (get_abi(tracee) != ABI_2 ? PR_select : PR__newselect); +#else + modif.new_sysarg_num = PR__newselect; +#endif + + modify_syscall(tracee, config, &modif); + return 0; + } + + case PR_readlinkat: { + Modif modif = { + .expected_release = KERNEL_VERSION(2,6,16), + .new_sysarg_num = PR_readlink, + .shifts = { [0] = { + .sysarg = SYSARG_2, + .nb_args = 3, + .offset = -1} + } + }; + modify_syscall(tracee, config, &modif); + return 0; + } + + case PR_renameat: { + Modif modif = { + .expected_release = KERNEL_VERSION(2,6,16), + .new_sysarg_num = PR_rename, + .shifts = { [0] = { + .sysarg = SYSARG_2, + .nb_args = 1, + .offset =-1 }, + [1] = { + .sysarg = SYSARG_4, + .nb_args = 1, + .offset = -2 } + } + }; + modify_syscall(tracee, config, &modif); + return 0; + } + + case PR_signalfd4: { + bool modified; + Modif modif = { + .expected_release = KERNEL_VERSION(2,6,27), + .new_sysarg_num = PR_signalfd, + .shifts = NONE + }; + + /* "In Linux up to version 2.6.26, the flags argument + * is unused, and must be specified as zero." -- man + * signalfd */ + modified = modify_syscall(tracee, config, &modif); + if (modified) + poke_reg(tracee, SYSARG_4, 0); + return 0; + } + + case PR_socket: + case PR_socketpair: + case PR_timerfd_create: + discard_fd_flags(tracee, config, O_CLOEXEC | O_NONBLOCK, + KERNEL_VERSION(2,6,27), SYSARG_2); + return 0; + + case PR_symlinkat: { + Modif modif = { + .expected_release = KERNEL_VERSION(2,6,16), + .new_sysarg_num = PR_symlink, + .shifts = { [0] = { + .sysarg = SYSARG_3, + .nb_args = 1, + .offset = -1 } + } + }; + modify_syscall(tracee, config, &modif); + return 0; + } + + case PR_unlinkat: { + word_t flags; + Modif modif = { + .expected_release = KERNEL_VERSION(2,6,16), + .shifts = { [0] = { + .sysarg = SYSARG_2, + .nb_args = 1, + .offset = -1 + } + } + }; + + flags = peek_reg(tracee, CURRENT, SYSARG_3); + modif.new_sysarg_num = ((flags & AT_REMOVEDIR) != 0 + ? PR_rmdir + : PR_unlink); + + modify_syscall(tracee, config, &modif); + return 0; + } + + default: + return 0; + } +} + +/** + * Adjust some ELF auxiliary vectors to improve the compatibility. + * This function assumes the "argv, envp, auxv" stuff is pointed to by + * @tracee's stack pointer, as expected right after a successful call + * to execve(2). + */ +static void adjust_elf_auxv(Tracee *tracee, Config *config) +{ + ElfAuxVector *vectors; + ElfAuxVector *vector; + word_t vectors_address; + word_t stack_pointer; + void *argv_envp; + size_t size; + int status; + + vectors_address = get_elf_aux_vectors_address(tracee); + if (vectors_address == 0) + return; + + vectors = fetch_elf_aux_vectors(tracee, vectors_address); + if (vectors == NULL) + return; + + for (vector = vectors; vector->type != AT_NULL; vector++) { + switch (vector->type) { + /* Discard AT_SYSINFO* vectors: they can be used to + * get the OS release number from memory instead of + * from the uname syscall, and only this latter is + * currently hooked by PRoot. */ + case AT_SYSINFO_EHDR: + case AT_SYSINFO: + vector->type = AT_IGNORE; + vector->value = 0; + break; + + case AT_HWCAP: + if (config->hwcap != (word_t) -1) + vector->value = config->hwcap; + break; + + case AT_RANDOM: + /* Skip only if not in forced mode. */ + if (config->actual_release != 0) + goto end; + break; + + default: + break; + } + } + + /* Add the AT_RANDOM vector only if needed. */ + if (!needs_kompat(config, KERNEL_VERSION(2,6,29))) + goto end; + + status = add_elf_aux_vector(&vectors, AT_RANDOM, vectors_address); + if (status < 0) + goto end; /* Not fatal. */ + + /* Since a new vector needs to be added, the ELF auxiliary + * vectors array can't be pushed in place. As a consequence, + * argv[] and envp[] arrays are moved one vector downward to + * make room for the new ELF auxiliary vectors array. + * Remember, the stack layout is as follow right after execve: + * + * argv[], envp[], auxv[] + */ + stack_pointer = peek_reg(tracee, CURRENT, STACK_POINTER); + size = vectors_address - stack_pointer; + argv_envp = talloc_size(tracee->ctx, size); + if (argv_envp == NULL) + goto end; + + status = read_data(tracee, argv_envp, stack_pointer, size); + if (status < 0) + goto end; + + /* Allocate enough room in tracee's stack for the new ELF + * auxiliary vector. */ + stack_pointer -= 2 * sizeof_word(tracee); + vectors_address -= 2 * sizeof_word(tracee); + + /* Note that it is safe to update the stack pointer manually + * since we are in execve sysexit. However it should be done + * before transfering data since the kernel might not allow + * page faults below the stack pointer. */ + poke_reg(tracee, STACK_POINTER, stack_pointer); + + status = write_data(tracee, stack_pointer, argv_envp, size); + if (status < 0) + return; + +end: + push_elf_aux_vectors(tracee, vectors, vectors_address); + return; +} + +/** + * Append to the @tracee's current syscall enough calls to fcntl(@fd) + * in order to set the flags from the original @sysarg register, if + * there are also set in @emulated_flags. + */ +static void emulate_fd_flags(Tracee *tracee, word_t fd, Reg sysarg, int emulated_flags) +{ + word_t flags; + + flags = peek_reg(tracee, ORIGINAL, sysarg); + if (flags == 0) + return; + + if ((emulated_flags & flags & O_CLOEXEC) != 0) + register_chained_syscall(tracee, PR_fcntl, fd, F_SETFD, FD_CLOEXEC, 0, 0, 0); + + if ((emulated_flags & flags & O_NONBLOCK) != 0) + register_chained_syscall(tracee, PR_fcntl, fd, F_SETFL, O_NONBLOCK, 0, 0, 0); + + force_chain_final_result(tracee, peek_reg(tracee, CURRENT, SYSARG_RESULT)); +} + +/** + * Adjust the results/output parameters for syscalls that were + * modified in handle_sysenter_end(). This function returns -errno if + * an error occured, otherwise 0. + */ +static int handle_sysexit_end(Tracee *tracee, Config *config) +{ + word_t result; + word_t sysnum; + int status; + + result = peek_reg(tracee, CURRENT, SYSARG_RESULT); + sysnum = get_sysnum(tracee, ORIGINAL); + + /* Error reported by the kernel. */ + status = (int) result; + if (status < 0) + return 0; + + switch (sysnum) { + case PR_uname: { + word_t address; + + address = peek_reg(tracee, ORIGINAL, SYSARG_1); + + /* The layout of struct utsname does not depend on the + * architecture, it only depends on the kernel + * version. In this regards, this structure is stable + * since < 2.6.0. */ + status = write_data(tracee, address, &config->utsname, sizeof(config->utsname)); + if (status < 0) + return status; + return 0; + } + + case PR_setdomainname: + case PR_sethostname: { + word_t address; + word_t length; + char *name; + + name = (sysnum == PR_setdomainname + ? config->utsname.domainname + : config->utsname.nodename); + + length = peek_reg(tracee, ORIGINAL, SYSARG_2); + if (length > sizeof(config->utsname.domainname) - 1) + return -EINVAL; + + /* Because of the test above. */ + assert(sizeof(config->utsname.domainname) == sizeof(config->utsname.nodename)); + + address = peek_reg(tracee, ORIGINAL, SYSARG_1); + status = read_data(tracee, name, address, length); + if (status < 0) + return status; + + /* "name does not require a terminating null byte." -- + * man 2 set{domain,host}name. */ + name[length] = '\0'; + + return 0; + } + + case PR_accept4: + if (get_sysnum(tracee, MODIFIED) == PR_accept) + emulate_fd_flags(tracee, result, SYSARG_4, O_CLOEXEC | O_NONBLOCK); + return 0; + + case PR_dup3: + if (get_sysnum(tracee, MODIFIED) == PR_dup2) + emulate_fd_flags(tracee, peek_reg(tracee, ORIGINAL, SYSARG_2), + SYSARG_3, O_CLOEXEC | O_NONBLOCK); + return 0; + + case PR_epoll_create1: + if (get_sysnum(tracee, MODIFIED) == PR_epoll_create) + emulate_fd_flags(tracee, result, SYSARG_1, O_CLOEXEC | O_NONBLOCK); + return 0; + + case PR_eventfd2: + if (get_sysnum(tracee, MODIFIED) == PR_eventfd) + emulate_fd_flags(tracee, result, SYSARG_2, O_CLOEXEC | O_NONBLOCK); + return 0; + + case PR_fcntl: { + word_t command; + + if (!needs_kompat(config, KERNEL_VERSION(2,6,24))) + return 0; + + command = peek_reg(tracee, ORIGINAL, SYSARG_2); + if (command != F_DUPFD_CLOEXEC) + return 0; + + register_chained_syscall(tracee, PR_fcntl, result, F_SETFD, FD_CLOEXEC, 0, 0, 0); + force_chain_final_result(tracee, peek_reg(tracee, CURRENT, SYSARG_RESULT)); + return 0; + } + + case PR_inotify_init1: + if (get_sysnum(tracee, MODIFIED) == PR_inotify_init) + emulate_fd_flags(tracee, result, SYSARG_1, O_CLOEXEC | O_NONBLOCK); + return 0; + + case PR_open: + if (needs_kompat(config, KERNEL_VERSION(2,6,23))) + emulate_fd_flags(tracee, result, SYSARG_2, O_CLOEXEC); + return 0; + + case PR_openat: + if (needs_kompat(config, KERNEL_VERSION(2,6,23))) + emulate_fd_flags(tracee, result, SYSARG_3, O_CLOEXEC); + return 0; + + case PR_pipe2: { + int fds[2]; + + if (get_sysnum(tracee, MODIFIED) != PR_pipe) + return 0; + + status = read_data(tracee, fds, peek_reg(tracee, MODIFIED, SYSARG_1), sizeof(fds)); + if (status < 0) + return 0; + + emulate_fd_flags(tracee, fds[0], SYSARG_2, O_CLOEXEC | O_NONBLOCK); + emulate_fd_flags(tracee, fds[1], SYSARG_2, O_CLOEXEC | O_NONBLOCK); + + return 0; + } + + case PR_signalfd4: + if (get_sysnum(tracee, MODIFIED) == PR_signalfd) + emulate_fd_flags(tracee, result, SYSARG_4, O_CLOEXEC | O_NONBLOCK); + return 0; + + case PR_socket: + case PR_timerfd_create: + if (needs_kompat(config, KERNEL_VERSION(2,6,27))) + emulate_fd_flags(tracee, result, SYSARG_2, O_CLOEXEC | O_NONBLOCK); + return 0; + + case PR_socketpair: { + int fds[2]; + + if (!needs_kompat(config, KERNEL_VERSION(2,6,27))) + return 0; + + status = read_data(tracee, fds, peek_reg(tracee, MODIFIED, SYSARG_4), sizeof(fds)); + if (status < 0) + return 0; + + emulate_fd_flags(tracee, fds[0], SYSARG_2, O_CLOEXEC | O_NONBLOCK); + emulate_fd_flags(tracee, fds[1], SYSARG_2, O_CLOEXEC | O_NONBLOCK); + + return 0; + } + + default: + return 0; + } + + return 0; +} + +/** + * Fill @config->utsname and @config->hwcap according to the content + * of @string. This function returns -1 if there is a parsing error, + * otherwise 0. + */ +static int parse_utsname(Config *config, const char *string) +{ + struct utsname utsname; + int status; + + assert(string != NULL); + + status = uname(&utsname); + if (status < 0 || getenv("PROOT_FORCE_KOMPAT") != NULL) + config->actual_release = 0; + else + config->actual_release = parse_kernel_release(utsname.release); + + /* Check whether it is the simple format (ie. release number), + * or the complex one: + * + * '\sysname\nodename\release\version\machine\domainname\hwcap\' + * + * This complex format is ugly on purpose: it ain't to be used + * directly by users. */ + if (string[0] == '\\') { + const char *start; + const char *end; + char *end2; + + /* Initial state of the parser. */ + end = string; + +#define PARSE(field) do { \ + size_t length; \ + \ + start = end + 1; \ + end = strchr(start, '\\'); \ + if (end == NULL) { \ + note(NULL, ERROR, USER, \ + "can't find %s field in '%s'", #field, string); \ + return -1; \ + } \ + \ + length = end - start; \ + length = MIN(length, sizeof(config->utsname.field) - 1); \ + strncpy(config->utsname.field, start, length); \ + config->utsname.field[length] = '\0'; \ + } while(0) + + PARSE(sysname); + PARSE(nodename); + PARSE(release); + PARSE(version); + PARSE(machine); + PARSE(domainname); + +#undef PARSE + + /* The hwcap field is parsed as an hexadecimal value. */ + errno = 0; + config->hwcap = strtol(end + 1, &end2, 16); + if (errno != 0 || end2[0] != '\\') { + note(NULL, ERROR, USER, "can't find hwcap field in '%s'", string); + return -1; + } + } + else { + size_t length; + + memcpy(&config->utsname, &utsname, sizeof(config->utsname)); + + length = MIN(strlen(string), sizeof(config->utsname.release) - 1); + strncpy(config->utsname.release, string, length); + config->utsname.release[length] = '\0'; + + config->hwcap = (word_t) -1; + } + + config->virtual_release = parse_kernel_release(config->utsname.release); + + return 0; +} + +/* List of syscalls handled by this extensions. */ +static FilteredSysnum filtered_sysnums[] = { + { PR_accept4, FILTER_SYSEXIT }, + { PR_dup3, FILTER_SYSEXIT }, + { PR_epoll_create1, FILTER_SYSEXIT }, + { PR_epoll_pwait, 0 }, + { PR_eventfd2, FILTER_SYSEXIT }, + { PR_execve, FILTER_SYSEXIT }, + { PR_faccessat, 0 }, + { PR_fchmodat, 0 }, + { PR_fchownat, 0 }, + { PR_fcntl, FILTER_SYSEXIT }, + { PR_fstatat64, 0 }, + { PR_futimesat, 0 }, + { PR_futex, 0 }, + { PR_inotify_init1, FILTER_SYSEXIT }, + { PR_linkat, 0 }, + { PR_mkdirat, 0 }, + { PR_mknodat, 0 }, + { PR_newfstatat, 0 }, + { PR_open, FILTER_SYSEXIT }, + { PR_openat, FILTER_SYSEXIT }, + { PR_pipe2, FILTER_SYSEXIT }, + { PR_pselect6, 0 }, + { PR_readlinkat, 0 }, + { PR_renameat, 0 }, + { PR_setdomainname, FILTER_SYSEXIT }, + { PR_sethostname, FILTER_SYSEXIT }, + { PR_signalfd4, FILTER_SYSEXIT }, + { PR_socket, FILTER_SYSEXIT }, + { PR_socketpair, FILTER_SYSEXIT }, + { PR_symlinkat, 0 }, + { PR_timerfd_create, FILTER_SYSEXIT }, + { PR_uname, FILTER_SYSEXIT }, + { PR_unlinkat, 0 }, + FILTERED_SYSNUM_END, +}; + +/** + * Handler for this @extension. It is triggered each time an @event + * occured. See ExtensionEvent for the meaning of @data1 and @data2. + */ +int kompat_callback(Extension *extension, ExtensionEvent event, + intptr_t data1, intptr_t data2 UNUSED) +{ + int status; + + switch (event) { + case INITIALIZATION: { + Config *config; + + extension->config = talloc_zero(extension, Config); + if (extension->config == NULL) + return -1; + config = extension->config; + + status = parse_utsname(config, (const char *) data1); + if (status < 0) + return -1; + + extension->filtered_sysnums = filtered_sysnums; + return 0; + } + + case SYSCALL_ENTER_END: { + Tracee *tracee = TRACEE(extension); + Config *config = talloc_get_type_abort(extension->config, Config); + + /* Nothing to do if this syscall is being discarded + * (because of an error detected by PRoot). */ + if ((int) data1 < 0) + return 0; + + return handle_sysenter_end(tracee, config); + } + + case SYSCALL_EXIT_END: { + Tracee *tracee = TRACEE(extension); + Config *config = talloc_get_type_abort(extension->config, Config); + + return handle_sysexit_end(tracee, config); + } + + case SYSCALL_EXIT_START: { + Tracee *tracee = TRACEE(extension); + Config *config = talloc_get_type_abort(extension->config, Config); + word_t result = peek_reg(tracee, CURRENT, SYSARG_RESULT);; + word_t sysnum = get_sysnum(tracee, ORIGINAL); + + /* Note: this can be done only before PRoot pushes the + * load script into tracee's stack. */ + if ((int) result >= 0 && sysnum == PR_execve) + adjust_elf_auxv(tracee, config); + return 0; + } + + default: + return 0; + } +} diff --git a/core/proot/src/main/cpp/extension/link2symlink/link2symlink.c b/core/proot/src/main/cpp/extension/link2symlink/link2symlink.c new file mode 100644 index 000000000..d5f6241d2 --- /dev/null +++ b/core/proot/src/main/cpp/extension/link2symlink/link2symlink.c @@ -0,0 +1,816 @@ +#include /* rename(2), */ +#include /* atoi */ +#include /* symlink(2), symlinkat(2), readlink(2), lstat(2), unlink(2), unlinkat(2)*/ +#include /* str*, strrchr, strcat, strcpy, strncpy, strncmp */ +#include /* lstat(2), */ +#include /* lstat(2), */ +#include /* E*, */ +#include /* PATH_MAX, */ +#include /* isdigit, */ + +#include "cli/note.h" +#include "extension/extension.h" +#include "tracee/tracee.h" +#include "tracee/mem.h" +#include "tracee/statx.h" +#include "syscall/syscall.h" +#include "syscall/sysnum.h" +#include "path/path.h" +#include "path/f2fs-bug.h" +#include "arch.h" +#include "attribute.h" + +#ifdef USERLAND +#define PREFIX ".proot.l2s." +#endif +#ifndef USERLAND +#define PREFIX ".l2s." +#endif +#define DELETED_SUFFIX " (deleted)" + +static int decrement_link_count(Tracee *tracee, Reg sysarg); + +/** + * Copy the contents of the @symlink into @value (nul terminated). + * This function returns -errno if an error occured, otherwise 0. + */ +static int my_readlink(const char symlink[PATH_MAX], char value[PATH_MAX]) +{ + ssize_t size; + + size = readlink(symlink, value, PATH_MAX); + if (size < 0) + return size; + if (size >= PATH_MAX) + return -ENAMETOOLONG; + value[size] = '\0'; + + return 0; +} + +/** + * Move the path pointed to by @tracee's @sysarg to a new location, + * symlink the original path to this new one, make @tracee's @sysarg + * point to the new location. This function returns -errno if an + * error occured, otherwise 0. + */ +static int move_and_symlink_path(Tracee *tracee, Reg sysarg, Reg link_target_sysarg) +{ + char original[PATH_MAX]; + char intermediate[PATH_MAX]; + char new_intermediate[PATH_MAX]; + char final[PATH_MAX]; + char new_final[PATH_MAX]; + char * name; + const char * l2s_directory; + struct stat statl; + ssize_t size; + int status; + int link_count; + int first_link = 1; + int intermediate_suffix = 1; + + /* Note: this path was already canonicalized. */ + size = read_string(tracee, original, peek_reg(tracee, CURRENT, sysarg), PATH_MAX); + if (size < 0) + return size; + if (size >= PATH_MAX) + return -ENAMETOOLONG; + + /* Sanity check: directories can't be linked. */ + status = lstat(original, &statl); + if (status < 0) + return errno > 0 ? -errno : -ENOENT; + if (S_ISDIR(statl.st_mode)) + return -EPERM; + + /* Check if it is a symbolic link. */ + if (S_ISLNK(statl.st_mode)) { + /* get name */ + size = my_readlink(original, intermediate); + if (size < 0) + return size; + + name = strrchr(intermediate, '/'); + if (name == NULL) + name = intermediate; + else + name++; + + if (strncmp(name, PREFIX, strlen(PREFIX)) == 0) + first_link = 0; + } else { + /* compute new name */ + name = strrchr(original,'/'); + if (name == NULL) + name = original; + else + name++; + + l2s_directory = getenv("PROOT_L2S_DIR"); + if (l2s_directory != NULL && l2s_directory[0]) { + if (strlen(PREFIX) + strlen(l2s_directory) + (strlen(original) - strlen(name)) + 6 >= PATH_MAX) + return -ENAMETOOLONG; + + strcpy(intermediate, l2s_directory); + if (l2s_directory[strlen(l2s_directory) - 1] != '/') { + strcat(intermediate, "/"); + } + } else { + if (strlen(PREFIX) + strlen(original) + 5 >= PATH_MAX) + return -ENAMETOOLONG; + + strncpy(intermediate, original, strlen(original) - strlen(name)); + intermediate[strlen(original) - strlen(name)] = '\0'; + } + strcat(intermediate, PREFIX); + strcat(intermediate, name); + } + + if (first_link) { + /*Move the original content to the new path. */ + do { + sprintf(new_intermediate, "%s%04d", intermediate, intermediate_suffix); + intermediate_suffix++; + } while ((access(new_intermediate,F_OK) != -1) && (intermediate_suffix < 1000)); + strcpy(intermediate, new_intermediate); + + strcpy(final, intermediate); + strcat(final, ".0002"); + status = rename(original, final); + if (status < 0) + return status; + status = notify_extensions(tracee, LINK2SYMLINK_RENAME, (intptr_t) original, (intptr_t) final); + if (status < 0) + return status; + + /* Symlink the intermediate to the final file. */ + status = symlink(final, intermediate); + if (status < 0) + return status; + + /* Symlink the original path to the intermediate one. */ + status = symlink(intermediate, original); + if (status < 0) + return status; + } else { + /*Move the original content to new location, by incrementing count at end of path. */ + size = my_readlink(intermediate, final); + if (size < 0) + return size; + + link_count = atoi(final + strlen(final) - 4); + link_count++; + + strncpy(new_final, final, strlen(final) - 4); + sprintf(new_final + strlen(final) - 4, "%04d", link_count); + + status = rename(final, new_final); + if (status < 0) + return status; + status = notify_extensions(tracee, LINK2SYMLINK_RENAME, (intptr_t) final, (intptr_t) new_final); + if (status < 0) + return status; + strcpy(final, new_final); + /* Symlink the intermediate to the final file. */ + status = unlink(intermediate); + if (status < 0) + return status; + status = symlink(final, intermediate); + if (status < 0) + return status; + } + + /* Perform symlink() operation within PRoot. */ + status = read_path(tracee, final, peek_reg(tracee, CURRENT, link_target_sysarg)); + if (status >= 0) { + status = symlink(intermediate, final); + if (status < 0) status = -errno; + } + if (status < 0) { + status = -errno; + decrement_link_count(tracee, sysarg); + return status; + } + poke_reg(tracee, SYSARG_RESULT, 0); + set_sysnum(tracee, PR_void); + + return 0; +} + + +/* If path points a file that is a symlink to a file that begins + * with PREFIX, let the file be deleted, but also delete the + * symlink that was created and decremnt the count that is tacked + * to end of original file. + */ +static int decrement_link_count(Tracee *tracee, Reg sysarg) +{ + char original[PATH_MAX]; + char intermediate[PATH_MAX]; + char final[PATH_MAX]; + char new_final[PATH_MAX]; + char * name; + struct stat statl; + ssize_t size; + int status; + int link_count; + + /* Note: this path was already canonicalized. */ + size = read_string(tracee, original, peek_reg(tracee, CURRENT, sysarg), PATH_MAX); + if (size < 0) + return size; + if (size >= PATH_MAX) + return -ENAMETOOLONG; + + /* Check if it is a converted link already. */ + status = lstat(original, &statl); + if (status < 0) + return 0; + + if (!S_ISLNK(statl.st_mode)) + return 0; + + size = my_readlink(original, intermediate); + if (size < 0) + return size; + + name = strrchr(intermediate, '/'); + if (name == NULL) + name = intermediate; + else + name++; + + /* Check if an l2s file is pointed to */ + if (strncmp(name, PREFIX, strlen(PREFIX)) != 0) + return 0; + + /* Read intermediate link - if this fails then + * this link2symlink is broken and we silently + * skip as we were removing it anyway. */ + size = my_readlink(intermediate, final); + if (size < 0) { + VERBOSE(tracee, 1, "Skiping deref of broken link2symlink \"%s\" -> \"%s\"", original, intermediate); + return 0; + } + + link_count = atoi(final + strlen(final) - 4); + link_count--; + + /* Check if it is or is not the last link to delete */ + if (link_count > 0) { + strncpy(new_final, final, strlen(final) - 4); + sprintf(new_final + strlen(final) - 4, "%04d", link_count); + + status = rename(final, new_final); + if (status < 0) + return status; + status = notify_extensions(tracee, LINK2SYMLINK_RENAME, (intptr_t) final, (intptr_t) new_final); + if (status < 0) + return status; + + strcpy(final, new_final); + + /* Symlink the intermediate to the final file. */ + status = unlink(intermediate); + if (status < 0) + return status; + + status = symlink(final, intermediate); + if (status < 0) + return status; + } else { + /* If it is the last, delete the intermediate and final */ + status = unlink(intermediate); + if (status < 0) + return status; + status = unlink(final); + if (status < 0) + return status; + status = notify_extensions(tracee, LINK2SYMLINK_UNLINK, (intptr_t) final, 0); + if (status < 0) + return status; + } + + return 0; +} + +/** + * sizeof(struct stat) cut to contain only fields that are at same addresses + * regardless of whenever tracee is 32-bit or 64-bit. + * + * This allows modification of following fields: + * - st_dev + * - st_mode + * - st_nlink + * - st_uid + * - st_gid + * - st_rdev + * - st_size + * - st_blksize + * - st_blocks + */ +#define SIZEOF_RELEVANT_STRUCT_STAT 72 + +/** + * Make it so fake hard links look like real hard link with respect to number of links and inode + * This function returns -errno if an error occured, otherwise 0. + */ +static int handle_sysexit_end(Tracee *tracee) +{ + word_t sysnum; + + sysnum = get_sysnum(tracee, ORIGINAL); + + #ifdef USERLAND + if ((get_sysnum(tracee, CURRENT) == PR_fstat) || (get_sysnum(tracee, CURRENT) == PR_fstat64)) + return 0; + + if (((sysnum == PR_fstat) || (sysnum == PR_fstat64)) && (get_sysnum(tracee, CURRENT) == PR_readlinkat)) + return 0; + #endif + + switch (sysnum) { + + case PR_fstatat64: //int fstatat(int dirfd, const char *pathname, struct stat *buf, int flags); + case PR_newfstatat: //int fstatat(int dirfd, const char *pathname, struct stat *buf, int flags); + case PR_stat64: //int stat(const char *path, struct stat *buf); + case PR_lstat64: //int lstat(const char *path, struct stat *buf); + case PR_fstat64: //int fstat(int fd, struct stat *buf); + case PR_stat: //int stat(const char *path, struct stat *buf); + case PR_lstat: //int lstat(const char *path, struct stat *buf); + case PR_fstat: { //int fstat(int fd, struct stat *buf); + word_t result; + Reg sysarg_stat; + Reg sysarg_path; + int status; + struct stat statl = {}; + ssize_t size; + char original[PATH_MAX]; + char intermediate[PATH_MAX]; + char final[PATH_MAX]; + char * name; + struct stat finalStat; + + /* Override only if it succeed. */ + result = peek_reg(tracee, CURRENT, SYSARG_RESULT); + if (result != 0) + return 0; + + if (sysnum == PR_fstat64 || sysnum == PR_fstat) { + #ifndef USERLAND + status = readlink_proc_pid_fd(tracee->pid, peek_reg(tracee, MODIFIED, SYSARG_1), original); + if (status < 0) { + VERBOSE(tracee, 3, "link2symlink: readlink_proc_pid_fd failed, status=%d", status); + return 0; // Don't alter syscall result + } + if (strlen(original) > strlen(DELETED_SUFFIX) && + strcmp(original + strlen(original) - strlen(DELETED_SUFFIX), DELETED_SUFFIX) == 0) + original[strlen(original) - strlen(DELETED_SUFFIX)] = '\0'; + #endif + #ifdef USERLAND + size = read_string(tracee, original, peek_reg(tracee, CURRENT, SYSARG_2), PATH_MAX); + if (size < 0) + return size; + if (size >= PATH_MAX) + return -ENAMETOOLONG; + #endif + } else { + if (sysnum == PR_fstatat64 || sysnum == PR_newfstatat) + sysarg_path = SYSARG_2; + else + sysarg_path = SYSARG_1; + size = read_string(tracee, original, peek_reg(tracee, MODIFIED, sysarg_path), PATH_MAX); + if (size < 0) + return size; + if (size >= PATH_MAX) + return -ENAMETOOLONG; + } + + name = strrchr(original, '/'); + if (name == NULL) + name = original; + else + name++; + + /* Check if it is a link */ + status = lstat(original, &statl); + + if (strncmp(name, PREFIX, strlen(PREFIX)) == 0) { + if (S_ISLNK(statl.st_mode)) { + strcpy(intermediate,original); + goto intermediate_proc; + } else { + strcpy(final,original); + goto final_proc; + } + } + + if (!S_ISLNK(statl.st_mode)) + return 0; + + size = my_readlink(original, intermediate); + if (size < 0) + return size; + + name = strrchr(intermediate, '/'); + if (name == NULL) + name = intermediate; + else + name++; + + if (strncmp(name, PREFIX, strlen(PREFIX)) != 0) + return 0; + + intermediate_proc: size = my_readlink(intermediate, final); + if (size < 0) + return size; + + final_proc: status = lstat(final,&finalStat); + if (status < 0) + return status; + + finalStat.st_nlink = atoi(final + strlen(final) - 4); + + /* Get the address of the 'stat' structure. */ + if (sysnum == PR_fstatat64 || sysnum == PR_newfstatat) + sysarg_stat = SYSARG_3; + else + sysarg_stat = SYSARG_2; + + #ifdef USERLAND + /* Overwrite the stat struct with the correct number of "links". */ + read_data(tracee, &statl, peek_reg(tracee, ORIGINAL, sysarg_stat), sizeof(statl)); + finalStat.st_mode = statl.st_mode; + finalStat.st_uid = statl.st_uid; + finalStat.st_gid = statl.st_gid; + #endif + status = write_data(tracee, peek_reg(tracee, ORIGINAL, sysarg_stat), &finalStat, + is_32on64_mode(tracee) ? SIZEOF_RELEVANT_STRUCT_STAT : sizeof(finalStat)); + if (status < 0) + return status; + + return 0; + } + + default: + return 0; + } +} + +static void link2symlink_handle_statx(struct statx_syscall_state *state) +{ + if (!(state->statx_buf.stx_mask & STATX_NLINK)) + return; + + const char *path_ending = strrchr(state->host_path, '/'); + if (NULL == path_ending) + return; + + size_t ending_len = strlen(path_ending); + if (ending_len < strlen(PREFIX) + 6) /* 6 = strlen("/") + strlen(".0002") */ + return; + + if (0 != strncmp(path_ending + 1, PREFIX, strlen(PREFIX))) + return; + + if (path_ending[ending_len - 5] != '.') + return; + + for (size_t i = 1; i <= 4; i++) { + if (!isdigit(path_ending[ending_len - i])) + return; + } + + state->statx_buf.stx_nlink = atoi(&path_ending[ending_len - 4]); +} + +/** + * When @translated_path is a faked hard-link, replace it with the + * point it (internally) points to. + */ +static void translated_path(Tracee *tracee, char translated_path[PATH_MAX]) +{ + char path2[PATH_MAX]; + char path[PATH_MAX]; + char *component; + int status; + + /* Don't translate l2s symlinks if call is (un)link */ + Sysnum sysnum = get_sysnum(tracee, ORIGINAL); + if ( sysnum == PR_unlink + || sysnum == PR_unlinkat + || sysnum == PR_link + || sysnum == PR_linkat + || sysnum == PR_rename + || sysnum == PR_renameat + || sysnum == PR_renameat2) { + return; + } + + if (should_skip_file_access_due_to_f2fs_bug(tracee, translated_path)) + return; + + status = my_readlink(translated_path, path); + if (status < 0) + return; + + component = strrchr(path, '/'); + if (component == NULL) + return; + component++; + + if (strncmp(component, PREFIX, strlen(PREFIX)) != 0) + return; + + status = my_readlink(path, path2); + if (status < 0) + return; + +#if 0 /* Sanity check. */ + component = strrchr(path, '/'); + if (component == NULL) + return; + component++; + + if (strncmp(component, PREFIX, strlen(PREFIX)) != 0) + return; +#endif + + strcpy(translated_path, path2); + return; +} + +/** + * Handler for linkat(..., "/proc/X/fd/Y", ..., AT_SYMLINK_FOLLOW) + * + * Returns: + * 1 if operation was handled successfully + * (Syscall should be marked as successful without further actions) + * 0 if this wasn't linkat from /proc//fd + * (Caller should proceed with usual link2symlink) + * <0 if operation failed + * (Syscall should be marked as failed without further actions) + */ +static int handle_linkat_from_proc_fd(Tracee *tracee) { + /* Read source path, return if it doesn't belong to /proc */ + char proc_path[128]; + ssize_t size = read_string(tracee, proc_path, peek_reg(tracee, CURRENT, SYSARG_2), sizeof(proc_path)); + if (size <= 0 || size >= (ssize_t) sizeof(proc_path)) { + return 0; + } + if (compare_paths(proc_path, "/proc") != PATH2_IS_PREFIX) { + return 0; + } + + /* Ensure provided path is symlink to " (deleted)" file */ + char target_path[PATH_MAX] = {}; + int status = readlink(proc_path, target_path, sizeof(target_path)); + if (status < 10 || status >= (ssize_t) sizeof(target_path)) { + return 0; + } + if (0 != memcmp(&target_path[status - 10], DELETED_SUFFIX, 10)) { + return 0; + } + + /* Read stats of source file, ensure it is regular file */ + struct stat stats = {}; + if (0 != stat(proc_path, &stats)) { + return 0; + } + if (!S_ISREG(stats.st_mode)) { + return 0; + } + + /* Read path of target file (already translated by proot) */ + size = read_string(tracee, target_path, peek_reg(tracee, CURRENT, SYSARG_4), PATH_MAX); + if (size < 0 || size >= (ssize_t) sizeof(target_path)) { + return 0; + } + + /* Open source file for reading */ + int source_fd = open(proc_path, O_RDONLY); + if (source_fd < 0) { + return 0; + } + + /* Point of no return, below we no longer are allowed to "return 0", + * any errors will be propagated to caller + * + * Delete target file (we'll be replacing it). + * Ignore result of unlink, file could or could not exist, + * we'll report failure of open though */ + unlink(target_path); + + /* Open target file for writing */ + int target_fd = open(target_path, O_WRONLY|O_CREAT|O_EXCL, stats.st_mode & 0777); + if (target_fd < 0) { + status = -errno; + if (status >= 0) + status = -EPERM; + close(source_fd); + return status; + } + + /* Copy contents of file */ + char buf[4096]; + int nread; + while (0 != (nread = read(source_fd, buf, sizeof(buf)))) { + if (nread < 0) { + status = -errno; + if (status >= 0) + status = -EPERM; + close(source_fd); + close(target_fd); + return status; + } + int pos = 0; + while (pos < nread) { + int nwrite = write(target_fd, buf + pos, nread - pos); + if (nwrite <= 0) { + status = -errno; + if (status >= 0) + status = -EPERM; + close(source_fd); + close(target_fd); + return status; + } + pos += nwrite; + } + } + + /* Copy successful, nothing more to be done for this syscall */ + close(source_fd); + close(target_fd); + return 1; +} + +/** + * Handler for this @extension. It is triggered each time an @event + * occurred. See ExtensionEvent for the meaning of @data1 and @data2. + */ +int link2symlink_callback(Extension *extension, ExtensionEvent event, + intptr_t data1, intptr_t data2 UNUSED) +{ + int status; + + switch (event) { + case INITIALIZATION: { + /* List of syscalls handled by this extensions. */ + static FilteredSysnum filtered_sysnums[] = { + { PR_link, FILTER_SYSEXIT }, + { PR_linkat, FILTER_SYSEXIT }, + { PR_unlink, FILTER_SYSEXIT }, + { PR_unlinkat, FILTER_SYSEXIT }, + { PR_fstat, FILTER_SYSEXIT }, + { PR_fstat64, FILTER_SYSEXIT }, + { PR_fstatat64, FILTER_SYSEXIT }, + { PR_lstat, FILTER_SYSEXIT }, + { PR_lstat64, FILTER_SYSEXIT }, + { PR_newfstatat, FILTER_SYSEXIT }, + { PR_stat, FILTER_SYSEXIT }, + { PR_stat64, FILTER_SYSEXIT }, + { PR_rename, FILTER_SYSEXIT }, + { PR_renameat, FILTER_SYSEXIT }, + { PR_renameat2, FILTER_SYSEXIT }, + FILTERED_SYSNUM_END, + }; + extension->filtered_sysnums = filtered_sysnums; + return 0; + } + + case SYSCALL_ENTER_END: { + Tracee *tracee = TRACEE(extension); + + switch (get_sysnum(tracee, ORIGINAL)) { + case PR_rename: + /*int rename(const char *oldpath, const char *newpath); + *If newpath is a psuedo hard link decrement the link count. + */ + + status = decrement_link_count(tracee, SYSARG_2); + if (status < 0) + return status; + + break; + + case PR_renameat: + case PR_renameat2: + /*int renameat(int olddirfd, const char *oldpath, int newdirfd, const char *newpath); + *If newpath is a psuedo hard link decrement the link count. + */ + + status = decrement_link_count(tracee, SYSARG_4); + if (status < 0) + return status; + + break; + + case PR_unlink: + /* If path points a file that is an symlink to a file that begins + * with PREFIX, let the file be deleted, but also decrement the + * hard link count, if it is greater than 1, otherwise delete + * the original file and intermediate file too. + */ + + status = decrement_link_count(tracee, SYSARG_1); + if (status < 0) + return status; + + break; + + case PR_unlinkat: + /* If this is request to delete directory, don't handle it here. + * directories cannot be hard links. */ + if ((peek_reg(tracee, CURRENT, SYSARG_3) & AT_REMOVEDIR) != 0) + { + return 0; + } + + /* If path points a file that is a symlink to a file that begins + * with PREFIX, let the file be deleted, but also delete the + * symlink that was created and decremnt the count that is tacked + * to end of original file. + */ + + status = decrement_link_count(tracee, SYSARG_2); + if (status < 0) + return status; + + break; + + case PR_link: + /* Convert: + * + * int link(const char *oldpath, const char *newpath); + * + * into: + * + * int symlink(const char *oldpath, const char *newpath); + */ + + status = move_and_symlink_path(tracee, SYSARG_1, SYSARG_2); + if (status < 0) + return status; + + break; + + case PR_linkat: + /* + * Handle linkat(..., "/proc/X/fd/Y", ..., AT_SYMLINK_FOLLOW) + */ + if (peek_reg(tracee, CURRENT, SYSARG_5) & AT_SYMLINK_FOLLOW) { + status = handle_linkat_from_proc_fd(tracee); + if (status < 0) + return status; + if (status == 1) { + set_sysnum(tracee, PR_void); + poke_reg(tracee, SYSARG_RESULT, 0); + return 0; + } + } + + /* Convert: + * + * int linkat(int olddirfd, const char *oldpath, + * int newdirfd, const char *newpath, int flags); + * + * into: + * + * int symlink(const char *oldpath, const char *newpath); + * + * Note: PRoot has already canonicalized + * linkat() paths this way: + * + * olddirfd + oldpath -> oldpath + * newdirfd + newpath -> newpath + */ + + status = move_and_symlink_path(tracee, SYSARG_2, SYSARG_4); + if (status < 0) + return status; + + break; + + default: + break; + } + return 0; + } + + case SYSCALL_EXIT_END: { + return handle_sysexit_end(TRACEE(extension)); + } + + case TRANSLATED_PATH: + translated_path(TRACEE(extension), (char *) data1); + return 0; + + case STATX_SYSCALL: + link2symlink_handle_statx((struct statx_syscall_state *) data1); + return 0; + + default: + return 0; + } +} diff --git a/core/proot/src/main/cpp/extension/mountinfo/mountinfo.c b/core/proot/src/main/cpp/extension/mountinfo/mountinfo.c new file mode 100644 index 000000000..1abf2735d --- /dev/null +++ b/core/proot/src/main/cpp/extension/mountinfo/mountinfo.c @@ -0,0 +1,208 @@ +#include "extension/extension.h" +#include "path/path.h" /* translate_path, */ +#include "path/binding.h" /* Binding, bindings */ +#include "path/temp.h" /* create_temp_file, */ +#include /* INT_MAX, */ +#include /* PATH_MAX, */ +#include /* strlen, strcmp */ +#include /* FILE, getline, fprintf */ +#include /* free, */ +#include /* CIRCLEQ_*, */ + +/** + * Append a synthesized mount-table line to @fp for each runtime + * binding (i.e. one that wasn't part of the static -r/-b set). This + * is what lets sandbox helpers like bubblewrap find the mount they + * just asked PRoot to create via emulate_mount(). + */ +static void append_runtime_binding_lines(Tracee *target_tracee, FILE *fp) +{ + Binding *binding; + int next_id = 1000000; + int parent_id = 1; + + if (target_tracee->fs->bindings.guest == NULL) + return; + + for (binding = CIRCLEQ_FIRST(target_tracee->fs->bindings.guest); + binding != (void *) target_tracee->fs->bindings.guest; + binding = CIRCLEQ_NEXT(binding, link.guest)) { + /* Skip the root binding "/" — already present as the kernel root. */ + if (strcmp(binding->guest.path, "/") == 0) + continue; + + fprintf(fp, + "%d %d 0:1 / %s rw,relatime - bind %s rw,relatime\n", + next_id++, parent_id, + binding->guest.path, binding->host.path); + } +} + +static void mountinfo_check_open_path(Tracee *tracee, char path[PATH_MAX]) { + /* Try matching "/proc//mountinfo" */ + size_t len = strlen(path); + if ( + len > (6 + 10) && + 0 == strncmp(path, "/proc/", 6) && + 0 == strcmp(path + (len - 10), "/mountinfo") + ) { + /* Check if current root is under /data and if so replace contents + * of /proc//mountinfo to make it contain /data as / mountpoint. + * This is needed because on Android / is read only mount + * + * https://github.com/termux/proot/issues/294 + */ + char *path_end = NULL; + long target_pid = strtol(path + 6, &path_end, 10); + if (path_end != path + (len - 10) || target_pid <= 0 || target_pid > INT_MAX) { + return; + } + Tracee *target_tracee = get_tracee(tracee, target_pid, false); + if (target_tracee == NULL) { + return; + } + + /* Check if our root is under "/data" */ + char root_path[PATH_MAX]; // Host path to guest root + translate_path(target_tracee, root_path, AT_FDCWD, "/", true); + Comparison compare_result = compare_paths(root_path, "/data"); + bool is_android_data = (compare_result == PATH2_IS_PREFIX || compare_result == PATHS_ARE_EQUAL); + + /* Are there bindings to expose as fake mounts (mount(2) + * calls from sandbox helpers are converted into + * bindings — see emulate_mount). Skip the root + * binding, which the real kernel mount table already + * covers. */ + bool has_extra_bindings = false; + if (target_tracee->fs->bindings.guest != NULL) { + Binding *b; + for (b = CIRCLEQ_FIRST(target_tracee->fs->bindings.guest); + b != (void *) target_tracee->fs->bindings.guest; + b = CIRCLEQ_NEXT(b, link.guest)) { + if (strcmp(b->guest.path, "/") != 0) { + has_extra_bindings = true; + break; + } + } + } + + if (!is_android_data && !has_extra_bindings) + return; + + /* Open real /proc//mountinfo */ + FILE *real_mountinfo_fp = fopen(path, "r"); + if (real_mountinfo_fp == NULL) { + return; + } + + /* Prepare faked mountinfo */ + const char *new_path = create_temp_file(tracee->ctx, "mountinfo"); + FILE *new_mountinfo_fp = fopen(new_path, "w"); + if (new_mountinfo_fp == NULL) { + fclose(real_mountinfo_fp); + return; + } + + char *line = NULL; + size_t line_buf_len = 0; + ssize_t line_len = 0; + bool found_line = false; + + if (is_android_data) { + while ((line_len = getline(&line, &line_buf_len, real_mountinfo_fp)) > 0) { + char *chunk = line; + /* Skip columns before 'root' */ + for (int i = 0; i < 4 && chunk - line < line_len; i++) { + chunk = strchr(chunk, ' '); + if (chunk == NULL) goto end_line_scan; + chunk++; + } + + /* Match path */ + char *chunk_end = strchr(chunk, ' '); + if (chunk_end == NULL) continue; + + if (chunk_end - chunk == 5 && 0 == memcmp(chunk, "/data", 5)) { + /* Write line into new file keeping only "/" from root column */ + fwrite(line, chunk - line + 1, 1, new_mountinfo_fp); + fwrite(chunk_end, line_len - (chunk_end - line), 1, new_mountinfo_fp); + found_line = true; + break; + } +end_line_scan: ; + } + + /* Once root was added, rescan and add other standard mounts */ + if (found_line) { + fseek(real_mountinfo_fp, 0, SEEK_SET); + while ((line_len = getline(&line, &line_buf_len, real_mountinfo_fp)) > 0) { + char *chunk = line; + /* Skip columns before 'root' */ + for (int i = 0; i < 4 && chunk - line < line_len; i++) { + chunk = strchr(chunk, ' '); + if (chunk == NULL) goto end_line_scan2; + chunk++; + } + + /* Match path */ + char *chunk_end = strchr(chunk, ' '); + if (chunk_end == NULL) continue; + + size_t mount_len = chunk_end - chunk; + if ( + (mount_len == 4 && 0 == memcmp(chunk, "/dev", 4)) || + (mount_len >= 5 && 0 == memcmp(chunk, "/dev/", 5)) || + (mount_len == 5 && 0 == memcmp(chunk, "/proc", 5)) || + (mount_len == 4 && 0 == memcmp(chunk, "/sys", 4)) || + (mount_len >= 5 && 0 == memcmp(chunk, "/sys/", 5)) || + (mount_len == 4 && 0 == memcmp(chunk, "/tmp", 4)) + ) { + /* Copy line into new file verbatim */ + fwrite(line, line_len, 1, new_mountinfo_fp); + } +end_line_scan2: ; + } + } + } else { + /* Non-Android case: copy real mountinfo verbatim. */ + while ((line_len = getline(&line, &line_buf_len, real_mountinfo_fp)) > 0) + fwrite(line, line_len, 1, new_mountinfo_fp); + found_line = true; + } + + /* Append synthesized entries for runtime bindings so + * helpers like bubblewrap find the mounts they think + * they just created. */ + append_runtime_binding_lines(target_tracee, new_mountinfo_fp); + + free(line); + fclose(new_mountinfo_fp); + fclose(real_mountinfo_fp); + + /* Redirect open to our temp file */ + if (found_line) { + strncpy(path, new_path, PATH_MAX); + } + return; + } + +} + +int mountinfo_callback(Extension *extension, ExtensionEvent event, + intptr_t data1 UNUSED, intptr_t data2 UNUSED) +{ + switch (event) { + case TRANSLATED_PATH: + { + Tracee *tracee = TRACEE(extension); + Sysnum num = get_sysnum(tracee, ORIGINAL); + if (num == PR_open || num == PR_openat) { + mountinfo_check_open_path(tracee, (char*) data1); + } + return 0; + } + + default: + return 0; + } +} diff --git a/core/proot/src/main/cpp/extension/port_switch/port_switch.c b/core/proot/src/main/cpp/extension/port_switch/port_switch.c new file mode 100644 index 000000000..abcbd15db --- /dev/null +++ b/core/proot/src/main/cpp/extension/port_switch/port_switch.c @@ -0,0 +1,256 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "extension/extension.h" +#include "tracee/tracee.h" +#include "tracee/mem.h" + +#define PORT_THRESHOLD 1024 +#define PORT_ADDITION 2000 + +/** A function to modify the port number of the socket address sent to system calls by 1024. + * Uses tracee to modify the system call. + * Uses is_socketcall to determine if the system call is the socketcall wrapper. + * Uses is_bind during bind calls, so that a modification message can be printed. + * Uses is_udp to determine if the call is *likely* a UDP communication, in which case the location of + * the system call arguments are different. + * Uses my_sockaddr to hold the structure that contains the port number to modify. + * Uses socketcall_arg2 in the socketcall case, as socketcall's second argument is a long pointer. + */ +void mod_port(Tracee *tracee, bool is_socketcall, bool is_bind, bool is_udp, struct sockaddr_storage *my_sockaddr, long socketcall_arg2[]); + +/** A function to determine whether the IP address of a system call is localhost. + * Uses my_sockaddr to hold the structure that contains the IP address. + */ +bool is_localhost(struct sockaddr_storage *my_sockaddr); + +int port_switch_callback(Extension *extension, ExtensionEvent event, intptr_t data1 UNUSED, intptr_t data2 UNUSED) { + switch (event) { + case INITIALIZATION: { + static FilteredSysnum filtered_sysnums[] = { + { PR_bind, FILTER_SYSEXIT }, + { PR_connect, FILTER_SYSEXIT }, + { PR_socketcall, FILTER_SYSEXIT }, + { PR_sendto, FILTER_SYSEXIT }, + { PR_recvfrom, FILTER_SYSEXIT }, + FILTERED_SYSNUM_END + }; + extension->filtered_sysnums = filtered_sysnums; + return 0; + } + + case SYSCALL_ENTER_END: { + Tracee *tracee = TRACEE(extension); + + /** The 4 system calls that will be changed are bind, connect, sendto, and socketcall. + * Socketcall is a wrapper function for the i386 architecture that wraps the first 3 system calls. + * In every bind case the port is modified, as there is a high likelihood that it will be binding a socket locally. + * For connect and sendto, the port is only modified if the calls are attempting to reach localhost. This is + * to save users hassle when connecting to localhost while allowing them to reach servers outside their system + * without a difference from their regular usability. + */ + switch(get_sysnum(tracee, ORIGINAL)) { + + case PR_bind: { + struct sockaddr_storage my_sockaddr; + read_data(tracee, &my_sockaddr, peek_reg(tracee, ORIGINAL, SYSARG_2), sizeof(struct sockaddr_storage)); + mod_port(tracee, false, true, false, &my_sockaddr, NULL); + + return 0; + } + + case PR_connect: { + struct sockaddr_storage my_sockaddr; + read_data(tracee, &my_sockaddr, peek_reg(tracee, ORIGINAL, SYSARG_2), sizeof(struct sockaddr_storage)); + if(is_localhost(&my_sockaddr)) + mod_port(tracee, false, false, false, &my_sockaddr, NULL); + + return 0; + } + + case PR_sendto: { + struct sockaddr_storage my_sockaddr; + /** There are some cases where sendto() is called during a TCP communication, even though + * send() is the norm. The port doesn't need to be modified in these cases. If sendto() + * is used while in a connected case (non-UDP), its sockaddr argument is null. To avoid + * modifying the port, these cases are ignored. + */ + if(peek_reg(tracee, ORIGINAL, SYSARG_5) != 0) { + read_data(tracee, &my_sockaddr, peek_reg(tracee, ORIGINAL, SYSARG_5), sizeof(struct sockaddr_storage)); + if(is_localhost(&my_sockaddr)) + mod_port(tracee, false, false, true, &my_sockaddr, NULL); + } + return 0; + } + + case PR_socketcall: { + /** Socketcall's 1st argument is an int that signifies which actual socket system call it is being used to wrap. + * Socketcall's 2nd argument is a long* that leads to the location in memory where the arguments to that system + * call are. Those arguments are extracted first, and then the sockaddr can be extracted from those. + */ + int call; + long a[6]; + call = peek_reg(tracee, ORIGINAL, SYSARG_1); + read_data(tracee, a, peek_reg(tracee, ORIGINAL, SYSARG_2), sizeof(a)); + switch(call) { + + case SYS_BIND: { + struct sockaddr_storage my_sockaddr; + read_data(tracee, &my_sockaddr, a[1], sizeof(struct sockaddr_storage)); + mod_port(tracee, true, true, false, &my_sockaddr, a); + + break; + } + + case SYS_CONNECT: { + struct sockaddr_storage my_sockaddr; + read_data(tracee, &my_sockaddr, a[1], sizeof(struct sockaddr_storage)); + if(is_localhost(&my_sockaddr)) + mod_port(tracee, true, false, false, &my_sockaddr, a); + + break; + } + + case SYS_SENDTO: { + struct sockaddr_storage my_sockaddr; + if(a[4] != 0) { + read_data(tracee, &my_sockaddr, a[4], sizeof(struct sockaddr_storage)); + if(is_localhost(&my_sockaddr)) + mod_port(tracee, true, false, true, &my_sockaddr, a); + } + + break; + } + default: + break; + } + + return 0; + } + + default: + return 0; + } + } + default: + return 0; + } +} + +void mod_port(Tracee *tracee, bool is_socketcall, bool is_bind, bool is_udp, struct sockaddr_storage *my_sockaddr, long socketcall_arg2[]) { + /** IPv4 and IPv6 addresses are stored in different structures. Because of this, the port must be changed + * in different ways depending on the address family. + * + * Socketcall stores the arguments to the system call it wraps in a pointer, the modified sockaddr must be written + * separately to an array and then to the tracee. + * + * Modifying the port is avoided if the port is not within the section of ports that Android reserves. This is to + * avoid problems in UDP server/client communications, in cases where the server sends a message back to the client. + * Since the client normally doesn't specifically call bind(), a port >1024 is usually assigned for it to recvfrom. + * + * UDP communications are also treated differently, as the sendto() function has the sockaddr in its 5th location + * instead of the 2nd as every other call does. + */ + switch(my_sockaddr->ss_family) { + case AF_INET: { + struct sockaddr_in *in = (struct sockaddr_in *)my_sockaddr; + if(ntohs(in->sin_port) > 0 && ntohs(in->sin_port) < PORT_THRESHOLD) { + + if(is_bind) { + printf("\nATTENTION: A bind system call was requested on port: %d\n", ntohs(in->sin_port)); + in->sin_port = htons(ntohs(in->sin_port) + PORT_ADDITION); + printf("The port has been changed. If connecting from outside Termux, use: %d\n\n", ntohs(in->sin_port)); + } + else + in->sin_port = htons(ntohs(in->sin_port) + PORT_ADDITION); + + if(is_socketcall && is_udp) { + write_data(tracee, socketcall_arg2[4], in, sizeof(in)); + write_data(tracee, peek_reg(tracee, CURRENT, SYSARG_2), socketcall_arg2, sizeof(socketcall_arg2)); + } + + else if(!is_socketcall && is_udp) + write_data(tracee, peek_reg(tracee, CURRENT, SYSARG_5), in, sizeof(in)); + + else if(is_socketcall && !is_udp) { + write_data(tracee, socketcall_arg2[1], in, sizeof(in)); + write_data(tracee, peek_reg(tracee, CURRENT, SYSARG_2), socketcall_arg2, sizeof(socketcall_arg2)); + } + + else if(!is_socketcall && !is_udp) + write_data(tracee, peek_reg(tracee, CURRENT, SYSARG_2), in, sizeof(in)); + } + break; + } + + case AF_INET6: { + struct sockaddr_in6 *in6 = (struct sockaddr_in6 *)my_sockaddr; + if(ntohs(in6->sin6_port) > 0 && ntohs(in6->sin6_port) < PORT_THRESHOLD) { + if(is_bind) { + printf("\nATTENTION: A bind system call was requested on port: %d\n", ntohs(in6->sin6_port)); + in6->sin6_port = htons(ntohs(in6->sin6_port) + PORT_ADDITION); + printf("The port has been changed. If connecting from outside Termux, use: %d\n\n", ntohs(in6->sin6_port)); + } + else + in6->sin6_port = htons(ntohs(in6->sin6_port) + PORT_ADDITION); + + if(is_socketcall && is_udp) { + write_data(tracee, socketcall_arg2[4], in6, sizeof(in6)); + write_data(tracee, peek_reg(tracee, CURRENT, SYSARG_2), socketcall_arg2, sizeof(socketcall_arg2)); + } + + else if(is_socketcall && is_udp) + write_data(tracee, peek_reg(tracee, CURRENT, SYSARG_5), in6, sizeof(in6)); + + else if(is_socketcall && !is_udp) { + write_data(tracee, socketcall_arg2[1], in6, sizeof(in6)); + write_data(tracee, peek_reg(tracee, CURRENT, SYSARG_2), socketcall_arg2, sizeof(socketcall_arg2)); + } + + else if(!is_socketcall && !is_udp) + write_data(tracee, peek_reg(tracee, CURRENT, SYSARG_2), in6, sizeof(in6)); + } + break; + } + + default: + break; + } +} + +bool is_localhost(struct sockaddr_storage *my_sockaddr) { + /** Localhost is represented differently in the two IPv families, so determining if an address's destination is localhost + * must be done differently for each family. Both must also be compared to 0, which is the enumeration for INADDR_ANY + * which is often used bind() calls. + */ + switch(my_sockaddr->ss_family) { + + case AF_INET: { + struct sockaddr_in *in = (struct sockaddr_in *)my_sockaddr; + char ipAddress[INET_ADDRSTRLEN]; + inet_ntop(AF_INET, &(in->sin_addr), ipAddress, INET_ADDRSTRLEN); + if(strcmp(ipAddress, "127.0.0.1") == 0) + return true; + else + return false; + } + + case AF_INET6: { + struct sockaddr_in6 *in6 = (struct sockaddr_in6 *)my_sockaddr; + char ipAddress[INET6_ADDRSTRLEN]; + inet_ntop(AF_INET6, &(in6->sin6_addr), ipAddress, INET6_ADDRSTRLEN); + if(strcmp(ipAddress, "::1") == 0) + return true; + else + return false; + } + + default: + return false; + } +} diff --git a/core/proot/src/main/cpp/extension/sysvipc/sysvipc.c b/core/proot/src/main/cpp/extension/sysvipc/sysvipc.c new file mode 100644 index 000000000..16f4599e5 --- /dev/null +++ b/core/proot/src/main/cpp/extension/sysvipc/sysvipc.c @@ -0,0 +1,365 @@ +#include "extension/sysvipc/sysvipc.h" +#include "tracee/seccomp.h" +#include "syscall/chain.h" +#include "path/path.h" +#include "path/temp.h" + +#include /* assert */ +#include /* syscall */ +#include /* __NR_tkill */ +#include /* E* */ +#include /* CLONE_THREAD */ +#include /* strcmp */ +#include /* SIGSTOP */ + + +#include "sysvipc_internal.h" + + +static FilteredSysnum filtered_sysnums[] = { + { PR_msgget, 0 }, + { PR_msgsnd, 0 }, + { PR_msgrcv, 0 }, + { PR_msgctl, 0 }, + { PR_semget, 0 }, + { PR_semop, 0 }, + { PR_semtimedop, 0 }, + { PR_semctl, 0 }, + { PR_shmget, 0 }, + { PR_shmat, 0 }, + { PR_shmdt, 0 }, + { PR_shmctl, 0 }, + FILTERED_SYSNUM_END, +}; + + +static int sysvipc_syscall_common(Tracee *tracee, struct SysVIpcConfig *config, bool from_sigsys) { + int status = 0; + word_t timeout = 0; + + assert(config->wait_state == WSTATE_NOT_WAITING); + + word_t sysnum = get_sysnum(tracee, CURRENT); + switch (sysnum) { + case PR_msgget: + status = sysvipc_msgget(tracee, config); + break; + case PR_msgsnd: + status = sysvipc_msgsnd(tracee, config); + break; + case PR_msgrcv: + status = sysvipc_msgrcv(tracee, config); + break; + case PR_msgctl: + status = sysvipc_msgctl(tracee, config); + break; + case PR_semget: + status = sysvipc_semget(tracee, config); + break; + case PR_semtimedop: + timeout = peek_reg(tracee, CURRENT, SYSARG_4); + // Fall-throug + case PR_semop: + status = sysvipc_semop(tracee, config); + break; + case PR_semctl: + status = sysvipc_semctl(tracee, config); + break; + case PR_shmget: + status = sysvipc_shmget(tracee, config); + break; + case PR_shmat: + status = sysvipc_shmat(tracee, config); + break; + case PR_shmdt: + status = sysvipc_shmdt(tracee, config); + break; + case PR_shmctl: + status = sysvipc_shmctl(tracee, config); + break; + default: + return 0; + } + + if (config->chain_state != CSTATE_NOT_CHAINED) { + /* Check if chain_state is one of initial ones + * (others not go through SYSCALL_ENTER_START). */ + assert( + config->chain_state == CSTATE_SINGLE || + config->chain_state == CSTATE_SHMAT_SOCKET + ); + if (config->chain_state == CSTATE_SINGLE) { + config->chain_state = CSTATE_NOT_CHAINED; + } + tracee->restart_how = PTRACE_SYSCALL; + if (from_sigsys) { + restart_syscall_after_seccomp(tracee); + return 2; + } else { + return 1; + } + } else if (config->wait_reason != WR_NOT_WAITING) { + poke_reg(tracee, SYSARG_1, 0); + poke_reg(tracee, SYSARG_2, 0); + poke_reg(tracee, SYSARG_3, timeout); + poke_reg(tracee, SYSARG_4, 0); + set_sysnum(tracee, PR_ppoll); + tracee->restart_how = PTRACE_SYSCALL; + if (from_sigsys) { + config->wait_state = WSTATE_RESTARTED_INTO_PPOLL; + restart_syscall_after_seccomp(tracee); + return 2; + } else { + config->wait_state = WSTATE_ENTERED_PPOLL; + return 1; + } + } else { + if (from_sigsys) { + set_result_after_seccomp(tracee, status); + return 2; + } else { + config->status_after_wait = status; + config->wait_state = WSTATE_ENTERED_GETPID; + set_sysnum(tracee, PR_getpid); + tracee->restart_how = PTRACE_SYSCALL; + return 1; + } + } +} + +static int sysvipc_proc_handler( + char *out_path, + Extension *extension, + void (*handler)(FILE *proc_file, struct SysVIpcNamespace *ipc_namespace) + ) { + Tracee *tracee = TRACEE(extension); + struct SysVIpcConfig *config = extension->config; + + const char *path = create_temp_file(tracee->ctx, "prootseq"); + if (path == NULL) { + return -ENOMEM; + } + + FILE *fp = fopen(path, "w"); + if (fp == NULL) { + return -ENOMEM; + } + handler(fp, config->ipc_namespace); + fclose(fp); + + strncpy(out_path, path, PATH_MAX); + return 1; +} + +/** + * Handler for this @extension. It is triggered each time an @event + * occurred. See ExtensionEvent for the meaning of @data1 and @data2. + */ +int sysvipc_callback(Extension *extension, ExtensionEvent event, intptr_t data1, intptr_t data2) +{ + switch (event) { + case INITIALIZATION: + { + Tracee *tracee = TRACEE(extension); + struct SysVIpcConfig *config = talloc_zero(extension, struct SysVIpcConfig); + config->ipc_namespace = talloc_zero(config, struct SysVIpcNamespace); + talloc_set_destructor(config->ipc_namespace, sysvipc_shm_namespace_destructor); + config->process = talloc_zero(config, struct SysVIpcProcess); + config->process->pgid = tracee->pid; + extension->config = config; + extension->filtered_sysnums = filtered_sysnums; + return 0; + } + + case INHERIT_PARENT: /* Inheritable for sub reconfiguration ... */ + return 1; + + case INHERIT_CHILD: { + Extension *parent = (Extension *) data1; + struct SysVIpcConfig *parent_config = parent->config; + struct SysVIpcConfig *child_config = talloc_zero(extension, struct SysVIpcConfig); + if (child_config == NULL) + return -1; + + if (data2 & CLONE_THREAD) { + child_config->process = talloc_reference(child_config, parent_config->process); + } else { + Tracee *tracee = TRACEE(extension); + child_config->process = talloc_zero(child_config, struct SysVIpcProcess); + child_config->process->pgid = tracee->pid; + sysvipc_shm_inherit_process(parent_config->process, child_config->process); + } + + child_config->ipc_namespace = talloc_reference(child_config, parent_config->ipc_namespace); + extension->config = child_config; + + return 0; + } + + case SYSCALL_ENTER_END: + /* If we've just finished execve remove mapped shms from this process */ + if (data1 == 0) { + Tracee *tracee = TRACEE(extension); + if (get_sysnum(tracee, CURRENT) == PR_execve) { + struct SysVIpcConfig *config = extension->config; + sysvipc_shm_remove_mappings_from_process(config->process); + } + } + return 0; + + case SYSCALL_ENTER_START: + { + Tracee *tracee = TRACEE(extension); + struct SysVIpcConfig *config = extension->config; + switch (config->wait_state) { + case WSTATE_NOT_WAITING: + return sysvipc_syscall_common(tracee, config, false); + case WSTATE_RESTARTED_INTO_PPOLL: + assert(get_sysnum(tracee, CURRENT) == PR_ppoll); + config->wait_state = WSTATE_ENTERED_PPOLL; + tracee->restart_how = PTRACE_SYSCALL; + return 1; + case WSTATE_RESTARTED_INTO_PPOLL_CANCELED: + { + int status = config->status_after_wait; + if (config->chain_state == CSTATE_MSGRCV_RETRY) { + status = sysvipc_msgrcv_retry(tracee, config); + } + poke_reg(tracee, SYSARG_RESULT, status); + set_sysnum(tracee, PR_void); + config->wait_state = WSTATE_NOT_WAITING; + return 1; + } + default: + assert(!"Bad wait_state on SYSCALL_ENTER_START"); + } + } + + case SIGSYS_OCC: + { + Tracee *tracee = TRACEE(extension); + struct SysVIpcConfig *config = extension->config; + return sysvipc_syscall_common(tracee, config, true); + } + + case SYSCALL_EXIT_START: + { + Tracee *tracee = TRACEE(extension); + struct SysVIpcConfig *config = extension->config; + if (config->chain_state >= CSTATE_SHMAT_SOCKET && config->chain_state <= CSTATE_SHMAT_MMAP) { + assert(config->chain_state == CSTATE_SHMAT_SOCKET); + return sysvipc_shmat_chain(tracee, config); + } + switch (config->wait_state) { + case WSTATE_NOT_WAITING: + return 0; + case WSTATE_ENTERED_PPOLL: + assert(config->wait_state != WSTATE_NOT_WAITING); + config->wait_state = WSTATE_NOT_WAITING; + switch (config->wait_reason) { + case WR_NOT_WAITING: + assert(!"Unexpected wait_reason=WR_NOT_WAITING in SYSCALL_EXIT_START/WSTATE_ENTERED_PPOLL"); + case WR_WAIT_SEMOP: + sysvipc_semop_timedout(tracee, config); + break; + default: + config->wait_reason = WR_NOT_WAITING; + } + assert(config->wait_reason == WR_NOT_WAITING); + int ppoll_status = (int) peek_reg(tracee, CURRENT, SYSARG_RESULT); + if (ppoll_status == -EFAULT || ppoll_status == -EINTR) { + return 1; + } + return -EINTR; + case WSTATE_SIGNALED_PPOLL: + case WSTATE_ENTERED_GETPID: + { + assert(config->wait_reason == WR_NOT_WAITING); + config->wait_state = WSTATE_NOT_WAITING; + int status = config->status_after_wait; + if (config->chain_state == CSTATE_MSGRCV_RETRY) { + status = sysvipc_msgrcv_retry(tracee, config); + } + poke_reg(tracee, SYSARG_RESULT, status); + return 1; + } + default: + assert(!"Bad wait_state on SYSCALL_EXIT_START"); + } + } + + case SYSCALL_CHAINED_ENTER: + { + struct SysVIpcConfig *config = extension->config; + switch (config->wait_state) { + case WSTATE_NOT_WAITING: + break; + case WSTATE_RESTARTED_INTO_PPOLL_CANCELED: + { + Tracee *tracee = TRACEE(extension); + poke_reg(tracee, SYSARG_3, 1); + config->wait_state = WSTATE_SIGNALED_PPOLL; + break; + } + default: + assert(!"Bad wait_state on SYSCALL_CHAINED_ENTER"); + } + return 0; + } + + case SYSCALL_CHAINED_EXIT: + { + Tracee *tracee = TRACEE(extension); + struct SysVIpcConfig *config = extension->config; + switch (config->wait_state) { + case WSTATE_NOT_WAITING: + break; + case WSTATE_SIGNALED_PPOLL: + config->wait_state = WSTATE_NOT_WAITING; + /* Don't run chain handlers, return instead of break */ + return 0; + default: + assert(!"Bad wait_state on SYSCALL_CHAINED_EXIT"); + } + if (config->chain_state >= CSTATE_SHMAT_SOCKET && config->chain_state <= CSTATE_SHMAT_MMAP) { + sysvipc_shmat_chain(tracee, config); + } + return 0; + } + + case GUEST_PATH: + { + if (strcmp((const char *) data2, "/proc/sysvipc/shm") == 0) { + return sysvipc_proc_handler((char *) data1, extension, sysvipc_shm_fill_proc); + } + return 0; + } + + default: + return 0; + } +} + +struct SysVIpcConfig *sysvipc_get_config(Tracee *tracee) +{ + Extension *extension = get_extension(tracee, sysvipc_callback); + if (extension == NULL) + return NULL; + return talloc_get_type_abort(extension->config, struct SysVIpcConfig); +} + +void sysvipc_wake_tracee(Tracee *tracee, struct SysVIpcConfig *config, int status) +{ + assert(config->wait_reason != WR_NOT_WAITING); + config->wait_reason = WR_NOT_WAITING; + config->status_after_wait = status; + if (config->wait_state == WSTATE_ENTERED_PPOLL) { + config->wait_state = WSTATE_SIGNALED_PPOLL; + syscall(__NR_tkill, tracee->pid, SIGSTOP); + tracee->sigstop = SIGSTOP_IGNORED; + } else if (config->wait_state == WSTATE_RESTARTED_INTO_PPOLL) { + config->wait_state = WSTATE_RESTARTED_INTO_PPOLL_CANCELED; + } else { + assert(!"Bad wait_state in sysvipc_wake_tracee"); + } +} diff --git a/core/proot/src/main/cpp/extension/sysvipc/sysvipc.h b/core/proot/src/main/cpp/extension/sysvipc/sysvipc.h new file mode 100644 index 000000000..8eb50529f --- /dev/null +++ b/core/proot/src/main/cpp/extension/sysvipc/sysvipc.h @@ -0,0 +1,9 @@ +#ifndef SYSVIPC_H +#define SYSVIPC_H + +#include "extension/extension.h" + +int sysvipc_callback(Extension *extension, ExtensionEvent event, intptr_t data1, intptr_t data2); +void sysvipc_shm_helper_main() __attribute__((noreturn)); + +#endif // SYSVIPC_H diff --git a/core/proot/src/main/cpp/extension/sysvipc/sysvipc_internal.h b/core/proot/src/main/cpp/extension/sysvipc/sysvipc_internal.h new file mode 100644 index 000000000..06730083c --- /dev/null +++ b/core/proot/src/main/cpp/extension/sysvipc/sysvipc_internal.h @@ -0,0 +1,271 @@ +#ifndef SYSVIPC_INTERNAL_H +#define SYSVIPC_INTERNAL_H + +#include "tracee/tracee.h" +#include "sysvipc_sys.h" + +#include +#include +#include +#include +#include + +/****************** + * Message queues * + *****************/ +struct SysVIpcMsgQueueItem { + long mtype; + char *mtext; + size_t mtext_length; + STAILQ_ENTRY(SysVIpcMsgQueueItem) link; +}; +STAILQ_HEAD(SysVIpcMsgQueueItems, SysVIpcMsgQueueItem); + +struct SysVIpcMsgQueue { + int32_t key; + int16_t generation; + bool valid; + struct SysVIpcMsgQueueItems *items; + struct msqid_ds stats; +}; + +/************** + * Semaphores * + *************/ + +struct SysVIpcSemaphore { + int32_t key; + int16_t generation; + bool valid; + uint16_t *sems; + int nsems; +}; + +/***************** + * Shared Memory * + ****************/ + +/** + * Currently mapped region of shared memory + * (For shmdt and shm_nattch) + */ +struct SysVIpcSharedMemMap { + word_t addr; + + /** + * Size of mmap-ed region or 0 if mmap hasn't been done yet + * (but is scheduled in syscall chain) + */ + size_t size; + + /** + * SysVIpcNamespace containing this shm id + * + * Note: This reference isn't tracked by talloc, + * however as we don't currently implement unshare/clone(CLONE_NEWIPC) + * IPC namespace will be kept because process that has mapping + * must be in same IPC namespace + */ + struct SysVIpcNamespace *ipc_namespace; + + /** + * Index of this mapping in SysVIpcNamespace.shms + */ + size_t shm_index; + + /** + * List pointers for SysVIpcSharedMem.mappings + */ + LIST_ENTRY(SysVIpcSharedMemMap) link_shmid; + + /** + * List pointers for SysVIpcProcess.mapped_shms + */ + LIST_ENTRY(SysVIpcSharedMemMap) link_process; +}; +LIST_HEAD(SysVIpcSharedMemMaps, SysVIpcSharedMemMap); + +struct SysVIpcSharedMem { + int32_t key; + int16_t generation; + bool valid; + bool rmid_pending; + int fd; + struct SysVIpcShmidDs stats; + + /** + * Currently shmat'ed memory for this shm id. + * + * While there is any map, IPC_RMID cannot be completed + * (will cause rmid_pending flag to be set instead) + */ + struct SysVIpcSharedMemMaps *mappings; +}; + +struct SysVIpcNamespace { + /** Array of Message Queues + * Since arrays are 0-indexed and queues are 1-indexed, + * queues with id is at queues[id-1] */ + struct SysVIpcMsgQueue *queues; + struct SysVIpcSemaphore *semaphores; + struct SysVIpcSharedMem *shms; +}; + +enum SysVIpcWaitReason { + WR_NOT_WAITING, + WR_WAIT_QUEUE_RECV, + WR_WAIT_SEMOP, + WR_WAIT_SHMAT_HELPER_BUSY, +}; + +enum SysVIpcWaitState { + WSTATE_NOT_WAITING, + WSTATE_RESTARTED_INTO_PPOLL_CANCELED, + WSTATE_RESTARTED_INTO_PPOLL, + WSTATE_ENTERED_PPOLL, + WSTATE_SIGNALED_PPOLL, + WSTATE_ENTERED_GETPID, +}; + +enum SysVIpcChainState { + CSTATE_NOT_CHAINED, + CSTATE_SINGLE, + CSTATE_SHMAT_SOCKET, + CSTATE_SHMAT_CONNECT, + CSTATE_SHMAT_RECVMSG, + CSTATE_SHMAT_MMAP, + CSTATE_MSGRCV_RETRY, +}; + +/** Per-process (thread group) structure with state of this extension */ +struct SysVIpcProcess { + int pgid; + struct SysVIpcSharedMemMaps mapped_shms; +}; + +/** Per-tracee (thread) structure with state of this extension */ +struct SysVIpcConfig { + struct SysVIpcNamespace *ipc_namespace; + struct SysVIpcProcess *process; + + /* Reason why this tracee should wait + * + * When syscall handler requests tracee to wait + * (for example because semaphore is blocked) + * it sets wait_reason to one of WR_WAIT_* + * values + * + * When tracee has to resume, use sysvipc_wake_tracee + * Only sysvipc_wake_tracee function should set + * this to WR_NOT_WAITING value */ + enum SysVIpcWaitReason wait_reason; + + /* Internal state of tracee wait mechanism + * + * This should only be accessed from + * wait mechanism implementation in sysvipc.c, + * not syscall handlers in sysvipc_[msg|sem|shm].c */ + enum SysVIpcWaitState wait_state; + + /* State of syscall chaining inside this extension + * + * This is used for shmat as it needs to perform + * sequence of operations that rely on results + * from previous chained syscalls + * + * When this is set to non-CSTATE_NOT_CHAINED value, + * sysvipc_syscall_common won't cancel syscall set + * by handler */ + enum SysVIpcChainState chain_state; + + /* Result of syscall that will be reported + * after waiting + * + * This should be accessed by mechanisms in sysvipc.c + * Handlers shouldn't access this, instead they should + * return result directly or pass it to sysvipc_wake_tracee */ + word_t status_after_wait; + + size_t waiting_object_index; + + word_t msgrcv_msgp; + size_t msgrcv_msgsz; + int msgrcv_msgtyp; + int msgrcv_msgflg; + + struct SysVIpcSembuf *semop_sops; + + word_t shmat_guest_buf; + int shmat_socket_fd; + int shmat_mem_fd; +}; + +/** + * Find given IPC object requested by tracee + * + * out_index should be size_t + * out_object should be pointer to struct SysVIpc[MsgQueue] + */ +#define LOOKUP_IPC_OBJECT(out_index, out_object, objects_array) \ +{ \ + int object_id = peek_reg(tracee, CURRENT, SYSARG_1); \ + int object_index = object_id & 0xFFF; \ + if (object_index <= 0 || object_index > (int)talloc_array_length(objects_array)) { \ + return -EINVAL; \ + } \ + out_index = object_index - 1; \ + out_object = &(objects_array)[object_index - 1]; \ + if (!out_object->valid || out_object->generation != ((object_id >> 12) & 0xFFFF)) { \ + return -EINVAL; \ + } \ +} + +#define IPC_OBJECT_ID(index, object) \ + ((index + 1) | (object->generation << 12)) + +/** + * Iterate over all Tracees in given SysVIpc namespace + * + * out_tracee should be 'Tracee *' + * out_config should be 'struct SysVIpcConfig *' + * checked_namespace is SysVIpc namespace to find tracees in + */ +#define SYSVIPC_FOREACH_TRACEE(out_tracee, out_config, checked_namespace) \ + LIST_FOREACH((out_tracee), get_tracees_list_head(), link) \ + if ( \ + ((out_config) = sysvipc_get_config(out_tracee)) != NULL && \ + (out_config)->ipc_namespace == (checked_namespace) \ + ) + +#define SYSVIPC_FOREACH_TRACEE_ANY_NAMESPACE(out_tracee, out_config) \ + LIST_FOREACH((out_tracee), get_tracees_list_head(), link) \ + if ( \ + ((out_config) = sysvipc_get_config(out_tracee)) != NULL \ + ) + +void sysvipc_wake_tracee(Tracee *tracee, struct SysVIpcConfig *config, int status); +struct SysVIpcConfig *sysvipc_get_config(Tracee *tracee); + +int sysvipc_msgget(Tracee *tracee, struct SysVIpcConfig *config); +int sysvipc_msgsnd(Tracee *tracee, struct SysVIpcConfig *config); +int sysvipc_msgrcv(Tracee *tracee, struct SysVIpcConfig *config); +int sysvipc_msgrcv_retry(Tracee *tracee, struct SysVIpcConfig *config); +int sysvipc_msgctl(Tracee *tracee, struct SysVIpcConfig *config); + +int sysvipc_semget(Tracee *tracee, struct SysVIpcConfig *config); +int sysvipc_semop(Tracee *tracee, struct SysVIpcConfig *config); +void sysvipc_semop_timedout(Tracee *tracee, struct SysVIpcConfig *config); +int sysvipc_semctl(Tracee *tracee, struct SysVIpcConfig *config); + +int sysvipc_shmget(Tracee *tracee, struct SysVIpcConfig *config); +int sysvipc_shmat(Tracee *tracee, struct SysVIpcConfig *config); +int sysvipc_shmat_chain(Tracee *tracee, struct SysVIpcConfig *config); +int sysvipc_shmdt(Tracee *tracee, struct SysVIpcConfig *config); +int sysvipc_shmctl(Tracee *tracee, struct SysVIpcConfig *config); +void sysvipc_shm_inherit_process(struct SysVIpcProcess *parent, struct SysVIpcProcess *child); +void sysvipc_shm_remove_mappings_from_process(struct SysVIpcProcess *process); +void sysvipc_shm_fill_proc(FILE *proc_file, struct SysVIpcNamespace *ipc_namespace); +int sysvipc_shm_namespace_destructor(struct SysVIpcNamespace *ipc_namespace); + +#endif // SYSVIPC_INTERNAL_H + diff --git a/core/proot/src/main/cpp/extension/sysvipc/sysvipc_msg.c b/core/proot/src/main/cpp/extension/sysvipc/sysvipc_msg.c new file mode 100644 index 000000000..1fc290ae1 --- /dev/null +++ b/core/proot/src/main/cpp/extension/sysvipc/sysvipc_msg.c @@ -0,0 +1,314 @@ +#include "sysvipc_internal.h" + +#include "tracee/reg.h" +#include "tracee/mem.h" +#include "tracee/tracee.h" +#include "extension/extension.h" + +#include /* E* */ +#include /* IPC_PRIVATE */ +#include /* syscall() */ +#include /* memset */ +#include /* time */ +#include /* assert */ + +#define SYSVIPC_MAX_MSG_SIZE 0xFFFF + +int sysvipc_msgget(Tracee *tracee, struct SysVIpcConfig *config) +{ + word_t queue_id = peek_reg(tracee, CURRENT, SYSARG_1); + word_t msgflg = peek_reg(tracee, CURRENT, SYSARG_2); + struct SysVIpcMsgQueue *queues = config->ipc_namespace->queues; + size_t unused_slot = 0; + size_t queue_index = 0; + size_t num_queues = talloc_array_length(queues); + bool found_unused_slot = false; + bool found_queue = false; + for (; queue_index < num_queues; queue_index++) { + if (queues[queue_index].valid) { + if(queue_id != IPC_PRIVATE && queues[queue_index].key == (int32_t) queue_id) { + found_queue = true; + break; + } + } else if (!found_unused_slot) { + unused_slot = queue_index; + found_unused_slot = true; + } + } + struct SysVIpcMsgQueue *queue = NULL; + if (!found_queue) { + if (!(msgflg & IPC_CREAT)) { + return -ENOENT; + } + if (found_unused_slot) { + queue_index = unused_slot; + } else { + queue_index = num_queues; + config->ipc_namespace->queues = queues = talloc_realloc(config->ipc_namespace, queues, struct SysVIpcMsgQueue, num_queues + 1); + memset(&queues[queue_index], 0, sizeof(queues[queue_index])); + } + queue = &queues[queue_index]; + queue->key = queue_id; + queue->valid = true; + queue->items = talloc(config->ipc_namespace->queues, struct SysVIpcMsgQueueItems); + STAILQ_INIT(queue->items); + memset(&queue->stats, 0, sizeof(queue->stats)); + queue->stats.msg_qbytes = 1024 * 64; // Not enforced limit + } else { + if ((msgflg & IPC_CREAT) && (msgflg & IPC_EXCL)) { + return -EEXIST; + } + queue = &queues[queue_index]; + } + return (queue_index + 1) | (queue->generation << 12); +} + +static bool sysvipc_msg_match(int sender_type, int receiver_filter, int receiver_flag) +{ + bool matched = + receiver_filter == 0 || + sender_type == receiver_filter || + (receiver_filter < 0 && sender_type <= -receiver_filter); + + if (receiver_flag & MSG_EXCEPT) { + matched = !matched; + } + + return matched; +} + +static int sysvipc_msg_deliver(Tracee *recipent_tracee, struct SysVIpcConfig *recipent_config, struct SysVIpcMsgQueue *queue, struct SysVIpcMsgQueueItem *msg, time_t delivery_time) +{ + size_t msgsz = recipent_config->msgrcv_msgsz; + if (msg->mtext_length > msgsz) { + if (!(recipent_config->msgrcv_msgflg & MSG_NOERROR)) { + return -E2BIG; + } + } else { + msgsz = msg->mtext_length; + } + + int status = write_data(recipent_tracee, recipent_config->msgrcv_msgp, &msg->mtype, sizeof(long)); + if (status < 0) return status; + status = write_data(recipent_tracee, recipent_config->msgrcv_msgp + sizeof(long), msg->mtext, msgsz); + if (status < 0) return status; + queue->stats.msg_lrpid = recipent_tracee->pid; + queue->stats.msg_rtime = delivery_time; + return msgsz; +} + +int sysvipc_msgsnd(Tracee *tracee, struct SysVIpcConfig *config) { + /** Lookup queue */ + size_t queue_index; + struct SysVIpcMsgQueue *queue; + LOOKUP_IPC_OBJECT(queue_index, queue, config->ipc_namespace->queues) + + /** Read and check arguments */ + word_t msgp = peek_reg(tracee, CURRENT, SYSARG_2); + size_t msgsz = peek_reg(tracee, CURRENT, SYSARG_3); + //int msgflg = peek_reg(tracee, CURRENT, SYSARG_4); + if (msgsz > SYSVIPC_MAX_MSG_SIZE) { + return -EINVAL; + } + long mtype = 0; + { + int status = read_data(tracee, &mtype, msgp, sizeof(long)); + if (status < 0) { + return status; + } + } + if (mtype < 1) { + return -EINVAL; + } + + /** Create queue item */ + struct SysVIpcMsgQueueItem *item = talloc_zero(queue->items, struct SysVIpcMsgQueueItem); + item->mtype = mtype; + item->mtext_length = msgsz; + item->mtext = talloc_array(item, char, msgsz); + { + int status = read_data(tracee, item->mtext, msgp + sizeof(long), msgsz); + if (status < 0) { + talloc_free(item); + return status; + } + } + + /** Update stats */ + time_t current_time = 0; + time(¤t_time); + queue->stats.msg_lspid = tracee->pid; + queue->stats.msg_stime = current_time; + + /** Deliver to waiting msgrcv */ + Tracee *receiver_tracee; + struct SysVIpcConfig *receiver_config; + SYSVIPC_FOREACH_TRACEE(receiver_tracee, receiver_config, config->ipc_namespace) { + if ( + receiver_config->wait_reason == WR_WAIT_QUEUE_RECV && + receiver_config->waiting_object_index == queue_index && + sysvipc_msg_match( + item->mtype, + receiver_config->msgrcv_msgtyp, + receiver_config->msgrcv_msgflg + ) + ) { + receiver_config->chain_state = CSTATE_MSGRCV_RETRY; + sysvipc_wake_tracee( + receiver_tracee, + receiver_config, + -EAGAIN + ); + break; + } + } + + STAILQ_INSERT_TAIL(queue->items, item, link); + queue->stats.msg_qnum += 1; + queue->stats.msg_cbytes += item->mtext_length; + + return 0; +} + +static int sysvipc_do_msgrcv(Tracee *tracee, struct SysVIpcConfig *config, size_t queue_index, struct SysVIpcMsgQueue *queue) { + if ((int) config->msgrcv_msgsz < 0) { + return -EINVAL; + } + + if ((config->msgrcv_msgflg & ~(IPC_NOWAIT | MSG_NOERROR | MSG_COPY | MSG_EXCEPT)) != 0) { + return -EINVAL; + } + + bool copy = (config->msgrcv_msgflg & MSG_COPY) != 0; + if (copy) { + if ((config->msgrcv_msgflg & IPC_NOWAIT) == 0) { + return -EINVAL; + } + if ((config->msgrcv_msgflg & MSG_EXCEPT) != 0) { + return -EINVAL; + } + } + + struct SysVIpcMsgQueueItem *found_item = NULL; + struct SysVIpcMsgQueueItem *candidate_item; + if (copy) { + int index = 0; + STAILQ_FOREACH(candidate_item, queue->items, link) { + if (index == config->msgrcv_msgtyp) { + found_item = candidate_item; + break; + } + index += 1; + } + } else { + STAILQ_FOREACH(candidate_item, queue->items, link) { + if (sysvipc_msg_match(candidate_item->mtype, config->msgrcv_msgtyp, config->msgrcv_msgflg)) { + found_item = candidate_item; + break; + } + } + } + + if (found_item == NULL) { + if (config->msgrcv_msgflg & IPC_NOWAIT) { + return -ENOMSG; + } else { + config->wait_reason = WR_WAIT_QUEUE_RECV; + config->waiting_object_index = queue_index; + return 0; + } + } + + time_t current_time = 0; + time(¤t_time); + + int status = sysvipc_msg_deliver(tracee, config, queue, found_item, current_time); + + if (status >= 0 && !copy) { + queue->stats.msg_qnum -= 1; + queue->stats.msg_cbytes -= found_item->mtext_length; + STAILQ_REMOVE(queue->items, found_item, SysVIpcMsgQueueItem, link); + talloc_free(found_item); + } + + return status; +} + +int sysvipc_msgrcv(Tracee *tracee, struct SysVIpcConfig *config) { + size_t queue_index; + struct SysVIpcMsgQueue *queue; + LOOKUP_IPC_OBJECT(queue_index, queue, config->ipc_namespace->queues) + + config->msgrcv_msgp = peek_reg(tracee, CURRENT, SYSARG_2); + config->msgrcv_msgsz = peek_reg(tracee, CURRENT, SYSARG_3); + config->msgrcv_msgtyp = peek_reg(tracee, CURRENT, SYSARG_4); + config->msgrcv_msgflg = peek_reg(tracee, CURRENT, SYSARG_5); + + return sysvipc_do_msgrcv(tracee, config, queue_index, queue); +} + +int sysvipc_msgrcv_retry(Tracee *tracee, struct SysVIpcConfig *config) { + assert(config->chain_state == CSTATE_MSGRCV_RETRY); + + int status = config->status_after_wait; + + if (status == -EAGAIN) { + size_t queue_index = config->waiting_object_index; + assert(queue_index < talloc_array_length(config->ipc_namespace->queues)); + struct SysVIpcMsgQueue *queue = &config->ipc_namespace->queues[queue_index]; + assert(queue->valid); + status = sysvipc_do_msgrcv(tracee, config, queue_index, queue); + + /* Retry handler requested wait? This is uncommon path + * (but can happen due to e.g. concurrent msgrcv consuming message), + * do a spurious wakeup */ + if (config->wait_reason != WR_NOT_WAITING) { + status = -EINTR; + config->wait_reason = WR_NOT_WAITING; + } + } + + config->chain_state = CSTATE_NOT_CHAINED; + + return status; +} + +int sysvipc_msgctl(Tracee *tracee, struct SysVIpcConfig *config) { + size_t queue_index; + struct SysVIpcMsgQueue *queue; + LOOKUP_IPC_OBJECT(queue_index, queue, config->ipc_namespace->queues) + + int cmd = peek_reg(tracee, CURRENT, SYSARG_2); + word_t buf = peek_reg(tracee, CURRENT, SYSARG_3); + + switch (cmd) { + case IPC_RMID: + case IPC_RMID | SYSVIPC_IPC_64: + { + Tracee *waiting_tracee; + struct SysVIpcConfig *waiting_config; + SYSVIPC_FOREACH_TRACEE(waiting_tracee, waiting_config, config->ipc_namespace) { + if ( + waiting_config->wait_reason == WR_WAIT_QUEUE_RECV && + waiting_config->waiting_object_index == queue_index + ) { + sysvipc_wake_tracee(waiting_tracee, waiting_config, -EIDRM); + } + } + + queue->valid = false; + queue->generation++; + TALLOC_FREE(queue->items); + return 0; + } + case IPC_STAT: + case IPC_STAT | SYSVIPC_IPC_64: + { + int status = write_data(tracee, buf, &queue->stats, sizeof(struct msqid_ds)); + if (status < 0) return status; + return 0; + } + default: + return -EINVAL; + } +} diff --git a/core/proot/src/main/cpp/extension/sysvipc/sysvipc_sem.c b/core/proot/src/main/cpp/extension/sysvipc/sysvipc_sem.c new file mode 100644 index 000000000..89383ca90 --- /dev/null +++ b/core/proot/src/main/cpp/extension/sysvipc/sysvipc_sem.c @@ -0,0 +1,279 @@ +#include "sysvipc_internal.h" + +#include "tracee/reg.h" +#include "tracee/mem.h" + +#include /* E* */ +#include /* memset */ +#include /* assert */ + +#define SYSVIPC_MAX_SEMS 512 +#define SYSVIPC_MAX_NSEMS 512 +#define SYSVIPC_MAX_NSOPS 512 +#define SYSVIPC_MAX_SEMVAL 0x7000 + +int sysvipc_semget(Tracee *tracee, struct SysVIpcConfig *config) { + word_t semaphore_id = peek_reg(tracee, CURRENT, SYSARG_1); + int nsems = peek_reg(tracee, CURRENT, SYSARG_2); + int semflg = peek_reg(tracee, CURRENT, SYSARG_3); + + if (nsems <= 0 || nsems > SYSVIPC_MAX_NSEMS) { + return -EINVAL; + } + + struct SysVIpcSemaphore *semaphores = config->ipc_namespace->semaphores; + size_t unused_slot = 0; + size_t semaphore_index = 0; + size_t num_semaphores = talloc_array_length(semaphores); + bool found_unused_slot = false; + bool found_semaphore = false; + for (; semaphore_index < num_semaphores; semaphore_index++) { + if (semaphores[semaphore_index].valid) { + if(semaphore_id != IPC_PRIVATE && semaphores[semaphore_index].key == (int32_t) semaphore_id) { + found_semaphore = true; + break; + } + } else if (!found_unused_slot) { + unused_slot = semaphore_index; + found_unused_slot = true; + } + } + struct SysVIpcSemaphore *semaphore = NULL; + if (!found_semaphore) { + if (!(semflg & IPC_CREAT)) { + return -ENOENT; + } + if (found_unused_slot) { + semaphore_index = unused_slot; + } else { + if (num_semaphores >= SYSVIPC_MAX_SEMS) { + return -ENOSPC; + } + semaphore_index = num_semaphores; + config->ipc_namespace->semaphores = semaphores = talloc_realloc(config->ipc_namespace, semaphores, struct SysVIpcSemaphore, num_semaphores + 1); + memset(&semaphores[semaphore_index], 0, sizeof(semaphores[semaphore_index])); + } + semaphore = &semaphores[semaphore_index]; + semaphore->key = semaphore_id; + semaphore->valid = true; + semaphore->sems = talloc_array(config->ipc_namespace, uint16_t, nsems); + memset(semaphore->sems, 0, nsems * sizeof(uint16_t)); + semaphore->nsems = nsems; + } else { + if ((semflg & IPC_CREAT) && (semflg & IPC_EXCL)) { + return -EEXIST; + } + semaphore = &semaphores[semaphore_index]; + if (semaphore->nsems < nsems) { + return -EINVAL; + } + } + return (semaphore_index + 1) | (semaphore->generation << 12); +} + +/** + * Check/execute semops of given tracee + * + * Returns 1 if tracee should still wait, otherwise + * returns result semop syscall shall return + * + * If out_wait_type is non-NULL when semaphore waits + * 'n' or 'z' is written to indicate this semaphore + * is counted for GETNCNT/GETZCNT. + */ +static int sysvipc_sem_check(struct SysVIpcConfig *config, struct SysVIpcSemaphore *semaphore, char *out_wait_type) { + assert(config->wait_reason == WR_WAIT_SEMOP); + + size_t nsops = talloc_array_length(config->semop_sops); + uint16_t new_sems[semaphore->nsems]; + memcpy(new_sems, semaphore->sems, semaphore->nsems * sizeof(uint16_t)); + + for (size_t i = 0; i < nsops; i++) { + int op = config->semop_sops[i].sem_op; + size_t sem_num = config->semop_sops[i].sem_num; + if (op == 0) { + if (new_sems[sem_num]) { + if (config->semop_sops[i].sem_flg & IPC_NOWAIT) { + return -EAGAIN; + } + if (out_wait_type != NULL) *out_wait_type = 'z'; + return 1; + } + } else { + int new_value = (int)new_sems[sem_num] + op; + if (new_value < 0) { + if (config->semop_sops[i].sem_flg & IPC_NOWAIT) { + return -EAGAIN; + } + if (out_wait_type != NULL) *out_wait_type = 'n'; + return 1; + } + if (new_value > SYSVIPC_MAX_SEMVAL) { + return -ERANGE; + } + new_sems[sem_num] = new_value; + } + } + memcpy(semaphore->sems, new_sems, semaphore->nsems * sizeof(uint16_t)); + + return 0; +} + +int sysvipc_semop(Tracee *tracee, struct SysVIpcConfig *config) +{ + /** Lookup semaphore */ + size_t semaphore_index; + struct SysVIpcSemaphore *semaphore; + LOOKUP_IPC_OBJECT(semaphore_index, semaphore, config->ipc_namespace->semaphores) + + /** Read and check arguments */ + word_t sops_ptr = peek_reg(tracee, CURRENT, SYSARG_2); + size_t nsops = peek_reg(tracee, CURRENT, SYSARG_3); + + if (nsops > SYSVIPC_MAX_NSOPS) { + return -E2BIG; + } + + if (nsops == 0) { + return -EINVAL; + } + + struct SysVIpcSembuf *sops = talloc_array(config, struct SysVIpcSembuf, nsops); + int status = read_data(tracee, sops, sops_ptr, sizeof(struct SysVIpcSembuf) * nsops); + if (status < 0) { + talloc_free(sops); + return status; + } + + for (size_t i = 0; i < nsops; i++) { + if (sops[i].sem_num < 0 || sops[i].sem_num >= semaphore->nsems) { + talloc_free(sops); + return -EFBIG; + } + } + + config->wait_reason = WR_WAIT_SEMOP; + config->waiting_object_index = semaphore_index; + config->semop_sops = sops; + int this_semop_status = sysvipc_sem_check(config, semaphore, NULL); + + Tracee *other_tracee; + struct SysVIpcConfig *other_config; + SYSVIPC_FOREACH_TRACEE(other_tracee, other_config, config->ipc_namespace) { + if ( + other_config != config && + other_config->wait_reason == WR_WAIT_SEMOP && + other_config->waiting_object_index == semaphore_index) { + int other_semop_status = sysvipc_sem_check(other_config, semaphore, NULL); + if (other_semop_status != 1) { + TALLOC_FREE(other_config->semop_sops); + sysvipc_wake_tracee(other_tracee, other_config, other_semop_status); + } + } + } + + + if (this_semop_status == 1) { + assert(config->wait_reason == WR_WAIT_SEMOP); + return 0; + } else { + TALLOC_FREE(config->semop_sops); + config->wait_reason = WR_NOT_WAITING; + return this_semop_status; + } +} + +void sysvipc_semop_timedout(Tracee *tracee, struct SysVIpcConfig *config) { + (void) tracee; + + TALLOC_FREE(config->semop_sops); + config->wait_reason = WR_NOT_WAITING; +} + +int sysvipc_semctl(Tracee *tracee, struct SysVIpcConfig *config) { + /** Lookup semaphore */ + size_t semaphore_index; + struct SysVIpcSemaphore *semaphore; + LOOKUP_IPC_OBJECT(semaphore_index, semaphore, config->ipc_namespace->semaphores) + + int semnum = peek_reg(tracee, CURRENT, SYSARG_2); + int cmd = peek_reg(tracee, CURRENT, SYSARG_3); + word_t cmdarg = peek_reg(tracee, CURRENT, SYSARG_4); + + switch (cmd & ~SYSVIPC_IPC_64) { + case SYSVIPC_GETVAL: + { + if (semnum < 0 || semnum >= semaphore->nsems) return -EINVAL; + return semaphore->sems[semnum]; + } + case SYSVIPC_SETVAL: + { + if (cmdarg > SYSVIPC_MAX_SEMVAL) return -ERANGE; + if (semnum < 0 || semnum >= semaphore->nsems) return -EINVAL; + semaphore->sems[semnum] = cmdarg; + return 0; + } + case SYSVIPC_GETALL: + { + int status = write_data(tracee, cmdarg, semaphore->sems, semaphore->nsems * sizeof(uint16_t)); + if (status < 0) return status; + return 0; + } + case IPC_RMID: + { + Tracee *waiting_tracee; + struct SysVIpcConfig *waiting_config; + SYSVIPC_FOREACH_TRACEE(waiting_tracee, waiting_config, config->ipc_namespace) { + if ( + waiting_config->wait_reason == WR_WAIT_SEMOP && + waiting_config->waiting_object_index == semaphore_index + ) { + sysvipc_wake_tracee(waiting_tracee, waiting_config, -EIDRM); + } + } + + semaphore->valid = false; + semaphore->generation++; + TALLOC_FREE(semaphore->sems); + return 0; + } +#if 0 + case IPC_STAT: + { + int status = write_data(tracee, buf, &queue->stats, sizeof(struct msqid_ds)); + if (status < 0) return status; + return 0; + } +#endif + case SYSVIPC_IPC_INFO: + case SYSVIPC_SEM_INFO: + { + struct SysVIpcSeminfo info = { + // semmap + .semmni = SYSVIPC_MAX_SEMS, + .semmns = SYSVIPC_MAX_SEMS * SYSVIPC_MAX_NSEMS, + // semmnu + .semmsl = SYSVIPC_MAX_NSEMS, + .semopm = SYSVIPC_MAX_NSOPS, + // semume + // semusz + .semvmx = SYSVIPC_MAX_SEMVAL + // semaem + }; + if (cmd == SYSVIPC_SEM_INFO) { + struct SysVIpcSemaphore *semaphores = config->ipc_namespace->semaphores; + size_t num_semaphores = talloc_array_length(semaphores); + info.semusz = num_semaphores; + info.semaem = 0; + for (size_t i = 0; i < num_semaphores; i++) { + info.semaem += semaphores[i].nsems; + } + } + int status = write_data(tracee, cmdarg, &info, sizeof(info)); + if (status < 0) return status; + return 0; + } + default: + return -EINVAL; + } +} diff --git a/core/proot/src/main/cpp/extension/sysvipc/sysvipc_shm.c b/core/proot/src/main/cpp/extension/sysvipc/sysvipc_shm.c new file mode 100644 index 000000000..6cb56e171 --- /dev/null +++ b/core/proot/src/main/cpp/extension/sysvipc/sysvipc_shm.c @@ -0,0 +1,749 @@ +#include "sysvipc.h" +#include "sysvipc_internal.h" + +#include "tracee/reg.h" +#include "tracee/mem.h" +#include "tracee/tracee.h" +#include "extension/extension.h" +#include "path/temp.h" +#include "syscall/chain.h" + +#include +#include + +#include +#include /* assert */ +#include /* E* */ +#include /* S_IR */ +#include /* MAP_SHARED */ +#include /* syscall() */ +#include /* memset */ +#include /* time */ +#include /* open, fcntl */ + +#ifdef __ANDROID__ +#include /* ASHMEM_* */ +#else +#include /* ftruncate */ +#endif + + +#define SYSVIPC_MAX_SHM_SIZE 100 * 4096 + +struct SysVIpcRecvMsgPointers { + word_t msghdr_ptr; + word_t cmsg_controllen_ptr; + word_t cmsg_control_ptr; +}; + +int sysvipc_shm_recvmsg_pointers(Tracee *tracee, struct SysVIpcRecvMsgPointers *out_pointers, word_t guest_buf, bool do_write) +{ +#if defined(ARCH_X86_64) || defined(ARCH_ARM64) + bool is32 = is_32on64_mode(tracee); +#else + bool is32 = true; +#endif + + /* Calculate addresses of given fields in guest userspace. */ + word_t ptr_len = is32 ? 4 : 8; + word_t buf_end = guest_buf + sizeof(struct sockaddr_un); + + word_t data_addr = buf_end - 4 ; + + word_t data_iov_length = data_addr - ptr_len; + word_t data_iov_addr = data_iov_length - ptr_len; + + word_t msghdr_flags = data_iov_addr - ptr_len; + word_t msghdr_controllen = msghdr_flags - ptr_len; + word_t msghdr_control = msghdr_controllen - ptr_len; + word_t msghdr_iovlen = msghdr_control - ptr_len; + word_t msghdr_iov = msghdr_iovlen - ptr_len; + word_t msghdr = msghdr_iov - ptr_len * 2; // name & namelen unused + // control data is at guest_buf + + if (do_write) { + char data[sizeof(struct sockaddr_un)] = {}; + + if (is32) { + *(uint32_t*) &data[data_iov_addr - guest_buf] = data_addr; + *(uint32_t*) &data[data_iov_length - guest_buf] = 1; + *(uint32_t*) &data[msghdr_iov - guest_buf] = data_iov_addr; + *(uint32_t*) &data[msghdr_iovlen - guest_buf] = 1; + *(uint32_t*) &data[msghdr_control - guest_buf] = guest_buf; + *(uint32_t*) &data[msghdr_controllen - guest_buf] = 20; // sizeof(cmsghdr) + sizeof(uint64_t) + } else { + *(uint64_t*) &data[data_iov_addr - guest_buf] = data_addr; + *(uint64_t*) &data[data_iov_length - guest_buf] = 1; + *(uint64_t*) &data[msghdr_iov - guest_buf] = data_iov_addr; + *(uint64_t*) &data[msghdr_iovlen - guest_buf] = 1; + *(uint64_t*) &data[msghdr_control - guest_buf] = guest_buf; + *(uint64_t*) &data[msghdr_controllen - guest_buf] = 20; // sizeof(cmsghdr) + sizeof(uint64_t) + } + + int status = write_data(tracee, guest_buf, data, sizeof(data)); + if (status < 0) return status; + } + + out_pointers->msghdr_ptr = msghdr; + out_pointers->cmsg_controllen_ptr = msghdr_controllen; + out_pointers->cmsg_control_ptr = guest_buf; + return 0; +} + +enum SysVIpcShmHelperRequestOp { + SHMHELPER_DISTRIBUTE, + SHMHELPER_ALLOC, + SHMHELPER_FREE, +}; +struct SysVIpcShmHelperRequest { + enum SysVIpcShmHelperRequestOp op; + int fd; + size_t size; +}; +#define SYSVIPC_SHMHELPER_SOCKET_LEN 108 + +static struct sockaddr_un sysvipc_shm_helper_addr; +static int sysvipc_shm_send_helper_request(struct SysVIpcShmHelperRequest *request) +{ + static bool launched_helper; + static int proot2helper; + static int helper2proot; + + if (!launched_helper) { + int pipe_proot2helper[2]; + int pipe_helper2proot[2]; + if (pipe2(pipe_proot2helper, O_CLOEXEC) < 0) { + return -1; + } + if (pipe2(pipe_helper2proot, O_CLOEXEC) < 0) { + close(pipe_proot2helper[0]); + close(pipe_proot2helper[1]); + return -1; + } + pid_t forked = fork(); + if (forked == 0) { + close(pipe_proot2helper[1]); + close(pipe_helper2proot[0]); + dup2(pipe_proot2helper[0], 0); + dup2(pipe_helper2proot[1], 1); + close(pipe_proot2helper[0]); + close(pipe_helper2proot[1]); + fcntl(0, F_SETFL, 0); + fcntl(1, F_SETFL, 0); + + /* Fork again to detach from proot waitpid() */ + forked = fork(); + if (forked == 0) { + execl("/proc/self/exe", "proot", "--shm-helper", NULL); + perror("proot-shm-helper: execl"); + _exit(1); + } else { + if (forked < 0) { + perror("proot-shm-helper: second fork"); + } + _exit(0); + } + } else if (forked < 0) { + perror("proot-shm-helper: first fork"); + close(pipe_proot2helper[0]); + close(pipe_proot2helper[1]); + close(pipe_helper2proot[0]); + close(pipe_helper2proot[1]); + return -1; + } else { + close(pipe_proot2helper[0]); + close(pipe_helper2proot[1]); + int nread = read(pipe_helper2proot[0], sysvipc_shm_helper_addr.sun_path, SYSVIPC_SHMHELPER_SOCKET_LEN); + if (nread != SYSVIPC_SHMHELPER_SOCKET_LEN) { + close(pipe_proot2helper[1]); + close(pipe_helper2proot[0]); + return -1; + } + sysvipc_shm_helper_addr.sun_family = AF_UNIX; + launched_helper = true; + proot2helper = pipe_proot2helper[1]; + helper2proot = pipe_helper2proot[0]; + } + } + + write(proot2helper, request, sizeof(*request)); + if (request->op == SHMHELPER_ALLOC) { + int fd = -1; + read(helper2proot, &fd, sizeof(fd)); + return fd; + } + return 0; +} + +int sysvipc_shmget(Tracee *tracee, struct SysVIpcConfig *config) +{ + word_t shm_key = peek_reg(tracee, CURRENT, SYSARG_1); + size_t shm_size = peek_reg(tracee, CURRENT, SYSARG_2); + word_t shmflg = peek_reg(tracee, CURRENT, SYSARG_3); + struct SysVIpcSharedMem *shms = config->ipc_namespace->shms; + size_t unused_slot = 0; + size_t shm_index = 0; + size_t num_shms = talloc_array_length(shms); + bool found_unused_slot = false; + bool found_queue = false; + for (; shm_index < num_shms; shm_index++) { + if (shms[shm_index].valid) { + if(shm_key != IPC_PRIVATE && shms[shm_index].key == (int32_t) shm_key) { + found_queue = true; + break; + } + } else if (!found_unused_slot) { + unused_slot = shm_index; + found_unused_slot = true; + } + } + struct SysVIpcSharedMem *shm = NULL; + if (!found_queue) { + if (!(shmflg & IPC_CREAT)) { + return -ENOENT; + } + if (found_unused_slot) { + shm_index = unused_slot; + } else { + shm_index = num_shms; + config->ipc_namespace->shms = shms = talloc_realloc(config->ipc_namespace, shms, struct SysVIpcSharedMem, num_shms + 1); + memset(&shms[shm_index], 0, sizeof(shms[shm_index])); + } + shm = &shms[shm_index]; + struct SysVIpcShmHelperRequest request = { + .op = SHMHELPER_ALLOC, + .fd = IPC_OBJECT_ID(shm_index, shm), + .size = shm_size + }; + shm->fd = sysvipc_shm_send_helper_request(&request); + if (shm->fd < 0) { + return -ENOSPC; + } + memset(&shm->stats.shm_segsz, 0, sizeof(shm->stats.shm_segsz)); + shm->stats.shm_perm.mode = shmflg & 0777; + shm->stats.shm_segsz = shm_size; + shm->stats.shm_cpid = config->process->pgid; + shm->key = shm_key; + shm->valid = true; + shm->mappings = talloc_zero(config->ipc_namespace, struct SysVIpcSharedMemMaps); + LIST_INIT(shm->mappings); + } else { + if ((shmflg & IPC_CREAT) && (shmflg & IPC_EXCL)) { + return -EEXIST; + } + shm = &shms[shm_index]; + if (shm_size && shm_size != shm->stats.shm_segsz) { + return -EINVAL; + } + } + return IPC_OBJECT_ID(shm_index, shm); +} + +static void sysvipc_do_rmid(struct SysVIpcSharedMem *shm) +{ + assert(LIST_EMPTY(shm->mappings)); + + /* Close ashmem fd in helper process */ + struct SysVIpcShmHelperRequest request = { + .op = SHMHELPER_FREE, + .fd = shm->fd, + }; + sysvipc_shm_send_helper_request(&request); + + /* Mark shm as freed */ + TALLOC_FREE(shm->mappings); + shm->valid = false; + shm->rmid_pending = false; + shm->generation = (shm->generation + 1) & 0xFFFF; + shm->fd = -1; +} + +static int sysvipc_shm_memmap_destructor(struct SysVIpcSharedMemMap *mapping) { + LIST_REMOVE(mapping, link_shmid); + LIST_REMOVE(mapping, link_process); + + struct SysVIpcSharedMem *shm = &mapping->ipc_namespace->shms[mapping->shm_index]; + if (shm->rmid_pending && LIST_EMPTY(shm->mappings)) { + sysvipc_do_rmid(shm); + } + return 0; +} + +static void sysvipc_shm_wake_pending_shmat() +{ + Tracee *other_tracee; + struct SysVIpcConfig *other_config; + SYSVIPC_FOREACH_TRACEE_ANY_NAMESPACE(other_tracee, other_config) { + if (other_config->wait_reason == WR_WAIT_SHMAT_HELPER_BUSY) { + // Restart shmat operation with socket(AF_UNIX, SOCK_SEQPACKET, 0) + sysvipc_wake_tracee(other_tracee, other_config, 0); + register_chained_syscall(other_tracee, PR_socket, AF_UNIX, SOCK_SEQPACKET, 0, 0, 0, 0); + other_config->chain_state = CSTATE_SHMAT_SOCKET; + return; + } + } +} + +int sysvipc_shmat(Tracee *tracee, struct SysVIpcConfig *config) +{ + size_t shm_index; + struct SysVIpcSharedMem *shm; + LOOKUP_IPC_OBJECT(shm_index, shm, config->ipc_namespace->shms) + + config->waiting_object_index = shm_index; + + // Register mapping for process (to prevent IPC_RMID concurrent to shmat) + struct SysVIpcSharedMemMap *mapping = talloc_zero(config->process, struct SysVIpcSharedMemMap); + talloc_set_destructor(mapping, sysvipc_shm_memmap_destructor); + mapping->ipc_namespace = config->ipc_namespace; + mapping->shm_index = shm_index; + LIST_INSERT_HEAD(&config->process->mapped_shms, mapping, link_process); + LIST_INSERT_HEAD(shm->mappings, mapping, link_shmid); + + // Check if any tracee is running concurrent shmat and wait if so + Tracee *other_tracee; + struct SysVIpcConfig *other_config; + SYSVIPC_FOREACH_TRACEE_ANY_NAMESPACE(other_tracee, other_config) { + if (other_config->chain_state > CSTATE_SHMAT_SOCKET && other_config->chain_state <= CSTATE_SHMAT_MMAP) { + config->wait_reason = WR_WAIT_SHMAT_HELPER_BUSY; + return 0; + } + } + + // Start operation with socket(AF_UNIX, SOCK_SEQPACKET, 0) + set_sysnum(tracee, PR_socket); + poke_reg(tracee, SYSARG_1, AF_UNIX); + poke_reg(tracee, SYSARG_2, SOCK_SEQPACKET); + poke_reg(tracee, SYSARG_3, 0); + config->chain_state = CSTATE_SHMAT_SOCKET; + return 0; +} + +static struct SysVIpcSharedMemMap *sysvipc_shm_find_pending_mapping(struct SysVIpcProcess *process, struct SysVIpcNamespace *ipc_namespace, size_t shm_index) +{ + struct SysVIpcSharedMemMap *mapping; + LIST_FOREACH(mapping, &process->mapped_shms, link_process) { + if (mapping->size == 0 && mapping->shm_index == shm_index && mapping->ipc_namespace == ipc_namespace) { + return mapping; + } + } + assert(!"No pending mapping found"); +} + +int sysvipc_shmat_chain(Tracee *tracee, struct SysVIpcConfig *config) +{ + assert(config->waiting_object_index < talloc_array_length(config->ipc_namespace->shms)); + struct SysVIpcSharedMem *shm = &config->ipc_namespace->shms[config->waiting_object_index]; + assert(shm->valid); + switch (config->chain_state) { + case CSTATE_SHMAT_SOCKET: + { + config->shmat_socket_fd = peek_reg(tracee, CURRENT, SYSARG_RESULT); + if (config->shmat_socket_fd < 0) { + goto fail_dont_close; + } + word_t guest_addr = peek_reg(tracee, CURRENT, STACK_POINTER) - sizeof(struct sockaddr_un); + if (guest_addr == 0) { + goto fail_dont_close; + } + if (write_data(tracee, guest_addr, &sysvipc_shm_helper_addr, sizeof(struct sockaddr_un)) < 0) { + goto fail_dont_close; + } + register_chained_syscall(tracee, PR_connect, config->shmat_socket_fd, guest_addr, sizeof(struct sockaddr_un), 0, 0, 0); + config->shmat_guest_buf = guest_addr; + config->chain_state = CSTATE_SHMAT_CONNECT; + return 1; + } + case CSTATE_SHMAT_CONNECT: + { + if ((int) peek_reg(tracee, CURRENT, SYSARG_RESULT) != 0) { + goto fail_close_socket; + } + + struct SysVIpcRecvMsgPointers pointers; + if (sysvipc_shm_recvmsg_pointers(tracee, &pointers, config->shmat_guest_buf, true)) { + goto fail_close_socket; + } + + struct SysVIpcShmHelperRequest request = { + .op = SHMHELPER_DISTRIBUTE, + .fd = shm->fd, + }; + if (sysvipc_shm_send_helper_request(&request) < 0) { + goto fail_close_socket; + } + + register_chained_syscall(tracee, PR_recvmsg, config->shmat_socket_fd, pointers.msghdr_ptr, 0, 0, 0, 0); + config->chain_state = CSTATE_SHMAT_RECVMSG; + + return 1; + } + case CSTATE_SHMAT_RECVMSG: + { + struct SysVIpcRecvMsgPointers pointers; + if (sysvipc_shm_recvmsg_pointers(tracee, &pointers, config->shmat_guest_buf, false)) { + goto fail_close_socket; + } + + struct cmsghdr cmsg = {}; + if (read_data(tracee, &cmsg, pointers.cmsg_control_ptr, sizeof(cmsg)) < 0) { + goto fail_close_socket; + } + if (cmsg.cmsg_level != SOL_SOCKET || cmsg.cmsg_type != SCM_RIGHTS) { + goto fail_close_socket; + } + + word_t fd = 0; + if (read_data(tracee, &fd, pointers.cmsg_control_ptr + sizeof(cmsg), 4) < 0) { + goto fail_close_socket; + } + if (fd > 0xFFFF) { + goto fail_close_socket; + } + config->shmat_mem_fd = fd; + + size_t page_size = sysconf(_SC_PAGESIZE); + size_t map_size = shm->stats.shm_segsz; + map_size = (map_size + (page_size - 1)) & ~(page_size - 1); + + word_t mmap_sysnum = detranslate_sysnum(get_abi(tracee), PR_mmap2) != SYSCALL_AVOIDER + ? PR_mmap2 + : PR_mmap; + register_chained_syscall(tracee, mmap_sysnum, 0, map_size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0); + config->chain_state = CSTATE_SHMAT_MMAP; + + return 1; + } + case CSTATE_SHMAT_MMAP: + { + word_t addr = peek_reg(tracee, CURRENT, SYSARG_RESULT); + + struct SysVIpcSharedMemMap *mapping = sysvipc_shm_find_pending_mapping(config->process, config->ipc_namespace, config->waiting_object_index); + mapping->addr = addr; + mapping->size = peek_reg(tracee, CURRENT, SYSARG_2); + + register_chained_syscall(tracee, PR_close, config->shmat_mem_fd, 0, 0, 0, 0, 0); + register_chained_syscall(tracee, PR_close, config->shmat_socket_fd, 0, 0, 0, 0, 0); + force_chain_final_result(tracee, addr); + config->chain_state = CSTATE_NOT_CHAINED; + sysvipc_shm_wake_pending_shmat(); + + return 1; + } + + default: + assert(!"Invalid chain_state in sysvipc_shmat_chain"); + } + assert(!"Switch in sysvipc_shmat_chain has fallen thorugh"); + +fail_close_socket: + register_chained_syscall(tracee, PR_close, config->shmat_socket_fd, 0, 0, 0, 0, 0); + force_chain_final_result(tracee, -ENOMEM); + config->chain_state = CSTATE_NOT_CHAINED; + talloc_free(sysvipc_shm_find_pending_mapping(config->process, config->ipc_namespace, config->waiting_object_index)); + sysvipc_shm_wake_pending_shmat(); + return 1; +fail_dont_close: + config->chain_state = CSTATE_NOT_CHAINED; + talloc_free(sysvipc_shm_find_pending_mapping(config->process, config->ipc_namespace, config->waiting_object_index)); + sysvipc_shm_wake_pending_shmat(); + return -ENOMEM; +} + +int sysvipc_shmdt(Tracee *tracee, struct SysVIpcConfig *config) +{ + word_t addr = peek_reg(tracee, CURRENT, SYSARG_1); + struct SysVIpcSharedMemMap *mapped; + LIST_FOREACH(mapped, &config->process->mapped_shms, link_process) { + if (mapped->addr == addr) { + set_sysnum(tracee, PR_munmap); + poke_reg(tracee, SYSARG_2, mapped->size); + config->chain_state = CSTATE_SINGLE; + /* This talloc_free executes destructor and unlinks region */ + talloc_free(mapped); + return 0; + } + } + return -EINVAL; +} + +static void sysvipc_shm_update_stats(struct SysVIpcSharedMem *shm) +{ + shm->stats.shm_nattch = 0; + struct SysVIpcSharedMemMap *mapping = NULL; + LIST_FOREACH(mapping, shm->mappings, link_shmid) { + shm->stats.shm_nattch++; + } +} + +int sysvipc_shmctl(Tracee *tracee, struct SysVIpcConfig *config) +{ + size_t shm_index; + struct SysVIpcSharedMem *shm; + LOOKUP_IPC_OBJECT(shm_index, shm, config->ipc_namespace->shms) + + int cmd = peek_reg(tracee, CURRENT, SYSARG_2); + word_t buf = peek_reg(tracee, CURRENT, SYSARG_3); + + switch (cmd) { + case IPC_RMID: + case IPC_RMID | SYSVIPC_IPC_64: + { + /* Perform rmid only if this region is not mapped, + * otherwise set flag to do so once all maps are unmapped */ + if (LIST_EMPTY(shm->mappings)) { + sysvipc_do_rmid(shm); + } else { + shm->rmid_pending = true; + } + + return 0; + } + case IPC_STAT: + { + /* Update shm_nattch */ + sysvipc_shm_update_stats(shm); + + /* Copy stats to user */ + int status = write_data(tracee, buf, &shm->stats, sizeof(struct SysVIpcShmidDs)); + if (status < 0) return status; + return 0; + } + default: + return -EINVAL; + } +} + +void sysvipc_shm_inherit_process(struct SysVIpcProcess *parent, struct SysVIpcProcess *child) +{ + struct SysVIpcSharedMemMap *parent_mapping = NULL; + struct SysVIpcSharedMemMap *prev_inserted_mapping = NULL; + LIST_FOREACH(parent_mapping, &parent->mapped_shms, link_process) { + struct SysVIpcSharedMemMap *new_mapping = talloc_zero(child, struct SysVIpcSharedMemMap); + talloc_set_destructor(new_mapping, sysvipc_shm_memmap_destructor); + + new_mapping->addr = parent_mapping->addr; + new_mapping->size = parent_mapping->size; + new_mapping->ipc_namespace = parent_mapping->ipc_namespace; + new_mapping->shm_index = parent_mapping->shm_index; + LIST_INSERT_AFTER(parent_mapping, new_mapping, link_shmid); + + if (prev_inserted_mapping != NULL) { + LIST_INSERT_AFTER(prev_inserted_mapping, new_mapping, link_process); + } else { + LIST_INSERT_HEAD(&child->mapped_shms, new_mapping, link_process); + } + prev_inserted_mapping = new_mapping; + } +} + +void sysvipc_shm_remove_mappings_from_process(struct SysVIpcProcess *process) +{ + /* Remove all mappings from process + * Using manual le_ pointer operations because we free during loop */ + struct SysVIpcSharedMemMap *mapping = process->mapped_shms.lh_first; + while (mapping != NULL) { + struct SysVIpcSharedMemMap *next_mapping = mapping->link_process.le_next; + talloc_free(mapping); + mapping = next_mapping; + } +} + +void sysvipc_shm_fill_proc(FILE *proc_file, struct SysVIpcNamespace *ipc_namespace) +{ + fprintf( + proc_file, + " key shmid perms size cpid lpid nattch uid gid cuid cgid atime dtime ctime rss swap\n" + ); + + size_t page_size = sysconf(_SC_PAGESIZE); + struct SysVIpcSharedMem *shms = ipc_namespace->shms; + size_t shm_index = 0; + size_t num_shms = talloc_array_length(shms); + for (; shm_index < num_shms; shm_index++) { + struct SysVIpcSharedMem *shm = &shms[shm_index]; + if (!shms[shm_index].valid) { + continue; + } + + sysvipc_shm_update_stats(shm); + size_t map_size = shm->stats.shm_segsz; + map_size = (map_size + (page_size - 1)) & ~page_size; + + fprintf( + proc_file, + "%10d %10d %4o %21lu %5u %5u " + "%5lu %5u %5u %5u %5u %10llu %10llu %10llu " + "%21lu %21lu\n", + shm->key, + (int) IPC_OBJECT_ID(shm_index, shm), + shm->stats.shm_perm.mode, + shm->stats.shm_segsz, + shm->stats.shm_cpid, + shm->stats.shm_lpid, + shm->stats.shm_nattch, + shm->stats.shm_perm.uid, + shm->stats.shm_perm.gid, + shm->stats.shm_perm.cuid, + shm->stats.shm_perm.cgid, + (unsigned long long) shm->stats.shm_atime, + (unsigned long long) shm->stats.shm_dtime, + (unsigned long long) shm->stats.shm_ctime, + map_size, + 0L + ); + } +} + +int sysvipc_shm_namespace_destructor(struct SysVIpcNamespace *ipc_namespace) { + struct SysVIpcSharedMem *shms = ipc_namespace->shms; + size_t shm_index = 0; + size_t num_shms = talloc_array_length(shms); + for (; shm_index < num_shms; shm_index++) { + struct SysVIpcSharedMem *shm = &shms[shm_index]; + if (shm->valid) { + struct SysVIpcSharedMemMap *mapping; + LIST_FOREACH(mapping, shm->mappings, link_shmid) { + talloc_set_destructor(mapping, NULL); + } + } + } + return 0; +} + +static int sysvipc_shm_do_allocate(size_t size, int shmid) { +#ifdef __ANDROID__ + int fd = open("/dev/ashmem", O_RDWR, 0); + if (fd < 0) return -ENOSPC; + + char name_buffer[ASHMEM_NAME_LEN] = {0}; + snprintf(name_buffer, ASHMEM_NAME_LEN - 1, "sysvshm_0x%X", shmid); + ioctl(fd, ASHMEM_SET_NAME, name_buffer); + + int ret = ioctl(fd, ASHMEM_SET_SIZE, size); + if (ret < 0) { + close(fd); + return -ENOSPC; + } + + return fd; +#else + (void) shmid; + FILE *fdesc = tmpfile(); + if (!fdesc) return -ENOSPC; + int fd = dup(fileno(fdesc)); + fclose(fdesc); + if (fd < 0) return -ENOSPC; + + if (ftruncate(fd, size) == -1) { + return -ENOSPC; + } + + return fd; +#endif +} + +void sysvipc_shm_helper_main() { + char *path; + int socket_server_fd = socket(AF_UNIX, SOCK_SEQPACKET, 0); + struct sockaddr_un addr = { + .sun_family = AF_UNIX + }; + for (int i = 0;; i++) { + path = create_temp_name(NULL, "prootshm"); + (void) mktemp(path); + + if (strlen(path) > SYSVIPC_SHMHELPER_SOCKET_LEN) { + close(socket_server_fd); + fprintf(stderr, "proot-shm-helper: Temporary path too long\n"); + _exit(1); + } + + memset(addr.sun_path, 0 , sizeof(addr.sun_path)); + strncpy(addr.sun_path, path, sizeof(addr.sun_path)); + + if (bind(socket_server_fd, (struct sockaddr *) &addr, sizeof(addr)) == 0) { + break; + } + + if (i >= 64) { + perror("proot-shm-helper: bind"); + TALLOC_FREE(path); + close(socket_server_fd); + _exit(1); + } + TALLOC_FREE(path); + } + + if (listen(socket_server_fd, 1) < 0) { + perror("proot-shm-helper: listen"); + unlink(path); + _exit(0); + } + + write(1, addr.sun_path, SYSVIPC_SHMHELPER_SOCKET_LEN); + for (;;) { + struct SysVIpcShmHelperRequest request; + int status = TEMP_FAILURE_RETRY(read(0, &request, sizeof(request))); + if (status == 0) { + break; + } + if (status < 0) { + perror("proot-shm-helper: read"); + break; + } + if (status != sizeof(request)) { + fprintf(stderr, "proot-shm-helper: Incomplete request\n"); + break; + } + switch (request.op) { + case SHMHELPER_ALLOC: + { + int fd = sysvipc_shm_do_allocate(request.size, request.fd); + write(1, &fd, sizeof(int)); + break; + } + case SHMHELPER_FREE: + close(request.fd); + break; + case SHMHELPER_DISTRIBUTE: + { + int client_fd = accept(socket_server_fd, NULL, 0); + + char nothing = '!'; + struct iovec nothing_ptr = { .iov_base = ¬hing, .iov_len = 1 }; + + struct { + struct cmsghdr align; + int fd[1]; + } ancillary_data_buffer; + + struct msghdr message_header = { + .msg_name = NULL, + .msg_namelen = 0, + .msg_iov = ¬hing_ptr, + .msg_iovlen = 1, + .msg_flags = 0, + .msg_control = &ancillary_data_buffer, + .msg_controllen = sizeof(struct cmsghdr) + sizeof(int) + }; + + struct cmsghdr* cmsg = CMSG_FIRSTHDR(&message_header); + cmsg->cmsg_len = message_header.msg_controllen; // sizeof(int); + cmsg->cmsg_level = SOL_SOCKET; + cmsg->cmsg_type = SCM_RIGHTS; + ((int*) CMSG_DATA(cmsg))[0] = request.fd; + + sendmsg(client_fd, &message_header, 0); + close(client_fd); + break; + } + default: + fprintf(stderr, "proot-shm-helper: Bad request\n"); + break; + } + } + + unlink(path); + _exit(0); +} diff --git a/core/proot/src/main/cpp/extension/sysvipc/sysvipc_sys.h b/core/proot/src/main/cpp/extension/sysvipc/sysvipc_sys.h new file mode 100644 index 000000000..114b1d7c2 --- /dev/null +++ b/core/proot/src/main/cpp/extension/sysvipc/sysvipc_sys.h @@ -0,0 +1,95 @@ +/* + * This file contains definitions taken from glibc, + * since proot is being built with no sys/sem.h + * available + * + * Definitions were changed to include SysVIpc/SYSVIPC_ prefixes + * and merged into single file + * + * glibc uses following license header: + */ +/* Copyright (C) 1995-2020 Free Software Foundation, Inc. + This file is part of the GNU C Library. + + The GNU C Library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + The GNU C Library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with the GNU C Library; if not, see + . */ + + +#ifndef SYSVIPC_SYS_H +#define SYSVIPC_SYS_H + +#include +#include + +/* Flags for `semop'. */ +#define SYSVIPC_SEM_UNDO 0x1000 /* undo the operation on exit */ + +/* Commands for `semctl'. */ +#define SYSVIPC_GETPID 11 /* get sempid */ +#define SYSVIPC_GETVAL 12 /* get semval */ +#define SYSVIPC_GETALL 13 /* get all semval's */ +#define SYSVIPC_GETNCNT 14 /* get semncnt */ +#define SYSVIPC_GETZCNT 15 /* get semzcnt */ +#define SYSVIPC_SETVAL 16 /* set semval */ +#define SYSVIPC_SETALL 17 /* set all semval's */ + +struct SysVIpcSembuf +{ + uint16_t sem_num; + int16_t sem_op; + int16_t sem_flg; +}; + +#define SYSVIPC_IPC_INFO 3 /* See ipcs. */ +/* ipcs ctl cmds */ +#define SYSVIPC_SEM_STAT 18 +#define SYSVIPC_SEM_INFO 19 +#define SYSVIPC_SEM_STAT_ANY 20 + +struct SysVIpcSeminfo +{ + int semmap; + int semmni; + int semmns; + int semmnu; + int semmsl; + int semopm; + int semume; + int semusz; + int semvmx; + int semaem; +}; + + +struct SysVIpcShmidDs +{ + struct ipc_perm shm_perm; /* operation permission struct */ + size_t shm_segsz; /* size of segment in bytes */ + int64_t shm_atime; /* time of last shmat() */ + int64_t shm_dtime; /* time of last shmdt() */ + int64_t shm_ctime; /* time of last change by shmctl() */ + int32_t shm_cpid; /* pid of creator */ + int32_t shm_lpid; /* pid of last shmop */ + uint64_t shm_nattch; /* number of current attaches */ + uint64_t __glibc_reserved5; + uint64_t __glibc_reserved6; +}; + +/* Flag for *ctl `cmd` argument to force use + * of 64-bit structures regardless of architecture + * + * Matches glibc's __IPC_64 */ +#define SYSVIPC_IPC_64 0x100 + +#endif // SYSVIPC_SYS_H diff --git a/core/proot/src/main/cpp/loader/assembly-arm.h b/core/proot/src/main/cpp/loader/assembly-arm.h new file mode 100644 index 000000000..6848d4dd1 --- /dev/null +++ b/core/proot/src/main/cpp/loader/assembly-arm.h @@ -0,0 +1,111 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +/* According to the ARM EABI, all registers have undefined values at + * program startup except: + * + * - the instruction pointer (r15) + * - the stack pointer (r13) + * - the rtld_fini pointer (r0) + */ +#define BRANCH(stack_pointer, destination) do { \ + asm volatile ( \ + "// Restore initial stack pointer. \n\t" \ + "mov sp, %0 \n\t" \ + " \n\t" \ + "// Clear rtld_fini. \n\t" \ + "mov r0, #0 \n\t" \ + " \n\t" \ + "// Start the program. \n\t" \ + "bx %1 \n" \ + : /* no output */ \ + : "r" (stack_pointer), "r" (destination) \ + : "memory", "sp", "r0", "pc"); \ + __builtin_unreachable(); \ + } while (0) + +#define PREPARE_ARGS_1(arg1_) \ + register word_t arg1 asm("r0") = arg1_; \ + +#define PREPARE_ARGS_3(arg1_, arg2_, arg3_) \ + PREPARE_ARGS_1(arg1_) \ + register word_t arg2 asm("r1") = arg2_; \ + register word_t arg3 asm("r2") = arg3_; \ + +#define PREPARE_ARGS_6(arg1_, arg2_, arg3_, arg4_, arg5_, arg6_) \ + PREPARE_ARGS_3(arg1_, arg2_, arg3_) \ + register word_t arg4 asm("r3") = arg4_; \ + register word_t arg5 asm("r4") = arg5_; \ + register word_t arg6 asm("r5") = arg6_; + +#define OUTPUT_CONTRAINTS_1 \ + "r" (arg1) + +#define OUTPUT_CONTRAINTS_3 \ + OUTPUT_CONTRAINTS_1, \ + "r" (arg2), "r" (arg3) + +#define OUTPUT_CONTRAINTS_6 \ + OUTPUT_CONTRAINTS_3, \ + "r" (arg4), "r" (arg5), "r" (arg6) + +#ifdef __thumb__ +#define STRINGIFY_EXPANDED(s) #s +#define SYSCALL(number_, nb_args, args...) \ + ({ \ + register word_t result asm("r0"); \ + PREPARE_ARGS_##nb_args(args) \ + asm volatile ( \ + "push {r7} \n\t" \ + "mov r7, #" STRINGIFY_EXPANDED(number_) "\n\t" \ + "svc #0x00000000 \n\t" \ + "pop {r7} \n\t" \ + : "=r" (result) \ + : OUTPUT_CONTRAINTS_##nb_args \ + : "memory"); \ + result; \ + }) +#else +#define SYSCALL(number_, nb_args, args...) \ + ({ \ + register word_t number asm("r7") = number_; \ + register word_t result asm("r0"); \ + PREPARE_ARGS_##nb_args(args) \ + asm volatile ( \ + "svc #0x00000000 \n\t" \ + : "=r" (result) \ + : "r" (number), \ + OUTPUT_CONTRAINTS_##nb_args \ + : "memory"); \ + result; \ + }) +#endif + +#define OPEN 5 +#define CLOSE 6 +#define MMAP 192 +#define MMAP_OFFSET_SHIFT 12 +#define EXECVE 11 +#define EXIT 1 +#define PRCTL 172 +#define MPROTECT 125 + diff --git a/core/proot/src/main/cpp/loader/assembly-arm64.h b/core/proot/src/main/cpp/loader/assembly-arm64.h new file mode 100644 index 000000000..36f6825b7 --- /dev/null +++ b/core/proot/src/main/cpp/loader/assembly-arm64.h @@ -0,0 +1,98 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +/* According to the ARM64 EABI, all registers have undefined values at + * program startup except: + * + * - the instruction pointer (pc) + * - the stack pointer (sp) + * - the rtld_fini pointer (x0) + */ +#define BRANCH(stack_pointer, destination) do { \ + asm volatile ( \ + "// Restore initial stack pointer. \n\t" \ + "mov sp, %0 \n\t" \ + " \n\t" \ + "// Clear rtld_fini. \n\t" \ + "mov x0, #0 \n\t" \ + " \n\t" \ + "// Start the program. \n\t" \ + "br %1 \n" \ + : /* no output */ \ + : "r" (stack_pointer), "r" (destination) \ + : "memory", "x0"); \ + __builtin_unreachable(); \ + } while (0) + +#define PREPARE_ARGS_1(arg1_) \ + register word_t arg1 asm("x0") = arg1_; \ + +#define PREPARE_ARGS_3(arg1_, arg2_, arg3_) \ + PREPARE_ARGS_1(arg1_) \ + register word_t arg2 asm("x1") = arg2_; \ + register word_t arg3 asm("x2") = arg3_; \ + +#define PREPARE_ARGS_4(arg1_, arg2_, arg3_, arg4_) \ + PREPARE_ARGS_3(arg1_, arg2_, arg3_) \ + register word_t arg4 asm("x3") = arg4_; \ + +#define PREPARE_ARGS_6(arg1_, arg2_, arg3_, arg4_, arg5_, arg6_) \ + PREPARE_ARGS_4(arg1_, arg2_, arg3_, arg4_) \ + register word_t arg5 asm("x4") = arg5_; \ + register word_t arg6 asm("x5") = arg6_; + +#define OUTPUT_CONTRAINTS_1 \ + "r" (arg1) + +#define OUTPUT_CONTRAINTS_3 \ + OUTPUT_CONTRAINTS_1, \ + "r" (arg2), "r" (arg3) + +#define OUTPUT_CONTRAINTS_4 \ + OUTPUT_CONTRAINTS_3, \ + "r" (arg4) + +#define OUTPUT_CONTRAINTS_6 \ + OUTPUT_CONTRAINTS_4, \ + "r" (arg5), "r" (arg6) + +#define SYSCALL(number_, nb_args, args...) \ + ({ \ + register word_t number asm("x8") = number_; \ + register word_t result asm("x0"); \ + PREPARE_ARGS_##nb_args(args) \ + asm volatile ( \ + "svc #0x00000000 \n\t" \ + : "=r" (result) \ + : "r" (number), \ + OUTPUT_CONTRAINTS_##nb_args \ + : "memory"); \ + result; \ + }) + +#define OPENAT 56 +#define CLOSE 57 +#define MMAP 222 +#define EXECVE 221 +#define EXIT 93 +#define PRCTL 167 +#define MPROTECT 226 diff --git a/core/proot/src/main/cpp/loader/assembly-x86.h b/core/proot/src/main/cpp/loader/assembly-x86.h new file mode 100644 index 000000000..40451440a --- /dev/null +++ b/core/proot/src/main/cpp/loader/assembly-x86.h @@ -0,0 +1,68 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +/* According to the x86 ABI, all registers have undefined values at + * program startup except: + * + * - the instruction pointer (rip) + * - the stack pointer (rsp) + * - the rtld_fini pointer (rdx) + * - the system flags (eflags) + */ +#define BRANCH(stack_pointer, destination) do { \ + asm volatile ( \ + "// Restore initial stack pointer. \n\t" \ + "movl %0, %%esp \n\t" \ + " \n\t" \ + "// Clear state flags. \n\t" \ + "pushl $0 \n\t" \ + "popfl \n\t" \ + " \n\t" \ + "// Clear rtld_fini. \n\t" \ + "movl $0, %%edx \n\t" \ + " \n\t" \ + "// Start the program. \n\t" \ + "jmpl *%%eax \n" \ + : /* no output */ \ + : "irm" (stack_pointer), "a" (destination) \ + : "memory", "cc", "esp", "edx"); \ + __builtin_unreachable(); \ + } while (0) + +extern word_t syscall_6(word_t number, + word_t arg1, word_t arg2, word_t arg3, + word_t arg4, word_t arg5, word_t arg6); + +extern word_t syscall_3(word_t number, word_t arg1, word_t arg2, word_t arg3); + +extern word_t syscall_1(word_t number, word_t arg1); + +#define SYSCALL(number, nb_args, args...) syscall_##nb_args(number, args) + +#define OPEN 5 +#define CLOSE 6 +#define MMAP 192 +#define MMAP_OFFSET_SHIFT 12 +#define EXECVE 11 +#define EXIT 1 +#define PRCTL 172 +#define MPROTECT 125 diff --git a/core/proot/src/main/cpp/loader/assembly-x86_64.h b/core/proot/src/main/cpp/loader/assembly-x86_64.h new file mode 100644 index 000000000..6f431be59 --- /dev/null +++ b/core/proot/src/main/cpp/loader/assembly-x86_64.h @@ -0,0 +1,96 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +/* According to the x86_64 ABI, all registers have undefined values at + * program startup except: + * + * - the instruction pointer (rip) + * - the stack pointer (rsp) + * - the rtld_fini pointer (rdx) + * - the system flags (rflags) + */ +#define BRANCH(stack_pointer, destination) do { \ + asm volatile ( \ + "// Restore initial stack pointer. \n\t" \ + "movq %0, %%rsp \n\t" \ + " \n\t" \ + "// Clear state flags. \n\t" \ + "pushq $0 \n\t" \ + "popfq \n\t" \ + " \n\t" \ + "// Clear rtld_fini. \n\t" \ + "movq $0, %%rdx \n\t" \ + " \n\t" \ + "// Start the program. \n\t" \ + "jmpq *%%rax \n" \ + : /* no output */ \ + : "irm" (stack_pointer), "a" (destination) \ + : "memory", "cc", "rsp", "rdx"); \ + __builtin_unreachable(); \ + } while (0) + +#define PREPARE_ARGS_1(arg1_) \ + register word_t arg1 asm("rdi") = arg1_; \ + +#define PREPARE_ARGS_3(arg1_, arg2_, arg3_) \ + PREPARE_ARGS_1(arg1_) \ + register word_t arg2 asm("rsi") = arg2_; \ + register word_t arg3 asm("rdx") = arg3_; \ + +#define PREPARE_ARGS_6(arg1_, arg2_, arg3_, arg4_, arg5_, arg6_) \ + PREPARE_ARGS_3(arg1_, arg2_, arg3_) \ + register word_t arg4 asm("r10") = arg4_; \ + register word_t arg5 asm("r8") = arg5_; \ + register word_t arg6 asm("r9") = arg6_; + +#define OUTPUT_CONTRAINTS_1 \ + "r" (arg1) + +#define OUTPUT_CONTRAINTS_3 \ + OUTPUT_CONTRAINTS_1, \ + "r" (arg2), "r" (arg3) + +#define OUTPUT_CONTRAINTS_6 \ + OUTPUT_CONTRAINTS_3, \ + "r" (arg4), "r" (arg5), "r" (arg6) + +#define SYSCALL(number_, nb_args, args...) \ + ({ \ + register word_t number asm("rax") = number_; \ + register word_t result asm("rax"); \ + PREPARE_ARGS_##nb_args(args) \ + asm volatile ( \ + "syscall \n\t" \ + : "=r" (result) \ + : "r" (number), \ + OUTPUT_CONTRAINTS_##nb_args \ + : "memory", "cc", "rcx", "r11"); \ + result; \ + }) + +#define OPEN 2 +#define CLOSE 3 +#define MMAP 9 +#define EXECVE 59 +#define EXIT 60 +#define PRCTL 157 +#define MPROTECT 10 diff --git a/core/proot/src/main/cpp/loader/assembly.S b/core/proot/src/main/cpp/loader/assembly.S new file mode 100644 index 000000000..8e4b086d3 --- /dev/null +++ b/core/proot/src/main/cpp/loader/assembly.S @@ -0,0 +1,69 @@ +#if defined(__i386__) + .text + +/* + ABI user-land kernel-land + ====== ========= =========== + number %eax %eax + arg1 %edx %ebx + arg2 %ecx %ecx + arg3 16(%esp) %edx + arg4 12(%esp) %esi + arg5 8(%esp) %edi + arg6 4(%esp) %ebp + result N/A %eax +*/ +.globl syscall_6 +.type syscall_6, @function +syscall_6: + /* Callee-saved registers. */ + pushl %ebp // %esp -= 0x04 + pushl %edi // %esp -= 0x08 + pushl %esi // %esp -= 0x0c + pushl %ebx // %esp -= 0x10 + +// mov %eax, %eax // number + mov %edx, %ebx // arg1 +// mov %ecx, %ecx // arg2 + mov 0x14(%esp), %edx // arg3 + mov 0x18(%esp), %esi // arg4 + mov 0x1c(%esp), %edi // arg5 + mov 0x20(%esp), %ebp // arg6 + + int $0x80 + + popl %ebx + popl %esi + popl %edi + popl %ebp + +// mov %eax, %eax // result + ret + +.globl syscall_3 +.type syscall_3, @function +syscall_3: + pushl %ebx + mov %edx, %ebx + mov 0x8(%esp), %edx + int $0x80 + popl %ebx + ret + +.globl syscall_1 +.type syscall_1, @function +syscall_1: + pushl %ebx + mov %edx, %ebx + int $0x80 + popl %ebx + ret + +#endif /* defined(__i386__) */ +#if defined(__aarch64__) +.globl pokedata_workaround +pokedata_workaround: +str x1, [x2] +// https://stackoverflow.com/a/16087057 +.word 0xf7f0a000 +#endif /* defined(__aarch64__) */ diff --git a/core/proot/src/main/cpp/loader/loader-info.awk b/core/proot/src/main/cpp/loader/loader-info.awk new file mode 100644 index 000000000..c352be6c7 --- /dev/null +++ b/core/proot/src/main/cpp/loader/loader-info.awk @@ -0,0 +1,19 @@ +# Note: This file is included only for targets which have pokedata workaround +# It extracts addresses of 'pokedata_workaround' and '_start' from readelf output. +# We let the C compiler handle the hex math to avoid gawk dependencies. + +BEGIN { + print "#include " + print "#include \"arch.h\"" + print "#ifdef HAS_POKEDATA_WORKAROUND" +} + +$NF == "pokedata_workaround" { pokedata_workaround = "0x" $2 } +$NF == "_start" { start = "0x" $2 } + +END { + if (pokedata_workaround == "") pokedata_workaround = "0" + if (start == "") start = "0" + print "const ssize_t offset_to_pokedata_workaround = (ssize_t)(" pokedata_workaround " - " start ");" + print "#endif" +} diff --git a/core/proot/src/main/cpp/loader/loader-info.d b/core/proot/src/main/cpp/loader/loader-info.d new file mode 100644 index 000000000..668a79945 --- /dev/null +++ b/core/proot/src/main/cpp/loader/loader-info.d @@ -0,0 +1,63 @@ +loader/loader-info.o: loader/loader-info.c \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/unistd.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/sys/cdefs.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/android/versioning.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/android/api-level.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/bits/get_device_api_level_inlines.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/android/ndk-version.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/19/include/stddef.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/19/include/__stddef_ptrdiff_t.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/19/include/__stddef_size_t.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/19/include/__stddef_rsize_t.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/19/include/__stddef_wchar_t.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/19/include/__stddef_null.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/19/include/__stddef_max_align_t.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/19/include/__stddef_offsetof.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/sys/types.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/19/include/stdint.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/stdint.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/bits/wchar_limits.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/linux/types.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/aarch64-linux-android/asm/types.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/asm-generic/types.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/asm-generic/int-ll64.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/aarch64-linux-android/asm/bitsperlong.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/asm-generic/bitsperlong.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/linux/posix_types.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/linux/stddef.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/linux/compiler_types.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/linux/compiler.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/aarch64-linux-android/asm/posix_types.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/asm-generic/posix_types.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/bits/pthread_types.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/sys/select.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/linux/time.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/bits/timespec.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/linux/time_types.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/signal.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/aarch64-linux-android/asm/sigcontext.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/aarch64-linux-android/asm/sve_context.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/bits/signal_types.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/19/include/limits.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/limits.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/19/include/float.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/linux/limits.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/bits/posix_limits.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/linux/signal.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/aarch64-linux-android/asm/signal.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/asm-generic/signal.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/asm-generic/signal-defs.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/aarch64-linux-android/asm/siginfo.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/asm-generic/siginfo.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/sys/ucontext.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/sys/user.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/bits/page_size.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/bits/fcntl.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/bits/getentropy.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/bits/getopt.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/bits/ioctl.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/bits/lockf.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/bits/seek_constants.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/bits/sysconf.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/android/legacy_unistd_inlines.h \ + /home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/include/bits/swab.h diff --git a/core/proot/src/main/cpp/loader/loader.c b/core/proot/src/main/cpp/loader/loader.c new file mode 100644 index 000000000..512c6bae6 --- /dev/null +++ b/core/proot/src/main/cpp/loader/loader.c @@ -0,0 +1,263 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#include /* bool, true, false, */ + +#define NO_LIBC_HEADER +#include "loader/script.h" +#include "compat.h" +#include "arch.h" + +#if defined(ARCH_X86_64) +# include "loader/assembly-x86_64.h" +#elif defined(ARCH_ARM_EABI) +# include "loader/assembly-arm.h" +#elif defined(ARCH_X86) +# include "loader/assembly-x86.h" +#elif defined(ARCH_ARM64) +# include "loader/assembly-arm64.h" +#else +# error "Unsupported architecture" +#endif + +#if !defined(MMAP_OFFSET_SHIFT) +# define MMAP_OFFSET_SHIFT 0 +#endif + +#define FATAL() do { \ + SYSCALL(EXIT, 1, 182); \ + __builtin_unreachable(); \ + } while (0) + +#define unlikely(expr) __builtin_expect(!!(expr), 0) + +/** + * Clear the memory from @start (inclusive) to @end (exclusive). + */ +static inline void clear(word_t start, word_t end) +{ + byte_t *start_misaligned; + byte_t *end_misaligned; + + word_t *start_aligned; + word_t *end_aligned; + + /* Compute the number of mis-aligned bytes. */ + word_t start_bytes = start % sizeof(word_t); + word_t end_bytes = end % sizeof(word_t); + + /* Compute aligned addresses. */ + start_aligned = (word_t *) (start_bytes ? start + sizeof(word_t) - start_bytes : start); + end_aligned = (word_t *) (end - end_bytes); + + /* Clear leading mis-aligned bytes. */ + start_misaligned = (byte_t *) start; + while (start_misaligned < (byte_t *) start_aligned) + *start_misaligned++ = 0; + + /* Clear aligned bytes. */ + while (start_aligned < end_aligned) + *start_aligned++ = 0; + + /* Clear trailing mis-aligned bytes. */ + end_misaligned = (byte_t *) end_aligned; + while (end_misaligned < (byte_t *) end) + *end_misaligned++ = 0; +} + +/** + * Return the address of the last path component of @string_. Note + * that @string_ is not modified. + */ +static inline word_t basename(word_t string_) +{ + byte_t *string = (byte_t *) string_; + byte_t *cursor; + + for (cursor = string; *cursor != 0; cursor++) + ; + + for (; *cursor != (byte_t) '/' && cursor > string; cursor--) + ; + + if (cursor != string) + cursor++; + + return (word_t) cursor; +} + +/** + * Interpret the load script pointed to by @cursor. + */ +void _start(void *cursor) +{ + bool traced = false; + bool reset_at_base = true; + word_t at_base = 0; + + word_t fd = -1; + word_t status; + + while(1) { + LoadStatement *stmt = cursor; + + switch (stmt->action) { + case LOAD_ACTION_OPEN_NEXT: + status = SYSCALL(CLOSE, 1, fd); + if (unlikely((int) status < 0)) + FATAL(); + /* Fall through. */ + + case LOAD_ACTION_OPEN: +#if defined(OPEN) + fd = SYSCALL(OPEN, 3, stmt->open.string_address, O_RDONLY, 0); +#else + fd = SYSCALL(OPENAT, 4, AT_FDCWD, stmt->open.string_address, O_RDONLY, 0); +#endif + if (unlikely((int) fd < 0)) + FATAL(); + + reset_at_base = true; + + cursor += LOAD_STATEMENT_SIZE(*stmt, open); + break; + + case LOAD_ACTION_MMAP_FILE: + status = SYSCALL(MMAP, 6, stmt->mmap.addr, stmt->mmap.length, + stmt->mmap.prot, MAP_PRIVATE | MAP_FIXED, fd, + stmt->mmap.offset >> MMAP_OFFSET_SHIFT); + if (unlikely(status != stmt->mmap.addr)) + FATAL(); + + if (stmt->mmap.clear_length != 0) + clear(stmt->mmap.addr + stmt->mmap.length - stmt->mmap.clear_length, + stmt->mmap.addr + stmt->mmap.length); + + if (reset_at_base) { + at_base = stmt->mmap.addr; + reset_at_base = false; + } + + cursor += LOAD_STATEMENT_SIZE(*stmt, mmap); + break; + + case LOAD_ACTION_MMAP_ANON: + status = SYSCALL(MMAP, 6, stmt->mmap.addr, stmt->mmap.length, + stmt->mmap.prot, MAP_PRIVATE | MAP_FIXED | MAP_ANONYMOUS, -1, 0); + if (unlikely(status != stmt->mmap.addr)) + FATAL(); + + cursor += LOAD_STATEMENT_SIZE(*stmt, mmap); + break; + + case LOAD_ACTION_MAKE_STACK_EXEC: + SYSCALL(MPROTECT, 3, + stmt->make_stack_exec.start, 1, + PROT_READ | PROT_WRITE | PROT_EXEC | PROT_GROWSDOWN); + + cursor += LOAD_STATEMENT_SIZE(*stmt, make_stack_exec); + break; + + case LOAD_ACTION_START_TRACED: + traced = true; + /* Fall through. */ + + case LOAD_ACTION_START: { + word_t *cursor2 = (word_t *) stmt->start.stack_pointer; + const word_t argc = cursor2[0]; + const word_t at_execfn = cursor2[1]; + word_t name; + + status = SYSCALL(CLOSE, 1, fd); + if (unlikely((int) status < 0)) + FATAL(); + + /* Right after execve, the stack content is as follow: + * + * +------+--------+--------+--------+ + * | argc | argv[] | envp[] | auxv[] | + * +------+--------+--------+--------+ + */ + + /* Skip argv[]. */ + cursor2 += argc + 1; + + /* Skip envp[]. */ + do cursor2++; while (cursor2[0] != 0); + cursor2++; + + /* Adjust auxv[]. */ + do { + switch (cursor2[0]) { + case AT_PHDR: + cursor2[1] = stmt->start.at_phdr; + break; + + case AT_PHENT: + cursor2[1] = stmt->start.at_phent; + break; + + case AT_PHNUM: + cursor2[1] = stmt->start.at_phnum; + break; + + case AT_ENTRY: + cursor2[1] = stmt->start.at_entry; + break; + + case AT_BASE: + cursor2[1] = at_base; + break; + + case AT_EXECFN: + /* stmt->start.at_execfn can't be used for now since it is + * currently stored in a location that will be scratched + * by the process (below the final stack pointer). */ + cursor2[1] = at_execfn; + break; + + default: + break; + } + cursor2 += 2; + } while (cursor2[0] != AT_NULL); + + /* Note that only 2 arguments are actually necessary... */ + name = basename(stmt->start.at_execfn); + SYSCALL(PRCTL, 3, PR_SET_NAME, name, 0); + + if (unlikely(traced)) + SYSCALL(EXECVE, 6, 1, + stmt->start.stack_pointer, + stmt->start.entry_point, 2, 3, 4); + else + BRANCH(stmt->start.stack_pointer, stmt->start.entry_point); + FATAL(); + } + + default: + FATAL(); + } + } + + FATAL(); +} diff --git a/core/proot/src/main/cpp/loader/script.h b/core/proot/src/main/cpp/loader/script.h new file mode 100644 index 000000000..6ae762132 --- /dev/null +++ b/core/proot/src/main/cpp/loader/script.h @@ -0,0 +1,78 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#ifndef SCRIPT +#define SCRIPT + +#include "arch.h" +#include "attribute.h" + +struct load_statement { + word_t action; + + union { + struct { + word_t string_address; + } open; + + struct { + word_t addr; + word_t length; + word_t prot; + word_t offset; + word_t clear_length; + } mmap; + + struct { + word_t start; + } make_stack_exec; + + struct { + word_t stack_pointer; + word_t entry_point; + word_t at_phdr; + word_t at_phent; + word_t at_phnum; + word_t at_entry; + word_t at_execfn; + } start; + }; +} PACKED; + +typedef struct load_statement LoadStatement; + +#define LOAD_STATEMENT_SIZE(statement, type) \ + (sizeof((statement).action) + sizeof((statement).type)) + +/* Don't use enum, since sizeof(enum) doesn't have to be equal to + * sizeof(word_t). Keep values in the same order as their respective + * actions appear in loader.c to get a change GCC produces a jump + * table. */ +#define LOAD_ACTION_OPEN_NEXT 0 +#define LOAD_ACTION_OPEN 1 +#define LOAD_ACTION_MMAP_FILE 2 +#define LOAD_ACTION_MMAP_ANON 3 +#define LOAD_ACTION_MAKE_STACK_EXEC 4 +#define LOAD_ACTION_START_TRACED 5 +#define LOAD_ACTION_START 6 + +#endif /* SCRIPT */ diff --git a/core/proot/src/main/cpp/loader/wrap_loader-m32.sh b/core/proot/src/main/cpp/loader/wrap_loader-m32.sh new file mode 100644 index 000000000..d2b447275 --- /dev/null +++ b/core/proot/src/main/cpp/loader/wrap_loader-m32.sh @@ -0,0 +1,6 @@ +#!/bin/sh +set -e +CLI_OBJ="/home/rohit/Projects/proot/proot/CMakeFiles/cli_obj.dir/cli/cli.c.o" +OUTPUT_TARGET=$(/home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-objdump -f "$CLI_OBJ" | grep 'file format' | awk '{print $4}') +ARCH=$(/home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-objdump -f "$CLI_OBJ" | grep architecture | cut -f 1 -d , | awk '{print $2}') +/home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-objcopy --input-target=binary --output-target=$OUTPUT_TARGET --binary-architecture $ARCH loader-m32.exe loader-m32-wrapped.o diff --git a/core/proot/src/main/cpp/loader/wrap_loader.sh b/core/proot/src/main/cpp/loader/wrap_loader.sh new file mode 100644 index 000000000..415543664 --- /dev/null +++ b/core/proot/src/main/cpp/loader/wrap_loader.sh @@ -0,0 +1,6 @@ +#!/bin/sh +set -e +CLI_OBJ="/home/rohit/Projects/proot/proot/CMakeFiles/cli_obj.dir/cli/cli.c.o" +OUTPUT_TARGET=$(/home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-objdump -f "$CLI_OBJ" | grep 'file format' | awk '{print $4}') +ARCH=$(/home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-objdump -f "$CLI_OBJ" | grep architecture | cut -f 1 -d , | awk '{print $2}') +/home/rohit/Android/Sdk/ndk/28.0.13004108/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-objcopy --input-target=binary --output-target=$OUTPUT_TARGET --binary-architecture $ARCH loader.exe loader-wrapped.o diff --git a/core/proot/src/main/cpp/path/binding.c b/core/proot/src/main/cpp/path/binding.c new file mode 100644 index 000000000..c0e2f84c1 --- /dev/null +++ b/core/proot/src/main/cpp/path/binding.c @@ -0,0 +1,735 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#include /* lstat(2), */ +#include /* getcwd(2), lstat(2), */ +#include /* string(3), */ +#include /* bzero(3), */ +#include /* assert(3), */ +#include /* PATH_MAX, */ +#include /* E* */ +#include /* CIRCLEQ_*, */ +#include /* talloc_*, */ + +#include "path/binding.h" +#include "path/path.h" +#include "path/canon.h" +#include "cli/note.h" + +#include "compat.h" + +#define HEAD(tracee, side) \ + (side == GUEST \ + ? (tracee)->fs->bindings.guest \ + : (side == HOST \ + ? (tracee)->fs->bindings.host \ + : (tracee)->fs->bindings.pending)) + +#define NEXT(binding, side) \ + (side == GUEST \ + ? CIRCLEQ_NEXT(binding, link.guest) \ + : (side == HOST \ + ? CIRCLEQ_NEXT(binding, link.host) \ + : CIRCLEQ_NEXT(binding, link.pending))) + +#define CIRCLEQ_FOREACH_(tracee, binding, side) \ + for (binding = CIRCLEQ_FIRST(HEAD(tracee, side)); \ + binding != (void *) HEAD(tracee, side); \ + binding = NEXT(binding, side)) + +#define CIRCLEQ_INSERT_AFTER_(tracee, previous, binding, side) do { \ + switch (side) { \ + case GUEST: CIRCLEQ_INSERT_AFTER(HEAD(tracee, side), previous, binding, link.guest); break; \ + case HOST: CIRCLEQ_INSERT_AFTER(HEAD(tracee, side), previous, binding, link.host); break; \ + default: CIRCLEQ_INSERT_AFTER(HEAD(tracee, side), previous, binding, link.pending); break; \ + } \ + (void) talloc_reference(HEAD(tracee, side), binding); \ +} while (0) + +#define CIRCLEQ_INSERT_BEFORE_(tracee, next, binding, side) do { \ + switch (side) { \ + case GUEST: CIRCLEQ_INSERT_BEFORE(HEAD(tracee, side), next, binding, link.guest); break; \ + case HOST: CIRCLEQ_INSERT_BEFORE(HEAD(tracee, side), next, binding, link.host); break; \ + default: CIRCLEQ_INSERT_BEFORE(HEAD(tracee, side), next, binding, link.pending); break; \ + } \ + (void) talloc_reference(HEAD(tracee, side), binding); \ +} while (0) + +#define CIRCLEQ_INSERT_HEAD_(tracee, binding, side) do { \ + switch (side) { \ + case GUEST: CIRCLEQ_INSERT_HEAD(HEAD(tracee, side), binding, link.guest); break; \ + case HOST: CIRCLEQ_INSERT_HEAD(HEAD(tracee, side), binding, link.host); break; \ + default: CIRCLEQ_INSERT_HEAD(HEAD(tracee, side), binding, link.pending); break; \ + } \ + (void) talloc_reference(HEAD(tracee, side), binding); \ +} while (0) + +#define IS_LINKED(binding, link) \ + ((binding)->link.cqe_next != NULL && (binding)->link.cqe_prev != NULL) + +#define CIRCLEQ_REMOVE_(tracee, binding, name) do { \ + CIRCLEQ_REMOVE((tracee)->fs->bindings.name, binding, link.name);\ + (binding)->link.name.cqe_next = NULL; \ + (binding)->link.name.cqe_prev = NULL; \ + talloc_unlink((tracee)->fs->bindings.name, binding); \ +} while (0) + + +/** + * Print all bindings (verbose purpose). + */ +static void print_bindings(const Tracee *tracee) +{ + const Binding *binding; + + if (tracee->fs->bindings.guest == NULL) + return; + + CIRCLEQ_FOREACH_(tracee, binding, GUEST) { + if (compare_paths(binding->host.path, binding->guest.path) == PATHS_ARE_EQUAL) + note(tracee, INFO, USER, "binding = %s", binding->host.path); + else + note(tracee, INFO, USER, "binding = %s:%s", + binding->host.path, binding->guest.path); + } +} + +/** + * Get the binding for the given @path (relatively to the given + * binding @side). + */ +Binding *get_binding(const Tracee *tracee, Side side, const char path[PATH_MAX]) +{ + Binding *binding; + size_t path_length = strlen(path); + + /* Sanity checks. */ + assert(path != NULL && path[0] == '/'); + + CIRCLEQ_FOREACH_(tracee, binding, side) { + Comparison comparison; + const Path *ref; + + switch (side) { + case GUEST: + ref = &binding->guest; + break; + + case HOST: + ref = &binding->host; + break; + + default: + assert(0); + return NULL; + } + + comparison = compare_paths2(ref->path, ref->length, path, path_length); + if ( comparison != PATHS_ARE_EQUAL + && comparison != PATH1_IS_PREFIX) + continue; + + /* Avoid false positive when a prefix of the rootfs is + * used as an asymmetric binding, ex.: + * + * proot -m /usr:/location /usr/local/slackware + */ + if ( side == HOST + && compare_paths(get_root(tracee), "/") != PATHS_ARE_EQUAL + && belongs_to_guestfs(tracee, path)) + continue; + + return binding; + } + + return NULL; +} + +/** + * Get the binding path for the given @path (relatively to the given + * binding @side). + */ +const char *get_path_binding(const Tracee *tracee, Side side, const char path[PATH_MAX]) +{ + const Binding *binding; + + binding = get_binding(tracee, side, path); + if (!binding) + return NULL; + + switch (side) { + case GUEST: + return binding->guest.path; + + case HOST: + return binding->host.path; + + default: + assert(0); + return NULL; + } +} + +/** + * Return the path to the guest rootfs for the given @tracee, from the + * host point-of-view obviously. Depending on whether + * initialize_bindings() was called or not, the path is retrieved from + * the "bindings.guest" list or from the "bindings.pending" list, + * respectively. + */ +const char *get_root(const Tracee* tracee) +{ + const Binding *binding; + + if (tracee == NULL || tracee->fs == NULL) + return NULL; + + if (tracee->fs->bindings.guest == NULL) { + if (tracee->fs->bindings.pending == NULL + || CIRCLEQ_EMPTY(tracee->fs->bindings.pending)) + return NULL; + + binding = CIRCLEQ_LAST(tracee->fs->bindings.pending); + if (compare_paths(binding->guest.path, "/") != PATHS_ARE_EQUAL) + return NULL; + + return binding->host.path; + } + + assert(!CIRCLEQ_EMPTY(tracee->fs->bindings.guest)); + + binding = CIRCLEQ_LAST(tracee->fs->bindings.guest); + + assert(strcmp(binding->guest.path, "/") == 0); + + return binding->host.path; +} + +/** + * Substitute the guest path (if any) with the host path in @path. + * This function returns: + * + * * -errno if an error occured + * + * * 0 if it is a binding location but no substitution is needed + * ("symetric" binding) + * + * * 1 if it is a binding location and a substitution was performed + * ("asymmetric" binding) + */ +int substitute_binding(const Tracee *tracee, Side side, char path[PATH_MAX]) +{ + const Path *reverse_ref; + const Path *ref; + const Binding *binding; + + binding = get_binding(tracee, side, path); + if (!binding) + return -ENOENT; + + /* Is it a "symetric" binding? */ + if (!binding->need_substitution) + return 0; + + switch (side) { + case GUEST: + ref = &binding->guest; + reverse_ref = &binding->host; + break; + + case HOST: + ref = &binding->host; + reverse_ref = &binding->guest; + break; + + default: + assert(0); + return -EACCES; + } + + substitute_path_prefix(path, ref->length, reverse_ref->path, reverse_ref->length); + + return 1; +} + +/** + * Remove @binding from all the @tracee's lists of bindings it belongs to. + */ +void remove_binding_from_all_lists(const Tracee *tracee, Binding *binding) +{ + if (IS_LINKED(binding, link.pending)) + CIRCLEQ_REMOVE_(tracee, binding, pending); + + if (IS_LINKED(binding, link.guest)) + CIRCLEQ_REMOVE_(tracee, binding, guest); + + if (IS_LINKED(binding, link.host)) + CIRCLEQ_REMOVE_(tracee, binding, host); +} + +/** + * Insert @binding into the list of @bindings, in a sorted manner so + * as to make the substitution of nested bindings determistic, ex.: + * + * -b /bin:/foo/bin -b /usr/bin/more:/foo/bin/more + * + * Note: "nested" from the @side point-of-view. + */ +static void insort_binding(const Tracee *tracee, Side side, Binding *binding) +{ + Binding *iterator; + Binding *previous = NULL; + Binding *next = CIRCLEQ_FIRST(HEAD(tracee, side)); + + /* Find where it should be added in the list. */ + CIRCLEQ_FOREACH_(tracee, iterator, side) { + Comparison comparison; + const Path *binding_path; + const Path *iterator_path; + + switch (side) { + case PENDING: + case GUEST: + binding_path = &binding->guest; + iterator_path = &iterator->guest; + break; + + case HOST: + binding_path = &binding->host; + iterator_path = &iterator->host; + break; + + default: + assert(0); + return; + } + + comparison = compare_paths2(binding_path->path, binding_path->length, + iterator_path->path, iterator_path->length); + switch (comparison) { + case PATHS_ARE_EQUAL: + if (side == HOST) { + previous = iterator; + break; + } + + if (tracee->verbose > 0 && getenv("PROOT_IGNORE_MISSING_BINDINGS") == NULL) { + note(tracee, WARNING, USER, + "both '%s' and '%s' are bound to '%s', " + "only the last binding is active.", + iterator->host.path, binding->host.path, + binding->guest.path); + } + + /* Replace this iterator with the new binding. */ + CIRCLEQ_INSERT_AFTER_(tracee, iterator, binding, side); + remove_binding_from_all_lists(tracee, iterator); + return; + + case PATH1_IS_PREFIX: + /* The new binding contains the iterator. */ + previous = iterator; + break; + + case PATH2_IS_PREFIX: + /* The iterator contains the new binding. + * Use the deepest container. */ + if (next == (void *) HEAD(tracee, side)) + next = iterator; + break; + + case PATHS_ARE_NOT_COMPARABLE: + break; + + default: + assert(0); + return; + } + } + + /* Insert this binding in the list. */ + if (previous != NULL) + CIRCLEQ_INSERT_AFTER_(tracee, previous, binding, side); + else if (next != (void *) HEAD(tracee, side)) + CIRCLEQ_INSERT_BEFORE_(tracee, next, binding, side); + else + CIRCLEQ_INSERT_HEAD_(tracee, binding, side); +} + +/** + * c.f. function above. + */ +static void insort_binding2(const Tracee *tracee, Binding *binding) +{ + binding->need_substitution = + compare_paths(binding->host.path, binding->guest.path) != PATHS_ARE_EQUAL; + + insort_binding(tracee, GUEST, binding); + insort_binding(tracee, HOST, binding); +} + +/** + * Create and insert a new binding (@host_path:@guest_path) into the + * list of @tracee's bindings. The Talloc parent of this new binding + * is @context. This function returns NULL if an error occurred, + * otherwise a pointer to the newly created binding. + */ +Binding *insort_binding3(const Tracee *tracee, const TALLOC_CTX *context, + const char host_path[PATH_MAX], + const char guest_path[PATH_MAX]) +{ + Binding *binding; + + binding = talloc_zero(context, Binding); + if (binding == NULL) + return NULL; + + strcpy(binding->host.path, host_path); + strcpy(binding->guest.path, guest_path); + + binding->host.length = strlen(binding->host.path); + binding->guest.length = strlen(binding->guest.path); + + insort_binding2(tracee, binding); + + return binding; +} + +/** + * Free all bindings from @bindings. + * + * Note: this is a Talloc destructor. + */ +static int remove_bindings(Bindings *bindings) +{ + Binding *binding; + Tracee *tracee; + + /* Unlink all bindings from the @link list. */ +#define CIRCLEQ_REMOVE_ALL(name) do { \ + binding = CIRCLEQ_FIRST(bindings); \ + while (binding != (void *) bindings) { \ + Binding *next = CIRCLEQ_NEXT(binding, link.name);\ + CIRCLEQ_REMOVE_(tracee, binding, name); \ + binding = next; \ + } \ +} while (0) + + /* Search which link is used by this list. */ + tracee = TRACEE(bindings); + if (bindings == tracee->fs->bindings.pending) + CIRCLEQ_REMOVE_ALL(pending); + else if (bindings == tracee->fs->bindings.guest) + CIRCLEQ_REMOVE_ALL(guest); + else if (bindings == tracee->fs->bindings.host) + CIRCLEQ_REMOVE_ALL(host); + + bzero(bindings, sizeof(Bindings)); + + return 0; +} + +/** + * Allocate a new binding "@host:@guest" and attach it to + * @tracee->fs->bindings.pending. This function complains about + * missing @host path only if @must_exist is true. This function + * returns the allocated binding on success, NULL on error. + */ +Binding *new_binding(Tracee *tracee, const char *host, const char *guest, bool must_exist) +{ + Binding *binding; + char base[PATH_MAX]; + int status; + + /* Lasy allocation of the list of bindings specified by the + * user. This list will be used by initialize_bindings(). */ + if (tracee->fs->bindings.pending == NULL) { + tracee->fs->bindings.pending = talloc_zero(tracee->fs, Bindings); + if (tracee->fs->bindings.pending == NULL) + return NULL; + CIRCLEQ_INIT(tracee->fs->bindings.pending); + talloc_set_destructor(tracee->fs->bindings.pending, remove_bindings); + } + + /* Allocate an empty binding. */ + binding = talloc_zero(tracee->ctx, Binding); + if (binding == NULL) + return NULL; + + /* Canonicalize the host part of the binding, as expected by + * get_binding(). */ + status = realpath2(tracee->reconf.tracee, binding->host.path, host, true); + if (status < 0) { + if (must_exist && getenv("PROOT_IGNORE_MISSING_BINDINGS") == NULL) + note(tracee, WARNING, INTERNAL, "can't sanitize binding \"%s\": %s", + host, strerror(-status)); + goto error; + } + binding->host.length = strlen(binding->host.path); + + /* Symetric binding? */ + guest = guest ?: host; + + /* When not absolute, assume the guest path is relative to the + * current working directory, as with ``-b .`` for instance. */ + if (guest[0] != '/') { + status = getcwd2(tracee->reconf.tracee, base); + if (status < 0) { + note(tracee, WARNING, INTERNAL, "can't sanitize binding \"%s\": %s", + binding->guest.path, strerror(-status)); + goto error; + } + } + else + strcpy(base, "/"); + + status = join_paths(2, binding->guest.path, base, guest); + if (status < 0) { + note(tracee, WARNING, SYSTEM, "can't sanitize binding \"%s\"", + binding->guest.path); + goto error; + } + binding->guest.length = strlen(binding->guest.path); + + /* Keep the list of bindings specified by the user ordered, + * for the sake of consistency. For instance binding to "/" + * has to be the last in the list. */ + insort_binding(tracee, PENDING, binding); + + return binding; + +error: + TALLOC_FREE(binding); + return NULL; +} + +/** + * Canonicalize the guest part of the given @binding, insert it into + * @tracee->fs->bindings.guest and @tracee->fs->bindings.host. This + * function returns -1 if an error occured, 0 otherwise. + */ +static void initialize_binding(Tracee *tracee, Binding *binding) +{ + char path[PATH_MAX]; + struct stat statl; + int status; + + /* All bindings but "/" must be canonicalized. The exception + * for "/" is required to bootstrap the canonicalization. */ + if (compare_paths(binding->guest.path, "/") != PATHS_ARE_EQUAL) { + bool dereference; + size_t length; + + strcpy(path, binding->guest.path); + length = strlen(path); + assert(length > 0); + + /* Does the user explicitly tell not to dereference + * guest path? */ + dereference = (path[length - 1] != '!'); + if (!dereference) + path[length - 1] = '\0'; + + /* Initial state before canonicalization. */ + strcpy(binding->guest.path, "/"); + + /* Remember the type of the final component, it will + * be used in build_glue() later. */ + status = lstat(binding->host.path, &statl); + tracee->glue_type = (status < 0 || S_ISBLK(statl.st_mode) || S_ISCHR(statl.st_mode) + ? S_IFREG : statl.st_mode & S_IFMT); + + /* Sanitize the guest path of the binding within the + alternate rootfs since it is assumed by + substitute_binding(). */ + status = canonicalize(tracee, path, dereference, binding->guest.path, 0); + if (status < 0) { + note(tracee, WARNING, INTERNAL, + "sanitizing the guest path (binding) \"%s\": %s", + path, strerror(-status)); + return; + } + + /* Remove the trailing "/" or "/." as expected by + * substitute_binding(). */ + chop_finality(binding->guest.path); + + /* Disable definitively the creation of the glue for + * this binding. */ + tracee->glue_type = 0; + } + + binding->guest.length = strlen(binding->guest.path); + + insort_binding2(tracee, binding); +} + +/** + * Add bindings induced by @new_binding when @tracee is being sub-reconfigured. + * For example, if the previous configuration ("-r /rootfs1") contains this + * binding: + * + * -b /home/ced:/usr/local/ced + * + * and if the current configuration ("-r /rootfs2") introduces such a new + * binding: + * + * -b /usr:/media + * + * then the following binding is induced: + * + * -b /home/ced:/media/local/ced + */ +static void add_induced_bindings(Tracee *tracee, const Binding *new_binding) +{ + Binding *old_binding; + char path[PATH_MAX]; + int status; + + /* Only for reconfiguration. */ + if (tracee->reconf.tracee == NULL) + return; + + /* From the example, PRoot has already converted "-b /usr:/media" into + * "-b /rootfs1/usr:/media" in order to ensure the host part is really a + * host path. Here, the host part is converted back to "/usr" since the + * comparison can't be made on "/rootfs1/usr". + */ + strcpy(path, new_binding->host.path); + status = detranslate_path(tracee->reconf.tracee, path, NULL); + if (status < 0) + return; + + CIRCLEQ_FOREACH_(tracee->reconf.tracee, old_binding, GUEST) { + Binding *induced_binding; + Comparison comparison; + char path2[PATH_MAX]; + size_t prefix_length; + + /* Check if there's an induced binding by searching a common + * path prefix in between new/old bindings: + * + * -b /home/ced:[/usr]/local/ced + * -b [/usr]:/media + */ + comparison = compare_paths(path, old_binding->guest.path); + if (comparison != PATH1_IS_PREFIX) + continue; + + /* Convert the path of this induced binding to the new + * filesystem namespace. From the example, "/usr/local/ced" is + * converted into "/media/local/ced". Note: substitute_binding + * can't be used in this case since it would expect + * "/rootfs1/usr/local/ced instead". + */ + prefix_length = strlen(path); + if (prefix_length == 1) + prefix_length = 0; + + status = join_paths(2, path2, new_binding->guest.path, old_binding->guest.path + prefix_length); + if (status < 0) + continue; + + /* Install the induced binding. From the example: + * + * -b /home/ced:/media/local/ced + */ + induced_binding = talloc_zero(tracee->ctx, Binding); + if (induced_binding == NULL) + continue; + + strcpy(induced_binding->host.path, old_binding->host.path); + strcpy(induced_binding->guest.path, path2); + + induced_binding->host.length = strlen(induced_binding->host.path); + induced_binding->guest.length = strlen(induced_binding->guest.path); + + VERBOSE(tracee, 2, "induced binding: %s:%s (old) & %s:%s (new) -> %s:%s (induced)", + old_binding->host.path, old_binding->guest.path, path, new_binding->guest.path, + induced_binding->host.path, induced_binding->guest.path); + + insort_binding2(tracee, induced_binding); + } +} + +/** + * Allocate @tracee->fs->bindings.guest and + * @tracee->fs->bindings.host, then call initialize_binding() on each + * binding listed in @tracee->fs->bindings.pending. + */ +int initialize_bindings(Tracee *tracee) +{ + Binding *binding; + + /* Sanity checks. */ + assert(get_root(tracee) != NULL); + assert(tracee->fs->bindings.pending != NULL); + assert(tracee->fs->bindings.guest == NULL); + assert(tracee->fs->bindings.host == NULL); + + /* Allocate @tracee->fs->bindings.guest and + * @tracee->fs->bindings.host. */ + tracee->fs->bindings.guest = talloc_zero(tracee->fs, Bindings); + tracee->fs->bindings.host = talloc_zero(tracee->fs, Bindings); + if (tracee->fs->bindings.guest == NULL || tracee->fs->bindings.host == NULL) { + note(tracee, ERROR, INTERNAL, "can't allocate enough memory"); + TALLOC_FREE(tracee->fs->bindings.guest); + TALLOC_FREE(tracee->fs->bindings.host); + return -1; + } + + CIRCLEQ_INIT(tracee->fs->bindings.guest); + CIRCLEQ_INIT(tracee->fs->bindings.host); + + talloc_set_destructor(tracee->fs->bindings.guest, remove_bindings); + talloc_set_destructor(tracee->fs->bindings.host, remove_bindings); + + /* The binding to "/" has to be installed before other + * bindings since this former is required to canonicalize + * these latters. */ + binding = CIRCLEQ_LAST(tracee->fs->bindings.pending); + assert(compare_paths(binding->guest.path, "/") == PATHS_ARE_EQUAL); + + /* Call initialize_binding() on each pending binding in + * reverse order: the last binding "/" is used to bootstrap + * the canonicalization. */ + while (binding != (void *) tracee->fs->bindings.pending) { + Binding *previous; + previous = CIRCLEQ_PREV(binding, link.pending); + + /* Canonicalize then insert this binding into + * tracee->fs->bindings.guest/host. */ + initialize_binding(tracee, binding); + + /* Add induced bindings on sub-reconfiguration. */ + add_induced_bindings(tracee, binding); + + binding = previous; + } + + TALLOC_FREE(tracee->fs->bindings.pending); + + if (tracee->verbose > 0) + print_bindings(tracee); + + return 0; +} diff --git a/core/proot/src/main/cpp/path/binding.h b/core/proot/src/main/cpp/path/binding.h new file mode 100644 index 000000000..b7f8d462a --- /dev/null +++ b/core/proot/src/main/cpp/path/binding.h @@ -0,0 +1,58 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#ifndef BINDING_H +#define BINDING_H + +#include /* PATH_MAX, */ +#include + +#include "tracee/tracee.h" +#include "path.h" + +typedef struct binding { + Path host; + Path guest; + + bool need_substitution; + bool must_exist; + + struct { + CIRCLEQ_ENTRY(binding) pending; + CIRCLEQ_ENTRY(binding) guest; + CIRCLEQ_ENTRY(binding) host; + } link; +} Binding; + +typedef CIRCLEQ_HEAD(bindings, binding) Bindings; + +extern Binding *insort_binding3(const Tracee *tracee, const TALLOC_CTX *context, + const char host_path[PATH_MAX], const char guest_path[PATH_MAX]); +extern Binding *new_binding(Tracee *tracee, const char *host, const char *guest, bool must_exist); +extern int initialize_bindings(Tracee *tracee); +extern const char *get_path_binding(const Tracee* tracee, Side side, const char path[PATH_MAX]); +extern Binding *get_binding(const Tracee *tracee, Side side, const char path[PATH_MAX]); +extern const char *get_root(const Tracee* tracee); +extern int substitute_binding(const Tracee* tracee, Side side, char path[PATH_MAX]); +extern void remove_binding_from_all_lists(const Tracee *tracee, Binding *binding); + +#endif /* BINDING_H */ diff --git a/core/proot/src/main/cpp/path/canon.c b/core/proot/src/main/cpp/path/canon.c new file mode 100644 index 000000000..0fd58b0c1 --- /dev/null +++ b/core/proot/src/main/cpp/path/canon.c @@ -0,0 +1,411 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#include /* pid_t */ +#include /* PATH_MAX, */ +#include /* MAXSYMLINKS, */ +#include /* E*, */ +#include /* lstat(2), S_ISREG(), */ +#include /* access(2), lstat(2), */ +#include /* string(3), */ +#include /* assert(3), */ +#include /* sscanf(3), */ + +#include "path/canon.h" +#include "path/path.h" +#include "path/binding.h" +#include "path/glue.h" +#include "path/proc.h" +#include "path/f2fs-bug.h" +#include "extension/extension.h" + +/** + * Put an end-of-string ('\0') right before the last component of @path. + */ +static inline void pop_component(char *path) +{ + int offset; + + /* Sanity checks. */ + assert(path != NULL); + + offset = strlen(path) - 1; + assert(offset >= 0); + + /* Don't pop over "/", it doesn't mean anything. */ + if (offset == 0) { + assert(path[0] == '/' && path[1] == '\0'); + return; + } + + /* Skip trailing path separators. */ + while (offset > 1 && path[offset] == '/') + offset--; + + /* Search for the previous path separator. */ + while (offset > 1 && path[offset] != '/') + offset--; + + /* Cut the end of the string before the last component. */ + path[offset] = '\0'; + assert(path[0] == '/'); +} + +/** + * Copy in @component the first path component pointed to by @cursor, + * this later is updated to point to the next component for a further + * call. This function returns: + * + * - -errno if an error occured. + * + * - FINAL_SLASH if it the last component of the path but we + * really expect a directory. + * + * - FINAL_NORMAL if it the last component of the path. + * + * - 0 otherwise. + */ +static inline Finality next_component(char component[NAME_MAX], const char **cursor) +{ + const char *start; + ptrdiff_t length; + bool want_dir; + + /* Sanity checks. */ + assert(component != NULL); + assert(cursor != NULL); + + /* Skip leading path separators. */ + while (**cursor != '\0' && **cursor == '/') + (*cursor)++; + + /* Find the next component. */ + start = *cursor; + while (**cursor != '\0' && **cursor != '/') + (*cursor)++; + length = *cursor - start; + + if (length >= NAME_MAX) + return -ENAMETOOLONG; + + /* Extract the component. */ + strncpy(component, start, length); + component[length] = '\0'; + + /* Check if a [link to a] directory is expected. */ + want_dir = (**cursor == '/'); + + /* Skip trailing path separators. */ + while (**cursor != '\0' && **cursor == '/') + (*cursor)++; + + if (**cursor == '\0') + return (want_dir + ? FINAL_SLASH + : FINAL_NORMAL); + + return NOT_FINAL; +} + +/** + * Resolve bindings (if any) in @guest_path and copy the translated + * path into @host_path. Also, this function checks that a non-final + * component is either a directory (returned value is 0) or a symlink + * (returned value is 1), otherwise it returns -errno (-ENOENT or + * -ENOTDIR). + */ +static inline int substitute_binding_stat(Tracee *tracee, Finality finality, unsigned int recursion_level, + const char guest_path[PATH_MAX], char host_path[PATH_MAX]) +{ + struct stat statl; + int status; + + strcpy(host_path, guest_path); + status = substitute_binding(tracee, GUEST, host_path); + if (status < 0) + return status; + + /* Don't notify extensions during the initialization of a binding. */ + if (tracee->glue_type == 0) { + status = notify_extensions(tracee, HOST_PATH, (intptr_t)host_path, + IS_FINAL(finality) && recursion_level == 0); + if (status < 0) + return status; + } + + statl.st_mode = 0; + if (should_skip_file_access_due_to_f2fs_bug(tracee, host_path)) { + status = -ENOENT; + } else { + status = lstat(host_path, &statl); + /* /linkerconfig directory is present and accessible on Android, + * but cannot be stat()'d, use hardcoded stat if access was denied + * + * https://github.com/termux/proot/issues/254 + */ + if (status < 0 && errno == EACCES && strcmp(host_path, "/linkerconfig") == 0) { + status = 0; + statl.st_mode = S_IFDIR; + } + } + + /* Build the glue between the hostfs and the guestfs during + * the initialization of a binding. */ + if (status < 0 && tracee->glue_type != 0) { + statl.st_mode = build_glue(tracee, guest_path, host_path, finality); + if (statl.st_mode == 0) + status = -1; + } + + /* Return an error if a non-final component isn't a + * directory nor a symlink. The error is "No such + * file or directory" if this component doesn't exist, + * otherwise the error is "Not a directory". */ + if (!IS_FINAL(finality) && !S_ISDIR(statl.st_mode) && !S_ISLNK(statl.st_mode)) + return (status < 0 ? -ENOENT : -ENOTDIR); + + return (S_ISLNK(statl.st_mode) ? 1 : 0); +} + +/** + * Copy in @guest_path the canonicalization (see `man 3 realpath`) of + * @user_path regarding to @tracee->root. The path to canonicalize + * could be either absolute or relative to @guest_path. When the last + * component of @user_path is a link, it is dereferenced only if + * @deref_final is true -- it is useful for syscalls like lstat(2). + * The parameter @recursion_level should be set to 0 unless you know + * what you are doing. This function returns -errno if an error + * occured, otherwise it returns 0. + */ +int canonicalize(Tracee *tracee, const char *user_path, bool deref_final, + char guest_path[PATH_MAX], unsigned int recursion_level) +{ + char scratch_path[PATH_MAX]; + Finality finality; + const char *cursor; + int status; + unsigned int symlinks_followed = 0; + + /* Avoid infinite loop on circular links. */ + if (recursion_level > MAXSYMLINKS) + return -ELOOP; + + /* Sanity checks. */ + assert(user_path != NULL); + assert(guest_path != NULL); + assert(user_path != guest_path); + + if (strnlen(guest_path, PATH_MAX) >= PATH_MAX) + return -ENAMETOOLONG; + + if (user_path[0] != '/') { + /* Ensure 'guest_path' contains an absolute base of + * the relative `user_path`. */ + if (guest_path[0] != '/') + return -EINVAL; + } + else + strcpy(guest_path, "/"); + + /* Canonicalize recursely 'user_path' into 'guest_path'. */ + cursor = user_path; + finality = NOT_FINAL; + while (!IS_FINAL(finality)) { + Comparison comparison; + char component[NAME_MAX]; + char host_path[PATH_MAX]; + + finality = next_component(component, &cursor); + status = (int) finality; + if (status < 0) + return status; + + if (strcmp(component, ".") == 0) { + if (IS_FINAL(finality)) + finality = FINAL_DOT; + continue; + } + + if (strcmp(component, "..") == 0) { + pop_component(guest_path); + if (IS_FINAL(finality)) + finality = FINAL_SLASH; + continue; + } + + status = join_paths(2, scratch_path, guest_path, component); + if (status < 0) + return status; + + /* Resolve bindings and check that a non-final + * component exists and either is a directory or is a + * symlink. For this latter case, we check that the + * symlink points to a directory once it is + * canonicalized, at the end of this loop. */ + status = substitute_binding_stat(tracee, finality, recursion_level, scratch_path, host_path); + if (status < 0) + return status; + + /* Nothing special to do if it's not a link or if we + * explicitly ask to not dereference 'user_path', as + * required by syscalls like lstat(2). Obviously, this + * later condition does not apply to intermediate path + * components. Errors are explicitly ignored since + * they should be handled by the caller. */ + if (status <= 0 || (finality == FINAL_NORMAL && !deref_final)) { + strcpy(scratch_path, guest_path); + status = join_paths(2, guest_path, scratch_path, component); + if (status < 0) + return status; + continue; + } + + /* It's a link, so we have to dereference *and* + * canonicalize to ensure we are not going outside the + * new root. */ + { + const char *proc_base = guest_path; + char alias_base[PATH_MAX]; + + comparison = compare_paths("/proc", guest_path); + + /* If guest_path is not under /proc directly, + * check whether it aliases /proc via a binding + * (e.g. /oldroot/proc when /oldroot is bound to + * /). Otherwise links like /oldroot/proc/self + * would be resolved by the real kernel readlink + * and return PRoot's own pid. */ + if (comparison != PATHS_ARE_EQUAL && comparison != PATH1_IS_PREFIX) { + strncpy(alias_base, guest_path, PATH_MAX - 1); + alias_base[PATH_MAX - 1] = '\0'; + (void) substitute_binding(tracee, GUEST, alias_base); + if (strcmp(alias_base, guest_path) != 0) { + comparison = compare_paths("/proc", alias_base); + proc_base = alias_base; + } + } + + switch (comparison) { + case PATHS_ARE_EQUAL: + case PATH1_IS_PREFIX: + /* Some links in "/proc" are generated + * dynamically by the kernel. PRoot has to + * emulate some of them. */ + status = readlink_proc(tracee, scratch_path, + proc_base, component, comparison); + switch (status) { + case CANONICALIZE: + /* The symlink is already dereferenced, + * now canonicalize it. */ + goto canon; + + case DONT_CANONICALIZE: + /* If and only very final, this symlink + * shouldn't be dereferenced nor canonicalized. */ + if (finality == FINAL_NORMAL) { + strcpy(guest_path, scratch_path); + return 0; + } + break; + + default: + if (status < 0) + return status; + } + + default: + break; + } + } + + status = readlink(host_path, scratch_path, sizeof(scratch_path)); + if (status < 0) + return status; + else if (status == sizeof(scratch_path)) + return -ENAMETOOLONG; + scratch_path[status] = '\0'; + + /* Remove the leading "root" part if needed, it's + * useful for "/proc/self/cwd/" for instance. */ + status = detranslate_path(tracee, scratch_path, host_path); + if (status < 0) + return status; + + canon: + /* Canonicalize recursively the referee in case it + * is/contains a link, moreover if it is not an + * absolute link then it is relative to + * 'guest_path'. */ + { + char guest_path_before[PATH_MAX]; + strcpy(guest_path_before, guest_path); + status = canonicalize(tracee, scratch_path, true, guest_path, recursion_level + (++symlinks_followed)); + if (status < 0) + return status; + /* Detect self-referential symlinks (e.g. "foo -> .") + * where resolution leaves guest_path unchanged, + * causing infinite directory traversal. */ + if (strcmp(guest_path_before, guest_path) == 0) + return -ELOOP; + } + + /* Check that a non-final canonicalized/dereferenced + * symlink exists and is a directory. */ + status = substitute_binding_stat(tracee, finality, recursion_level, guest_path, host_path); + if (status < 0) + return status; + + /* Here, 'guest_path' shouldn't be a symlink anymore, + * unless it is a named file descriptor. */ + assert(status != 1 || sscanf(guest_path, "/proc/%*d/fd/%d", &status) == 1); + } + + /* At the exit stage of the first level of recursion, + * `guest_path` is fully canonicalized but a terminating '/' + * or a terminating '.' may be required to keep the initial + * semantic of `user_path`. */ + if (recursion_level == 0) { + switch (finality) { + case FINAL_NORMAL: + break; + + case FINAL_SLASH: + strcpy(scratch_path, guest_path); + status = join_paths(2, guest_path, scratch_path, ""); + if (status < 0) + return status; + break; + + case FINAL_DOT: + strcpy(scratch_path, guest_path); + status = join_paths(2, guest_path, scratch_path, "."); + if (status < 0) + return status; + break; + + default: + assert(0); + } + } + + return 0; +} diff --git a/core/proot/src/main/cpp/path/canon.h b/core/proot/src/main/cpp/path/canon.h new file mode 100644 index 000000000..fa6546f22 --- /dev/null +++ b/core/proot/src/main/cpp/path/canon.h @@ -0,0 +1,34 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#ifndef CANON_H +#define CANON_H + +#include +#include + +#include "tracee/tracee.h" + +extern int canonicalize(Tracee *tracee, const char *user_path, bool deref_final, + char guest_path[PATH_MAX], unsigned int nb_recursion); + +#endif /* CANON_H */ diff --git a/core/proot/src/main/cpp/path/f2fs-bug.c b/core/proot/src/main/cpp/path/f2fs-bug.c new file mode 100644 index 000000000..4a5ee4501 --- /dev/null +++ b/core/proot/src/main/cpp/path/f2fs-bug.c @@ -0,0 +1,167 @@ +#include /* bool, true, false, */ +#include /* assert(3), */ +#include /* str*(3), */ +#include /* PATH_MAX, */ +#include /* access(2), rmdir(2), */ +#include /* O_WRONLY, open(2) */ +#include /* waitpid(2), */ +#include /* errno, EEXIST, */ +#include /* dirname(3), basename(3), */ +#include /* readdir(3), opendir(3), */ + +#include "path/temp.h" +#include "tracee/tracee.h" +#include "cli/note.h" + +/** + * Check if device is affected by f2fs case sensitivity bug + */ +static bool probe_f2fs_bug(const Tracee *tracee) { + VERBOSE(tracee, 6, "Checking for f2fs case sensitivity bug"); + + bool result = false; + + /* Get base temporary directory */ + const char *base_tmp = get_temp_directory(); + assert(strlen(base_tmp) < PATH_MAX - 30); + + /* Create temporary subdirectory */ + char tmp[PATH_MAX]; + strcpy(tmp, base_tmp); + strcat(tmp, "/proot_f2fsbug_XXXXXX"); + if (mkdtemp(tmp) == NULL) { + note(tracee, WARNING, SYSTEM, "Unable to create temp directory for f2fs bug probe"); + goto end; + } + + /* Build test file paths */ + char file1[PATH_MAX]; + char file2[PATH_MAX]; + char file3[PATH_MAX]; + strcpy(file1, tmp); + strcat(file1, "/aa"); + strcpy(file2, tmp); + strcat(file2, "/Aa"); + strcpy(file3, tmp); + strcat(file3, "/aA"); + + /* Create first file */ + int fd = open(file1, O_WRONLY|O_CREAT|O_EXCL, 0600); + if (fd < 0) { + note(tracee, WARNING, SYSTEM, "Unable to create first file for f2fs bug probe"); + goto end_remove_temp_dir; + } + close(fd); + + /* Create second file, this checks if filesystem is normally case insensitive */ + fd = open(file2, O_WRONLY|O_CREAT|O_EXCL, 0600); + if (fd < 0) { + note(tracee, WARNING, SYSTEM, "Looks like there is case-insensitive file system in %s", tmp); + goto end_delete_temp_files; + } + close(fd); + + /* Prewarm third file (on normal kernel this won't have any side effect) */ + int access_result = access(file3, F_OK); + if (access_result == 0) { + note(tracee, WARNING, SYSTEM, "f2fs bug probe detected successful access() on non-existent file"); + goto end_delete_temp_files; + } + + /* Create third file from child process */ + int wstatus = 0; + pid_t pid = fork(); + if (pid == 0) { + errno = 0; + fd = open(file3, O_WRONLY|O_CREAT, 0600); + if (fd < 0) { + if (errno == EEXIST) { + VERBOSE(tracee, 1, "f2fs bug detected"); + _exit(1); + } else { + note(tracee, WARNING, SYSTEM, "f2fs bug probe failed to open third file with different errno than expected (errno=%d)", errno); + _exit(2); + } + } + close(fd); + _exit(0); + } else if (pid != -1) { + waitpid(pid, &wstatus, 0); + } else { + note(tracee, WARNING, SYSTEM, "fork() failed for f2fs bug probe"); + goto end_delete_temp_files; + } + + /* Set result basing on child exit status */ + if (WIFEXITED(wstatus) && WEXITSTATUS(wstatus) == 0) { + VERBOSE(tracee, 6, "f2fs bug not present on device"); + } else if (WIFEXITED(wstatus) && WEXITSTATUS(wstatus) == 1) { + /* Bug detected */ + VERBOSE(tracee, 1, "enabling f2fs bug workaround"); + result = true; + } else { + note(tracee, WARNING, SYSTEM, "got unexpected status from f2fs bug probe process (wstatus=0x%X)", wstatus); + } + +end_delete_temp_files: + unlink(file1); + unlink(file2); + unlink(file3); +end_remove_temp_dir: + rmdir(tmp); +end: + return result; +} + +bool should_skip_file_access_due_to_f2fs_bug(const Tracee *tracee, const char *path) { + /* On first call check if workaround should be enabled */ + static bool f2fs_bug_probed; + static bool f2fs_bug_detected; + if (!f2fs_bug_probed) { + const char *env = getenv("PROOT_F2FS_WORKAROUND"); + if (env != NULL && strcmp(env, "1") == 0) { + VERBOSE(tracee, 1, "enabling f2fs bug workaround due to env variable"); + f2fs_bug_detected = true; + } else if (env != NULL && strcmp(env, "0") == 0) { + VERBOSE(tracee, 1, "disabling f2fs bug workaround due to env variable"); + f2fs_bug_detected = false; + } else { + f2fs_bug_detected = probe_f2fs_bug(tracee); + } + f2fs_bug_probed = true; + } + + /* If workaround is not active don't skip access to file */ + if (!f2fs_bug_detected) { + return false; + } + + assert(strlen(path) < PATH_MAX - 1); + char buf[PATH_MAX]; + strcpy(buf, path); + const char *dname = dirname(buf); + + DIR *dir = opendir(dname); + if (dir == NULL) { + VERBOSE(tracee, 4, "f2fs bug workaround cannot list directory %s", dname); + /* Don't skip access to unlistable directory (e.g. "/data" on Android) */ + return false; + } + + strcpy(buf, path); + const char *bname = basename(buf); + struct dirent *entry; + while ((entry = readdir(dir)) != NULL) { + if (strcmp(entry->d_name, bname) == 0) { + /* Found file name on listing so it exists */ + VERBOSE(tracee, 4, "f2fs bug workaround found file %s", path); + closedir(dir); + return false; + } + } + + /* File not found in list, do not pass its name to kernel or inode will go into bad state */ + VERBOSE(tracee, 4, "f2fs bug workaround did not find file %s", path); + closedir(dir); + return true; +} diff --git a/core/proot/src/main/cpp/path/f2fs-bug.h b/core/proot/src/main/cpp/path/f2fs-bug.h new file mode 100644 index 000000000..b8eabdfca --- /dev/null +++ b/core/proot/src/main/cpp/path/f2fs-bug.h @@ -0,0 +1,9 @@ +#ifndef F2FS_BUG_H +#define F2FS_BUG_H + +#include /* bool, true, false, */ +#include "tracee/tracee.h" /* Tracee, */ + +bool should_skip_file_access_due_to_f2fs_bug(const Tracee *tracee, const char *path); + +#endif /* F2FS_BUG_H */ diff --git a/core/proot/src/main/cpp/path/glue.c b/core/proot/src/main/cpp/path/glue.c new file mode 100644 index 000000000..816a339e7 --- /dev/null +++ b/core/proot/src/main/cpp/path/glue.c @@ -0,0 +1,192 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#include /* mkdir(2), lstat(2), */ +#include /* mkdir(2), lstat(2), */ +#include /* mknod(2), */ +#include /* mknod(2), lstat(2), unlink(2), rmdir(2), */ +#include /* string(3), */ +#include /* assert(3), */ +#include /* PATH_MAX, */ +#include /* errno, E* */ +#include /* talloc_*, */ + +#include "path/binding.h" +#include "path/path.h" +#include "path/temp.h" +#include "cli/note.h" + +#include "compat.h" + +/** + * Remove @path if it is empty only. + * + * Note: this is a Talloc destructor. + */ +static int remove_placeholder(char *path) +{ + struct stat statl; + int status; + + status = lstat(path, &statl); + if (status) + return 0; /* Not fatal. */ + + if (!S_ISDIR(statl.st_mode)) { + if (statl.st_size != 0) + return 0; /* Not fatal. */ + status = unlink(path); + } + else + status = rmdir(path); + if (status) + return 0; /* Not fatal. */ + + return 0; +} + +/** + * Attach a copy of @path to the autofree context, and set its + * destructor to remove_placeholder(). + */ +static void set_placeholder_destructor(const char *path) +{ + TALLOC_CTX *autofreed; + char *placeholder; + + autofreed = talloc_autofree_context(); + if (autofreed == NULL) + return; + + placeholder = talloc_strdup(autofreed, path); + if (placeholder == NULL) + return; + + talloc_set_destructor(placeholder, remove_placeholder); +} + +/** + * Build in a temporary filesystem the glue between the guest part and + * the host part of the @binding_path. This function returns the type + * of the bound path, otherwise 0 if an error occured. + * + * For example, assuming the host path "/opt" is mounted/bound to the + * guest path "/black/holes/and/revelations", and assuming this path + * can't be created in the guest rootfs (eg. permission denied), then + * it is created in a temporary rootfs and all these paths are glued + * that way: + * + * $GUEST/black/ --> $GLUE/black/ + * ./holes + * ./holes/and + * ./holes/and/revelations --> $HOST/opt/ + * + * This glue allows operations on paths that do not exist in the guest + * rootfs but that were specified as the guest part of a binding. + */ +mode_t build_glue(Tracee *tracee, const char *guest_path, char host_path[PATH_MAX], + Finality finality) +{ + bool belongs_to_gluefs; + Comparison comparison; + Binding *binding; + mode_t type; + mode_t mode; + int status; + + assert(tracee->glue_type != 0); + + /* Create the temporary directory where the "glue" rootfs will + * lie. */ + if (tracee->glue == NULL) { + tracee->glue = create_temp_directory(NULL, tracee->tool_name); + if (tracee->glue == NULL) { + note(tracee, ERROR, INTERNAL, "can't create glue rootfs"); + return 0; + } + talloc_set_name_const(tracee->glue, "$glue"); + } + + comparison = compare_paths(tracee->glue, host_path); + belongs_to_gluefs = (comparison == PATHS_ARE_EQUAL || comparison == PATH1_IS_PREFIX); + + /* If it's not a final component then it is a directory. I definitely + * hate how the potential type of the final component is propagated + * from initialize_binding() down to here, sadly there's no elegant way + * to know its type at this stage. */ + if (IS_FINAL(finality)) { + type = tracee->glue_type; + mode = (belongs_to_gluefs ? 0777 : 0); + } + else { + type = S_IFDIR; + mode = 0777; + } + + if (getenv("PROOT_DONT_POLLUTE_ROOTFS") != NULL && !belongs_to_gluefs) + goto create_binding; + + /* Try to create this component into the "guest" or "glue" + * rootfs (depending if there were a glue previously). */ + if (S_ISDIR(type)) + status = mkdir(host_path, mode); + else /* S_IFREG, S_IFCHR, S_IFBLK, S_IFIFO or S_IFSOCK. */ + status = mknod(host_path, mode | type, 0); + + /* Remove placeholders from the guest rootfs once PRoot is + * terminated. */ + if (status >= 0 && !belongs_to_gluefs) + set_placeholder_destructor(host_path); + + /* Nothing else to do if the path already exists or if it is + * the final component since it will be pointed to by the + * binding being initialized (from the example, + * "$GUEST/black/holes/and/revelations" -> "$HOST/opt"). */ + if (status >= 0 || errno == EEXIST || IS_FINAL(finality)) + return type; + + /* mkdir/mknod are supposed to always succeed in + * tracee->glue. */ + if (belongs_to_gluefs) { + note(tracee, WARNING, SYSTEM, "mkdir/mknod"); + return 0; + } + +create_binding: + /* Sanity checks. */ + if ( strnlen(tracee->glue, PATH_MAX) >= PATH_MAX + || strnlen(guest_path, PATH_MAX) >= PATH_MAX) { + note(tracee, WARNING, INTERNAL, "installing the binding: guest path too long"); + return 0; + } + + /* From the example, create the binding "/black" -> + * "$GLUE/black". */ + binding = insort_binding3(tracee, tracee->glue, tracee->glue, guest_path); + if (binding == NULL) + return 0; + + /* TODO: emulation of getdents(parent(guest_path)) to finalize + * the glue, "black" in getdents("/") from the example. */ + + return type; +} diff --git a/core/proot/src/main/cpp/path/glue.h b/core/proot/src/main/cpp/path/glue.h new file mode 100644 index 000000000..cebf3a97b --- /dev/null +++ b/core/proot/src/main/cpp/path/glue.h @@ -0,0 +1,34 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#ifndef GLUE_H +#define GLUE_H + +#include /* PATH_MAX, */ + +#include "tracee/tracee.h" +#include "path.h" + +extern mode_t build_glue(Tracee *tracee, const char *guest_path, char host_path[PATH_MAX], + Finality finality); + +#endif /* GLUE_H */ diff --git a/core/proot/src/main/cpp/path/path.c b/core/proot/src/main/cpp/path/path.c new file mode 100644 index 000000000..d5c9ec66d --- /dev/null +++ b/core/proot/src/main/cpp/path/path.c @@ -0,0 +1,738 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#include /* string(3), */ +#include /* va_*(3), */ +#include /* assert(3), */ +#include /* AT_*, */ +#include /* readlink*(2), *stat(2), getpid(2), */ +#include /* pid_t, */ +#include /* S_ISDIR, */ +#include /* opendir(3), readdir(3), */ +#include /* snprintf(3), */ +#include /* E*, */ +#include /* ptrdiff_t, */ +#include /* PRI*, */ + +#include "path/path.h" +#include "path/binding.h" +#include "path/canon.h" +#include "path/proc.h" +#include "extension/extension.h" +#include "cli/note.h" +#include "build.h" + +#include "compat.h" + +/** + * Copy in @result the concatenation of several paths (@number_paths) + * and adds a path separator ('/') in between when needed. This + * function returns -errno if an error occured, otherwise it returns 0. + */ +int join_paths(int number_paths, char result[PATH_MAX], ...) +{ + va_list paths; + size_t length; + int status; + int i; + + result[0] = '\0'; + length = 0; + status = 0; + + /* Parse the list of variadic arguments. */ + va_start(paths, result); + for (i = 0; i < number_paths; i++) { + const char *path; + size_t path_length; + size_t new_length; + + path = va_arg(paths, const char *); + if (path == NULL) + continue; + path_length = strlen(path); + + /* A new path separator is needed. */ + if (length > 0 && result[length - 1] != '/' && path[0] != '/') { + new_length = length + path_length + 1; + if (new_length + 1 >= PATH_MAX) { + status = -ENAMETOOLONG; + break; + } + strcat(result + length, "/"); + strcat(result + length, path); + length = new_length; + } + /* There are already two path separators. */ + else if (length > 0 && result[length - 1] == '/' && path[0] == '/') { + new_length = length + path_length - 1; + if (new_length + 1 >= PATH_MAX) { + status = -ENAMETOOLONG; + break; + } + strcat(result + length, path + 1); + length += path_length - 1; + } + /* There's already one path separator or result[] is empty. */ + else { + new_length = length + path_length; + if (new_length + 1 >= PATH_MAX) { + status = -ENAMETOOLONG; + break; + } + strcat(result + length, path); + length += path_length; + } + + status = 0; + } + va_end(paths); + + return status; +} + +/** + * Put in @host_path the full path to the given shell @command. The + * @command is searched in @paths if not null, otherwise in $PATH + * (relatively to the @tracee's file-system name-space). This + * function always returns -1 on error, otherwise 0. + */ +int which(Tracee *tracee, const char *paths, char host_path[PATH_MAX], const char *command) +{ + char path[PATH_MAX]; + const char *cursor; + struct stat statr; + int status; + + bool is_explicit; + bool found; + + assert(command != NULL); + is_explicit = (strchr(command, '/') != NULL); + + /* Is the command available without any $PATH look-up? */ + status = realpath2(tracee, host_path, command, true); + if (status == 0 && stat(host_path, &statr) == 0) { + if (is_explicit && !S_ISREG(statr.st_mode)) { + note(tracee, ERROR, USER, "'%s' is not a regular file", command); + return -EACCES; + } + + if (is_explicit && (statr.st_mode & S_IXUSR) == 0) { + note(tracee, ERROR, USER, "'%s' is not executable", command); + return -EACCES; + } + + found = true; + + /* Don't dereference the final component to preserve + * argv0 in case it is a symlink to script. */ + (void) realpath2(tracee, host_path, command, false); + } + else + found = false; + + /* Is the the explicit command was found? */ + if (is_explicit) { + if (found) + return 0; + else + goto not_found; + } + + /* Otherwise search the command in $PATH. */ + paths = paths ?: getenv("PATH"); + if (paths == NULL || strcmp(paths, "") == 0) + goto not_found; + + cursor = paths; + do { + size_t length; + + length = strcspn(cursor, ":"); + cursor += length + 1; + + if (length >= PATH_MAX) + continue; + else if (length == 0) + strcpy(path, "."); + else { + strncpy(path, cursor - length - 1, length); + path[length] = '\0'; + } + + /* Avoid buffer-overflow. */ + if (length + strlen(command) + 2 >= PATH_MAX) + continue; + + strcat(path, "/"); + strcat(path, command); + + status = realpath2(tracee, host_path, path, true); + if (status == 0 + && stat(host_path, &statr) == 0 + && S_ISREG(statr.st_mode) + && (statr.st_mode & S_IXUSR) != 0) { + /* Don't dereference the final component to preserve + * argv0 in case it is a symlink to script. */ + (void) realpath2(tracee, host_path, path, false); + return 0; + } + } while (*(cursor - 1) != '\0'); + +not_found: + status = getcwd2(tracee, path); + if (status < 0) + strcpy(path, ""); + + note(tracee, ERROR, USER, "'%s' not found (root = %s, cwd = %s, $PATH=%s)", + command, get_root(tracee), path, paths); + + /* Check if the command was found without any $PATH look-up + * but it didn't contain "/". */ + if (found && !is_explicit) + note(tracee, ERROR, USER, + "to execute a local program, use the './' prefix, for example: ./%s", command); + + return -1; +} + +/** + * Put in @host_path the canonicalized form of @path. In the nominal + * case (@tracee == NULL), this function is barely equivalent to + * realpath(), but when doing sub-reconfiguration, the path is + * canonicalized relatively to the current @tracee's file-system + * name-space. This function returns -errno on error, otherwise 0. + */ +int realpath2(Tracee *tracee, char host_path[PATH_MAX], const char *path, bool deref_final) +{ + int status; + + if (tracee == NULL) + status = (realpath(path, host_path) == NULL ? -errno : 0); + else + status = translate_path(tracee, host_path, AT_FDCWD, path, deref_final); + return status; +} + +/** + * Put in @guest_path the canonicalized current working directory. In + * the nominal case (@tracee == NULL), this function is barely + * equivalent to realpath(), but when doing sub-reconfiguration, the + * path is canonicalized relatively to the current @tracee's + * file-system name-space. This function returns -errno on error, + * otherwise 0. + */ +int getcwd2(Tracee *tracee, char guest_path[PATH_MAX]) +{ + if (tracee == NULL) { + if (getcwd(guest_path, PATH_MAX) == NULL) + return -errno; + } + else { + if (strlen(tracee->fs->cwd) >= PATH_MAX) + return -ENAMETOOLONG; + + strcpy(guest_path, tracee->fs->cwd); + } + + return 0; +} + +/** + * Remove the trailing "/" or "/.". + */ +void chop_finality(char *path) +{ + size_t length = strlen(path); + + if (path[length - 1] == '.') { + assert(length >= 2); + /* Special case for "/." */ + if (length == 2) + path[length - 1] = '\0'; + else + path[length - 2] = '\0'; + } + else if (path[length - 1] == '/') { + /* Special case for "/" */ + if (length > 1) + path[length - 1] = '\0'; + } +} + +/** + * Put in @path the result of readlink(/proc/@pid/fd/@fd). This + * function returns -errno if an error occured, othrwise 0. + */ +int readlink_proc_pid_fd(pid_t pid, int fd, char path[PATH_MAX]) +{ + char link[32]; /* 32 > sizeof("/proc//cwd") + sizeof(#ULONG_MAX) */ + int status; + + /* Format the path to the "virtual" link. */ + status = snprintf(link, sizeof(link), "/proc/%d/fd/%d", pid, fd); + if (status < 0) + return -EBADF; + if ((size_t) status >= sizeof(link)) + return -EBADF; + + /* Read the value of this "virtual" link. */ + status = readlink(link, path, PATH_MAX); + if (status < 0) + return -EBADF; + if (status >= PATH_MAX) + return -ENAMETOOLONG; + path[status] = '\0'; + + return 0; +} + +/** + * Copy in @result the equivalent of "@tracee->root + canon(@dir_fd + + * @user_path)". If @user_path is not absolute then it is relative to + * the directory referred by the descriptor @dir_fd (AT_FDCWD is for + * the current working directory). See the documentation of + * canonicalize() for the meaning of @deref_final. This function + * returns -errno if an error occured, otherwise 0. + */ +int translate_path(Tracee *tracee, char result[PATH_MAX], int dir_fd, + const char *user_path, bool deref_final) +{ + char guest_path[PATH_MAX]; + int status; + + /* Use "/" as the base if it is an absolute guest path. */ + if (user_path[0] == '/') { + strcpy(result, "/"); + } + /* It is relative to a directory referred by a descriptor, see + * openat(2) for details. */ + else if (dir_fd != AT_FDCWD) { + /* /proc/@tracee->pid/fd/@dir_fd -> result. */ + status = readlink_proc_pid_fd(tracee->pid, dir_fd, result); + if (status < 0) + return status; + + /* Named file descriptors may reference special + * objects like pipes, sockets, inodes, ... Such + * objects do not belong to the file-system. */ + if (result[0] != '/') + return -ENOTDIR; + + /* Remove the leading "root" part of the base + * (required!). */ + status = detranslate_path(tracee, result, NULL); + if (status < 0) + return status; + } + /* It is relative to the current working directory. */ + else { + status = getcwd2(tracee, result); + if (status < 0) + return status; + } + + VERBOSE(tracee, 2, "vpid %" PRIu64 ": translate(\"%s\" + \"%s\")", + tracee != NULL ? tracee->vpid : 0, result, user_path); + + status = notify_extensions(tracee, GUEST_PATH, (intptr_t) result, (intptr_t) user_path); + if (status < 0) + return status; + if (status > 0) + goto skip; + + /* So far "result" was used as a base path, it's time to join + * it to the user path. */ + assert(result[0] == '/'); + status = join_paths(2, guest_path, result, user_path); + if (status < 0) + return status; + strcpy(result, "/"); + + /* Canonicalize regarding the new root. */ + status = canonicalize(tracee, guest_path, deref_final, result, 0); + if (status < 0) + return status; + + /* Final binding substitution to convert "result" into a host + * path, since canonicalize() works from the guest + * point-of-view. */ + status = substitute_binding(tracee, GUEST, result); + if (status < 0) + return status; + +skip: + VERBOSE(tracee, 2, "vpid %" PRIu64 ": -> \"%s\"", + tracee != NULL ? tracee->vpid : 0, result); + + status = notify_extensions(tracee, TRANSLATED_PATH, (intptr_t) result, 0); + if (status < 0) + return status; + + return 0; +} + +/** + * Remove/substitute the leading part of a "translated" @path. It + * returns 0 if no transformation is required (ie. symmetric binding), + * otherwise it returns the size in bytes of the updated @path, + * including the end-of-string terminator. On error it returns + * -errno. + */ +int detranslate_path(Tracee *tracee, char path[PATH_MAX], const char t_referrer[PATH_MAX]) +{ + size_t prefix_length; + ssize_t new_length; + + bool sanity_check; + bool follow_binding; + + /* Sanity check. */ + if (strnlen(path, PATH_MAX) >= PATH_MAX) + return -ENAMETOOLONG; + + /* Don't try to detranslate relative paths (typically the + * target of a relative symbolic link). */ + if (path[0] != '/') + return 0; + + /* Is it a symlink? */ + if (t_referrer != NULL) { + Comparison comparison; + + sanity_check = false; + follow_binding = false; + + /* In some cases bindings have to be resolved. */ + comparison = compare_paths("/proc", t_referrer); + if (comparison == PATH1_IS_PREFIX) { + /* Some links in "/proc" are generated + * dynamically by the kernel. PRoot has to + * emulate some of them. */ + char proc_path[PATH_MAX]; + strcpy(proc_path, path); + new_length = readlink_proc2(tracee, proc_path, t_referrer); + if (new_length < 0) + return new_length; + if (new_length != 0) { + strcpy(path, proc_path); + return new_length + 1; + } + + /* Always resolve bindings for symlinks in + * "/proc", they always point to the emulated + * file-system namespace by design. */ + follow_binding = true; + } + else if (!belongs_to_guestfs(tracee, t_referrer)) { + const char *binding_referree; + const char *binding_referrer; + + binding_referree = get_path_binding(tracee, HOST, path); + binding_referrer = get_path_binding(tracee, HOST, t_referrer); + assert(binding_referrer != NULL); + + /* Resolve bindings for symlinks that belong + * to a binding and point to the same binding. + * For example, if "-b /lib:/foo" is specified + * and the symlink "/lib/a -> /lib/b" exists + * in the host rootfs namespace, then it + * should appear as "/foo/a -> /foo/b" in the + * guest rootfs namespace for consistency + * reasons. */ + if (binding_referree != NULL) { + comparison = compare_paths(binding_referree, binding_referrer); + follow_binding = (comparison == PATHS_ARE_EQUAL); + } + } + } + else { + sanity_check = true; + follow_binding = true; + } + + if (follow_binding) { + switch (substitute_binding(tracee, HOST, path)) { + case 0: + return 0; + case 1: + return strlen(path) + 1; + default: + break; + } + } + + switch (compare_paths(get_root(tracee), path)) { + case PATH1_IS_PREFIX: + /* Remove the leading part, that is, the "root". */ + prefix_length = strlen(get_root(tracee)); + + /* Special case when path to the guest rootfs == "/". */ + if (prefix_length == 1) + prefix_length = 0; + + new_length = strlen(path) - prefix_length; + memmove(path, path + prefix_length, new_length); + + path[new_length] = '\0'; + break; + + case PATHS_ARE_EQUAL: + /* Special case when path == root. */ + new_length = 1; + strcpy(path, "/"); + break; + + default: + /* Ensure the path is within the new root. */ + if (sanity_check) + return -EPERM; + else + return 0; + } + + return new_length + 1; +} + +/** + * Check if the translated @host_path belongs to the guest rootfs, + * that is, isn't from a binding. + */ +bool belongs_to_guestfs(const Tracee *tracee, const char *host_path) +{ + Comparison comparison; + + comparison = compare_paths(get_root(tracee), host_path); + return (comparison == PATHS_ARE_EQUAL || comparison == PATH1_IS_PREFIX); +} + +/** + * Compare @path1 with @path2, which are respectively @length1 and + * @length2 long. + * + * This function works only with paths canonicalized in the same + * namespace (host/guest)! + */ +Comparison compare_paths2(const char *path1, size_t length1, const char *path2, size_t length2) +{ + size_t length_min; + bool is_prefix; + char sentinel; + +#if defined DEBUG_OPATH + assert(length(path1) == length1); + assert(length(path2) == length2); +#endif + assert(length1 > 0); + assert(length2 > 0); + + if (!length1 || !length2) { + return PATHS_ARE_NOT_COMPARABLE; + } + + /* Remove potential trailing '/' for the comparison. */ + if (path1[length1 - 1] == '/') + length1--; + + if (path2[length2 - 1] == '/') + length2--; + + if (length1 < length2) { + length_min = length1; + sentinel = path2[length_min]; + } + else { + length_min = length2; + sentinel = path1[length_min]; + } + + /* Optimize obvious cases. */ + if (sentinel != '/' && sentinel != '\0') + return PATHS_ARE_NOT_COMPARABLE; + + is_prefix = (strncmp(path1, path2, length_min) == 0); + + if (!is_prefix) + return PATHS_ARE_NOT_COMPARABLE; + + if (length1 == length2) + return PATHS_ARE_EQUAL; + else if (length1 < length2) + return PATH1_IS_PREFIX; + else if (length1 > length2) + return PATH2_IS_PREFIX; + + assert(0); + return PATHS_ARE_NOT_COMPARABLE; +} + +Comparison compare_paths(const char *path1, const char *path2) +{ + return compare_paths2(path1, strlen(path1), path2, strlen(path2)); +} + +typedef int (*foreach_fd_t)(const Tracee *tracee, int fd, char path[PATH_MAX]); + +/** + * Call @callback on each open file descriptors of @pid. It returns + * the status of the first failure, that is, if @callback returned + * seomthing lesser than 0, otherwise 0. + */ +static int foreach_fd(const Tracee *tracee, foreach_fd_t callback) +{ + struct dirent *dirent; + char path[PATH_MAX]; + char proc_fd[32]; /* 32 > sizeof("/proc//fd") + sizeof(#ULONG_MAX) */ + int status; + DIR *dirp; + + /* Format the path to the "virtual" directory. */ + status = snprintf(proc_fd, sizeof(proc_fd), "/proc/%d/fd", tracee->pid); + if (status < 0 || (size_t) status >= sizeof(proc_fd)) + return 0; + + /* Open the virtual directory "/proc/$pid/fd". */ + dirp = opendir(proc_fd); + if (dirp == NULL) + return 0; + + while ((dirent = readdir(dirp)) != NULL) { + /* Read the value of this "virtual" link. Don't use + * readlinkat(2) here since it would require Linux >= + * 2.6.16 and Glibc >= 2.4, whereas PRoot is supposed + * to work on any Linux 2.6 systems. */ + + char tmp[PATH_MAX]; + if (strlen(proc_fd) + strlen(dirent->d_name) + 1 >= PATH_MAX) + continue; + + strcpy(tmp, proc_fd); + strcat(tmp, "/"); + strcat(tmp, dirent->d_name); + + status = readlink(tmp, path, PATH_MAX); + if (status < 0 || status >= PATH_MAX) + continue; + path[status] = '\0'; + + /* Ensure it points to a path (not a socket or somethink like that). */ + if (path[0] != '/') + continue; + + status = callback(tracee, atoi(dirent->d_name), path); + if (status < 0) + goto end; + } + status = 0; + +end: + closedir(dirp); + return status; +} + +/** + * Helper for list_open_fd(). + */ +static int list_open_fd_callback(const Tracee *tracee, int fd, char path[PATH_MAX]) +{ + VERBOSE(tracee, 1, "pid %d: access to \"%s\" (fd %d) won't be translated until closed", + tracee->pid, path, fd); + return 0; +} + +/** + * Warn for files that are open. It is useful right after PRoot has + * attached a process. + */ +int list_open_fd(const Tracee *tracee) +{ + return foreach_fd(tracee, list_open_fd_callback); +} + +/** + * Substitute the first @old_prefix_length bytes of @path with + * @new_prefix (the caller has to provides a correct + * @new_prefix_length). This function returns the new length of + * @path. Note: this function takes care about special cases (like + * "/"). + */ +size_t substitute_path_prefix(char path[PATH_MAX], size_t old_prefix_length, + const char *new_prefix, size_t new_prefix_length) +{ + size_t path_length; + size_t new_length; + + path_length = strlen(path); + + assert(old_prefix_length < PATH_MAX); + assert(new_prefix_length < PATH_MAX); + + if (new_prefix_length == 1) { + /* Special case: "/foo" -> "/". Substitute "/foo/bin" + * with "/bin" not "//bin". */ + + new_length = path_length - old_prefix_length; + if (new_length != 0) + memmove(path, path + old_prefix_length, new_length); + else { + /* Special case: "/". */ + path[0] = '/'; + new_length = 1; + } + } + else if (old_prefix_length == 1) { + /* Special case: "/" -> "/foo". Substitute "/bin" with + * "/foo/bin" not "/foobin". */ + + new_length = new_prefix_length + path_length; + if (new_length >= PATH_MAX) + return -ENAMETOOLONG; + + if (path_length > 1) { + memmove(path + new_prefix_length, path, path_length); + memcpy(path, new_prefix, new_prefix_length); + } + else { + /* Special case: "/". */ + memcpy(path, new_prefix, new_prefix_length); + new_length = new_prefix_length; + } + } + else { + /* Generic case. */ + + new_length = path_length - old_prefix_length + new_prefix_length; + if (new_length >= PATH_MAX) + return -ENAMETOOLONG; + + memmove(path + new_prefix_length, + path + old_prefix_length, + path_length - old_prefix_length); + memcpy(path, new_prefix, new_prefix_length); + } + + assert(new_length < PATH_MAX); + path[new_length] = '\0'; + + return new_length; +} diff --git a/core/proot/src/main/cpp/path/path.h b/core/proot/src/main/cpp/path/path.h new file mode 100644 index 000000000..d4558942b --- /dev/null +++ b/core/proot/src/main/cpp/path/path.h @@ -0,0 +1,99 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#ifndef PATH_H +#define PATH_H + +#include /* pid_t, */ +#include /* AT_FDCWD, */ +#include /* PATH_MAX, */ +#include + +#include "tracee/tracee.h" + +/* File type. */ +typedef enum { + REGULAR, + SYMLINK, +} Type; + +/* Path point-of-view. */ +typedef enum { + GUEST, + HOST, + + /* Used for bindings as specified by the user but not + * canonicalized yet (new_binding, initialize_binding). */ + PENDING, +} Side; + +/* Path with cached attributes. */ +typedef struct { + char path[PATH_MAX]; + size_t length; + Side side; +} Path; + +/* Path ending type. */ +typedef enum { + NOT_FINAL, + FINAL_NORMAL, + FINAL_SLASH, + FINAL_DOT +} Finality; + +#define IS_FINAL(a) ((a) != NOT_FINAL) + +/* Comparison between two paths. */ +typedef enum Comparison { + PATHS_ARE_EQUAL, + PATH1_IS_PREFIX, + PATH2_IS_PREFIX, + PATHS_ARE_NOT_COMPARABLE, +} Comparison; + +extern int which(Tracee *tracee, const char *paths, char host_path[PATH_MAX], const char *command); +extern int realpath2(Tracee *tracee, char host_path[PATH_MAX], const char *path, bool deref_final); +extern int getcwd2(Tracee *tracee, char guest_path[PATH_MAX]); +extern void chop_finality(char *path); + +extern int translate_path(Tracee *tracee, char host_path[PATH_MAX], + int dir_fd, const char *guest_path, bool deref_final); + +extern int detranslate_path(Tracee *tracee, char path[PATH_MAX], const char t_referrer[PATH_MAX]); +extern bool belongs_to_guestfs(const Tracee *tracee, const char *path); + +extern int join_paths(int number_paths, char result[PATH_MAX], ...); +extern int list_open_fd(const Tracee *tracee); + +extern Comparison compare_paths(const char *path1, const char *path2); +extern Comparison compare_paths2(const char *path1, size_t length1, const char *path2, size_t length2); + +extern size_t substitute_path_prefix(char path[PATH_MAX], size_t old_prefix_length, + const char *new_prefix, size_t new_prefix_length); + +extern int readlink_proc_pid_fd(pid_t pid, int fd, char path[PATH_MAX]); + +/* Check if path interpretable relatively to dirfd, see openat(2) for details. */ +#define AT_FD(dirfd, path) ((dirfd) != AT_FDCWD && ((path) != NULL && (path)[0] != '/')) + +#endif /* PATH_H */ diff --git a/core/proot/src/main/cpp/path/proc.c b/core/proot/src/main/cpp/path/proc.c new file mode 100644 index 000000000..1c816dfdb --- /dev/null +++ b/core/proot/src/main/cpp/path/proc.c @@ -0,0 +1,198 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#include /* snprintf(3), */ +#include /* strcmp(3), */ +#include /* atoi(3), strtol(3), */ +#include /* E*, */ +#include /* assert(3), */ + +#include "path/proc.h" +#include "tracee/tracee.h" +#include "path/path.h" +#include "path/binding.h" + +/** + * This function emulates the @result of readlink("@base/@component") + * with respect to @tracee, where @base belongs to "/proc" (according + * to @comparison). This function returns -errno on error, an enum + * @action otherwise (c.f. above). + * + * Unlike readlink(), this function includes the nul terminating byte + * to @result. + */ +Action readlink_proc(const Tracee *tracee, char result[PATH_MAX], + const char base[PATH_MAX], const char component[NAME_MAX], + Comparison comparison) +{ + const Tracee *known_tracee; + char proc_path[64]; /* 64 > sizeof("/proc//fd/") + 2 * sizeof(#ULONG_MAX) */ + int status; + pid_t pid; + + /* TODO: Following assertion fails on some devices + * https://github.com/termux/termux-packages/issues/1679 + */ + //assert(comparison == compare_paths("/proc", base)); + + /* Remember: comparison = compare_paths("/proc", base) */ + switch (comparison) { + case PATHS_ARE_EQUAL: + /* Substitute "/proc/self" with "/proc/". */ + if (strcmp(component, "self") != 0) + return DEFAULT; + + status = snprintf(result, PATH_MAX, "/proc/%d", tracee->pid); + if (status < 0 || status >= PATH_MAX) + return -EPERM; + + return CANONICALIZE; + + case PATH1_IS_PREFIX: + /* Handle "/proc/" below, where is process + * monitored by PRoot. */ + break; + + default: + return DEFAULT; + } + + pid = atoi(base + strlen("/proc/")); + if (pid == 0) + return DEFAULT; + + /* Handle links in "/proc//". */ + status = snprintf(proc_path, sizeof(proc_path), "/proc/%d", pid); + if (status < 0 || (size_t) status >= sizeof(proc_path)) + return -EPERM; + + comparison = compare_paths(proc_path, base); + switch (comparison) { + case PATHS_ARE_EQUAL: + known_tracee = get_tracee(tracee, pid, false); + if (known_tracee == NULL) + return DEFAULT; + +#define SUBSTITUTE(name, string) \ + do { \ + if (strcmp(component, #name) != 0) \ + break; \ + \ + status = strlen(string); \ + if (status >= PATH_MAX) \ + return -EPERM; \ + \ + strncpy(result, string, status + 1); \ + return CANONICALIZE; \ + } while (0) + + /* Substitute link "/proc//???" with the content + * of tracee->???. */ + SUBSTITUTE(exe, known_tracee->exe); + SUBSTITUTE(cwd, known_tracee->fs->cwd); + SUBSTITUTE(root, get_root(known_tracee)); +#undef SUBSTITUTE + return DEFAULT; + + case PATH1_IS_PREFIX: + /* Handle "/proc//???" below. */ + break; + + default: + return DEFAULT; + } + + /* Handle links in "/proc//fd/". */ + status = snprintf(proc_path, sizeof(proc_path), "/proc/%d/fd", pid); + if (status < 0 || (size_t) status >= sizeof(proc_path)) + return -EPERM; + + comparison = compare_paths(proc_path, base); + switch (comparison) { + char *end_ptr; + + case PATHS_ARE_EQUAL: + /* Sanity check: a number is expected. */ + errno = 0; + (void) strtol(component, &end_ptr, 10); + if (errno != 0 || end_ptr == component) + return -EPERM; + + /* Don't dereference "/proc//fd/???" now: they + * can point to anonymous pipe, socket, ... otherwise + * they point to a path already canonicalized by the + * kernel. + * + * Note they are still correctly detranslated in + * syscall/exit.c if a monitored process uses + * readlink() against any of them. */ + status = snprintf(result, PATH_MAX, "%s/%s", base, component); + if (status < 0 || status >= PATH_MAX) + return -EPERM; + + return DONT_CANONICALIZE; + + default: + break; + } + + return DEFAULT; +} + +/** + * This function emulates the @result of readlink("@referer") with + * respect to @tracee, where @referer is a strict subpath of "/proc". + * This function returns -errno if an error occured, the length of + * @result if the readlink was emulated, 0 otherwise. + * + * Unlike readlink(), this function includes the nul terminating byte + * to @result (but this byte is not counted in the returned value). + */ +ssize_t readlink_proc2(const Tracee *tracee, char result[PATH_MAX], const char referer[PATH_MAX]) +{ + Action action; + char base[PATH_MAX]; + char *component; + + /* Sanity check. */ + if (strnlen(referer, PATH_MAX) >= PATH_MAX) + return -ENAMETOOLONG; + + assert(compare_paths("/proc", referer) == PATH1_IS_PREFIX); + + /* It's safe to use strrchr() here since @referer was + * previously canonicalized. */ + strcpy(base, referer); + component = strrchr(base, '/'); + + /* These cases are not possible: @referer is supposed to be a + * canonicalized subpath of "/proc". */ + assert(component != NULL && component != base); + + component[0] = '\0'; + component++; + if (component[0] == '\0') + return 0; + + action = readlink_proc(tracee, result, base, component, PATH1_IS_PREFIX); + return (action == CANONICALIZE ? strlen(result) : 0); +} diff --git a/core/proot/src/main/cpp/path/proc.h b/core/proot/src/main/cpp/path/proc.h new file mode 100644 index 000000000..7081dd71a --- /dev/null +++ b/core/proot/src/main/cpp/path/proc.h @@ -0,0 +1,44 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#ifndef PROC_H +#define PROC_H + +#include + +#include "tracee/tracee.h" +#include "path/path.h" + +/* Action to do after a call to readlink_proc(). */ +typedef enum { + DEFAULT, /* Nothing special to do, treat it as a regular link. */ + CANONICALIZE, /* The symlink was dereferenced, now canonicalize it. */ + DONT_CANONICALIZE, /* The symlink shouldn't be dereferenced nor canonicalized. */ +} Action; + + +extern Action readlink_proc(const Tracee *tracee, char result[PATH_MAX], const char path[PATH_MAX], + const char component[NAME_MAX], Comparison comparison); + +extern ssize_t readlink_proc2(const Tracee *tracee, char result[PATH_MAX], const char path[PATH_MAX]); + +#endif /* PROC_H */ diff --git a/core/proot/src/main/cpp/path/temp.c b/core/proot/src/main/cpp/path/temp.c new file mode 100644 index 000000000..dde2296d1 --- /dev/null +++ b/core/proot/src/main/cpp/path/temp.c @@ -0,0 +1,374 @@ +#include /* stat(2), opendir(3), */ +#include /* stat(2), chmod(2), */ +#include /* stat(2), rmdir(2), unlink(2), readlink(2), */ +#include /* errno(2), */ +#include /* readdir(3), opendir(3), */ +#include /* strcmp(3), */ +#include /* free(3), getenv(3), */ +#include /* P_tmpdir, */ +#include /* talloc(3), */ + +#include "cli/note.h" + +/** + * Return the path to a directory where temporary files should be + * created. + */ +const char *get_temp_directory() +{ + static const char *temp_directory = NULL; + char *tmp; + + if (temp_directory != NULL) + return temp_directory; + + temp_directory = getenv("PROOT_TMP_DIR"); + if (temp_directory == NULL) { + temp_directory = P_tmpdir; + } + + tmp = realpath(temp_directory, NULL); + if (tmp == NULL) { + note(NULL, WARNING, SYSTEM, + "can't canonicalize %s", temp_directory); + return temp_directory; + } + + temp_directory = talloc_strdup(talloc_autofree_context(), tmp); + if (temp_directory == NULL) + temp_directory = tmp; + else + free(tmp); + + return temp_directory; +} + +/** + * Remove recursively the content of the current working directory. + * This latter has to lie in temp_directory (ie. "/tmp" on most + * systems). This function returns -1 if a fatal error occured + * (ie. the recursion must be stopped), the number of non-fatal errors + * otherwise. + * + * WARNING: this function changes the current working directory for + * the calling process. + */ +static int clean_temp_cwd() +{ + const char *temp_directory = get_temp_directory(); + const size_t length_temp_directory = strlen(temp_directory); + char *prefix = NULL; + int nb_errors = 0; + DIR *dir = NULL; + int status; + + prefix = talloc_size(NULL, length_temp_directory + 1); + if (prefix == NULL) { + note(NULL, WARNING, INTERNAL, "can't allocate memory"); + nb_errors++; + goto end; + } + + /* Sanity check: ensure the current directory lies in + * "/tmp". */ + status = readlink("/proc/self/cwd", prefix, length_temp_directory); + if (status < 0) { + note(NULL, WARNING, SYSTEM, "can't readlink '/proc/self/cwd'"); + nb_errors++; + goto end; + } + prefix[status] = '\0'; + + if (strncmp(prefix, temp_directory, length_temp_directory) != 0) { + note(NULL, ERROR, INTERNAL, + "trying to remove a directory outside of '%s', " + "please report this error.", temp_directory); + nb_errors++; + goto end; + } + + dir = opendir("."); + if (dir == NULL) { + note(NULL, WARNING, SYSTEM, "can't open '.'"); + nb_errors++; + goto end; + } + + while (1) { + struct dirent *entry; + + errno = 0; + entry = readdir(dir); + if (entry == NULL) + break; + + if ( strcmp(entry->d_name, ".") == 0 + || strcmp(entry->d_name, "..") == 0) + continue; + + /* Skip chmod on symlinks: chmod follows them and would + * report spurious errors when the target no longer + * exists (common with the /dev/{stdin,fd,...} symlinks + * bubblewrap leaves behind in emulated tmpfs dirs). + * We only need to unlink the symlink itself. */ + if (entry->d_type != DT_LNK) { + status = chmod(entry->d_name, 0700); + if (status < 0) { + note(NULL, WARNING, SYSTEM, "cant chmod '%s'", entry->d_name); + nb_errors++; + continue; + } + } + + if (entry->d_type == DT_DIR) { + status = chdir(entry->d_name); + if (status < 0) { + note(NULL, WARNING, SYSTEM, "can't chdir '%s'", entry->d_name); + nb_errors++; + continue; + } + + /* Recurse. */ + status = clean_temp_cwd(); + if (status < 0) { + nb_errors = -1; + goto end; + } + nb_errors += status; + + status = chdir(".."); + if (status < 0) { + note(NULL, ERROR, SYSTEM, "can't chdir to '..'"); + nb_errors = -1; + goto end; + } + + status = rmdir(entry->d_name); + } + else { + status = unlink(entry->d_name); + } + if (status < 0) { + note(NULL, WARNING, SYSTEM, "can't remove '%s'", entry->d_name); + nb_errors++; + continue; + } + } + if (errno != 0) { + note(NULL, WARNING, SYSTEM, "can't readdir '.'"); + nb_errors++; + } + +end: + TALLOC_FREE(prefix); + + if (dir != NULL) + (void) closedir(dir); + + return nb_errors; +} + +/** + * Remove recursively @path. This latter has to be a directory lying + * in temp_directory (ie. "/tmp" on most systems). This function + * returns -1 on error, otherwise 0. + */ +static int remove_temp_directory2(const char *path) +{ + int result; + int status; + char *cwd; + +#ifdef __ANDROID__ + cwd = malloc(PATH_MAX); + getcwd(cwd, PATH_MAX); +#else + cwd = get_current_dir_name(); +#endif + + status = chmod(path, 0700); + if (status < 0) { + note(NULL, ERROR, SYSTEM, "can't chmod '%s'", path); + result = -1; + goto end; + } + + status = chdir(path); + if (status < 0) { + note(NULL, ERROR, SYSTEM, "can't chdir to '%s'", path); + result = -1; + goto end; + } + + status = clean_temp_cwd(); + result = (status == 0 ? 0 : -1); + + /* Try to remove path even if something went wrong. */ + status = chdir(".."); + if (status < 0) { + note(NULL, ERROR, SYSTEM, "can't chdir to '..'"); + result = -1; + goto end; + } + + status = rmdir(path); + if (status < 0) { + note(NULL, ERROR, SYSTEM, "cant remove '%s'", path); + result = -1; + goto end; + } + +end: + if (cwd != NULL) { + status = chdir(cwd); + if (status < 0) { + result = -1; + note(NULL, ERROR, SYSTEM, "can't chdir to '%s'", cwd); + } + free(cwd); + } + + return result; +} + +/** + * Like remove_temp_directory2() but always return 0. + * + * Note: this is a talloc destructor. + */ +static int remove_temp_directory(char *path) +{ + (void) remove_temp_directory2(path); + return 0; +} + +/** + * Remove the file @path. This function always returns 0. + * + * Note: this is a talloc destructor. + */ +static int remove_temp_file(char *path) +{ + int status; + + status = unlink(path); + if (status < 0) + note(NULL, ERROR, SYSTEM, "can't remove '%s'", path); + + return 0; +} + +/** + * Create a path name with the following format: + * "/tmp/@prefix-$PID-XXXXXX". The returned C string is either + * auto-freed if @context is NULL. This function returns NULL if an + * error occurred. + */ +char *create_temp_name(TALLOC_CTX *context, const char *prefix) +{ + const char *temp_directory = get_temp_directory(); + char *name; + + if (context == NULL) + context = talloc_autofree_context(); + + name = talloc_asprintf(context, "%s/%s-%d-XXXXXX", temp_directory, prefix, getpid()); + if (name == NULL) { + note(NULL, ERROR, INTERNAL, "can't allocate memory"); + return NULL; + } + + return name; +} + +/** + * Create a directory that will be automatically removed either on + * PRoot termination if @context is NULL, or once its path name + * (attached to @context) is freed. This function returns NULL on + * error, otherwise the absolute path name to the created directory + * (@prefix-ed). + */ +const char *create_temp_directory(TALLOC_CTX *context, const char *prefix) +{ + char *name; + + name = create_temp_name(context, prefix); + if (name == NULL) + return NULL; + + name = mkdtemp(name); + if (name == NULL) { + note(NULL, ERROR, SYSTEM, "can't create temporary directory"); + note(NULL, INFO, USER, "Please set PROOT_TMP_DIR env. variable " + "to an alternate location (with write permission)."); + return NULL; + } + + talloc_set_destructor(name, remove_temp_directory); + + return name; +} + +/** + * Create a file that will be automatically removed either on PRoot + * termination if @context is NULL, or once its path name (attached to + * @context) is freed. This function returns NULL on error, + * otherwise the absolute path name to the created file (@prefix-ed). + */ +const char *create_temp_file(TALLOC_CTX *context, const char *prefix) +{ + char *name; + int fd; + + name = create_temp_name(context, prefix); + if (name == NULL) + return NULL; + + fd = mkstemp(name); + if (fd < 0) { + note(NULL, ERROR, SYSTEM, "can't create temporary file"); + note(NULL, INFO, USER, "Please set PROOT_TMP_DIR env. variable " + "to an alternate location (with write permission)."); + return NULL; + } + close(fd); + + talloc_set_destructor(name, remove_temp_file); + + return name; +} + +/** + * Like create_temp_file() but returns an open file stream to the + * created file. It's up to the caller to close returned stream. + */ +FILE* open_temp_file(TALLOC_CTX *context, const char *prefix) +{ + char *name; + FILE *file; + int fd; + + name = create_temp_name(context, prefix); + if (name == NULL) + return NULL; + + fd = mkstemp(name); + if (fd < 0) + goto error; + + talloc_set_destructor(name, remove_temp_file); + + file = fdopen(fd, "w"); + if (file == NULL) + goto error; + + return file; + +error: + if (fd >= 0) + close(fd); + note(NULL, ERROR, SYSTEM, "can't create temporary file"); + note(NULL, INFO, USER, "Please set PROOT_TMP_DIR env. variable " + "to an alternate location (with write permission)."); + return NULL; +} diff --git a/core/proot/src/main/cpp/path/temp.h b/core/proot/src/main/cpp/path/temp.h new file mode 100644 index 000000000..27fca9f20 --- /dev/null +++ b/core/proot/src/main/cpp/path/temp.h @@ -0,0 +1,34 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#ifndef TEMP_H +#define TEMP_H + +#include + +extern char *create_temp_name(TALLOC_CTX *context, const char *prefix); +extern const char *create_temp_directory(TALLOC_CTX *context, const char *prefix); +extern const char *create_temp_file(TALLOC_CTX *context, const char *prefix); +extern FILE* open_temp_file(TALLOC_CTX *context, const char *prefix); +extern const char *get_temp_directory(); + +#endif /* TEMP_H */ diff --git a/core/proot/src/main/cpp/ptrace/ptrace.c b/core/proot/src/main/cpp/ptrace/ptrace.c new file mode 100644 index 000000000..451f0a935 --- /dev/null +++ b/core/proot/src/main/cpp/ptrace/ptrace.c @@ -0,0 +1,672 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#include /* PTRACE_*, */ +#include /* E*, */ +#include /* assert(3), */ +#include /* bool, true, false, */ +#include /* siginfo_t, */ +#include /* struct iovec, */ +#include /* MIN(), MAX(), */ +#include /* __WALL, */ +#include /* memcpy(3), */ + +#include "ptrace/ptrace.h" +#include "ptrace/user.h" +#include "tracee/tracee.h" +#include "syscall/sysnum.h" +#include "tracee/reg.h" +#include "tracee/mem.h" +#include "tracee/abi.h" +#include "tracee/event.h" +#include "cli/note.h" +#include "arch.h" + +#include "compat.h" + +#if defined(ARCH_X86_64) || defined(ARCH_X86) +#include /* struct user_desc, */ +#endif + +#if defined(ARCH_X86_64) +#include /* ARCH_{G,S}ET_{F,G}S, */ +#endif + +#if defined(ARCH_ARM_EABI) +#define user_fpregs_struct user_fpregs +#endif + +#if defined(ARCH_ARM64) +#define user_fpregs_struct user_fpsimd_struct +#endif + +static const char *stringify_ptrace( +#ifdef __GLIBC__ + enum __ptrace_request +#else + int +#endif + request) +{ +#define CASE_STR(a) case a: return #a; break; + switch ((int) request) { + CASE_STR(PTRACE_TRACEME) CASE_STR(PTRACE_PEEKTEXT) CASE_STR(PTRACE_PEEKDATA) + CASE_STR(PTRACE_PEEKUSER) CASE_STR(PTRACE_POKETEXT) CASE_STR(PTRACE_POKEDATA) + CASE_STR(PTRACE_POKEUSER) CASE_STR(PTRACE_CONT) CASE_STR(PTRACE_KILL) + CASE_STR(PTRACE_SINGLESTEP) CASE_STR(PTRACE_GETREGS) CASE_STR(PTRACE_SETREGS) + CASE_STR(PTRACE_GETFPREGS) CASE_STR(PTRACE_SETFPREGS) CASE_STR(PTRACE_ATTACH) + CASE_STR(PTRACE_DETACH) CASE_STR(PTRACE_GETFPXREGS) CASE_STR(PTRACE_SETFPXREGS) + CASE_STR(PTRACE_SYSCALL) CASE_STR(PTRACE_SETOPTIONS) CASE_STR(PTRACE_GETEVENTMSG) + CASE_STR(PTRACE_GETSIGINFO) CASE_STR(PTRACE_SETSIGINFO) CASE_STR(PTRACE_GETREGSET) + CASE_STR(PTRACE_SETREGSET) CASE_STR(PTRACE_SEIZE) CASE_STR(PTRACE_INTERRUPT) + CASE_STR(PTRACE_LISTEN) CASE_STR(PTRACE_SET_SYSCALL) + CASE_STR(PTRACE_GET_THREAD_AREA) CASE_STR(PTRACE_SET_THREAD_AREA) + CASE_STR(PTRACE_GETVFPREGS) CASE_STR(PTRACE_SINGLEBLOCK) CASE_STR(PTRACE_ARCH_PRCTL) + default: return "PTRACE_???"; } +} + +/** + * Translate the ptrace syscall made by @tracee into a "void" syscall + * in order to emulate the ptrace mechanism within PRoot. This + * function returns -errno if an error occured (unsupported request), + * otherwise 0. + */ +int translate_ptrace_enter(Tracee *tracee) +{ + /* The ptrace syscall have to be emulated since it can't be nested. */ + set_sysnum(tracee, PR_void); + return 0; +} + +/** + * Set @ptracee's tracer to @ptracer, and increment ptracees counter + * of this later. + */ +void attach_to_ptracer(Tracee *ptracee, Tracee *ptracer) +{ + bzero(&(PTRACEE), sizeof(PTRACEE)); + PTRACEE.ptracer = ptracer; + + PTRACER.nb_ptracees++; +} + +/** + * Unset @ptracee's tracer, and decrement ptracees counter of this + * later. + */ +void detach_from_ptracer(Tracee *ptracee) +{ + Tracee *ptracer = PTRACEE.ptracer; + + PTRACEE.ptracer = NULL; + + assert(PTRACER.nb_ptracees > 0); + PTRACER.nb_ptracees--; +} + +/** + * Emulate the ptrace syscall made by @tracee. This function returns + * -errno if an error occured (unsupported request), otherwise 0. + */ +int translate_ptrace_exit(Tracee *tracee) +{ + word_t request, pid, address, data, result; + Tracee *ptracee, *ptracer; + int forced_signal = -1; + int signal; + int status; + + /* Read ptrace parameters. */ + request = peek_reg(tracee, ORIGINAL, SYSARG_1); + pid = peek_reg(tracee, ORIGINAL, SYSARG_2); + address = peek_reg(tracee, ORIGINAL, SYSARG_3); + data = peek_reg(tracee, ORIGINAL, SYSARG_4); + + /* Propagate signedness for this special value. */ + if (is_32on64_mode(tracee) && pid == 0xFFFFFFFF) + pid = (word_t) -1; + + /* The TRACEME request is the only one used by a tracee. */ + if (request == PTRACE_TRACEME) { + ptracer = tracee->parent; + ptracee = tracee; + + /* The emulated ptrace in PRoot has the same + * limitation as the real ptrace in the Linux kernel: + * only one tracer per process. */ + if (PTRACEE.ptracer != NULL || ptracee == ptracer) + return -EPERM; + + attach_to_ptracer(ptracee, ptracer); + + /* Detect when the ptracer has gone to wait before the + * ptracee did the ptrace(ATTACHME) request. */ + if (PTRACER.waits_in == WAITS_IN_KERNEL) { + status = kill(ptracer->pid, SIGSTOP); + if (status < 0) + note(tracee, WARNING, INTERNAL, + "can't wake ptracer %d", ptracer->pid); + else { + ptracer->sigstop = SIGSTOP_IGNORED; + PTRACER.waits_in = WAITS_IN_PROOT; + } + } + + /* Disable seccomp acceleration for this tracee and + * all its children since we can't assume what are the + * syscalls its tracer is interested with. */ + if (tracee->seccomp == ENABLED) + tracee->seccomp = DISABLING; + + return 0; + } + + /* The ATTACH, SEIZE, and INTERRUPT requests are the only ones + * where the ptracee is in an unknown state. */ + if (request == PTRACE_ATTACH) { + ptracer = tracee; + ptracee = get_tracee(ptracer, pid, false); + if (ptracee == NULL) + return -ESRCH; + + /* The emulated ptrace in PRoot has the same + * limitation as the real ptrace in the Linux kernel: + * only one tracer per process. */ + if (PTRACEE.ptracer != NULL || ptracee == ptracer) + return -EPERM; + + attach_to_ptracer(ptracee, ptracer); + + /* The tracee is sent a SIGSTOP, but will not + * necessarily have stopped by the completion of this + * call. + * + * -- man 2 ptrace. */ + kill(pid, SIGSTOP); + + return 0; + } + + /* Here, the tracee is a ptracer. Also, the requested ptracee + * has to be in the "stopped for ptracer" state. */ + ptracer = tracee; + ptracee = get_stopped_ptracee(ptracer, pid, false, __WALL); + if (ptracee == NULL) { + static bool warned = false; + + /* Ensure we didn't get there only because inheritance + * mechanism has missed this one. */ + ptracee = get_tracee(tracee, pid, false); + if (ptracee != NULL && ptracee->exe == NULL && !warned) { + warned = true; + note(ptracer, WARNING, INTERNAL, "ptrace request to an unexpected ptracee"); + } + + return -ESRCH; + } + + /* Sanity checks. */ + if ( PTRACEE.is_zombie + || PTRACEE.ptracer != ptracer + || pid == (word_t) -1) + return -ESRCH; + + switch (request) { + case PTRACE_SYSCALL: + PTRACEE.ignore_syscalls = false; + forced_signal = (int) data; + status = 0; + break; /* Restart the ptracee. */ + + case PTRACE_CONT: + PTRACEE.ignore_syscalls = true; + forced_signal = (int) data; + status = 0; + break; /* Restart the ptracee. */ + + case PTRACE_SINGLESTEP: + ptracee->restart_how = PTRACE_SINGLESTEP; + forced_signal = (int) data; + status = 0; + break; /* Restart the ptracee. */ + + case PTRACE_SINGLEBLOCK: + ptracee->restart_how = PTRACE_SINGLEBLOCK; + forced_signal = (int) data; + status = 0; + break; /* Restart the ptracee. */ + + case PTRACE_DETACH: + detach_from_ptracer(ptracee); + status = 0; + break; /* Restart the ptracee. */ + + case PTRACE_KILL: + status = ptrace(request, pid, NULL, NULL); + break; /* Restart the ptracee. */ + + case PTRACE_SETOPTIONS: + PTRACEE.options = data; + return 0; /* Don't restart the ptracee. */ + + case PTRACE_GETEVENTMSG: { + status = ptrace(request, pid, NULL, &result); + if (status < 0) + return -errno; + + poke_word(ptracer, data, result); + if (errno != 0) + return -errno; + + return 0; /* Don't restart the ptracee. */ + } + + case PTRACE_PEEKUSER: + if (is_32on64_mode(ptracer)) { + address = convert_user_offset(address); + if (address == (word_t) -1) + return -EIO; + } + /* Fall through. */ + case PTRACE_PEEKTEXT: + case PTRACE_PEEKDATA: + address = untag_pointer(address); + errno = 0; + result = (word_t) ptrace(request, pid, address, NULL); + if (errno != 0) + return -errno; + + poke_word(ptracer, data, result); + if (errno != 0) + return -errno; + + return 0; /* Don't restart the ptracee. */ + + case PTRACE_POKEUSER: + if (is_32on64_mode(ptracer)) { + address = convert_user_offset(address); + if (address == (word_t) -1) + return -EIO; + } + + status = ptrace(request, pid, address, data); + if (status < 0) + return -errno; + + return 0; /* Don't restart the ptracee. */ + + case PTRACE_POKETEXT: + case PTRACE_POKEDATA: + address = untag_pointer(address); + if (is_32on64_mode(ptracer)) { + word_t tmp; + + errno = 0; + tmp = (word_t) ptrace(PTRACE_PEEKDATA, ptracee->pid, address, NULL); + if (errno != 0) + return -errno; + + data |= (tmp & 0xFFFFFFFF00000000ULL); + } + + status = ptrace(request, pid, address, data); + if (status < 0) + return -errno; + + return 0; /* Don't restart the ptracee. */ + + case PTRACE_GETSIGINFO: { + siginfo_t siginfo; + + status = ptrace(request, pid, NULL, &siginfo); + if (status < 0) + return -errno; + + status = write_data(ptracer, data, &siginfo, sizeof(siginfo)); + if (status < 0) + return status; + + return 0; /* Don't restart the ptracee. */ + } + + case PTRACE_SETSIGINFO: { + siginfo_t siginfo; + + status = read_data(ptracer, &siginfo, data, sizeof(siginfo)); + if (status < 0) + return status; + + status = ptrace(request, pid, NULL, &siginfo); + if (status < 0) + return -errno; + + return 0; /* Don't restart the ptracee. */ + } + + case PTRACE_GETREGS: { + size_t size; + union { + struct user_regs_struct regs; + uint32_t regs32[USER32_NB_REGS]; + } buffer; + + status = ptrace(request, pid, NULL, &buffer); + if (status < 0) + return -errno; + + if (is_32on64_mode(tracee)) { + struct user_regs_struct regs64; + + memcpy(®s64, &buffer.regs, sizeof(struct user_regs_struct)); + convert_user_regs_struct(false, (uint64_t *) ®s64, buffer.regs32); + + size = sizeof(buffer.regs32); + } + else + size = sizeof(buffer.regs); + + status = write_data(ptracer, data, &buffer, size); + if (status < 0) + return status; + + return 0; /* Don't restart the ptracee. */ + } + + case PTRACE_SETREGS: { + size_t size; + union { + struct user_regs_struct regs; + uint32_t regs32[USER32_NB_REGS]; + } buffer; + + size = (is_32on64_mode(ptracer) + ? sizeof(buffer.regs32) + : sizeof(buffer.regs)); + + status = read_data(ptracer, &buffer, data, size); + if (status < 0) + return status; + + if (is_32on64_mode(ptracer)) { + uint32_t regs32[USER32_NB_REGS]; + + memcpy(regs32, buffer.regs32, sizeof(regs32)); + convert_user_regs_struct(true, (uint64_t *) &buffer.regs, regs32); + } + + status = ptrace(request, pid, NULL, &buffer); + if (status < 0) + return -errno; + + return 0; /* Don't restart the ptracee. */ + } + + case PTRACE_GETFPREGS: { + size_t size; + union { + struct user_fpregs_struct fpregs; + uint32_t fpregs32[USER32_NB_FPREGS]; + } buffer; + + status = ptrace(request, pid, NULL, &buffer); + if (status < 0) + return -errno; + + if (is_32on64_mode(tracee)) { +#if 0 /* TODO */ + struct user_fpregs_struct fpregs64; + + memcpy(&fpregs64, &buffer.fpregs, sizeof(struct user_fpregs_struct)); + convert_user_fpregs_struct(false, (uint64_t *) &fpregs64, buffer.fpregs32); +#else + static bool warned = false; + if (!warned) + note(ptracer, WARNING, INTERNAL, + "ptrace 32-bit request '%s' not supported on 64-bit yet", + stringify_ptrace(request)); + warned = true; + bzero(&buffer, sizeof(buffer)); +#endif + size = sizeof(buffer.fpregs32); + } + else + size = sizeof(buffer.fpregs); + + status = write_data(ptracer, data, &buffer, size); + if (status < 0) + return status; + + return 0; /* Don't restart the ptracee. */ + } + + case PTRACE_SETFPREGS: { + size_t size; + union { + struct user_fpregs_struct fpregs; + uint32_t fpregs32[USER32_NB_FPREGS]; + } buffer; + + size = (is_32on64_mode(ptracer) + ? sizeof(buffer.fpregs32) + : sizeof(buffer.fpregs)); + + status = read_data(ptracer, &buffer, data, size); + if (status < 0) + return status; + + if (is_32on64_mode(ptracer)) { +#if 0 /* TODO */ + uint32_t fpregs32[USER32_NB_FPREGS]; + + memcpy(fpregs32, buffer.fpregs32, sizeof(fpregs32)); + convert_user_fpregs_struct(true, (uint64_t *) &buffer.fpregs, fpregs32); +#else + static bool warned = false; + if (!warned) + note(ptracer, WARNING, INTERNAL, + "ptrace 32-bit request '%s' not supported on 64-bit yet", + stringify_ptrace(request)); + warned = true; + return -ENOTSUP; +#endif + } + + status = ptrace(request, pid, NULL, &buffer); + if (status < 0) + return -errno; + + return 0; /* Don't restart the ptracee. */ + } + +#if defined(ARCH_X86_64) || defined(ARCH_X86) + case PTRACE_GET_THREAD_AREA: { + struct user_desc user_desc; + + status = ptrace(request, pid, address, &user_desc); + if (status < 0) + return -errno; + + status = write_data(ptracer, data, &user_desc, sizeof(user_desc)); + if (status < 0) + return status; + + return 0; /* Don't restart the ptracee. */ + } + + case PTRACE_SET_THREAD_AREA: { + struct user_desc user_desc; + + status = read_data(ptracer, &user_desc, data, sizeof(user_desc)); + if (status < 0) + return status; + + status = ptrace(request, pid, address, &user_desc); + if (status < 0) + return -errno; + + return 0; /* Don't restart the ptracee. */ + } +#endif + + case PTRACE_GETREGSET: { + struct iovec local_iovec; + word_t remote_iovec_base; + word_t remote_iovec_len; + + remote_iovec_base = peek_word(ptracer, data); + if (errno != 0) + return -errno; + + remote_iovec_len = peek_word(ptracer, data + sizeof_word(ptracer)); + if (errno != 0) + return -errno; + + /* Sanity check. */ + assert(sizeof(local_iovec.iov_len) == sizeof(word_t)); + + local_iovec.iov_len = remote_iovec_len; + local_iovec.iov_base = talloc_zero_size(ptracer->ctx, remote_iovec_len); + if (local_iovec.iov_base == NULL) + return -ENOMEM; + + status = ptrace(PTRACE_GETREGSET, pid, address, &local_iovec); + if (status < 0) + return status; + + remote_iovec_len = local_iovec.iov_len = + MIN(remote_iovec_len, local_iovec.iov_len); + + /* Update remote vector content. */ + status = writev_data(ptracer, remote_iovec_base, &local_iovec, 1); + if (status < 0) + return status; + + /* Update remote vector length. */ + poke_word(ptracer, data + sizeof_word(ptracer), remote_iovec_len); + if (errno != 0) + return -errno; + + return 0; /* Don't restart the ptracee. */ + } + + case PTRACE_SETREGSET: { + struct iovec local_iovec; + word_t remote_iovec_base; + word_t remote_iovec_len; + + remote_iovec_base = peek_word(ptracer, data); + if (errno != 0) + return -errno; + + remote_iovec_len = peek_word(ptracer, data + sizeof_word(ptracer)); + if (errno != 0) + return -errno; + + /* Sanity check. */ + assert(sizeof(local_iovec.iov_len) == sizeof(word_t)); + + local_iovec.iov_len = remote_iovec_len; + local_iovec.iov_base = talloc_zero_size(ptracer->ctx, remote_iovec_len); + if (local_iovec.iov_base == NULL) + return -ENOMEM; + + /* Copy remote content into the local vector. */ + status = read_data(ptracer, local_iovec.iov_base, + remote_iovec_base, local_iovec.iov_len); + if (status < 0) + return status; + + status = ptrace(PTRACE_SETREGSET, pid, address, &local_iovec); + if (status < 0) + return status; + + return 0; /* Don't restart the ptracee. */ + } + + case PTRACE_GETVFPREGS: + case PTRACE_GETFPXREGS: { + static bool warned = false; + if (!warned) + note(ptracer, WARNING, INTERNAL, "ptrace request '%s' not supported yet", + stringify_ptrace(request)); + warned = true; + return -ENOTSUP; + } + +#if defined(ARCH_X86_64) + case PTRACE_ARCH_PRCTL: + switch (data) { + case ARCH_GET_GS: + case ARCH_GET_FS: + status = ptrace(request, pid, &result, data); + if (status < 0) + return -errno; + + poke_word(ptracer, address, result); + if (errno != 0) + return -errno; + break; + + case ARCH_SET_GS: + case ARCH_SET_FS: { + static bool warned = false; + if (!warned) + note(ptracer, WARNING, INTERNAL, + "ptrace request '%s' ARCH_SET_{G,F}S not supported yet", + stringify_ptrace(request)); + return -ENOTSUP; + } + + default: + return -ENOTSUP; + } + + return 0; /* Don't restart the ptracee. */ +#endif + + case PTRACE_SET_SYSCALL: + status = ptrace(request, pid, address, data); + if (status < 0) + return -errno; + + return 0; /* Don't restart the ptracee. */ + + default: + note(ptracer, WARNING, INTERNAL, "ptrace request '%s' not supported yet", + stringify_ptrace(request)); + return -ENOTSUP; + } + + /* Now, the initial tracee's event can be handled. */ + signal = PTRACEE.event4.proot.pending + ? handle_tracee_event(ptracee, PTRACEE.event4.proot.value) + : PTRACEE.event4.proot.value; + + /* The restarting signal from the ptracer overrides the + * restarting signal from PRoot. */ + if (forced_signal != -1) + signal = forced_signal; + + (void) restart_tracee(ptracee, signal); + + return status; +} diff --git a/core/proot/src/main/cpp/ptrace/ptrace.h b/core/proot/src/main/cpp/ptrace/ptrace.h new file mode 100644 index 000000000..9083ea92a --- /dev/null +++ b/core/proot/src/main/cpp/ptrace/ptrace.h @@ -0,0 +1,36 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#ifndef PTRACE_H +#define PTRACE_H + +#include "tracee/tracee.h" + +extern int translate_ptrace_enter(Tracee *tracee); +extern int translate_ptrace_exit(Tracee *tracee); +extern void attach_to_ptracer(Tracee *ptracee, Tracee *ptracer); +extern void detach_from_ptracer(Tracee *ptracee); + +#define PTRACEE (ptracee->as_ptracee) +#define PTRACER (ptracer->as_ptracer) + +#endif /* PTRACE_H */ diff --git a/core/proot/src/main/cpp/ptrace/user.c b/core/proot/src/main/cpp/ptrace/user.c new file mode 100644 index 000000000..669dbe62f --- /dev/null +++ b/core/proot/src/main/cpp/ptrace/user.c @@ -0,0 +1,166 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#include +#include +#include +#include +#include +#include + +#include "ptrace/user.h" +#include "cli/note.h" + +#if defined(ARCH_X86_64) + +/** + * Return the index in the "regs" field of a 64-bit "user" area that + * corresponds to the specified @index in the "regs" field of a 32-bit + * "user" area. + */ +static inline size_t convert_user_regs_index(size_t index) +{ + static size_t mapping[USER32_NB_REGS] = { + 05, /* ?bx */ 11, /* ?cx */ 12, /* ?dx */ + 13, /* ?si */ 14, /* ?di */ 04, /* ?bp */ + 10, /* ?ax */ 23, /* ds */ 24, /* es */ + 25, /* fs */ 26, /* gs */ 15, /* orig_?ax */ + 16, /* ?ip */ 17, /* cs */ 18, /* eflags */ + 19, /* ?sp */ 20, /* ss */ }; + + /* Sanity check. */ + assert(index < USER32_NB_REGS); + + return mapping[index]; +} + +/* Layout of a 32-bit "user" area. */ +#define USER32_REGS_OFFSET 0 +#define USER32_REGS_SIZE (USER32_NB_REGS * sizeof(uint32_t)) +#define USER32_FPVALID_OFFSET (USER32_REGS_OFFSET + USER32_REGS_SIZE) +#define USER32_I387_OFFSET (USER32_FPVALID_OFFSET + sizeof(uint32_t)) +#define USER32_I387_SIZE (USER32_NB_FPREGS * sizeof(uint32_t)) +#define USER32_TSIZE_OFFSET (USER32_I387_OFFSET + USER32_I387_SIZE) +#define USER32_DSIZE_OFFSET (USER32_TSIZE_OFFSET + sizeof(uint32_t)) +#define USER32_SSIZE_OFFSET (USER32_DSIZE_OFFSET + sizeof(uint32_t)) +#define USER32_START_CODE_OFFSET (USER32_SSIZE_OFFSET + sizeof(uint32_t)) +#define USER32_START_STACK_OFFSET (USER32_START_CODE_OFFSET + sizeof(uint32_t)) +#define USER32_SIGNAL_OFFSET (USER32_START_STACK_OFFSET + sizeof(uint32_t)) +#define USER32_RESERVED_OFFSET (USER32_SIGNAL_OFFSET + sizeof(uint32_t)) +#define USER32_AR0_OFFSET (USER32_RESERVED_OFFSET + sizeof(uint32_t)) +#define USER32_FPSTATE_OFFSET (USER32_AR0_OFFSET + sizeof(uint32_t)) +#define USER32_MAGIC_OFFSET (USER32_FPSTATE_OFFSET + sizeof(uint32_t)) +#define USER32_COMM_OFFSET (USER32_MAGIC_OFFSET + sizeof(uint32_t)) +#define USER32_COMM_SIZE (32 * sizeof(uint8_t)) +#define USER32_DEBUGREG_OFFSET (USER32_COMM_OFFSET + USER32_COMM_SIZE) +#define USER32_DEBUGREG_SIZE (8 * sizeof(uint32_t)) + +/** + * Return the offset in the "debugreg" field of a 64-bit "user" area + * that corresponds to the specified @offset in the "debugreg" field + * of a 32-bit "user" area. + */ +static inline size_t convert_user_debugreg_offset(size_t offset) +{ + size_t index; + + /* Sanity check. */ + assert(offset >= USER32_DEBUGREG_OFFSET + && offset < USER32_DEBUGREG_OFFSET + USER32_DEBUGREG_SIZE); + + index = (offset - USER32_DEBUGREG_OFFSET) / sizeof(uint32_t); + return offsetof(struct user, u_debugreg) + index * sizeof(uint64_t); +} + +/** + * Return the offset in a 64-bit "user" area that corresponds to the + * specified @offset in a 32-bit "user" area. This function returns + * "(word_t) -1" if the specified @offset is invalid. + */ +word_t convert_user_offset(word_t offset) +{ + const char *area_name = NULL; + + if (/* offset >= 0 && */ offset < USER32_REGS_OFFSET + USER32_REGS_SIZE) { + /* Sanity checks. */ + if ((offset % sizeof(uint32_t)) != 0) + return (word_t) -1; + + return convert_user_regs_index(offset / sizeof(uint32_t)) * sizeof(uint64_t); + } + else if (offset == USER32_FPVALID_OFFSET) + area_name = "fpvalid"; /* Not yet supported. */ + else if (offset >= USER32_I387_OFFSET && offset < USER32_I387_OFFSET + USER32_I387_SIZE) + area_name = "i387"; /* Not yet supported. */ + else if (offset == USER32_TSIZE_OFFSET) + area_name = "tsize"; /* Not yet supported. */ + else if (offset == USER32_DSIZE_OFFSET) + area_name = "dsize"; /* Not yet supported. */ + else if (offset == USER32_SSIZE_OFFSET) + area_name = "ssize"; /* Not yet supported. */ + else if (offset == USER32_START_CODE_OFFSET) + area_name = "start_code"; /* Not yet supported. */ + else if (offset == USER32_START_STACK_OFFSET) + area_name = "start_stack"; /* Not yet supported. */ + else if (offset == USER32_SIGNAL_OFFSET) + area_name = "signal"; /* Not yet supported. */ + else if (offset == USER32_RESERVED_OFFSET) + area_name = "reserved"; /* Not yet supported. */ + else if (offset == USER32_AR0_OFFSET) + area_name = "ar0"; /* Not yet supported. */ + else if (offset == USER32_FPSTATE_OFFSET) + area_name = "fpstate"; /* Not yet supported. */ + else if (offset == USER32_MAGIC_OFFSET) + area_name = "magic"; /* Not yet supported. */ + else if (offset >= USER32_COMM_OFFSET && offset < USER32_COMM_OFFSET + USER32_COMM_SIZE) + area_name = "comm"; /* Not yet supported. */ + else if (offset >= USER32_DEBUGREG_OFFSET && offset < USER32_DEBUGREG_OFFSET + USER32_DEBUGREG_SIZE) + return convert_user_debugreg_offset(offset); + else + area_name = ""; + + note(NULL, WARNING, INTERNAL, "ptrace user area '%s' not supported yet", area_name); + return (word_t) -1; /* Unknown offset. */ +} + +/** + * Convert the "regs" field from a 64-bit "user" area into a "regs" + * field from a 32-bit "user" area, or vice versa according to + * @reverse. + */ +void convert_user_regs_struct(bool reverse, uint64_t *user_regs64, + uint32_t user_regs32[USER32_NB_REGS]) +{ + size_t index32; + + for (index32 = 0; index32 < USER32_NB_REGS; index32++) { + size_t index64 = convert_user_regs_index(index32); + assert(index64 != (size_t) -1); + + if (reverse) + user_regs64[index64] = (uint64_t) user_regs32[index32]; + else + user_regs32[index32] = (uint32_t) user_regs64[index64]; + } +} + +#endif /* ARCH_X86_64 */ diff --git a/core/proot/src/main/cpp/ptrace/user.h b/core/proot/src/main/cpp/ptrace/user.h new file mode 100644 index 000000000..474b87a21 --- /dev/null +++ b/core/proot/src/main/cpp/ptrace/user.h @@ -0,0 +1,56 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#include +#include +#include + +#include "arch.h" +#include "attribute.h" + +#if defined(ARCH_X86_64) + +#define USER32_NB_REGS 17 +#define USER32_NB_FPREGS 27 + +extern word_t convert_user_offset(word_t offset); +extern void convert_user_regs_struct(bool reverse, uint64_t *user_regs64, + uint32_t user_regs32[USER32_NB_REGS]); + +#else + +#define USER32_NB_REGS 0 +#define USER32_NB_FPREGS 0 + +static inline word_t convert_user_offset(word_t offset UNUSED) +{ + assert(0); +} + +static inline void convert_user_regs_struct(bool reverse UNUSED, + uint64_t *user_regs64 UNUSED, + uint32_t user_regs32[USER32_NB_REGS] UNUSED) +{ + assert(0); +} + +#endif diff --git a/core/proot/src/main/cpp/ptrace/wait.c b/core/proot/src/main/cpp/ptrace/wait.c new file mode 100644 index 000000000..af447d198 --- /dev/null +++ b/core/proot/src/main/cpp/ptrace/wait.c @@ -0,0 +1,393 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#include /* PTRACE_*, */ +#include /* E*, */ +#include /* assert(3), */ +#include /* bool, true, false, */ +#include /* SIG*, */ +#include /* talloc*, */ + +#include "ptrace/wait.h" +#include "ptrace/ptrace.h" +#include "syscall/sysnum.h" +#include "syscall/chain.h" +#include "tracee/tracee.h" +#include "tracee/event.h" +#include "tracee/reg.h" +#include "tracee/mem.h" + +#include "attribute.h" + +static const char *stringify_event(int event) UNUSED; +static const char *stringify_event(int event) +{ + if (WIFEXITED(event)) + return "exited"; + else if (WIFSIGNALED(event)) + return "signaled"; + else if (WIFCONTINUED(event)) + return "continued"; + else if (WIFSTOPPED(event)) { + switch ((event & 0xfff00) >> 8) { + case SIGTRAP: + return "stopped: SIGTRAP"; + case SIGTRAP | 0x80: + return "stopped: SIGTRAP: 0x80"; + case SIGTRAP | PTRACE_EVENT_VFORK << 8: + return "stopped: SIGTRAP: PTRACE_EVENT_VFORK"; + case SIGTRAP | PTRACE_EVENT_FORK << 8: + return "stopped: SIGTRAP: PTRACE_EVENT_FORK"; + case SIGTRAP | PTRACE_EVENT_VFORK_DONE << 8: + return "stopped: SIGTRAP: PTRACE_EVENT_VFORK_DONE"; + case SIGTRAP | PTRACE_EVENT_CLONE << 8: + return "stopped: SIGTRAP: PTRACE_EVENT_CLONE"; + case SIGTRAP | PTRACE_EVENT_EXEC << 8: + return "stopped: SIGTRAP: PTRACE_EVENT_EXEC"; + case SIGTRAP | PTRACE_EVENT_EXIT << 8: + return "stopped: SIGTRAP: PTRACE_EVENT_EXIT"; + case SIGTRAP | PTRACE_EVENT_SECCOMP2 << 8: + return "stopped: SIGTRAP: PTRACE_EVENT_SECCOMP2"; + case SIGTRAP | PTRACE_EVENT_SECCOMP << 8: + return "stopped: SIGTRAP: PTRACE_EVENT_SECCOMP"; + case SIGSTOP: + return "stopped: SIGSTOP"; + default: + return "stopped: unknown"; + } + } + return "unknown"; +} + +/** + * Translate the wait syscall made by @ptracer into a "void" syscall + * if the expected pid is one of its ptracees, in order to emulate the + * ptrace mechanism within PRoot. This function returns -errno if an + * error occured (unsupported request), otherwise 0. + */ +int translate_wait_enter(Tracee *ptracer) +{ + Tracee *ptracee; + pid_t pid; + + PTRACER.waits_in = WAITS_IN_KERNEL; + + /* Don't emulate the ptrace mechanism if it's not a ptracer. */ + if (PTRACER.nb_ptracees == 0) + return 0; + + /* Don't emulate the ptrace mechanism if the requested pid is + * not a ptracee. */ + pid = (pid_t) peek_reg(ptracer, ORIGINAL, SYSARG_1); + if (pid != -1) { + ptracee = get_tracee(ptracer, pid, false); + if (ptracee == NULL || PTRACEE.ptracer != ptracer) + return 0; + } + + /* This syscall is canceled at the enter stage in order to be + * handled at the exit stage. */ + set_sysnum(ptracer, PR_void); + PTRACER.waits_in = WAITS_IN_PROOT; + + return 0; +} + +/** + * Update pid & wait status of @ptracer's wait(2) for the given + * @ptracee. This function returns -errno if an error occurred, 0 if + * the wait syscall will be restarted (ie. the event is discarded), + * otherwise @ptracee's pid. + */ +static int update_wait_status(Tracee *ptracer, Tracee *ptracee) +{ + word_t address; + int result; + + /* Special case: the Linux kernel reports the terminating + * event issued by a process to both its parent and its + * tracer, except when they are the same. In this case the + * Linux kernel reports the terminating event only once to the + * tracing parent ... */ + if (PTRACEE.ptracer == ptracee->parent + && (WIFEXITED(PTRACEE.event4.ptracer.value) + || WIFSIGNALED(PTRACEE.event4.ptracer.value))) { + /* ... So hide this terminating event (toward its + * tracer, ie. PRoot) and make the second one appear + * (towards its parent, ie. the ptracer). This will + * ensure its exit status is collected from a kernel + * point-of-view (ie. it doesn't stay a zombie + * forever). */ + restart_original_syscall(ptracer); + + /* Detach this ptracee from its ptracer, PRoot doesn't + * have anything else to emulate. */ + detach_from_ptracer(ptracee); + + /* Zombies can rest in peace once the ptracer is + * notified. */ + if (PTRACEE.is_zombie) + TALLOC_FREE(ptracee); + + return 0; + } + + address = peek_reg(ptracer, ORIGINAL, SYSARG_2); + if (address != 0) { + poke_int32(ptracer, address, PTRACEE.event4.ptracer.value); + if (errno != 0) + return -errno; + } + + PTRACEE.event4.ptracer.pending = false; + + /* Be careful; ptracee might get freed before its pid is + * returned. */ + result = ptracee->pid; + + /* Zombies can rest in peace once the ptracer is notified. */ + if (PTRACEE.is_zombie) { + detach_from_ptracer(ptracee); + TALLOC_FREE(ptracee); + } + + return result; +} + +/** + * Emulate the wait* syscall made by @ptracer if it was in the context + * of the ptrace mechanism. This function returns -errno if an error + * occured, otherwise the pid of the expected tracee. + */ +int translate_wait_exit(Tracee *ptracer) +{ + Tracee *ptracee; + word_t options; + int status; + pid_t pid; + + assert(PTRACER.waits_in == WAITS_IN_PROOT); + PTRACER.waits_in = DOESNT_WAIT; + + pid = (pid_t) peek_reg(ptracer, ORIGINAL, SYSARG_1); + options = peek_reg(ptracer, ORIGINAL, SYSARG_3); + + /* Is there such a stopped ptracee with an event not yet + * passed to its ptracer? */ + ptracee = get_stopped_ptracee(ptracer, pid, true, options); + if (ptracee == NULL) { + /* Is there still living ptracees? */ + if (PTRACER.nb_ptracees == 0) + return -ECHILD; + + /* Non blocking wait(2) ? */ + if ((options & WNOHANG) != 0) { + /* if WNOHANG was specified and one or more + * child(ren) specified by pid exist, but have + * not yet changed state, then 0 is returned. + * On error, -1 is returned. + * + * -- man 2 waitpid */ + return (has_ptracees(ptracer, pid, options) ? 0 : -ECHILD); + } + + /* Otherwise put this ptracer in the "waiting for + * ptracee" state, it will be woken up in + * handle_ptracee_event() later. */ + PTRACER.wait_pid = pid; + PTRACER.wait_options = options; + + return 0; + } + + status = update_wait_status(ptracer, ptracee); + if (status < 0) + return status; + + return status; +} + +/** + * For the given @ptracee, pass its current @event to its ptracer if + * this latter is waiting for it, otherwise put the @ptracee in the + * "waiting for ptracer" state. This function returns whether + * @ptracee shall be kept in the stop state or not. + */ +bool handle_ptracee_event(Tracee *ptracee, int event) +{ + bool handled_by_proot_first = false; + bool handled_by_proot_first_may_suppress = false; + Tracee *ptracer = PTRACEE.ptracer; + bool keep_stopped; + + assert(ptracer != NULL); + + /* Remember what the event initially was, this will be + * required by PRoot to handle this event later. */ + PTRACEE.event4.proot.value = event; + PTRACEE.event4.proot.pending = true; + + /* By default, this ptracee should be kept stopped until its + * ptracer restarts it. */ + keep_stopped = true; + + /* Not all events are expected for this ptracee. */ + if (WIFSTOPPED(event)) { + switch ((event & 0xfff00) >> 8) { + case SIGTRAP | 0x80: + if (PTRACEE.ignore_syscalls || PTRACEE.ignore_loader_syscalls) + return false; + + if ((PTRACEE.options & PTRACE_O_TRACESYSGOOD) == 0) + event &= ~(0x80 << 8); + + handled_by_proot_first = IS_IN_SYSEXIT(ptracee); + break; + +#define PTRACE_EVENT_VFORKDONE PTRACE_EVENT_VFORK_DONE +#define CASE_FILTER_EVENT(name) \ + case SIGTRAP | PTRACE_EVENT_ ##name << 8: \ + if ((PTRACEE.options & PTRACE_O_TRACE ##name) == 0) \ + return false; \ + PTRACEE.tracing_started = true; \ + handled_by_proot_first = true; \ + break; + + CASE_FILTER_EVENT(FORK); + CASE_FILTER_EVENT(VFORK); + CASE_FILTER_EVENT(VFORKDONE); + CASE_FILTER_EVENT(CLONE); + CASE_FILTER_EVENT(EXIT); + CASE_FILTER_EVENT(EXEC); + + /* Never reached. */ + assert(0); + + case SIGTRAP | PTRACE_EVENT_SECCOMP2 << 8: + case SIGTRAP | PTRACE_EVENT_SECCOMP << 8: + /* These events are not supported [yet?] under + * ptrace emulation. */ + return false; + + case SIGSYS: + handled_by_proot_first = true; + handled_by_proot_first_may_suppress = true; + break; + + default: + PTRACEE.tracing_started = true; + break; + } + } + /* In these cases, the ptracee isn't really alive anymore. To + * ensure it will not be in limbo, PRoot restarts it whether + * its ptracer is waiting for it or not. */ + else if (WIFEXITED(event) || WIFSIGNALED(event)) { + PTRACEE.tracing_started = true; + keep_stopped = false; + } + + /* A process is not traced right from the TRACEME request; it + * is traced from the first received signal, whether it was + * raised by the process itself (implicitly or explicitly), or + * it was induced by a PTRACE_EVENT_*. */ + if (!PTRACEE.tracing_started) + return false; + + /* Under some circumstances, the event must be handled by + * PRoot first. */ + if (handled_by_proot_first) { + int signal; + signal = handle_tracee_event(ptracee, PTRACEE.event4.proot.value); + PTRACEE.event4.proot.value = signal; + + if (handled_by_proot_first_may_suppress) { + /* If we've decided to suppress signal + * (e.g. because seccomp policy blocked syscall + * but we emulate that syscall), + * don't notify ptracer and let ptracee resume. */ + if (signal == 0) { + if (seccomp_event_happens_after_enter_sigtrap()) { + if (PTRACEE.ignore_syscalls) + { + restart_tracee(ptracee, 0); + return true; + } + /* However if we've already notified ptracer about syscall entry + * before knowing it'll be blocked, notify ptracer about exit. */ + PTRACEE.event4.proot.value = 0; + if (PTRACEE.options & PTRACE_O_TRACESYSGOOD) { + event = (SIGTRAP | 0x80) << 8 | 0x7f; + } else { + event = SIGTRAP << 8 | 0x7f; + } + } else { + restart_tracee(ptracee, 0); + return true; + } + } + } else { + /* The computed signal is always 0 since we can come + * in this block only on sysexit and special events + * (as for now). */ + assert(signal == 0); + } + } + + /* Remember what the new event is, this will be required by + the ptracer in translate_ptrace_exit() in order to restart + this ptracee. */ + PTRACEE.event4.ptracer.value = event; + PTRACEE.event4.ptracer.pending = true; + + /* Notify asynchronously the ptracer about this event, as the + * kernel does. */ + kill(ptracer->pid, SIGCHLD); + + /* Note: wait_pid is set in translate_wait_exit() if no + * ptracee event was pending when the ptracer started to + * wait. */ + if ( (PTRACER.wait_pid == -1 || PTRACER.wait_pid == ptracee->pid) + && EXPECTED_WAIT_CLONE(PTRACER.wait_options, ptracee)) { + bool restarted; + int status; + + status = update_wait_status(ptracer, ptracee); + if (status == 0) + chain_next_syscall(ptracer); + else + poke_reg(ptracer, SYSARG_RESULT, (word_t) status); + + /* Write ptracer's register cache back. */ + (void) push_regs(ptracer); + + /* Restart the ptracer. */ + PTRACER.wait_pid = 0; + restarted = restart_tracee(ptracer, 0); + if (!restarted) + keep_stopped = false; + + return keep_stopped; + } + + return keep_stopped; +} diff --git a/core/proot/src/main/cpp/ptrace/wait.h b/core/proot/src/main/cpp/ptrace/wait.h new file mode 100644 index 000000000..8648f6f0d --- /dev/null +++ b/core/proot/src/main/cpp/ptrace/wait.h @@ -0,0 +1,49 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#ifndef PTRACE_WAIT_H +#define PTRACE_WAIT_H + +#include /* for __WALL */ + +#include "tracee/tracee.h" + +extern int translate_wait_enter(Tracee *ptracer); +extern int translate_wait_exit(Tracee *ptracer); +extern bool handle_ptracee_event(Tracee *ptracee, int wait_status); + +/* __WCLONE: Wait for "clone" children only. If omitted then wait for + * "non-clone" children only. (A "clone" child is one which delivers + * no signal, or a signal other than SIGCHLD to its parent upon + * termination.) This option is ignored if __WALL is also specified. + * + * __WALL: Wait for all children, regardless of type ("clone" or + * "non-clone"). + * + * -- wait(2) man-page + */ +#define EXPECTED_WAIT_CLONE(wait_options, tracee) \ + ((((wait_options) & __WALL) != 0) \ + || ((((wait_options) & __WCLONE) != 0) && (tracee)->clone) \ + || ((((wait_options) & __WCLONE) == 0) && !(tracee)->clone)) + +#endif /* PTRACE_WAIT_H */ diff --git a/core/proot/src/main/cpp/syscall/chain.c b/core/proot/src/main/cpp/syscall/chain.c new file mode 100644 index 000000000..9b7569c23 --- /dev/null +++ b/core/proot/src/main/cpp/syscall/chain.c @@ -0,0 +1,195 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#include /* talloc*, */ +#include /* STAILQ_*, */ +#include /* E*, */ +#include /* assert(3), */ + +#include "cli/note.h" +#include "syscall/chain.h" +#include "syscall/sysnum.h" +#include "tracee/tracee.h" +#include "tracee/reg.h" +#include "arch.h" + +struct chained_syscall { + Sysnum sysnum; + word_t sysargs[6]; + STAILQ_ENTRY(chained_syscall) link; +}; + +STAILQ_HEAD(chained_syscalls, chained_syscall); + +static int register_chained_syscall_internal(Tracee *tracee, Sysnum sysnum, + word_t sysarg_1, word_t sysarg_2, word_t sysarg_3, + word_t sysarg_4, word_t sysarg_5, word_t sysarg_6, + bool at_front) +{ + struct chained_syscall *syscall; + + if (tracee->chain.syscalls == NULL) { + tracee->chain.syscalls = talloc_zero(tracee, struct chained_syscalls); + if (tracee->chain.syscalls == NULL) + return -ENOMEM; + + STAILQ_INIT(tracee->chain.syscalls); + } + + syscall = talloc_zero(tracee->chain.syscalls, struct chained_syscall); + if (syscall == NULL) + return -ENOMEM; + + syscall->sysnum = sysnum; + syscall->sysargs[0] = sysarg_1; + syscall->sysargs[1] = sysarg_2; + syscall->sysargs[2] = sysarg_3; + syscall->sysargs[3] = sysarg_4; + syscall->sysargs[4] = sysarg_5; + syscall->sysargs[5] = sysarg_6; + + if (at_front) { + STAILQ_INSERT_HEAD(tracee->chain.syscalls, syscall, link); + } else { + STAILQ_INSERT_TAIL(tracee->chain.syscalls, syscall, link); + } + + return 0; +} + +/** + * Append a new syscall (@sysnum, @sysarg_*) to the list of + * "unrequested" syscalls for the given @tracee. These new syscalls + * will be triggered in order once the current syscall is done. The + * caller is free to force the last result of this syscall chain in + * @tracee->chain.final_result. This function returns -errno if an + * error occurred, otherwise 0. + */ +int register_chained_syscall(Tracee *tracee, Sysnum sysnum, + word_t sysarg_1, word_t sysarg_2, word_t sysarg_3, + word_t sysarg_4, word_t sysarg_5, word_t sysarg_6) { + return register_chained_syscall_internal( + tracee, sysnum, + sysarg_1, sysarg_2, sysarg_3, + sysarg_4, sysarg_5, sysarg_6, + false + ); +} + +/** + * Use/remove the first element of @tracee->chain.syscalls to forge a + * new syscall. This function should be called only at the end of in + * the sysexit stage. + */ +void chain_next_syscall(Tracee *tracee) +{ + struct chained_syscall *syscall; + word_t instr_pointer; + word_t sysnum; + + assert(tracee->chain.syscalls != NULL); + + /* No more chained syscalls: force the result of the initial + * syscall (the one explicitly requested by the tracee). */ + if (STAILQ_EMPTY(tracee->chain.syscalls)) { + TALLOC_FREE(tracee->chain.syscalls); + + if (tracee->chain.force_final_result) + poke_reg(tracee, SYSARG_RESULT, tracee->chain.final_result); + + tracee->chain.force_final_result = false; + tracee->chain.final_result = 0; + + VERBOSE(tracee, 2, "chain_next_syscall finish"); + + return; + } + + VERBOSE(tracee, 2, "chain_next_syscall continue"); + + /* Original register values will be restored right after the + * last chained syscall. */ + tracee->restore_original_regs = false; + + /* The list of chained syscalls is a FIFO. */ + syscall = STAILQ_FIRST(tracee->chain.syscalls); + STAILQ_REMOVE_HEAD(tracee->chain.syscalls, link); + + poke_reg(tracee, SYSARG_1, syscall->sysargs[0]); + poke_reg(tracee, SYSARG_2, syscall->sysargs[1]); + poke_reg(tracee, SYSARG_3, syscall->sysargs[2]); + poke_reg(tracee, SYSARG_4, syscall->sysargs[3]); + poke_reg(tracee, SYSARG_5, syscall->sysargs[4]); + poke_reg(tracee, SYSARG_6, syscall->sysargs[5]); + + sysnum = detranslate_sysnum(get_abi(tracee), syscall->sysnum); + poke_reg(tracee, SYSTRAP_NUM, sysnum); + + /* Move the instruction pointer back to the original trap. */ + instr_pointer = peek_reg(tracee, CURRENT, INSTR_POINTER); + poke_reg(tracee, INSTR_POINTER, instr_pointer - get_systrap_size(tracee)); + + /* Break after exit from syscall, there may be another one in chain */ + tracee->restart_how = PTRACE_SYSCALL; +} + +/** + * Force the last result of the @tracee's current syscall chain to be + * @forced_result. + */ +void force_chain_final_result(Tracee *tracee, word_t forced_result) +{ + tracee->chain.force_final_result = true; + tracee->chain.final_result = forced_result; +} + +/** + * Restart the original syscall of the given @tracee. The result of + * the current syscall will be overwritten. This function returns the + * same status as register_chained_syscall(). + */ +int restart_original_syscall(Tracee *tracee) +{ + return register_chained_syscall(tracee, + get_sysnum(tracee, ORIGINAL), + peek_reg(tracee, ORIGINAL, SYSARG_1), + peek_reg(tracee, ORIGINAL, SYSARG_2), + peek_reg(tracee, ORIGINAL, SYSARG_3), + peek_reg(tracee, ORIGINAL, SYSARG_4), + peek_reg(tracee, ORIGINAL, SYSARG_5), + peek_reg(tracee, ORIGINAL, SYSARG_6)); +} + +int restart_current_syscall_as_chained(Tracee *tracee) +{ + assert(tracee->chain.sysnum_workaround_state == SYSNUM_WORKAROUND_INACTIVE); + tracee->chain.sysnum_workaround_state = SYSNUM_WORKAROUND_PROCESS_FAULTY_CALL; + return register_chained_syscall_internal(tracee, + get_sysnum(tracee, CURRENT), + peek_reg(tracee, CURRENT, SYSARG_1), + peek_reg(tracee, CURRENT, SYSARG_2), + peek_reg(tracee, CURRENT, SYSARG_3), + peek_reg(tracee, CURRENT, SYSARG_4), + peek_reg(tracee, CURRENT, SYSARG_5), + peek_reg(tracee, CURRENT, SYSARG_6), + true); +} diff --git a/core/proot/src/main/cpp/syscall/chain.h b/core/proot/src/main/cpp/syscall/chain.h new file mode 100644 index 000000000..b1cf92de8 --- /dev/null +++ b/core/proot/src/main/cpp/syscall/chain.h @@ -0,0 +1,43 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#ifndef CHAIN_H +#define CHAIN_H + +#include "tracee/tracee.h" +#include "syscall/sysnum.h" +#include "arch.h" + +extern int register_chained_syscall(Tracee *tracee, Sysnum sysnum, + word_t sysarg_1, word_t sysarg_2, word_t sysarg_3, + word_t sysarg_4, word_t sysarg_5, word_t sysarg_6); + +extern void force_chain_final_result(Tracee *tracee, word_t forced_result); + +extern int restart_original_syscall(Tracee *tracee); + +extern void chain_next_syscall(Tracee *tracee); + +extern int restart_current_syscall_as_chained(Tracee *tracee); + + +#endif /* CHAIN_H */ diff --git a/core/proot/src/main/cpp/syscall/enter.c b/core/proot/src/main/cpp/syscall/enter.c new file mode 100644 index 000000000..dbec2d434 --- /dev/null +++ b/core/proot/src/main/cpp/syscall/enter.c @@ -0,0 +1,2377 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#include /* errno(3), E* */ +#include /* talloc_*, */ +#include /* struct sockaddr_un, */ +#include /* SYS_*, */ +#include /* AT_FDCWD, */ +#include /* close(2), */ +#include /* PATH_MAX, */ +#include /* strcpy */ +#include /* bool */ +#include /* uint32_t */ +#include /* PR_SET_DUMPABLE */ +#include /* MS_BIND, MS_REMOUNT, ... */ +#include /* AF_NETLINK, AF_UNIX, SOCK_DGRAM, SOCK_CLOEXEC */ +#include /* CLONE_NEW*, */ +#include /* TCSETS, TCSANOW */ +#include /* struct nlmsghdr, NLMSG_ERROR, struct nlmsgerr */ +#include /* RTM_*, struct ifinfomsg, struct rtattr, RTA_* */ +#include /* struct ifaddrmsg, IFA_*, IFA_F_PERMANENT */ +#include /* SIOCGIFINDEX */ +#include /* struct ifreq, IFNAMSIZ, IFF_* */ +#include /* getifaddrs(3), to enumerate host interfaces */ +#include /* struct sockaddr_in / sockaddr_in6 */ +#include /* struct sockaddr_ll (AF_PACKET) */ +#include /* ioctl(2): SIOCGIFMTU / SIOCGIFHWADDR */ +#include /* struct timeval, for SO_RCVTIMEO */ + +/* ABI-stable rtnetlink constants we synthesise for the loopback reply. + * Defined locally so we needn't pull in / , + * which clash with the already-included . */ +#ifndef ARPHRD_LOOPBACK +#define ARPHRD_LOOPBACK 772 +#endif +#ifndef ARPHRD_ETHER +#define ARPHRD_ETHER 1 +#endif +#ifndef IFF_LOWER_UP +#define IFF_LOWER_UP 0x10000 +#endif + +#include "cli/note.h" +#include "syscall/syscall.h" +#include "syscall/sysnum.h" +#include "syscall/socket.h" +#include "ptrace/ptrace.h" +#include "ptrace/wait.h" +#include "syscall/heap.h" +#include "extension/extension.h" +#include "execve/execve.h" +#include "tracee/tracee.h" +#include "tracee/reg.h" +#include "tracee/mem.h" +#include "tracee/abi.h" +#include "tracee/event.h" +#include "path/path.h" +#include "path/canon.h" +#include "path/binding.h" +#include "path/temp.h" +#include "arch.h" + +/* Older kernel headers may lack these. */ +#ifndef CLONE_NEWTIME +#define CLONE_NEWTIME 0x00000080 +#endif +#ifndef CLONE_NEWCGROUP +#define CLONE_NEWCGROUP 0x02000000 +#endif + +#define CLONE_NS_MASK (CLONE_NEWNS | CLONE_NEWUTS | CLONE_NEWIPC | \ + CLONE_NEWUSER | CLONE_NEWPID | CLONE_NEWNET | \ + CLONE_NEWCGROUP | CLONE_NEWTIME) + +/** + * Translate @path and put the result in the @tracee's memory address + * space pointed to by the @reg argument of the current syscall. See + * the documentation of translate_path() about the meaning of + * @type. This function returns -errno if an error occured, otherwise + * 0. + */ +static int translate_path2(Tracee *tracee, int dir_fd, char path[PATH_MAX], Reg reg, Type type) +{ + char new_path[PATH_MAX]; + int status; + + /* Special case where the argument was NULL. */ + if (path[0] == '\0') + return 0; + + /* Translate the original path. */ + status = translate_path(tracee, new_path, dir_fd, path, type != SYMLINK); + if (status < 0) + return status; + + return set_sysarg_path(tracee, new_path, reg); +} + +/** + * A helper, see the comment of the function above. + */ +static int translate_sysarg(Tracee *tracee, Reg reg, Type type) +{ + char old_path[PATH_MAX]; + int status; + + /* Extract the original path. */ + status = get_sysarg_path(tracee, old_path, reg); + if (status < 0) + return status; + + return translate_path2(tracee, AT_FDCWD, old_path, reg, type); +} + +/** + * Canonicalize @user_path as a guest path, relative to the @tracee's + * cwd when @user_path is relative. Stores the result in @guest_path + * with any trailing "/" or "/." stripped, so it can be used as a + * binding key. Returns 0 on success, -errno otherwise. + */ +static int guest_canonicalize(Tracee *tracee, const char *user_path, + char guest_path[PATH_MAX]) +{ + int status; + + if (user_path[0] == '/') + strcpy(guest_path, "/"); + else { + status = getcwd2(tracee, guest_path); + if (status < 0) + return status; + } + + status = canonicalize(tracee, user_path, true, guest_path, 0); + if (status < 0) + return status; + + chop_finality(guest_path); + return 0; +} + +/** + * Emulate mount(@src_user, @target_user, @fstype, @flags) by adding a + * PRoot binding from a host directory to the canonicalized target. + * Bind mounts use the translated source; "proc"/"sysfs" use the + * matching host file-system; "tmpfs"/"devpts"/"devtmpfs" get a fresh + * empty directory. Any other case is silently ignored: the caller + * will still see the syscall succeed (we always void it). + */ +static void emulate_mount(Tracee *tracee, const char *src_user, + const char *target_user, const char *fstype, + unsigned long flags) +{ + char host_path[PATH_MAX]; + char guest_path[PATH_MAX]; + const char *tmpdir; + + if ((flags & MS_REMOUNT) != 0) + return; + + if ((flags & MS_BIND) != 0) { + if (translate_path(tracee, host_path, AT_FDCWD, src_user, true) < 0) + return; + } + else if (strcmp(fstype, "proc") == 0) + strcpy(host_path, "/proc"); + else if (strcmp(fstype, "sysfs") == 0) + strcpy(host_path, "/sys"); + else if (strcmp(fstype, "devtmpfs") == 0) + strcpy(host_path, "/dev"); + else if (strcmp(fstype, "devpts") == 0) + strcpy(host_path, "/dev/pts"); + else if (strcmp(fstype, "tmpfs") == 0) { + tmpdir = create_temp_directory(tracee->fs, "proot-tmpfs-"); + if (tmpdir == NULL) + return; + strncpy(host_path, tmpdir, PATH_MAX - 1); + host_path[PATH_MAX - 1] = '\0'; + } + else + return; + + chop_finality(host_path); + + if (guest_canonicalize(tracee, target_user, guest_path) < 0) + return; + + (void) insort_binding3(tracee, tracee->fs, host_path, guest_path); +} + +/** + * Emulate pivot_root(@new_root_user, @put_old_user) by changing the + * tracee's root binding to point at @new_root_user (translated to + * host) and re-exposing the previous root at @put_old_user, so that + * sandbox helpers like bubblewrap can keep accessing the prior + * file-system through the agreed "oldroot" path. + */ +static void emulate_pivot_root(Tracee *tracee, const char *new_root_user, + const char *put_old_user) +{ + char new_root_host[PATH_MAX]; + char new_root_guest[PATH_MAX]; + char put_old_guest[PATH_MAX]; + char old_root_host[PATH_MAX]; + Binding *root_binding; + Binding **snapshot; + size_t new_root_len; + size_t put_old_len = 0; + char put_old_after[PATH_MAX]; + bool have_put_old = false; + size_t count = 0; + size_t i; + Binding *iter; + + if (translate_path(tracee, new_root_host, AT_FDCWD, new_root_user, true) < 0) + return; + chop_finality(new_root_host); + + if (guest_canonicalize(tracee, new_root_user, new_root_guest) < 0) + return; + + /* put_old is relative to new_root, so resolve it against + * new_root_guest rather than the current cwd. */ + if (put_old_user[0] == '/') + strcpy(put_old_guest, "/"); + else + strcpy(put_old_guest, new_root_guest); + if (canonicalize(tracee, put_old_user, true, put_old_guest, 0) < 0) + return; + + root_binding = get_binding(tracee, GUEST, "/"); + if (root_binding == NULL) + return; + strncpy(old_root_host, root_binding->host.path, PATH_MAX - 1); + old_root_host[PATH_MAX - 1] = '\0'; + + new_root_len = strlen(new_root_guest); + + /* Work out where the previous root becomes reachable: put_old as a + * path under the *new* root, e.g. "/oldroot". The pivot_root(".", + * ".") trick used to detach the old root leaves new_root == put_old, + * in which case there is nowhere to expose it. */ + if ( new_root_len > 0 + && strncmp(put_old_guest, new_root_guest, new_root_len) == 0 + && ( put_old_guest[new_root_len] == '/' + || (new_root_len == 1 && new_root_guest[0] == '/'))) { + const char *after = put_old_guest + (new_root_len == 1 ? 0 : new_root_len); + if (after[0] == '/' && after[1] != '\0') { + strncpy(put_old_after, after, PATH_MAX - 1); + put_old_after[PATH_MAX - 1] = '\0'; + put_old_len = strlen(put_old_after); + have_put_old = true; + } + } + + /* Snapshot the current bindings before mutating the lists: the loop + * below both inserts (rebased / re-exposed) and removes bindings, so + * walking the live list would be unsafe. */ + for (iter = CIRCLEQ_FIRST(tracee->fs->bindings.guest); + iter != (void *) tracee->fs->bindings.guest; + iter = CIRCLEQ_NEXT(iter, link.guest)) + count++; + + snapshot = talloc_array(tracee->ctx, Binding *, count); + if (snapshot == NULL) + return; + i = 0; + for (iter = CIRCLEQ_FIRST(tracee->fs->bindings.guest); + iter != (void *) tracee->fs->bindings.guest && i < count; + iter = CIRCLEQ_NEXT(iter, link.guest)) + snapshot[i++] = iter; + + /* Switch the root over to new_root and expose the previous root at + * put_old, so the tracee can still reach it (bubblewrap accesses + * everything via "/oldroot" right after the pivot). */ + remove_binding_from_all_lists(tracee, root_binding); + (void) insort_binding3(tracee, tracee->fs, new_root_host, "/"); + if (have_put_old) + (void) insort_binding3(tracee, tracee->fs, old_root_host, put_old_after); + + for (i = 0; i < count; i++) { + Binding *b = snapshot[i]; + size_t blen; + + if (b == root_binding || strcmp(b->guest.path, "/") == 0) + continue; + + blen = strlen(b->guest.path); + + /* Bindings that live under the new root move *with* the pivot: + * "/newroot/usr" becomes "/usr". Without this, the tracee's + * own bind mounts (e.g. bubblewrap's "--ro-bind /usr /usr" + * followed by pivot_root into that new root) stay at their + * pre-pivot guest path, so "/usr" resolves to the empty + * new-root mountpoint and exec'ing a binary under it fails + * with ENOENT. */ + if ( new_root_len > 0 + && blen > new_root_len + && strncmp(b->guest.path, new_root_guest, new_root_len) == 0 + && b->guest.path[new_root_len] == '/') { + (void) insort_binding3(tracee, tracee->fs, b->host.path, + b->guest.path + new_root_len); + /* Drop the stale pre-pivot binding so it can't shadow + * the rebased one (e.g. host->guest detranslation). */ + remove_binding_from_all_lists(tracee, b); + continue; + } + + /* Everything else belonged to the previous root tree; + * re-expose it under put_old (skipping what already sits + * there) and keep the original in place. */ + if (have_put_old) { + char aliased[PATH_MAX]; + + if ( strncmp(b->guest.path, put_old_after, put_old_len) == 0 + && ( b->guest.path[put_old_len] == '\0' + || b->guest.path[put_old_len] == '/')) + continue; + + if ((size_t) snprintf(aliased, sizeof(aliased), "%s%s", + put_old_after, b->guest.path) + >= sizeof(aliased)) + continue; + + (void) insort_binding3(tracee, tracee->fs, + b->host.path, aliased); + } + } + + talloc_free(snapshot); +} + +/** + * Emulate umount(@target_user) by removing the matching binding (if + * any) so that a subsequent access to @target_user no longer goes + * through the now-unmounted location. This is the inverse of + * emulate_mount(). Bindings put in place at PRoot startup + * (recommended -R bindings, the rootfs itself) are NOT removed: we + * only drop runtime bindings whose guest path exactly matches. + */ +static void emulate_umount(Tracee *tracee, const char *target_user) +{ + char guest_path[PATH_MAX]; + Binding *binding; + + if (guest_canonicalize(tracee, target_user, guest_path) < 0) + return; + + /* Never drop the root binding. */ + if (strcmp(guest_path, "/") == 0) + return; + + binding = get_binding(tracee, GUEST, guest_path); + if (binding == NULL) + return; + + /* Only drop the binding if its guest path is exactly the + * unmount target; otherwise we'd unbind something the tracee + * didn't ask to unmount (e.g. its containing rootfs). */ + if (strcmp(binding->guest.path, guest_path) != 0) + return; + + remove_binding_from_all_lists(tracee, binding); +} + +/** + * Read umount(2)/umount2(2) arguments from the @tracee's registers + * and apply emulate_umount(). + */ +void apply_emulated_umount(Tracee *tracee) +{ + char target_user[PATH_MAX]; + + if (get_sysarg_path(tracee, target_user, SYSARG_1) < 0) + return; + + emulate_umount(tracee, target_user); +} + +/** + * Read mount(2) arguments from the @tracee's registers and apply + * emulate_mount(). Safe to call from both the normal sysenter path + * and the SIGSYS handler (Android's parent seccomp filter traps + * mount, so the syscall never reaches our regular case). + */ +void apply_emulated_mount(Tracee *tracee) +{ + char src_user[PATH_MAX]; + char target_user[PATH_MAX]; + char fstype[256]; + word_t fstype_addr; + unsigned long flags; + + fstype[0] = '\0'; + /* read_string doesn't guarantee a trailing NUL when it hits the + * size limit before finding one in the tracee's memory; pin the + * last byte so the downstream strcmp can't read past the buffer. */ + fstype[sizeof(fstype) - 1] = '\0'; + + if (get_sysarg_path(tracee, src_user, SYSARG_1) < 0) + return; + if (get_sysarg_path(tracee, target_user, SYSARG_2) < 0) + return; + + fstype_addr = peek_reg(tracee, CURRENT, SYSARG_3); + if (fstype_addr != 0) + (void) read_string(tracee, fstype, fstype_addr, sizeof(fstype) - 1); + flags = peek_reg(tracee, CURRENT, SYSARG_4); + + emulate_mount(tracee, src_user, target_user, fstype, flags); +} + +/** + * Read pivot_root(2) arguments from the @tracee's registers and apply + * emulate_pivot_root(). See apply_emulated_mount() for context. + */ +void apply_emulated_pivot_root(Tracee *tracee) +{ + char new_root_user[PATH_MAX]; + char put_old_user[PATH_MAX]; + + if (get_sysarg_path(tracee, new_root_user, SYSARG_1) < 0) + return; + if (get_sysarg_path(tracee, put_old_user, SYSARG_2) < 0) + return; + + emulate_pivot_root(tracee, new_root_user, put_old_user); +} + +/** + * Helpers for emulating AF_NETLINK / NETLINK_ROUTE traffic. Some + * environments deny the tracee a real netlink socket (Android's + * SELinux policy on untrusted_app domains, seccomp filters inherited + * from a Termux-like launcher, hardened containers, ...); in that + * case we silently substitute an AF_UNIX/SOCK_DGRAM socket and + * intercept the netlink-shaped syscalls a tracee might issue on it + * (bind, sendto / sendmsg, recvfrom / recvmsg, getsockname, + * getpeername), synthesising responses that match what the request + * asked for: NLMSG_ERROR(err=0) for non-dump requests (bubblewrap's + * loopback_setup RTM_NEWADDR / RTM_NEWLINK) and an empty NLMSG_DONE + * for NLM_F_DUMP queries (apt / glibc getifaddrs RTM_GETADDR, + * iproute2 RTM_GETLINK), so callers see a well-formed empty result + * instead of a zero-byte recvmsg they treat as fatal. + * + * The substitution only happens when the host kernel actually + * refuses AF_NETLINK; otherwise the tracee gets a real netlink + * socket and ordinary users like c-ares (dnf, getaddrinfo, ...) + * keep working. + */ + +static bool host_blocks_af_netlink(const Tracee *tracee) +{ + enum { PROBE_UNKNOWN, PROBE_ALLOWED, PROBE_BLOCKED }; + static int cached = PROBE_UNKNOWN; + struct sockaddr_nl snl; + const char *blocked_op; + int fd; + int saved_errno; + + if (cached != PROBE_UNKNOWN) + return cached == PROBE_BLOCKED; + + /* Mirror what bubblewrap's loopback_setup() does: socket() then + * bind() with nl_groups == 0. Some hosts permit socket creation + * but reject bind() under separate SELinux/AppArmor/seccomp + * checks, so probing socket() alone would wrongly classify them + * as "AF_NETLINK works" and leave the tracee to fail later. */ + fd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE); + if (fd < 0) { + saved_errno = errno; + blocked_op = "socket"; + goto blocked; + } + + memset(&snl, 0, sizeof(snl)); + snl.nl_family = AF_NETLINK; + if (bind(fd, (struct sockaddr *) &snl, sizeof(snl)) < 0) { + saved_errno = errno; + close(fd); + blocked_op = "bind"; + goto blocked; + } + + close(fd); + cached = PROBE_ALLOWED; + return false; + +blocked: + cached = PROBE_BLOCKED; + VERBOSE(tracee, 1, "AF_NETLINK %s denied by host (%s); enabling " + "AF_UNIX fallback for sandbox helpers", + blocked_op, strerror(saved_errno)); + return true; +} + +static bool is_fake_netlink_fd(const Tracee *tracee, int fd) +{ + int i; + if (fd < 0) + return false; + for (i = 0; i < tracee->fake_netlink_fds_count; i++) + if (tracee->fake_netlink_fds[i] == fd) + return true; + return false; +} + +static void unmark_fake_netlink_fd(Tracee *tracee, int fd) +{ + int i; + for (i = 0; i < tracee->fake_netlink_fds_count; i++) { + if (tracee->fake_netlink_fds[i] == fd) { + tracee->fake_netlink_fds[i] = + tracee->fake_netlink_fds[--tracee->fake_netlink_fds_count]; + return; + } + } +} + +/** + * Append one rtattr (@type, @data/@dlen) to the netlink message being + * built at @off in @buf and return the new offset. Silently drops the + * attribute (returning @off unchanged) if it would overflow @max. + */ +static size_t nl_add_attr(uint8_t *buf, size_t off, size_t max, + uint16_t type, const void *data, uint16_t dlen) +{ + struct rtattr *rta; + size_t space = RTA_SPACE(dlen); + + if (off + space > max) + return off; + + rta = (struct rtattr *) (buf + off); + rta->rta_len = RTA_LENGTH(dlen); + rta->rta_type = type; + if (dlen > 0) + memcpy((char *) rta + RTA_LENGTH(0), data, dlen); + if (space > RTA_LENGTH(dlen)) + memset(buf + off + RTA_LENGTH(dlen), 0, space - RTA_LENGTH(dlen)); + return off + space; +} + +/** + * Append an RTM_NEWLINK message describing one interface (@ifindex, + * @iftype = ARPHRD_*, @ifflags, @mtu, @name, @hwaddr/@hwlen), so that + * iproute2 / glibc see a sane, well-formed link. + */ +static size_t nl_build_link(uint8_t *buf, size_t off, size_t max, + uint32_t seq, uint32_t pid, uint16_t nlflags, + int ifindex, uint16_t iftype, uint32_t ifflags, + uint32_t mtu, const char *name, + const uint8_t *hwaddr, uint8_t hwlen) +{ + size_t start = off; + struct nlmsghdr *nlh; + struct ifinfomsg ifi; + uint32_t txqlen = 1000; + uint8_t operstate = (ifflags & IFF_UP) ? 6 : 2; /* IF_OPER_UP : _DOWN */ + uint8_t brd[8]; + size_t len; + + if (start + NLMSG_HDRLEN + NLMSG_ALIGN(sizeof(ifi)) > max) + return start; + + memset(&ifi, 0, sizeof(ifi)); + ifi.ifi_family = AF_UNSPEC; + ifi.ifi_type = iftype; + ifi.ifi_index = ifindex; + ifi.ifi_flags = ifflags | ((ifflags & IFF_RUNNING) ? IFF_LOWER_UP : 0); + ifi.ifi_change = 0; + + off = start + NLMSG_HDRLEN; + memcpy(buf + off, &ifi, sizeof(ifi)); + off += NLMSG_ALIGN(sizeof(ifi)); + + off = nl_add_attr(buf, off, max, IFLA_IFNAME, name, strlen(name) + 1); + off = nl_add_attr(buf, off, max, IFLA_MTU, &mtu, sizeof(mtu)); + off = nl_add_attr(buf, off, max, IFLA_TXQLEN, &txqlen, sizeof(txqlen)); + off = nl_add_attr(buf, off, max, IFLA_OPERSTATE, &operstate, sizeof(operstate)); + if (hwlen > 0) { + memset(brd, (iftype == ARPHRD_LOOPBACK) ? 0x00 : 0xff, sizeof(brd)); + off = nl_add_attr(buf, off, max, IFLA_ADDRESS, hwaddr, hwlen); + off = nl_add_attr(buf, off, max, IFLA_BROADCAST, brd, hwlen); + } + + len = off - start; + nlh = (struct nlmsghdr *) (buf + start); + nlh->nlmsg_len = len; + nlh->nlmsg_type = RTM_NEWLINK; + nlh->nlmsg_flags = nlflags; + nlh->nlmsg_seq = seq; + nlh->nlmsg_pid = pid; + return start + NLMSG_ALIGN(len); +} + +/** + * Append an RTM_NEWADDR message for one address (@family, @addr/@addrlen, + * @prefixlen, @scope) on interface @ifindex, labelled @label (IPv4 only; + * may be NULL). + */ +static size_t nl_build_addr(uint8_t *buf, size_t off, size_t max, + uint32_t seq, uint32_t pid, uint16_t nlflags, + int family, int ifindex, + const uint8_t *addr, uint8_t addrlen, + uint8_t prefixlen, uint8_t scope, const char *label) +{ + size_t start = off; + struct nlmsghdr *nlh; + struct ifaddrmsg ifa; + size_t len; + + if (start + NLMSG_HDRLEN + NLMSG_ALIGN(sizeof(ifa)) > max) + return start; + + memset(&ifa, 0, sizeof(ifa)); + ifa.ifa_family = family; + ifa.ifa_prefixlen = prefixlen; + ifa.ifa_flags = IFA_F_PERMANENT; + ifa.ifa_scope = scope; + ifa.ifa_index = ifindex; + + off = start + NLMSG_HDRLEN; + memcpy(buf + off, &ifa, sizeof(ifa)); + off += NLMSG_ALIGN(sizeof(ifa)); + + off = nl_add_attr(buf, off, max, IFA_ADDRESS, addr, addrlen); + off = nl_add_attr(buf, off, max, IFA_LOCAL, addr, addrlen); + if (family == AF_INET && label != NULL) + off = nl_add_attr(buf, off, max, IFA_LABEL, label, strlen(label) + 1); + + len = off - start; + nlh = (struct nlmsghdr *) (buf + start); + nlh->nlmsg_len = len; + nlh->nlmsg_type = RTM_NEWADDR; + nlh->nlmsg_flags = nlflags; + nlh->nlmsg_seq = seq; + nlh->nlmsg_pid = pid; + return start + NLMSG_ALIGN(len); +} + +/** + * Append an NLMSG_DONE terminator (the canonical end-of-dump marker). + */ +static size_t nl_build_done(uint8_t *buf, size_t off, size_t max, + uint32_t seq, uint32_t pid) +{ + struct nlmsghdr *nlh; + int32_t error = 0; + size_t len = NLMSG_HDRLEN + sizeof(error); + + if (off + NLMSG_ALIGN(len) > max) + return off; + + nlh = (struct nlmsghdr *) (buf + off); + memcpy(buf + off + NLMSG_HDRLEN, &error, sizeof(error)); + nlh->nlmsg_len = len; + nlh->nlmsg_type = NLMSG_DONE; + nlh->nlmsg_flags = NLM_F_MULTI; + nlh->nlmsg_seq = seq; + nlh->nlmsg_pid = pid; + return off + NLMSG_ALIGN(len); +} + +/** + * Append an NLMSG_ERROR reply carrying @error (0 == success ack). An + * error==0 ack is what bubblewrap's loopback_setup() expects for its + * RTM_NEWADDR / RTM_NEWLINK; a negative error answers an unsupported + * single-get (e.g. RTM_GETLINK for a non-loopback device). + */ +static size_t nl_build_error(uint8_t *buf, size_t off, size_t max, + uint32_t seq, uint32_t pid, int error) +{ + struct nlmsghdr *nlh; + struct nlmsgerr err; + size_t len = NLMSG_HDRLEN + sizeof(err); + + if (off + NLMSG_ALIGN(len) > max) + return off; + + memset(&err, 0, sizeof(err)); + err.error = error; + /* err.msg is the (zeroed) header of the original request; callers + * only inspect err.error. */ + + nlh = (struct nlmsghdr *) (buf + off); + memcpy(buf + off + NLMSG_HDRLEN, &err, sizeof(err)); + nlh->nlmsg_len = len; + nlh->nlmsg_type = NLMSG_ERROR; + nlh->nlmsg_flags = 0; + nlh->nlmsg_seq = seq; + nlh->nlmsg_pid = pid; + return off + NLMSG_ALIGN(len); +} + +/** + * Decide whether a single (non-dump) RTM_GETLINK request in @req refers + * to the loopback interface: either it names "lo" via IFLA_IFNAME, or it + * asks by ifi_index 0/1. Used to answer real link lookups (iproute2's + * ll_link_get, "ip addr show lo") while reporting -ENODEV for anything + * else, which is the only interface PRoot can honestly present. + */ +static bool nl_request_is_loopback(const uint8_t *req, size_t req_len) +{ + const struct ifinfomsg *ifi; + size_t off = NLMSG_HDRLEN; + char name[IFNAMSIZ] = { 0 }; + bool have_name = false; + int ifindex; + + if (req_len < off + sizeof(*ifi)) + return true; /* no selector -> treat as loopback */ + + ifi = (const struct ifinfomsg *) (req + off); + ifindex = ifi->ifi_index; + + off += NLMSG_ALIGN(sizeof(*ifi)); + while (off + sizeof(struct rtattr) <= req_len) { + const struct rtattr *rta = (const struct rtattr *) (req + off); + size_t rlen = rta->rta_len; + + if (rlen < sizeof(*rta) || off + rlen > req_len) + break; + if (rta->rta_type == IFLA_IFNAME) { + size_t dlen = rlen - RTA_LENGTH(0); + size_t cpy = dlen < sizeof(name) ? dlen : sizeof(name) - 1; + memcpy(name, (const char *) rta + RTA_LENGTH(0), cpy); + name[cpy] = '\0'; + have_name = true; + } + off += RTA_ALIGN(rlen); + } + + if (have_name) + return strcmp(name, "lo") == 0; + return ifindex == 0 || ifindex == 1; +} + +/** + * Write a synthetic sockaddr_nl reply into the tracee's getsockname() + * / getpeername() buffer pair (@addr_ptr, @size_ptr). The kernel + * would otherwise hand back the AF_UNIX sockaddr from our substituted + * socket (length 2), which iproute2 rejects with "Wrong address + * length 2". Returns 0 on success or -errno (so the caller can + * propagate it as the syscall's result). + */ +static int write_fake_netlink_sockname(Tracee *tracee, word_t addr_ptr, + word_t size_ptr) +{ + struct sockaddr_nl snl; + uint32_t in_size; + uint32_t out_size; + + if (size_ptr == 0) + return -EINVAL; + + in_size = peek_uint32(tracee, size_ptr); + if (errno != 0) + return -errno; + + memset(&snl, 0, sizeof(snl)); + snl.nl_family = AF_NETLINK; + snl.nl_pid = (uint32_t) tracee->pid; + + if (addr_ptr != 0 && in_size > 0) { + uint32_t copy = in_size < sizeof(snl) ? in_size : sizeof(snl); + if (write_data(tracee, addr_ptr, &snl, copy) < 0) + return -EFAULT; + } + + /* Linux semantics: *size_ptr always reflects the real address + * length even when the caller's buffer was too small. */ + out_size = sizeof(snl); + poke_uint32(tracee, size_ptr, out_size); + if (errno != 0) + return -errno; + + return 0; +} + +/* Prefix length (CIDR) from a contiguous network mask of @len bytes. */ +static uint8_t nl_prefixlen(const uint8_t *mask, size_t len) +{ + uint8_t bits = 0; + size_t i; + + for (i = 0; i < len; i++) { + uint8_t b = mask[i]; + if (b == 0xff) { + bits += 8; + continue; + } + while (b & 0x80) { + bits++; + b <<= 1; + } + break; + } + return bits; +} + +/* rtnetlink address scope for @addr (host / link / universe). */ +static uint8_t nl_addr_scope(int family, const uint8_t *addr) +{ + if (family == AF_INET) { + if (addr[0] == 127) + return RT_SCOPE_HOST; /* 127/8 */ + if (addr[0] == 169 && addr[1] == 254) + return RT_SCOPE_LINK; /* 169.254/16 */ + return RT_SCOPE_UNIVERSE; + } else { + static const uint8_t loop[16] = { 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,1 }; + if (memcmp(addr, loop, 16) == 0) + return RT_SCOPE_HOST; /* ::1 */ + if (addr[0] == 0xfe && (addr[1] & 0xc0) == 0x80) + return RT_SCOPE_LINK; /* fe80::/10 */ + return RT_SCOPE_UNIVERSE; + } +} + +/* The hardcoded loopback link / addresses, used as a fallback when the + * host interfaces can't be enumerated. */ +static size_t nl_build_loopback_link(uint8_t *buf, size_t off, size_t max, + uint32_t seq, uint32_t pid, uint16_t nlflags) +{ + static const uint8_t zero[6] = { 0 }; + return nl_build_link(buf, off, max, seq, pid, nlflags, 1, ARPHRD_LOOPBACK, + IFF_UP | IFF_LOOPBACK | IFF_RUNNING, 65536, "lo", zero, 6); +} + +static size_t nl_build_loopback_addr(uint8_t *buf, size_t off, size_t max, + uint32_t seq, uint32_t pid, int family, + uint16_t nlflags) +{ + static const uint8_t v4[4] = { 127, 0, 0, 1 }; + static const uint8_t v6[16] = { 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,1 }; + if (family == AF_INET6) + return nl_build_addr(buf, off, max, seq, pid, nlflags, AF_INET6, 1, + v6, 16, 128, RT_SCOPE_HOST, NULL); + return nl_build_addr(buf, off, max, seq, pid, nlflags, AF_INET, 1, + v4, 4, 8, RT_SCOPE_HOST, "lo"); +} + +/* Extract the interface a single (non-dump) RTM_GETLINK asks for: returns + * its ifi_index and fills @name (empty if no IFLA_IFNAME was given). */ +static int nl_request_link_target(const uint8_t *req, size_t req_len, + char name[IFNAMSIZ]) +{ + const struct ifinfomsg *ifi; + size_t off = NLMSG_HDRLEN; + int ifindex; + + name[0] = '\0'; + if (req_len < off + sizeof(*ifi)) + return 0; + ifi = (const struct ifinfomsg *) (req + off); + ifindex = ifi->ifi_index; + + off += NLMSG_ALIGN(sizeof(*ifi)); + while (off + sizeof(struct rtattr) <= req_len) { + const struct rtattr *rta = (const struct rtattr *) (req + off); + size_t rlen = rta->rta_len; + + if (rlen < sizeof(*rta) || off + rlen > req_len) + break; + if (rta->rta_type == IFLA_IFNAME) { + size_t dlen = rlen - RTA_LENGTH(0); + size_t cpy = dlen < IFNAMSIZ ? dlen : IFNAMSIZ - 1; + memcpy(name, (const char *) rta + RTA_LENGTH(0), cpy); + name[cpy] = '\0'; + } + off += RTA_ALIGN(rlen); + } + return ifindex; +} + +/* Build RTM_NEWLINK messages for the host's interfaces (the set + * getifaddrs(3) exposes -- which keeps working on Android even when raw + * AF_NETLINK is denied, see termux-ip.c). A dump emits every interface; + * a single get only the one matching @want_name / @want_index. Returns + * the new offset and stores the number of links built in @built. */ +static size_t build_host_links(uint8_t *out, size_t max, uint32_t seq, + uint32_t pid, const char *want_name, + int want_index, bool dump, int *built) +{ + struct ifaddrs *ifaddr, *ifa; + char seen[64][IFNAMSIZ]; + int seen_count = 0; + size_t off = 0; + int sock; + + *built = 0; + if (getifaddrs(&ifaddr) != 0) + return 0; + sock = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0); + + for (ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next) { + uint32_t ifflags; + uint16_t iftype; + uint32_t mtu; + uint8_t hwaddr[8] = { 0 }; + uint8_t hwlen = 0; + int ifindex; + int i; + bool dup = false; + + if (ifa->ifa_name == NULL) + continue; + for (i = 0; i < seen_count; i++) + if (strncmp(seen[i], ifa->ifa_name, IFNAMSIZ) == 0) { + dup = true; + break; + } + if (dup) + continue; + if (seen_count < 64) { + strncpy(seen[seen_count], ifa->ifa_name, IFNAMSIZ - 1); + seen[seen_count][IFNAMSIZ - 1] = '\0'; + seen_count++; + } + + ifflags = ifa->ifa_flags; + iftype = (ifflags & IFF_LOOPBACK) ? ARPHRD_LOOPBACK : ARPHRD_ETHER; + mtu = (ifflags & IFF_LOOPBACK) ? 65536 : 1500; + ifindex = (int) if_nametoindex(ifa->ifa_name); + + /* AF_PACKET entries carry the authoritative index/type/hwaddr. */ + if (ifa->ifa_addr != NULL && ifa->ifa_addr->sa_family == AF_PACKET) { + struct sockaddr_ll *sll = (struct sockaddr_ll *) ifa->ifa_addr; + if (sll->sll_ifindex != 0) + ifindex = sll->sll_ifindex; + iftype = sll->sll_hatype; + if (sll->sll_halen > 0 && sll->sll_halen <= sizeof(hwaddr)) { + memcpy(hwaddr, sll->sll_addr, sll->sll_halen); + hwlen = sll->sll_halen; + } + } + + /* Best-effort MTU, and hwaddr/type when no AF_PACKET entry. */ + if (sock >= 0) { + struct ifreq ifr; + + memset(&ifr, 0, sizeof(ifr)); + strncpy(ifr.ifr_name, ifa->ifa_name, IFNAMSIZ - 1); + if (ioctl(sock, SIOCGIFMTU, &ifr) == 0) + mtu = ifr.ifr_mtu; + if (hwlen == 0) { + memset(&ifr, 0, sizeof(ifr)); + strncpy(ifr.ifr_name, ifa->ifa_name, IFNAMSIZ - 1); + if (ioctl(sock, SIOCGIFHWADDR, &ifr) == 0) { + iftype = ifr.ifr_hwaddr.sa_family; + memcpy(hwaddr, ifr.ifr_hwaddr.sa_data, 6); + hwlen = 6; + } + } + } + + if (!dump) { + if (want_name != NULL && want_name[0] != '\0') { + if (strcmp(want_name, ifa->ifa_name) != 0) + continue; + } else if (want_index > 0 && ifindex != want_index) { + continue; + } + } + + if (off + 256 > max) + break; + off = nl_build_link(out, off, max, seq, pid, + dump ? NLM_F_MULTI : 0, ifindex, iftype, + ifflags, mtu, ifa->ifa_name, hwaddr, hwlen); + (*built)++; + if (!dump) + break; + } + + if (sock >= 0) + close(sock); + freeifaddrs(ifaddr); + return off; +} + +/* Build RTM_NEWADDR messages for the host's addresses (optionally + * filtered to @want_family). Returns the new offset; @built gets the + * number of addresses emitted. */ +static size_t build_host_addrs(uint8_t *out, size_t max, uint32_t seq, + uint32_t pid, int want_family, bool dump, + int *built) +{ + struct ifaddrs *ifaddr, *ifa; + size_t off = 0; + + *built = 0; + if (getifaddrs(&ifaddr) != 0) + return 0; + + for (ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next) { + const uint8_t *addr; + const uint8_t *mask = NULL; + uint8_t addrlen, masklen = 0; + uint8_t prefixlen, scope; + int family, ifindex; + + if (ifa->ifa_name == NULL || ifa->ifa_addr == NULL) + continue; + family = ifa->ifa_addr->sa_family; + if (family != AF_INET && family != AF_INET6) + continue; + if (want_family != AF_UNSPEC && family != want_family) + continue; + + if (family == AF_INET) { + addr = (const uint8_t *) &((struct sockaddr_in *) ifa->ifa_addr)->sin_addr; + addrlen = 4; + if (ifa->ifa_netmask != NULL) { + mask = (const uint8_t *) &((struct sockaddr_in *) ifa->ifa_netmask)->sin_addr; + masklen = 4; + } + } else { + addr = (const uint8_t *) &((struct sockaddr_in6 *) ifa->ifa_addr)->sin6_addr; + addrlen = 16; + if (ifa->ifa_netmask != NULL) { + mask = (const uint8_t *) &((struct sockaddr_in6 *) ifa->ifa_netmask)->sin6_addr; + masklen = 16; + } + } + + prefixlen = (mask != NULL) ? nl_prefixlen(mask, masklen) + : (family == AF_INET ? 32 : 128); + scope = nl_addr_scope(family, addr); + ifindex = (int) if_nametoindex(ifa->ifa_name); + + if (off + 256 > max) + break; + off = nl_build_addr(out, off, max, seq, pid, + dump ? NLM_F_MULTI : 0, family, ifindex, + addr, addrlen, prefixlen, scope, ifa->ifa_name); + (*built)++; + } + + freeifaddrs(ifaddr); + return off; +} + +/** + * Relay a routing-table dump (RTM_GETROUTE) from the host kernel. The + * Android builds that deny *binding* an AF_NETLINK socket -- which is why + * we emulate it in the first place -- still let an unbound socket issue a + * dump, the same trick termux-ip.c uses for "ip route". We run that dump + * from PRoot and copy the kernel's RTM_NEWROUTE messages back, rewriting + * nlmsg_seq / nlmsg_pid so the tracee's iproute2 accepts them. Returns 0 + * (caller then falls back to an empty NLMSG_DONE, i.e. the previous + * behaviour) whenever the host won't cooperate, so nothing regresses. */ +static size_t relay_route_dump(const uint8_t *req, size_t req_len, + uint8_t *out, size_t max, + uint32_t seq, uint32_t pid) +{ + struct { + struct nlmsghdr nlh; + struct rtmsg rtm; + } dreq; + struct sockaddr_nl sa; + struct timeval tv = { .tv_sec = 1, .tv_usec = 0 }; + uint8_t family = (req_len > NLMSG_HDRLEN) ? req[NLMSG_HDRLEN] : 0; + size_t off = 0; + bool done = false; + bool saw_done = false; + int fd; + int rounds; + + fd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE); + if (fd < 0) + return 0; + (void) setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + + memset(&dreq, 0, sizeof(dreq)); + dreq.nlh.nlmsg_len = NLMSG_LENGTH(sizeof(struct rtmsg)); + dreq.nlh.nlmsg_type = RTM_GETROUTE; + dreq.nlh.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP; + dreq.nlh.nlmsg_seq = seq; + dreq.rtm.rtm_family = family; + + memset(&sa, 0, sizeof(sa)); + sa.nl_family = AF_NETLINK; + if (sendto(fd, &dreq, dreq.nlh.nlmsg_len, 0, + (struct sockaddr *) &sa, sizeof(sa)) < 0) { + close(fd); + return 0; + } + + for (rounds = 0; !done && rounds < 64; rounds++) { + uint8_t buf[8192] __attribute__((aligned(8))); + struct nlmsghdr *h; + ssize_t n; + size_t len; + + n = recv(fd, buf, sizeof(buf), 0); + if (n <= 0) + break; + len = (size_t) n; + for (h = (struct nlmsghdr *) buf; NLMSG_OK(h, len); + h = NLMSG_NEXT(h, len)) { + size_t mlen = h->nlmsg_len; + size_t aligned = NLMSG_ALIGN(mlen); + + /* Keep 64 bytes spare so the NLMSG_DONE terminator + * below always fits, even on a truncated dump. */ + if (off + aligned + 64 > max) { + done = true; + break; + } + h->nlmsg_seq = seq; + h->nlmsg_pid = pid; + memcpy(out + off, h, mlen); + if (aligned > mlen) + memset(out + off + mlen, 0, aligned - mlen); + off += aligned; + if (h->nlmsg_type == NLMSG_DONE) { + saw_done = true; + done = true; + break; + } + } + } + + close(fd); + + if (off == 0) + return 0; + /* Always hand the tracee a terminator: if the kernel's own + * NLMSG_DONE didn't make it (timeout, truncation), append one so + * iproute2 doesn't read past the reply and report "EOF on netlink". */ + if (!saw_done) + off = nl_build_done(out, off, max, seq, pid); + return off; +} + +/** + * Parse the netlink request the tracee just sent (whether via sendto + * with a flat buffer or via sendmsg with an iovec) and build the reply + * the kernel would have produced into tracee->fake_netlink_reply, so a + * later recvmsg / recvfrom on the same fake netlink fd can hand it back. + * + * RTM_GETLINK / RTM_GETADDR are answered with the *real* host interfaces + * (enumerated via getifaddrs, which keeps working under Android's netlink + * ban -- see termux-ip.c), falling back to a synthetic loopback when none + * can be gathered. Other dumps get an empty NLMSG_DONE; everything else + * (bubblewrap's RTM_NEWADDR / RTM_NEWLINK and friends) gets an error==0 + * ack. Replies carry the request's nlmsg_seq and the tracee's pid in + * nlmsg_pid, which iproute2 / bubblewrap match against before accepting. + */ +static void build_fake_netlink_reply(Tracee *tracee, word_t buf_addr, + word_t buf_len) +{ + uint8_t req[256] __attribute__((aligned(8))); + size_t req_len; + struct nlmsghdr hdr; + uint8_t *out = tracee->fake_netlink_reply; + size_t max = sizeof(tracee->fake_netlink_reply); + uint32_t pid = (uint32_t) tracee->pid; + uint32_t seq; + uint16_t type, flags; + bool dump; + size_t off = 0; + + tracee->fake_netlink_reply_len = 0; + + if (buf_addr == 0 || buf_len < sizeof(hdr)) + return; + req_len = buf_len < sizeof(req) ? buf_len : sizeof(req); + if (read_data(tracee, req, buf_addr, req_len) < 0) + return; + + memcpy(&hdr, req, sizeof(hdr)); + type = hdr.nlmsg_type; + flags = hdr.nlmsg_flags; + seq = hdr.nlmsg_seq; + dump = (flags & NLM_F_DUMP) == NLM_F_DUMP; + + switch (type) { + case RTM_GETLINK: { + char want_name[IFNAMSIZ]; + int want_index = nl_request_link_target(req, req_len, want_name); + int n = 0; + + off = build_host_links(out, max, seq, pid, + dump ? NULL : want_name, + dump ? 0 : want_index, dump, &n); + if (n == 0) { + /* Host enumeration unavailable: present loopback only. */ + off = 0; + if (dump) + off = nl_build_loopback_link(out, off, max, seq, pid, NLM_F_MULTI); + else if (nl_request_is_loopback(req, req_len)) + off = nl_build_loopback_link(out, off, max, seq, pid, 0); + else + off = nl_build_error(out, off, max, seq, pid, -ENODEV); + } + if (dump) + off = nl_build_done(out, off, max, seq, pid); + break; + } + + case RTM_GETADDR: { + uint8_t family = (req_len > NLMSG_HDRLEN) ? req[NLMSG_HDRLEN] : 0; + int want_family = (family == AF_INET || family == AF_INET6) + ? family : AF_UNSPEC; + int n = 0; + + off = build_host_addrs(out, max, seq, pid, want_family, dump, &n); + if (n == 0) { + /* Host enumeration unavailable: present loopback only. */ + off = 0; + if (family == 0 || family == AF_INET) + off = nl_build_loopback_addr(out, off, max, seq, pid, + AF_INET, dump ? NLM_F_MULTI : 0); + if (family == 0 || family == AF_INET6) + off = nl_build_loopback_addr(out, off, max, seq, pid, + AF_INET6, dump ? NLM_F_MULTI : 0); + } + if (dump) + off = nl_build_done(out, off, max, seq, pid); + break; + } + + case RTM_GETROUTE: + if (dump) { + off = relay_route_dump(req, req_len, out, max, seq, pid); + if (off == 0) + off = nl_build_done(out, off, max, seq, pid); + } else { + off = nl_build_error(out, off, max, seq, pid, 0); + } + break; + + default: + if (dump) + off = nl_build_done(out, off, max, seq, pid); + else + off = nl_build_error(out, off, max, seq, pid, 0); + break; + } + + tracee->fake_netlink_reply_len = off; +} + +/** + * Copy the pending fake netlink reply into the tracee's recvmsg iovec + * array (@iov_ptr, @iov_count), walking segments until the reply is + * exhausted. Returns the number of bytes actually scattered (which may + * be less than the reply when the caller's buffers are too small). + */ +static size_t scatter_fake_netlink_reply(Tracee *tracee, word_t iov_ptr, + word_t iov_count) +{ + size_t reply_len = tracee->fake_netlink_reply_len; + size_t w = sizeof_word(tracee); + size_t done = 0; + word_t i; + + for (i = 0; i < iov_count && done < reply_len; i++) { + word_t base = peek_word(tracee, iov_ptr + i * 2 * w); + word_t len = (errno == 0) ? peek_word(tracee, iov_ptr + i * 2 * w + w) : 0; + size_t chunk; + + errno = 0; + chunk = reply_len - done; + if (chunk > len) + chunk = len; + if (base != 0 && chunk > 0) { + if (write_data(tracee, base, + tracee->fake_netlink_reply + done, chunk) < 0) + break; + } + done += chunk; + } + + return done; +} + +/** + * If @cmd is SIOCGIFINDEX, answer it from the host's own interface + * table instead of letting the tracee's ioctl reach the kernel. + * + * Android denies this ioctl (EACCES) on the AF_UNIX/AF_INET socket + * glibc opens for if_nametoindex() for every device except loopback, + * when the caller lacks CAP_NET_ADMIN. That breaks more than + * bubblewrap's loopback_setup(): ifaddr.get_adapters() leaves + * Adapter.index == None for each real interface, and python-zeroconf's + * ip6_to_address_and_index() then refuses to match an address to its + * adapter ("No adapter found for IP address fe80::..."). + * + * We resolve the name with if_nametoindex() in the tracer, which keeps + * working under Android's netlink restrictions — the same way + * build_host_addrs() already fills each address's ifa_index — and write + * the real index back. Loopback is index 1 on every Linux kernel, so + * answer it even if the host enumeration somehow fails. Returns false + * for an unknown name, leaving the real ioctl to run (the previous + * behaviour, so nothing regresses). + * + * Only touch the ifr_name (read) and ifr_ifindex (write) fields, + * both at fixed offsets — sizeof(struct ifreq) differs between + * 32- and 64-bit ABIs (the trailing union contains pointer-sized + * members), and reading/writing the whole struct from PRoot would + * overrun the tracee's buffer when the two ABIs disagree. + */ +static bool maybe_fake_siocgifindex(Tracee *tracee, word_t cmd, word_t arg) +{ + char name[IFNAMSIZ]; + int ifindex; + + if (cmd != SIOCGIFINDEX) + return false; + if (arg == 0) + return false; + if (read_data(tracee, name, arg, sizeof(name)) < 0) + return false; + name[IFNAMSIZ - 1] = '\0'; + + ifindex = (int) if_nametoindex(name); + if (ifindex <= 0) { + if (strcmp(name, "lo") != 0) + return false; + ifindex = 1; + } + + if (write_data(tracee, arg + IFNAMSIZ, &ifindex, sizeof(ifindex)) < 0) + return false; + return true; +} + +/** + * Detect /proc//{uid_map,gid_map,setgroups}, which sandbox + * helpers like bubblewrap write to during user-namespace setup. The + * tracee cannot really create namespaces under PRoot, so silently + * redirect those writes to /dev/null. + */ +static bool is_proc_userns_file(const char *path) +{ + const char *p; + const char *suffix; + + if (strncmp(path, "/proc/", 6) != 0) + return false; + p = path + 6; + + if (strncmp(p, "self/", 5) == 0) + p += 5; + else { + const char *digits = p; + while (*p >= '0' && *p <= '9') + p++; + if (p == digits || *p != '/') + return false; + p++; + } + + suffix = p; + return strcmp(suffix, "uid_map") == 0 + || strcmp(suffix, "gid_map") == 0 + || strcmp(suffix, "setgroups") == 0; +} + +/** + * Redirect openat()/open() of /proc/.../uid_map etc. to /dev/null so + * that writes appear to succeed. @reg holds the path argument; the + * path has already been translated to host form. + */ +static void maybe_redirect_userns_file(Tracee *tracee, Reg reg) +{ + char host_path[PATH_MAX]; + + if (get_sysarg_path(tracee, host_path, reg) < 0) + return; + if (!is_proc_userns_file(host_path)) + return; + (void) set_sysarg_path(tracee, "/dev/null", reg); +} + +/** + * Translate the input arguments of the current @tracee's syscall in the + * @tracee->pid process area. This function sets @tracee->status to + * -errno if an error occured from the tracee's point-of-view (EFAULT + * for instance), otherwise 0. + */ +int translate_syscall_enter(Tracee *tracee) +{ + int flags; + int dirfd; + int olddirfd; + int newdirfd; + + int status; + int status2; + + char path[PATH_MAX]; + char oldpath[PATH_MAX]; + char newpath[PATH_MAX]; + + word_t syscall_number; + bool special = false; + + status = notify_extensions(tracee, SYSCALL_ENTER_START, 0, 0); + if (status < 0) + goto end; + if (status > 0) + return 0; + + /* Translate input arguments. */ + syscall_number = get_sysnum(tracee, ORIGINAL); + switch (syscall_number) { + default: + /* Nothing to do. */ + status = 0; + break; + + case PR_execve: + status = translate_execve_enter(tracee); + break; + + case PR_execveat: + if ((int) peek_reg(tracee, CURRENT, SYSARG_1) == AT_FDCWD) { + set_sysnum(tracee, PR_execve); + poke_reg(tracee, SYSARG_1, peek_reg(tracee, CURRENT, SYSARG_2)); + poke_reg(tracee, SYSARG_2, peek_reg(tracee, CURRENT, SYSARG_3)); + poke_reg(tracee, SYSARG_3, peek_reg(tracee, CURRENT, SYSARG_4)); + } else { + note(tracee, ERROR, SYSTEM, "execveat() with non-AT_FDCWD fd is not currently supported"); + status = -ENOSYS; + break; + } + status = translate_execve_enter(tracee); + break; + + case PR_ptrace: + status = translate_ptrace_enter(tracee); + break; + + case PR_wait4: + case PR_waitpid: + status = translate_wait_enter(tracee); + break; + + case PR_brk: + translate_brk_enter(tracee); + status = 0; + break; + + case PR_getcwd: + poke_reg(tracee, SYSARG_RESULT, 0); + set_sysnum(tracee, PR_void); + status = 0; + break; + + case PR_fchdir: + case PR_chdir: { + struct stat statl; + char *tmp; + + /* The ending "." ensures an error will be reported if + * path does not exist or if it is not a directory. */ + if (syscall_number == PR_chdir) { + status = get_sysarg_path(tracee, path, SYSARG_1); + if (status < 0) + break; + + status = join_paths(2, oldpath, path, "."); + if (status < 0) + break; + + dirfd = AT_FDCWD; + } + else { + strcpy(oldpath, "."); + dirfd = peek_reg(tracee, CURRENT, SYSARG_1); + } + + status = translate_path(tracee, path, dirfd, oldpath, true); + if (status < 0) + break; + + status = lstat(path, &statl); + if (status < 0) + break; + + /* Check this directory is accessible. */ + if ((statl.st_mode & S_IXUSR) == 0) + return -EACCES; + + /* Sadly this method doesn't detranslate statefully, + * this means that there's an ambiguity when several + * bindings are from the same host path: + * + * $ proot -m /tmp:/a -m /tmp:/b fchdir_getcwd /a + * /b + * + * $ proot -m /tmp:/b -m /tmp:/a fchdir_getcwd /a + * /a + * + * A solution would be to follow each file descriptor + * just like it is done for cwd. + */ + + status = detranslate_path(tracee, path, NULL); + if (status < 0) + break; + + /* Remove the trailing "/" or "/.". */ + chop_finality(path); + + tmp = talloc_strdup(tracee->fs, path); + if (tmp == NULL) { + status = -ENOMEM; + break; + } + TALLOC_FREE(tracee->fs->cwd); + + tracee->fs->cwd = tmp; + talloc_set_name_const(tracee->fs->cwd, "$cwd"); + + poke_reg(tracee, SYSARG_RESULT, 0); + set_sysnum(tracee, PR_void); + status = 0; + break; + } + + case PR_bind: + case PR_connect: { + word_t address; + word_t size; + + /* If we already redirected this fd to AF_UNIX as part + * of the AF_NETLINK emulation, fail the bind silently + * (the kernel would otherwise refuse our sockaddr_nl). */ + if (syscall_number == PR_bind + && is_fake_netlink_fd(tracee, peek_reg(tracee, CURRENT, SYSARG_1))) { + poke_reg(tracee, SYSARG_RESULT, 0); + set_sysnum(tracee, PR_void); + status = 0; + break; + } + + address = peek_reg(tracee, CURRENT, SYSARG_2); + size = peek_reg(tracee, CURRENT, SYSARG_3); + + status = translate_socketcall_enter(tracee, &address, size); + if (status <= 0) + break; + + poke_reg(tracee, SYSARG_2, address); + poke_reg(tracee, SYSARG_3, sizeof(struct sockaddr_un)); + + status = 0; + break; + } + +#define SYSARG_ADDR(n) (args_addr + ((n) - 1) * sizeof_word(tracee)) + +#define PEEK_WORD(addr, forced_errno) \ + peek_word(tracee, addr); \ + if (errno != 0) { \ + status = forced_errno ?: -errno; \ + break; \ + } + +#define POKE_WORD(addr, value) \ + poke_word(tracee, addr, value); \ + if (errno != 0) { \ + status = -errno; \ + break; \ + } + + case PR_accept: + case PR_accept4: + /* Nothing special to do if no sockaddr was specified. */ + if (peek_reg(tracee, ORIGINAL, SYSARG_2) == 0) { + status = 0; + break; + } + special = true; + /* Fall through. */ + case PR_getsockname: + case PR_getpeername:{ + int size; + + /* For an fd we substituted to AF_UNIX as part of the + * AF_NETLINK emulation, hand back a synthetic sockaddr_nl + * so callers like iproute2 don't error out with "Wrong + * address length 2" on the AF_UNIX sockname. */ + if ((syscall_number == PR_getsockname || syscall_number == PR_getpeername) + && is_fake_netlink_fd(tracee, peek_reg(tracee, CURRENT, SYSARG_1))) { + word_t addr_ptr = peek_reg(tracee, CURRENT, SYSARG_2); + word_t size_ptr = peek_reg(tracee, CURRENT, SYSARG_3); + int rc = write_fake_netlink_sockname(tracee, addr_ptr, size_ptr); + + poke_reg(tracee, SYSARG_RESULT, (word_t) rc); + set_sysnum(tracee, PR_void); + status = 0; + break; + } + + /* Remember: PEEK_WORD puts -errno in status and breaks if an + * error occured. */ + size = (int) PEEK_WORD(peek_reg(tracee, ORIGINAL, SYSARG_3), special ? -EINVAL : 0); + + /* The "size" argument is both used as an input parameter + * (max. size) and as an output parameter (actual size). The + * exit stage needs to know the max. size to not overwrite + * anything, that's why it is copied in the 6th argument + * (unused) before the kernel updates it. */ + poke_reg(tracee, SYSARG_6, size); + + status = 0; + break; + } + + /* Substitute an AF_UNIX/SOCK_DGRAM socket for AF_NETLINK + * requests so the kernel doesn't reject them with EACCES on + * Android, then track the resulting fd so bind/sendto/recvfrom + * on it can be faked too. Only NETLINK_ROUTE is emulated (it's + * what bubblewrap's loopback_setup needs); pass other netlink + * protocols (NETLINK_AUDIT, NETLINK_KOBJECT_UEVENT, ...) through + * untouched so the tracee gets the real kernel error / behavior + * for them. */ + case PR_socket: { + word_t domain = peek_reg(tracee, CURRENT, SYSARG_1); + word_t protocol = peek_reg(tracee, CURRENT, SYSARG_3); + if ( domain == AF_NETLINK + && protocol == NETLINK_ROUTE + && host_blocks_af_netlink(tracee)) { + word_t type = peek_reg(tracee, CURRENT, SYSARG_2); + poke_reg(tracee, SYSARG_1, AF_UNIX); + poke_reg(tracee, SYSARG_2, SOCK_DGRAM | (type & SOCK_CLOEXEC)); + poke_reg(tracee, SYSARG_3, 0); + tracee->pending_fake_netlink_socket = true; + tracee->sysexit_pending = true; + tracee->restart_how = PTRACE_SYSCALL; + } + status = 0; + break; + } + + case PR_sendto: { + int fd = peek_reg(tracee, CURRENT, SYSARG_1); + if (is_fake_netlink_fd(tracee, fd)) { + word_t buf = peek_reg(tracee, CURRENT, SYSARG_2); + word_t len = peek_reg(tracee, CURRENT, SYSARG_3); + + build_fake_netlink_reply(tracee, buf, len); + + poke_reg(tracee, SYSARG_RESULT, len); + set_sysnum(tracee, PR_void); + status = 0; + break; + } + status = 0; + break; + } + + case PR_sendmsg: { + int fd = peek_reg(tracee, CURRENT, SYSARG_1); + if (is_fake_netlink_fd(tracee, fd)) { + word_t msghdr_addr = peek_reg(tracee, CURRENT, SYSARG_2); + size_t w = sizeof_word(tracee); + word_t total = 0; + word_t iov_ptr, iov_count; + + /* struct msghdr layout (Linux, both ABIs): + * word msg_name + * u32 msg_namelen (followed by pad to word align) + * word msg_iov + * word msg_iovlen + * ... + */ + if (msghdr_addr != 0) { + iov_ptr = peek_word(tracee, msghdr_addr + 2 * w); + iov_count = (errno == 0) + ? peek_word(tracee, msghdr_addr + 3 * w) + : 0; + errno = 0; + + if (iov_ptr != 0 && iov_count > 0) { + word_t base = peek_word(tracee, iov_ptr); + word_t len = (errno == 0) + ? peek_word(tracee, iov_ptr + w) + : 0; + errno = 0; + + build_fake_netlink_reply(tracee, base, len); + /* Use the first iovec's length as the + * pretended bytes-sent. Multi-iovec + * netlink requests are unheard of for + * the bwrap / glibc / iproute2 callers + * we care about. */ + total = len; + } + } + + poke_reg(tracee, SYSARG_RESULT, total); + set_sysnum(tracee, PR_void); + status = 0; + break; + } + status = 0; + break; + } + + case PR_recvfrom: { + int fd = peek_reg(tracee, CURRENT, SYSARG_1); + if (is_fake_netlink_fd(tracee, fd)) { + word_t buf = peek_reg(tracee, CURRENT, SYSARG_2); + word_t len = peek_reg(tracee, CURRENT, SYSARG_3); + int flags = (int) peek_reg(tracee, CURRENT, SYSARG_4); + word_t addr_ptr = peek_reg(tracee, CURRENT, SYSARG_5); + word_t size_ptr = peek_reg(tracee, CURRENT, SYSARG_6); + size_t reply_len = tracee->fake_netlink_reply_len; + size_t copied = 0; + size_t result; + + if (reply_len > 0 && buf != 0) { + copied = len < reply_len ? len : reply_len; + if (copied > 0 && + write_data(tracee, buf, + tracee->fake_netlink_reply, copied) < 0) + copied = 0; + } + + /* MSG_PEEK leaves the reply pending for the real read + * that follows; MSG_TRUNC asks for the untruncated + * length (the libnetlink size-probe pattern). */ + if (!(flags & MSG_PEEK)) + tracee->fake_netlink_reply_len = 0; + result = (flags & MSG_TRUNC) ? reply_len : copied; + + /* Hand back a kernel sockaddr_nl (nl_pid == 0) source + * rather than the AF_UNIX address of our substitute. */ + if (addr_ptr != 0 && size_ptr != 0) + (void) write_fake_netlink_sockname(tracee, addr_ptr, + size_ptr); + errno = 0; + + poke_reg(tracee, SYSARG_RESULT, (word_t) result); + set_sysnum(tracee, PR_void); + status = 0; + break; + } + status = 0; + break; + } + + case PR_recvmsg: { + int fd = peek_reg(tracee, CURRENT, SYSARG_1); + if (is_fake_netlink_fd(tracee, fd)) { + word_t msghdr_addr = peek_reg(tracee, CURRENT, SYSARG_2); + int flags = (int) peek_reg(tracee, CURRENT, SYSARG_3); + size_t w = sizeof_word(tracee); + word_t msg_name = 0; + word_t iov_ptr = 0, iov_count = 0; + size_t reply_len = tracee->fake_netlink_reply_len; + size_t scattered = 0; + size_t result; + + if (msghdr_addr != 0) { + msg_name = peek_word(tracee, msghdr_addr); + if (errno != 0) { errno = 0; msg_name = 0; } + iov_ptr = peek_word(tracee, msghdr_addr + 2 * w); + if (errno != 0) { errno = 0; iov_ptr = 0; } + iov_count = peek_word(tracee, msghdr_addr + 3 * w); + if (errno != 0) { errno = 0; iov_count = 0; } + } + + if (iov_ptr != 0 && iov_count > 0) + scattered = scatter_fake_netlink_reply(tracee, iov_ptr, + iov_count); + + /* MSG_PEEK leaves the reply pending for the real read + * that follows; MSG_TRUNC asks for the untruncated + * length (iproute2's libnetlink size-probe pattern). */ + if (!(flags & MSG_PEEK)) + tracee->fake_netlink_reply_len = 0; + result = (flags & MSG_TRUNC) ? reply_len : scattered; + + /* glibc's getifaddrs() and friends inspect the + * source address: hand them a sockaddr_nl from the + * kernel (nl_pid == 0) rather than the AF_UNIX + * "address family 1" that a real recvmsg on our + * substituted socket would produce. */ + if (msg_name != 0 && msghdr_addr != 0) { + struct sockaddr_nl snl; + uint32_t in_namelen = peek_uint32(tracee, msghdr_addr + w); + if (errno == 0 && in_namelen > 0) { + uint32_t copy = in_namelen < sizeof(snl) + ? in_namelen + : sizeof(snl); + memset(&snl, 0, sizeof(snl)); + snl.nl_family = AF_NETLINK; + (void) write_data(tracee, msg_name, &snl, copy); + poke_uint32(tracee, msghdr_addr + w, + (uint32_t) sizeof(snl)); + } + errno = 0; + } + + /* msg_flags (offset 6 words in struct msghdr, both + * ABIs): report MSG_TRUNC iff the caller's buffers + * couldn't hold the whole reply, like the kernel. */ + if (msghdr_addr != 0) { + poke_uint32(tracee, msghdr_addr + 6 * w, + scattered < reply_len ? MSG_TRUNC : 0); + errno = 0; + } + + poke_reg(tracee, SYSARG_RESULT, (word_t) result); + set_sysnum(tracee, PR_void); + status = 0; + break; + } + status = 0; + break; + } + + case PR_socketcall: { + word_t args_addr; + word_t sock_addr_saved; + word_t sock_addr; + word_t size_addr; + word_t size; + + args_addr = peek_reg(tracee, CURRENT, SYSARG_2); + + switch (peek_reg(tracee, CURRENT, SYSARG_1)) { + case SYS_BIND: + case SYS_CONNECT: + /* Handle these cases below. */ + status = 1; + break; + + case SYS_ACCEPT: + case SYS_ACCEPT4: + /* Nothing special to do if no sockaddr was specified. */ + sock_addr = PEEK_WORD(SYSARG_ADDR(2), 0); + if (sock_addr == 0) { + status = 0; + break; + } + special = true; + /* Fall through. */ + case SYS_GETSOCKNAME: + case SYS_GETPEERNAME: + /* Remember: PEEK_WORD puts -errno in status and breaks + * if an error occured. */ + size_addr = PEEK_WORD(SYSARG_ADDR(3), 0); + size = (int) PEEK_WORD(size_addr, special ? -EINVAL : 0); + + /* See case PR_accept for explanation. */ + poke_reg(tracee, SYSARG_6, size); + status = 0; + break; + + default: + status = 0; + break; + } + + /* An error occured or there's nothing else to do. */ + if (status <= 0) + break; + + /* Remember: PEEK_WORD puts -errno in status and breaks if an + * error occured. */ + sock_addr = PEEK_WORD(SYSARG_ADDR(2), 0); + size = PEEK_WORD(SYSARG_ADDR(3), 0); + + sock_addr_saved = sock_addr; + status = translate_socketcall_enter(tracee, &sock_addr, size); + if (status <= 0) + break; + + /* These parameters are used/restored at the exit stage. */ + poke_reg(tracee, SYSARG_5, sock_addr_saved); + poke_reg(tracee, SYSARG_6, size); + + /* Remember: POKE_WORD puts -errno in status and breaks if an + * error occured. */ + POKE_WORD(SYSARG_ADDR(2), sock_addr); + POKE_WORD(SYSARG_ADDR(3), sizeof(struct sockaddr_un)); + + status = 0; + break; + } + +#undef SYSARG_ADDR +#undef PEEK_WORD +#undef POKE_WORD + + case PR_access: + case PR_acct: + case PR_chmod: + case PR_chown: + case PR_chown32: + case PR_chroot: + case PR_getxattr: + case PR_listxattr: + case PR_mknod: + case PR_oldstat: + case PR_creat: + case PR_removexattr: + case PR_setxattr: + case PR_stat: + case PR_stat64: + case PR_statfs: + case PR_statfs64: + case PR_swapoff: + case PR_swapon: + case PR_truncate: + case PR_truncate64: + case PR_uselib: + case PR_utime: + case PR_utimes: + status = translate_sysarg(tracee, SYSARG_1, REGULAR); + break; + + /* Pretend namespace syscalls succeed without doing anything; + * PRoot can't really create namespaces, and sandbox helpers + * like bubblewrap only check the return value. */ + case PR_unshare: + case PR_setns: + poke_reg(tracee, SYSARG_RESULT, 0); + set_sysnum(tracee, PR_void); + status = 0; + break; + + case PR_umount: + case PR_umount2: + apply_emulated_umount(tracee); + poke_reg(tracee, SYSARG_RESULT, 0); + set_sysnum(tracee, PR_void); + status = 0; + break; + + /* Strip CLONE_NEW* flags from clone(2)/clone3(2) so the + * syscall doesn't fail with EPERM on kernels that disallow + * unprivileged namespace creation (typical on Android). The + * fork/thread itself still proceeds normally and PRoot keeps + * tracking the child through PTRACE_EVENT_CLONE. When the + * caller asked for CLONE_NEWNS, remember it on the tracee so + * the new child gets isolated bindings (otherwise emulated + * mount(2) calls in the child would leak into the parent). */ + case PR_clone: { + word_t flags = peek_reg(tracee, CURRENT, SYSARG_1); + if ((flags & CLONE_NS_MASK) != 0) { + if ((flags & CLONE_NEWNS) != 0) + tracee->clone_stripped_newns = true; + poke_reg(tracee, SYSARG_1, flags & ~(word_t) CLONE_NS_MASK); + } + status = 0; + break; + } + + case PR_clone3: { + word_t args_addr = peek_reg(tracee, CURRENT, SYSARG_1); + word_t flags; + + if (args_addr != 0) { + errno = 0; + flags = peek_word(tracee, args_addr); + if (errno == 0 && (flags & CLONE_NS_MASK) != 0) { + if ((flags & CLONE_NEWNS) != 0) + tracee->clone_stripped_newns = true; + poke_word(tracee, args_addr, + flags & ~(word_t) CLONE_NS_MASK); + } + } + status = 0; + break; + } + + /* mount(2) and pivot_root(2) are emulated by translating them + * into PRoot bindings (see emulate_mount/emulate_pivot_root) + * so the resulting paths actually become accessible. */ + case PR_mount: + apply_emulated_mount(tracee); + poke_reg(tracee, SYSARG_RESULT, 0); + set_sysnum(tracee, PR_void); + status = 0; + break; + + case PR_pivot_root: + apply_emulated_pivot_root(tracee); + poke_reg(tracee, SYSARG_RESULT, 0); + set_sysnum(tracee, PR_void); + status = 0; + break; + + case PR_open: + flags = peek_reg(tracee, CURRENT, SYSARG_2); + + if (tracee->execfn_addr != 0 + && read_string(tracee, path, peek_reg(tracee, CURRENT, SYSARG_1), PATH_MAX) > 0 + && strcmp(path, "/proc/self/auxv") == 0) { + tracee->sysexit_pending = true; + tracee->restart_how = PTRACE_SYSCALL; + } + + if ( ((flags & O_NOFOLLOW) != 0) + || ((flags & O_EXCL) != 0 && (flags & O_CREAT) != 0)) + status = translate_sysarg(tracee, SYSARG_1, SYMLINK); + else + status = translate_sysarg(tracee, SYSARG_1, REGULAR); + if (status >= 0) + maybe_redirect_userns_file(tracee, SYSARG_1); + break; + + case PR_fchownat: + case PR_fstatat64: + case PR_newfstatat: + case PR_utimensat: + case PR_name_to_handle_at: + dirfd = peek_reg(tracee, CURRENT, SYSARG_1); + + status = get_sysarg_path(tracee, path, SYSARG_2); + if (status < 0) + break; + + flags = ( syscall_number == PR_fchownat + || syscall_number == PR_name_to_handle_at) + ? peek_reg(tracee, CURRENT, SYSARG_5) + : peek_reg(tracee, CURRENT, SYSARG_4); + + if ((flags & AT_SYMLINK_NOFOLLOW) != 0) + status = translate_path2(tracee, dirfd, path, SYSARG_2, SYMLINK); + else + status = translate_path2(tracee, dirfd, path, SYSARG_2, REGULAR); + break; + + case PR_fchmodat: + case PR_faccessat: + case PR_faccessat2: + case PR_futimesat: + case PR_mknodat: + dirfd = peek_reg(tracee, CURRENT, SYSARG_1); + + status = get_sysarg_path(tracee, path, SYSARG_2); + if (status < 0) + break; + + status = translate_path2(tracee, dirfd, path, SYSARG_2, REGULAR); + break; + + case PR_inotify_add_watch: + flags = peek_reg(tracee, CURRENT, SYSARG_3); + + if ((flags & IN_DONT_FOLLOW) != 0) + status = translate_sysarg(tracee, SYSARG_2, SYMLINK); + else + status = translate_sysarg(tracee, SYSARG_2, REGULAR); + break; + + case PR_readlink: + case PR_lchown: + case PR_lchown32: + case PR_lgetxattr: + case PR_llistxattr: + case PR_lremovexattr: + case PR_lsetxattr: + case PR_lstat: + case PR_lstat64: + case PR_oldlstat: + case PR_unlink: + case PR_rmdir: + case PR_mkdir: + status = translate_sysarg(tracee, SYSARG_1, SYMLINK); + break; + + case PR_linkat: + olddirfd = peek_reg(tracee, CURRENT, SYSARG_1); + newdirfd = peek_reg(tracee, CURRENT, SYSARG_3); + flags = peek_reg(tracee, CURRENT, SYSARG_5); + + status = get_sysarg_path(tracee, oldpath, SYSARG_2); + if (status < 0) + break; + + status = get_sysarg_path(tracee, newpath, SYSARG_4); + if (status < 0) + break; + + if ((flags & AT_SYMLINK_FOLLOW) != 0) + status = translate_path2(tracee, olddirfd, oldpath, SYSARG_2, REGULAR); + else + status = translate_path2(tracee, olddirfd, oldpath, SYSARG_2, SYMLINK); + if (status < 0) + break; + + status = translate_path2(tracee, newdirfd, newpath, SYSARG_4, SYMLINK); + break; + + case PR_openat2: { + /* int openat2(int dirfd, const char *pathname, + * struct open_how *how, size_t size); + * + * Rewrite into openat() and translate it as such: the path is + * in SYSARG_2 like openat(), but the open flags live inside the + * open_how struct rather than in a register, so move them into + * SYSARG_3. The how.resolve flags (RESOLVE_BENEATH, ...) are + * dropped: they reject the absolute host paths PRoot produces, + * and PRoot already keeps path resolution inside the rootfs. */ + struct proot_open_how how = {}; + word_t how_size = peek_reg(tracee, CURRENT, SYSARG_4); + if (how_size > sizeof(how)) + how_size = sizeof(how); + status = read_data(tracee, &how, peek_reg(tracee, CURRENT, SYSARG_3), how_size); + if (status < 0) + break; + set_sysnum(tracee, PR_openat); + poke_reg(tracee, SYSARG_3, how.flags); + poke_reg(tracee, SYSARG_4, how.mode); + } + /* Fall through. */ + + case PR_openat: + dirfd = peek_reg(tracee, CURRENT, SYSARG_1); + flags = peek_reg(tracee, CURRENT, SYSARG_3); + + status = get_sysarg_path(tracee, path, SYSARG_2); + if (status < 0) + break; + + if (tracee->execfn_addr != 0 && strcmp(path, "/proc/self/auxv") == 0) { + tracee->sysexit_pending = true; + tracee->restart_how = PTRACE_SYSCALL; + } + + if ( ((flags & O_NOFOLLOW) != 0) + || ((flags & O_EXCL) != 0 && (flags & O_CREAT) != 0)) + status = translate_path2(tracee, dirfd, path, SYSARG_2, SYMLINK); + else + status = translate_path2(tracee, dirfd, path, SYSARG_2, REGULAR); + if (status >= 0) + maybe_redirect_userns_file(tracee, SYSARG_2); + break; + + case PR_readlinkat: + case PR_unlinkat: + case PR_mkdirat: + dirfd = peek_reg(tracee, CURRENT, SYSARG_1); + + status = get_sysarg_path(tracee, path, SYSARG_2); + if (status < 0) + break; + + status = translate_path2(tracee, dirfd, path, SYSARG_2, SYMLINK); + break; + + case PR_link: + case PR_rename: + status = translate_sysarg(tracee, SYSARG_1, SYMLINK); + if (status < 0) + break; + + status = translate_sysarg(tracee, SYSARG_2, SYMLINK); + break; + + case PR_renameat: + case PR_renameat2: + olddirfd = peek_reg(tracee, CURRENT, SYSARG_1); + newdirfd = peek_reg(tracee, CURRENT, SYSARG_3); + + status = get_sysarg_path(tracee, oldpath, SYSARG_2); + if (status < 0) + break; + + status = get_sysarg_path(tracee, newpath, SYSARG_4); + if (status < 0) + break; + + status = translate_path2(tracee, olddirfd, oldpath, SYSARG_2, SYMLINK); + if (status < 0) + break; + + status = translate_path2(tracee, newdirfd, newpath, SYSARG_4, SYMLINK); + break; + + case PR_symlink: + status = translate_sysarg(tracee, SYSARG_2, SYMLINK); + break; + + case PR_symlinkat: + newdirfd = peek_reg(tracee, CURRENT, SYSARG_2); + + status = get_sysarg_path(tracee, newpath, SYSARG_3); + if (status < 0) + break; + + status = translate_path2(tracee, newdirfd, newpath, SYSARG_3, SYMLINK); + break; + + case PR_statx: + newdirfd = peek_reg(tracee, CURRENT, SYSARG_1); + + status = get_sysarg_path(tracee, newpath, SYSARG_2); + if (status < 0) + break; + + status = translate_path2( + tracee, + newdirfd, + newpath, + SYSARG_2, + (peek_reg(tracee, CURRENT, SYSARG_3) & AT_SYMLINK_NOFOLLOW) ? SYMLINK : REGULAR + ); + break; + + case PR_prctl: + /* Prevent tracees from setting dumpable flag. + * (Otherwise it could break tracee memory access) */ + if (peek_reg(tracee, CURRENT, SYSARG_1) == PR_SET_DUMPABLE) { + poke_reg(tracee, SYSARG_RESULT, 0); + set_sysnum(tracee, PR_void); + status = 0; + } + /* On kernels that don't support PTRACE_O_TRACESECCOMP, + * SECCOMP_RET_TRACE causes filtered syscalls to return + * -ENOSYS to the tracee without generating a ptrace event. + * If a tracee installs its own SECCOMP_MODE_FILTER, the + * syscalls proot must intercept (open, execve, ...) would + * silently fail from proot's perspective. Block the filter + * installation so proot's PTRACE_SYSCALL path keeps working. + * This situation is typical on old ARM 32-bit Android kernels + * that backported seccomp but not PTRACE_O_TRACESECCOMP. */ +#ifndef SECCOMP_MODE_FILTER +#define SECCOMP_MODE_FILTER 2 +#endif + if (peek_reg(tracee, CURRENT, SYSARG_1) == PR_SET_SECCOMP + && peek_reg(tracee, CURRENT, SYSARG_2) == SECCOMP_MODE_FILTER + && !seccomp_ptrace_event_is_supported()) { + VERBOSE(tracee, 1, "blocking tracee prctl(PR_SET_SECCOMP, " + "SECCOMP_MODE_FILTER): kernel lacks " + "PTRACE_EVENT_SECCOMP support"); + poke_reg(tracee, SYSARG_RESULT, (word_t) -EPERM); + set_sysnum(tracee, PR_void); + status = 0; + } + /* Need sysexit to patch AT_EXECFN in the returned buffer. */ +#ifndef PR_GET_AUXV +#define PR_GET_AUXV 0x41555856 +#endif + if (peek_reg(tracee, CURRENT, SYSARG_1) == PR_GET_AUXV) { + tracee->sysexit_pending = true; + tracee->restart_how = PTRACE_SYSCALL; + } + /* PRoot always sets PR_SET_NO_NEW_PRIVS in the child before + * execve (a precondition for its seccomp filter), so the real + * flag is on even though the guest never asked for it. Report + * the guest's own intent instead: answer PR_GET_NO_NEW_PRIVS + * from tracee->no_new_privs without running the real syscall, + * and observe the tracee's own PR_SET_NO_NEW_PRIVS at sysexit. + * Tools like sudo-rs refuse to run when the flag appears set. */ + if (peek_reg(tracee, CURRENT, SYSARG_1) == PR_GET_NO_NEW_PRIVS) { + poke_reg(tracee, SYSARG_RESULT, tracee->no_new_privs ? 1 : 0); + set_sysnum(tracee, PR_void); + status = 0; + } + if (peek_reg(tracee, CURRENT, SYSARG_1) == PR_SET_NO_NEW_PRIVS) { + tracee->sysexit_pending = true; + tracee->restart_how = PTRACE_SYSCALL; + } + break; + + case PR_ioctl: { + word_t cmd = peek_reg(tracee, CURRENT, SYSARG_2); + word_t arg = peek_reg(tracee, CURRENT, SYSARG_3); + + /* SIOCGIFINDEX: Android often denies this with EACCES for + * real interfaces (and loopback), so resolve the index in + * the tracer instead — bubblewrap's loopback_setup() and + * ifaddr/zeroconf's interface lookup both depend on it. */ + if (cmd == SIOCGIFINDEX && maybe_fake_siocgifindex(tracee, cmd, arg)) { + poke_reg(tracee, SYSARG_RESULT, 0); + set_sysnum(tracee, PR_void); + break; + } + +#ifdef __ANDROID__ + /* Using literal value because Termux build system patches TCSAFLUSH */ + if (cmd == TCSETS + 2 /* + TCSAFLUSH */) + poke_reg(tracee, SYSARG_2, TCSETS + TCSANOW); + + if (cmd == TCGETS2) + poke_reg(tracee, SYSARG_2, TCGETS); + + if (cmd == TCSETS2) + poke_reg(tracee, SYSARG_2, TCSETS); + + if (cmd == TCSETSW2) + poke_reg(tracee, SYSARG_2, TCSETSW); + + if (cmd == TCSETSF2) + poke_reg(tracee, SYSARG_2, TCSETS); +#endif + + break; + } + + case PR_memfd_create: + { + char memfd_name[20] = {}; + if (read_string(tracee, memfd_name, peek_reg(tracee, CURRENT, SYSARG_1), sizeof(memfd_name) - 1) < 0) { + /* Failed to read memfd name, do nothing and let normal memfd proceed. */ + break; + } + /* If this memfd is one of those used by Qt/QML for executable code, + * deny memfd_create() call and let Qt fall back to anonymous mmap. */ + if (0 == strncmp(memfd_name, "JITCode:", 8)) { + status = -EACCES; + } + /* php8.3 attempts using memfd as lock through fcntl(F_SETLKW), + * which is not allowed on Android, + * deny memfd_create() call and let php fall back to open(O_TMPFILE). + * https://github.com/php/php-src/blob/26c432d850c153aaf79a1b24e4753bc0533e02b0/ext/opcache/zend_shared_alloc.c#L91 + */ + if (0 == strcmp(memfd_name, "opcache_lock")) { + status = -EACCES; + } + /* apk-tools v3 use memfd_create + execveat, which is not supported under PRoot + * https://github.com/termux/proot-distro/issues/595#issuecomment-3705344471 + * https://git.alpinelinux.org/apk-tools/tree/src/package.c?h=v3.0.3#n737 + */ + if (0 == strncmp(memfd_name, "lib/apk/exec/", 13)) { + status = -EACCES; + } + break; + } + case PR_close: { + int closed_fd = (int) peek_reg(tracee, CURRENT, SYSARG_1); + + /* Stop tracking auxv_fd once the tracee closes it. */ + if (tracee->auxv_fd >= 0 && closed_fd == tracee->auxv_fd) + tracee->auxv_fd = -1; + + /* Drop the fd from the fake-AF_NETLINK tracking set, + * otherwise its number could be reused for an unrelated + * file and we'd keep intercepting sendto/recvfrom on + * it. */ + unmark_fake_netlink_fd(tracee, closed_fd); + break; + } + + } + + +end: + status2 = notify_extensions(tracee, SYSCALL_ENTER_END, status, 0); + if (status2 < 0) + status = status2; + + return status; +} + diff --git a/core/proot/src/main/cpp/syscall/exit.c b/core/proot/src/main/cpp/syscall/exit.c new file mode 100644 index 000000000..d3bc07bf0 --- /dev/null +++ b/core/proot/src/main/cpp/syscall/exit.c @@ -0,0 +1,748 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#include /* errno(3), E* */ +#include /* struct utsname, */ +#include /* SYS_*, */ +#include /* _IOW, */ +#include /* PR_GET_AUXV, */ +#include /* strlen(3), */ +#include /* readlink(2), */ + +#include "cli/note.h" +#include "syscall/syscall.h" +#include "syscall/sysnum.h" +#include "syscall/socket.h" +#include "syscall/chain.h" +#include "syscall/heap.h" +#include "syscall/rlimit.h" +#include "execve/execve.h" +#include "tracee/tracee.h" +#include "tracee/reg.h" +#include "tracee/mem.h" +#include "tracee/abi.h" +#include "tracee/seccomp.h" +#include "tracee/statx.h" +#include "path/path.h" +#include "ptrace/ptrace.h" +#include "ptrace/wait.h" +#include "extension/extension.h" +#include "arch.h" + +/** + * Translate the output arguments of the current @tracee's syscall in + * the @tracee->pid process area. This function sets the result of + * this syscall to @tracee->status if an error occured previously + * during the translation, that is, if @tracee->status is less than 0. + */ +void translate_syscall_exit(Tracee *tracee) +{ + word_t syscall_number; + word_t syscall_result; + int status; + + status = notify_extensions(tracee, SYSCALL_EXIT_START, 0, 0); + if (status < 0) { + poke_reg(tracee, SYSARG_RESULT, (word_t) status); + goto end; + } + if (status > 0) + return; + + /* Set the tracee's errno if an error occured previously during + * the translation. */ + if (tracee->status < 0) { + poke_reg(tracee, SYSARG_RESULT, (word_t) tracee->status); + goto end; + } + + /* If proot changed syscall to PR_void during enter, + * keep syscall result set during entry. */ + if (peek_reg(tracee, MODIFIED, SYSARG_NUM) == +#if defined(ARCH_ARM64) || defined(ARCH_X86_64) + (is_32on64_mode(tracee) ? (SYSCALL_AVOIDER & 0xFFFFFFFF) : SYSCALL_AVOIDER) +#else + SYSCALL_AVOIDER +#endif + && + peek_reg(tracee, ORIGINAL, SYSARG_NUM) != peek_reg(tracee, MODIFIED, SYSARG_NUM)) { + poke_reg(tracee, SYSARG_RESULT, peek_reg(tracee, MODIFIED, SYSARG_RESULT)); + } + + /* Translate output arguments: + * - break: update the syscall result register with "status" + * - goto end: nothing else to do. + */ + syscall_number = get_sysnum(tracee, ORIGINAL); + syscall_result = peek_reg(tracee, CURRENT, SYSARG_RESULT); + switch (syscall_number) { + case PR_brk: + translate_brk_exit(tracee); + goto end; + + case PR_getcwd: { + char path[PATH_MAX]; + size_t new_size; + size_t size; + word_t output; + + size = (size_t) peek_reg(tracee, ORIGINAL, SYSARG_2); + if (size == 0) { + status = -EINVAL; + break; + } + + /* Ensure cwd still exists. */ + status = translate_path(tracee, path, AT_FDCWD, ".", false); + if (status < 0) + break; + + new_size = strlen(tracee->fs->cwd) + 1; + if (size < new_size) { + status = -ERANGE; + break; + } + + /* Overwrite the path. */ + output = peek_reg(tracee, ORIGINAL, SYSARG_1); + status = write_data(tracee, output, tracee->fs->cwd, new_size); + if (status < 0) + break; + + /* The value of "status" is used to update the returned value + * in translate_syscall_exit(). */ + status = new_size; + break; + } + + case PR_accept: + case PR_accept4: + /* Nothing special to do if no sockaddr was specified. */ + if (peek_reg(tracee, ORIGINAL, SYSARG_2) == 0) + goto end; + /* Fall through. */ + case PR_getsockname: + case PR_getpeername: { + word_t sock_addr; + word_t size_addr; + word_t max_size; + + /* Error reported by the kernel. */ + if ((int) syscall_result < 0) + goto end; + + sock_addr = peek_reg(tracee, ORIGINAL, SYSARG_2); + size_addr = peek_reg(tracee, MODIFIED, SYSARG_3); + max_size = peek_reg(tracee, MODIFIED, SYSARG_6); + + status = translate_socketcall_exit(tracee, sock_addr, size_addr, max_size); + if (status < 0) + break; + + /* Don't overwrite the syscall result. */ + goto end; + } + +#define SYSARG_ADDR(n) (args_addr + ((n) - 1) * sizeof_word(tracee)) + +#define POKE_WORD(addr, value) \ + poke_word(tracee, addr, value); \ + if (errno != 0) { \ + status = -errno; \ + break; \ + } + +#define PEEK_WORD(addr) \ + peek_word(tracee, addr); \ + if (errno != 0) { \ + status = -errno; \ + break; \ + } + + case PR_socketcall: { + word_t args_addr; + word_t sock_addr; + word_t size_addr; + word_t max_size; + + args_addr = peek_reg(tracee, ORIGINAL, SYSARG_2); + + switch (peek_reg(tracee, ORIGINAL, SYSARG_1)) { + case SYS_ACCEPT: + case SYS_ACCEPT4: + /* Nothing special to do if no sockaddr was specified. */ + sock_addr = PEEK_WORD(SYSARG_ADDR(2)); + if (sock_addr == 0) + goto end; + /* Fall through. */ + case SYS_GETSOCKNAME: + case SYS_GETPEERNAME: + /* Handle these cases below. */ + status = 1; + break; + + case SYS_BIND: + case SYS_CONNECT: + /* Restore the initial parameters: this memory was + * overwritten at the enter stage. Remember: POKE_WORD + * puts -errno in status and breaks if an error + * occured. */ + POKE_WORD(SYSARG_ADDR(2), peek_reg(tracee, MODIFIED, SYSARG_5)); + POKE_WORD(SYSARG_ADDR(3), peek_reg(tracee, MODIFIED, SYSARG_6)); + + status = 0; + break; + + default: + status = 0; + break; + } + + /* Error reported by the kernel or there's nothing else to do. */ + if ((int) syscall_result < 0 || status == 0) + goto end; + + /* An error occured in SYS_BIND or SYS_CONNECT. */ + if (status < 0) + break; + + /* Remember: PEEK_WORD puts -errno in status and breaks if an + * error occured. */ + sock_addr = PEEK_WORD(SYSARG_ADDR(2)); + size_addr = PEEK_WORD(SYSARG_ADDR(3)); + max_size = peek_reg(tracee, MODIFIED, SYSARG_6); + + status = translate_socketcall_exit(tracee, sock_addr, size_addr, max_size); + if (status < 0) + break; + + /* Don't overwrite the syscall result. */ + goto end; + } + +#undef SYSARG_ADDR +#undef PEEK_WORD +#undef POKE_WORD + + case PR_fchdir: + case PR_chdir: + /* These syscalls are voided in enter.c; make sure the + * tracee always sees a 0 return value even on kernels where + * the SYSCALL_AVOIDER trick leaks -ENOSYS through. */ + case PR_unshare: + case PR_setns: + case PR_mount: + case PR_umount: + case PR_umount2: + case PR_pivot_root: + /* These syscalls are fully emulated, see enter.c for details + * (like errors). */ + status = 0; + break; + + case PR_rename: + case PR_renameat: { + char old_path[PATH_MAX]; + char new_path[PATH_MAX]; + ssize_t old_length; + ssize_t new_length; + Comparison comparison; + Reg old_reg; + Reg new_reg; + char *tmp; + + /* Error reported by the kernel. */ + if ((int) syscall_result < 0) + goto end; + + if (syscall_number == PR_rename) { + old_reg = SYSARG_1; + new_reg = SYSARG_2; + } + else { + old_reg = SYSARG_2; + new_reg = SYSARG_4; + } + + /* Get the old path, then convert it to the same + * "point-of-view" as tracee->fs->cwd (guest). */ + status = read_path(tracee, old_path, peek_reg(tracee, MODIFIED, old_reg)); + if (status < 0) + break; + + status = detranslate_path(tracee, old_path, NULL); + if (status < 0) + break; + old_length = (status > 0 ? status - 1 : (ssize_t) strlen(old_path)); + + /* Nothing special to do if the moved path is not the + * current working directory. */ + comparison = compare_paths(old_path, tracee->fs->cwd); + if (comparison != PATH1_IS_PREFIX && comparison != PATHS_ARE_EQUAL) { + status = 0; + break; + } + + /* Get the new path, then convert it to the same + * "point-of-view" as tracee->fs->cwd (guest). */ + status = read_path(tracee, new_path, peek_reg(tracee, MODIFIED, new_reg)); + if (status < 0) + break; + + status = detranslate_path(tracee, new_path, NULL); + if (status < 0) + break; + new_length = (status > 0 ? status - 1 : (ssize_t) strlen(new_path)); + + /* Sanity check. */ + if (strlen(tracee->fs->cwd) >= PATH_MAX) { + status = 0; + break; + } + strcpy(old_path, tracee->fs->cwd); + + /* Update the virtual current working directory. */ + substitute_path_prefix(old_path, old_length, new_path, new_length); + + tmp = talloc_strdup(tracee->fs, old_path); + if (tmp == NULL) { + status = -ENOMEM; + break; + } + + TALLOC_FREE(tracee->fs->cwd); + tracee->fs->cwd = tmp; + + status = 0; + break; + } + + case PR_readlink: + case PR_readlinkat: { + char referee[PATH_MAX]; + char referer[PATH_MAX]; + size_t old_size; + size_t new_size; + size_t max_size; + word_t input; + word_t output; + + /* Error reported by the kernel. */ + if ((int) syscall_result < 0) + goto end; + + old_size = syscall_result; + + if (syscall_number == PR_readlink) { + output = peek_reg(tracee, ORIGINAL, SYSARG_2); + max_size = peek_reg(tracee, ORIGINAL, SYSARG_3); + input = peek_reg(tracee, MODIFIED, SYSARG_1); + } + else { + output = peek_reg(tracee, ORIGINAL, SYSARG_3); + max_size = peek_reg(tracee, ORIGINAL, SYSARG_4); + input = peek_reg(tracee, MODIFIED, SYSARG_2); + } + + if (max_size > PATH_MAX) + max_size = PATH_MAX; + + if (max_size == 0) { + status = -EINVAL; + break; + } + + /* The kernel does NOT put the NULL terminating byte for + * readlink(2). */ + status = read_data(tracee, referee, output, old_size); + if (status < 0) + break; + referee[old_size] = '\0'; + + /* Not optimal but safe (path is fully translated). */ + status = read_path(tracee, referer, input); + if (status < 0) + break; + + if (status >= PATH_MAX) { + status = -ENAMETOOLONG; + break; + } + + if (status == 1) { + /* Empty path was passed (""), + * indicating that path is pointed to by fd passed in first argument */ + word_t dirfd = peek_reg(tracee, ORIGINAL, SYSARG_1); + if (syscall_number == PR_readlink || dirfd < 0) { + status = -EBADF; + break; + } + status = readlink_proc_pid_fd(tracee->pid, dirfd, referer); + if (status < 0) + break; + } + + /* If the kernel filled the whole output buffer, the symlink + * content was truncated to fit. Detranslating a truncated + * host path yields a wrong, wrongly-short guest path -- and + * callers that only enlarge their buffer when readlink(2) + * returns exactly the buffer size (bubblewrap's + * readlink_malloc, glibc realpath, ...) never notice and + * silently use the broken path. Host paths are much longer + * than the guest paths they map to (deep proot-distro rootfs + * prefix), so short guest targets truncate easily. Re-read + * the link with a full-size buffer (referer is the translated + * host path) so the detranslation below sees the real target; + * readlink() simply fails for a non-symlink referer, leaving + * the original content untouched. */ + if (old_size == max_size) { + ssize_t full = readlink(referer, referee, sizeof(referee) - 1); + if (full > 0) { + referee[full] = '\0'; + old_size = (size_t) full; + } + } + + status = detranslate_path(tracee, referee, referer); + if (status < 0) + break; + + /* The original path doesn't require any transformation, i.e + * it is a symetric binding. */ + if (status == 0) + goto end; + + /* Overwrite the path. Note: the output buffer might be + * initialized with zeros but it was updated with the kernel + * result, and then with the detranslated result. This later + * might be shorter than the former, so it's safier to add a + * NULL terminating byte when possible. This problem was + * exposed by IDA Demo 6.3. */ + if ((size_t) status < max_size) { + new_size = status - 1; + status = write_data(tracee, output, referee, status); + } + else { + new_size = max_size; + status = write_data(tracee, output, referee, max_size); + } + if (status < 0) + break; + + /* The value of "status" is used to update the returned value + * in translate_syscall_exit(). */ + status = new_size; + break; + } + +#if defined(ARCH_X86_64) + case PR_uname: { + struct utsname utsname; + word_t address; + size_t size; + + if (get_abi(tracee) != ABI_2) + goto end; + + /* Error reported by the kernel. */ + if ((int) syscall_result < 0) + goto end; + + address = peek_reg(tracee, ORIGINAL, SYSARG_1); + + status = read_data(tracee, &utsname, address, sizeof(utsname)); + if (status < 0) + break; + + /* Some 32-bit programs like package managers can be + * confused when the kernel reports "x86_64". */ + size = sizeof(utsname.machine); + strncpy(utsname.machine, "i686", size); + utsname.machine[size - 1] = '\0'; + + status = write_data(tracee, address, &utsname, sizeof(utsname)); + if (status < 0) + break; + + status = 0; + break; + } +#endif + + case PR_execve: + case PR_execveat: + translate_execve_exit(tracee); + goto end; + + case PR_openat2: + case PR_openat: + case PR_open: { + /* Track /proc/self/auxv opens so read() results can be patched. + * Needed on kernels < 6.4 where prctl(PR_GET_AUXV) is absent and + * rustix falls back to reading /proc/self/auxv directly. */ + char path_buf[sizeof("/proc/self/auxv")]; + Reg path_reg = (syscall_number == PR_open) ? SYSARG_1 : SYSARG_2; + + if ((int) syscall_result < 0) + goto end; + if (tracee->execfn_addr == 0) + goto end; + if (read_string(tracee, path_buf, + peek_reg(tracee, ORIGINAL, path_reg), + sizeof(path_buf)) <= 0) + goto end; + if (strcmp(path_buf, "/proc/self/auxv") != 0) + goto end; + + tracee->auxv_fd = (int) syscall_result; + tracee->sysexit_pending = true; + tracee->restart_how = PTRACE_SYSCALL; + goto end; + } + + case PR_read: { + /* Patch AT_EXECFN in data read from /proc/self/auxv. */ + word_t fd, buf_addr, result, offset, entry_size, type; + + if (tracee->auxv_fd < 0 || tracee->execfn_addr == 0) + goto end; + + result = syscall_result; + if ((word_t) result == 0 || (ssize_t) result < 0) + goto end; + + fd = peek_reg(tracee, ORIGINAL, SYSARG_1); + if ((int) fd != tracee->auxv_fd) + goto end; + + buf_addr = peek_reg(tracee, ORIGINAL, SYSARG_2); + entry_size = 2 * sizeof_word(tracee); + + for (offset = 0; offset + entry_size <= result; offset += entry_size) { + errno = 0; + type = peek_word(tracee, buf_addr + offset); + if (errno != 0) + break; + if (type == AT_NULL) + break; + if (type == AT_EXECFN) { + poke_word(tracee, buf_addr + offset + sizeof_word(tracee), + tracee->execfn_addr); + break; + } + } + + /* Stay in PTRACE_SYSCALL mode to intercept close(auxv_fd). */ + tracee->sysexit_pending = true; + tracee->restart_how = PTRACE_SYSCALL; + goto end; + } + + case PR_prctl: { +#ifndef PR_GET_AUXV +#define PR_GET_AUXV 0x41555856 +#endif + word_t option; + word_t buf_addr; + word_t buf_max; + word_t offset; + word_t entry_size; + word_t type; + + option = peek_reg(tracee, ORIGINAL, SYSARG_1); + + /* Record the tracee's own request for the "no new privileges" + * flag so a later PR_GET_NO_NEW_PRIVS (answered at sysenter) + * reports the guest's intent rather than the flag PRoot set + * itself. A successful call implies arg2 == 1, the only value + * the kernel accepts. PRoot sets the real flag in the launch + * child before the initial execve (see enable_syscall_filtering), + * so only count calls made once the guest program is running + * (tracee->seen_execve); the flag is a one-way latch and is + * never cleared, matching the kernel's fork/execve semantics. */ + if (option == PR_SET_NO_NEW_PRIVS) { + if (tracee->seen_execve && (int) syscall_result == 0) + tracee->no_new_privs = true; + goto end; + } + + /* Only intercept PR_GET_AUXV. */ + if (option != PR_GET_AUXV) + goto end; + + /* Error or no execfn to fix: nothing to do. */ + if ((int) syscall_result < 0) + goto end; + if (tracee->execfn_addr == 0) + goto end; + + /* PR_GET_AUXV returns the auxv size; if it exceeds the buffer + * arg, the kernel did not write anything (buffer too small). */ + buf_max = peek_reg(tracee, ORIGINAL, SYSARG_3); + if (syscall_result > buf_max) + goto end; + + /* Scan the returned auxv buffer for AT_EXECFN and patch its + * value to point to argv[0] instead of the loader temp file. */ + buf_addr = peek_reg(tracee, ORIGINAL, SYSARG_2); + entry_size = 2 * sizeof_word(tracee); + + for (offset = 0; offset + entry_size <= syscall_result; offset += entry_size) { + errno = 0; + type = peek_word(tracee, buf_addr + offset); + if (errno != 0) + break; + if (type == AT_NULL) + break; + if (type == AT_EXECFN) { + poke_word(tracee, buf_addr + offset + sizeof_word(tracee), + tracee->execfn_addr); + break; + } + } + goto end; + } + + case PR_ptrace: + status = translate_ptrace_exit(tracee); + break; + + case PR_wait4: + case PR_waitpid: + if (tracee->as_ptracer.waits_in != WAITS_IN_PROOT) + goto end; + + status = translate_wait_exit(tracee); + break; + + case PR_setrlimit: + case PR_prlimit64: + /* Error reported by the kernel. */ + if ((int) syscall_result < 0) + goto end; + + status = translate_setrlimit_exit(tracee, syscall_number == PR_prlimit64); + if (status < 0) + break; + + /* Don't overwrite the syscall result. */ + goto end; + + case PR_utime: + if ((int) syscall_result == -ENOSYS) + { + fix_and_restart_enosys_syscall(tracee); + } + goto end; + + case PR_statfs: + case PR_statfs64: { + /* Possibly fake that /dev/shm is living on tmpfs */ + char devshm_path[PATH_MAX]; + char statfs_path[PATH_MAX]; + + /* Only perform changes to result of successful syscall + * (that is, path was valid, it doesn't have to point + * to mount root) */ + if (syscall_result != 0) { + goto end; + } + + if (translate_path(tracee, devshm_path, AT_FDCWD, "/dev/shm", true) < 0) { + VERBOSE(tracee, 5, "/dev/shm is not mounted, not changing statfs() result"); + goto end; + } + + if (read_path(tracee, statfs_path, peek_reg(tracee, MODIFIED, SYSARG_1)) < 0) { + VERBOSE(tracee, 5, "statfs() exit couldn't read statfs_path"); + goto end; + } + + Comparison comparison = compare_paths(devshm_path, statfs_path); + if (comparison == PATHS_ARE_EQUAL || comparison == PATH1_IS_PREFIX) { + VERBOSE(tracee, 5, "Updating statfs() result to fake tmpfs /dev/shm"); + /* Write TMPFS_MAGIC at beginning of statfs structure. + * + * (It's at beginning of structure regardless of syscall variant + * (statfs vs statfs64) and architecture bitness + * (on 64 bit this field is 8 bytes long, but as long as it's + * little endian, it will need only first 4 bytes to be modified, + * as next 4 bytes will always be 0)) + * */ + word_t stat_addr = peek_reg(tracee, ORIGINAL, syscall_number == PR_statfs64 ? SYSARG_3 : SYSARG_2); + int write_status = write_data(tracee, stat_addr, "\x94\x19\x02\x01", 4); + if (write_status < 0) { + VERBOSE(tracee, 5, "Updating statfs() result failed"); + } + } + else { + VERBOSE(tracee, 5, "statfs() not for /dev/shm, not changing result"); + } + + goto end; + } + + case PR_statx: + status = handle_statx_syscall(tracee, false); + break; + + case PR_ioctl: + if (peek_reg(tracee, ORIGINAL, SYSARG_2) == _IOW(0x94, 9, int) /* FICLONE */ && + (int) peek_reg(tracee, CURRENT, SYSARG_RESULT) == -EACCES) { + poke_reg(tracee, SYSARG_RESULT, -EOPNOTSUPP); + } + goto end; + + case PR_socket: + /* Record the fd we substituted for an AF_NETLINK request. */ + if (tracee->pending_fake_netlink_socket) { + int fd = (int) peek_reg(tracee, CURRENT, SYSARG_RESULT); + if (fd >= 0) { + int i; + if (tracee->fake_netlink_fds_count < MAX_FAKE_NETLINK_FDS) { + /* Avoid duplicates. */ + bool present = false; + for (i = 0; i < tracee->fake_netlink_fds_count; i++) { + if (tracee->fake_netlink_fds[i] == fd) { + present = true; + break; + } + } + if (!present) + tracee->fake_netlink_fds[tracee->fake_netlink_fds_count++] = fd; + } + } + tracee->pending_fake_netlink_socket = false; + } + goto end; + + default: + goto end; + } + + poke_reg(tracee, SYSARG_RESULT, (word_t) status); + +end: + status = notify_extensions(tracee, SYSCALL_EXIT_END, 0, 0); + if (status < 0) + poke_reg(tracee, SYSARG_RESULT, (word_t) status); +} diff --git a/core/proot/src/main/cpp/syscall/heap.c b/core/proot/src/main/cpp/syscall/heap.c new file mode 100644 index 000000000..adbb29031 --- /dev/null +++ b/core/proot/src/main/cpp/syscall/heap.c @@ -0,0 +1,213 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#include /* PROT_*, MAP_*, */ +#include /* assert(3), */ +#include /* strerror(3), */ +#include /* sysconf(3), */ +#include /* MIN(), MAX(), */ + +#include "tracee/tracee.h" +#include "tracee/reg.h" +#include "tracee/mem.h" +#include "syscall/sysnum.h" +#include "execve/execve.h" +#include "cli/note.h" + +#include "compat.h" + +#define DEBUG_BRK(...) /* fprintf(stderr, __VA_ARGS__) */ + +/* The size of the heap can be zero, unlike the size of a memory + * mapping. As a consequence, the first page of the "heap" memory + * mapping is discarded in order to emulate an empty heap. */ +static word_t heap_offset = 0; + +/** + * Put @tracee's heap to a reliable location. By default the Linux + * kernel puts it near loader's BSS, but this default location is not + * reliable since the kernel might put another memory mapping right + * after it (ie. continuously). In this case, @tracee's heap can't + * grow anymore and some programs like Bash will abort. This issue + * can be reproduced when using a Ubuntu 12.04 x86_64 rootfs on RHEL 5 + * x86_64. + */ +void translate_brk_enter(Tracee *tracee) +{ + word_t new_brk_address; + size_t old_heap_size; + size_t new_heap_size; + + if (tracee->heap->disabled) + return; + + if (heap_offset == 0) { + heap_offset = sysconf(_SC_PAGE_SIZE); + if ((int) heap_offset <= 0) + heap_offset = 0x1000; + } + + new_brk_address = peek_reg(tracee, CURRENT, SYSARG_1); + DEBUG_BRK("brk(0x%lx)\n", new_brk_address); + + /* Allocate a new mapping for the emulated heap. */ + if (tracee->heap->base == 0) { + Sysnum sysnum; + Mapping *mappings; + Mapping *bss; + + /* From PRoot's point-of-view this is the first time this + * tracee calls brk(2), although an address was specified. + * This is not supposed to happen the first time. It is + * likely because this tracee is the very first child of PRoot + * but the first execve(2) didn't happen yet (so this is not + * its first call to brk(2)). For instance, the installation + * of seccomp filters is made after this very first process is + * traced, and might call malloc(3) before the first + * execve(2). */ + if (new_brk_address != 0) { + if (tracee->verbose > 0) + note(tracee, WARNING, INTERNAL, + "process %d is doing suspicious brk()", tracee->pid); + return; + } + + /* Put the heap as close to the BSS as possible since + * some programs -- like dump-emacs -- assume the gap + * between the end of the BSS and the start of the + * heap is relatively small (ie. < 1MB) even if ALSR + * is enabled. Note that bss->addr + bss->length is + * naturally aligned to a page boundary according to + * add_mapping() in execve/enter.c, ie. no need to + * align new_brk_address again. Now, the gap between + * the BSS and the heap is only "heap_offset" bytes + * long. To emulate ADDR_NO_RANDOMIZE personality, + * this gap should be removed (not yet supported). */ + mappings = tracee->load_info->mappings; + bss = &mappings[talloc_array_length(mappings) - 1]; + new_brk_address = bss->addr + bss->length; + + /* I don't understand yet why mmap(2) fails (EFAULT) + * on architectures that also have mmap2(2). Maybe + * this former implies MAP_FIXED in such cases. */ + sysnum = detranslate_sysnum(get_abi(tracee), PR_mmap2) != SYSCALL_AVOIDER + ? PR_mmap2 + : PR_mmap; + + set_sysnum(tracee, sysnum); + poke_reg(tracee, SYSARG_1 /* address */, new_brk_address); + poke_reg(tracee, SYSARG_2 /* length */, heap_offset); + poke_reg(tracee, SYSARG_3 /* prot */, PROT_READ | PROT_WRITE); + poke_reg(tracee, SYSARG_4 /* flags */, MAP_PRIVATE | MAP_ANONYMOUS); + poke_reg(tracee, SYSARG_5 /* fd */, -1); + poke_reg(tracee, SYSARG_6 /* offset */, 0); + + return; + } + + /* The size of the heap can't be negative. */ + if (new_brk_address < tracee->heap->base) { + set_sysnum(tracee, PR_void); + return; + } + + new_heap_size = new_brk_address - tracee->heap->base; + old_heap_size = tracee->heap->size; + + /* Actually resizing. */ + set_sysnum(tracee, PR_mremap); + poke_reg(tracee, SYSARG_1 /* old_address */, tracee->heap->base - heap_offset); + poke_reg(tracee, SYSARG_2 /* old_size */, old_heap_size + heap_offset); + poke_reg(tracee, SYSARG_3 /* new_size */, new_heap_size + heap_offset); + poke_reg(tracee, SYSARG_4 /* flags */, 0); + poke_reg(tracee, SYSARG_5 /* new_address */, 0); + + return; +} + +/** + * c.f. function above. + */ +void translate_brk_exit(Tracee *tracee) +{ + word_t result; + word_t sysnum; + int tracee_errno; + + if (tracee->heap->disabled) + return; + + assert(heap_offset > 0); + + sysnum = get_sysnum(tracee, MODIFIED); + result = peek_reg(tracee, CURRENT, SYSARG_RESULT); + tracee_errno = (int) result; + + switch (sysnum) { + case PR_void: + poke_reg(tracee, SYSARG_RESULT, tracee->heap->base + tracee->heap->size); + break; + + case PR_mmap: + case PR_mmap2: + /* On error, mmap(2) returns -errno (the last 4k is + * reserved for this), whereas brk(2) returns the + * previous value. */ + if (tracee_errno < 0 && tracee_errno > -4096) { + poke_reg(tracee, SYSARG_RESULT, 0); + break; + } + + tracee->heap->base = result + heap_offset; + tracee->heap->size = 0; + + poke_reg(tracee, SYSARG_RESULT, tracee->heap->base + tracee->heap->size); + break; + + case PR_mremap: + /* On error, mremap(2) returns -errno (the last 4k is + * reserved this), whereas brk(2) returns the previous + * value. */ + if ( (tracee_errno < 0 && tracee_errno > -4096) + || (tracee->heap->base != result + heap_offset)) { + poke_reg(tracee, SYSARG_RESULT, tracee->heap->base + tracee->heap->size); + break; + } + + tracee->heap->size = peek_reg(tracee, MODIFIED, SYSARG_3) - heap_offset; + + poke_reg(tracee, SYSARG_RESULT, tracee->heap->base + tracee->heap->size); + break; + + case PR_brk: + /* Is it confirmed that this suspicious call to brk(2) + * is actually legit? */ + if (result == peek_reg(tracee, ORIGINAL, SYSARG_1)) + tracee->heap->disabled = true; + break; + + default: + assert(0); + } + + DEBUG_BRK("brk() = 0x%lx\n", peek_reg(tracee, CURRENT, SYSARG_RESULT)); +} diff --git a/core/proot/src/main/cpp/syscall/heap.h b/core/proot/src/main/cpp/syscall/heap.h new file mode 100644 index 000000000..809783425 --- /dev/null +++ b/core/proot/src/main/cpp/syscall/heap.h @@ -0,0 +1,31 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#ifndef HEAP_H +#define HEAP_H + +#include "tracee/tracee.h" + +extern void translate_brk_enter(Tracee *tracee); +extern void translate_brk_exit(Tracee *tracee); + +#endif /* HEAP_H */ diff --git a/core/proot/src/main/cpp/syscall/rlimit.c b/core/proot/src/main/cpp/syscall/rlimit.c new file mode 100644 index 000000000..d16784343 --- /dev/null +++ b/core/proot/src/main/cpp/syscall/rlimit.c @@ -0,0 +1,117 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#include /* bool, */ +#include /* prlimit(2), */ +#include /* prlimit(2), */ + +#include "tracee/tracee.h" +#include "tracee/reg.h" +#include "tracee/mem.h" +#include "tracee/abi.h" +#include "cli/note.h" + +/** + * Set PRoot's stack soft limit to @tracee's one if this latter is + * greater. This allows to workaround a Linux kernel bug that + * prevents a tracer to access a tracee's stack beyond its last mapped + * page, as it might by the case under PRoot. This function returns + * -errno if an error occurred, otherwise 0. + * + * Details: when a tracer tries to access a tracee's stack beyond its + * last mapped page, the Linux kernel should be able to increase + * tracee's stack up to its soft limit. Unfortunately the Linux + * kernel checks the limit of the tracer instead the limit of the + * tracee. This bug was exposed using UMEQ under PRoot. + * + * Ref.: https://bugzilla.kernel.org/show_bug.cgi?id=91791 + * + * Three strategies were possible: + * + * - set PRoot's stack soft limit to the hard limit; this might make + * the system collapse if PRoot starts to recurses indefinitely. + * + * - as it's done here; this appears to be a good compromise between + * the strategy above and the one below. + * + * - as it's done here + reduce PRoot's stack soft limit as soon as + * it's possible; this would be overly complicated. + */ +int translate_setrlimit_exit(const Tracee *tracee, bool is_prlimit) +{ + struct rlimit64 proot_stack; + word_t resource; + word_t address; + word_t tracee_stack_limit; + Reg sysarg; + int status; + + sysarg = (is_prlimit ? SYSARG_2 : SYSARG_1); + + resource = peek_reg(tracee, ORIGINAL, sysarg); + address = peek_reg(tracee, ORIGINAL, sysarg + 1); + + /* Not the resource we're looking for? */ + if (resource != RLIMIT_STACK) + return 0; + + /* Retrieve new tracee's stack limit. */ + if (is_prlimit) { + /* Not the prlimit usage we're looking for? */ + if (address == 0) + return 0; + + tracee_stack_limit = peek_uint64(tracee, address); + } + else { + tracee_stack_limit = peek_word(tracee, address); + + /* Convert this special value from 32-bit to 64-bit, + * if needed. */ + if (is_32on64_mode(tracee) && tracee_stack_limit == (uint32_t) -1) + tracee_stack_limit = RLIM_INFINITY; + } + if (errno != 0) + return -errno; + + /* Get current PRoot's stack limit. */ + status = prlimit64(0, RLIMIT_STACK, NULL, &proot_stack); + if (status < 0) { + VERBOSE(tracee, 1, "can't get stack limit."); + return 0; /* Not fatal. */ + } + + /* No need to increase current PRoot's stack limit? */ + if (proot_stack.rlim_cur >= tracee_stack_limit) + return 0; + + proot_stack.rlim_cur = tracee_stack_limit; + + /* Increase current PRoot's stack limit. */ + status = prlimit64(0, RLIMIT_STACK, &proot_stack, NULL); + if (status < 0) + VERBOSE(tracee, 1, "can't set stack limit."); + return 0; /* Not fatal. */ + + VERBOSE(tracee, 1, "stack soft limit increased to %llu bytes", proot_stack.rlim_cur); + return 0; +} diff --git a/core/proot/src/main/cpp/syscall/rlimit.h b/core/proot/src/main/cpp/syscall/rlimit.h new file mode 100644 index 000000000..779202354 --- /dev/null +++ b/core/proot/src/main/cpp/syscall/rlimit.h @@ -0,0 +1,31 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#ifndef RLIMIT_H +#define RLIMIT_H + +#include +#include "tracee/tracee.h" + +extern int translate_setrlimit_exit(const Tracee *tracee, bool is_prlimit); + +#endif /* RLIMIT_H */ diff --git a/core/proot/src/main/cpp/syscall/seccomp.c b/core/proot/src/main/cpp/syscall/seccomp.c new file mode 100644 index 000000000..2f8930ea3 --- /dev/null +++ b/core/proot/src/main/cpp/syscall/seccomp.c @@ -0,0 +1,535 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#include "build.h" +#include "arch.h" + +#if defined(HAVE_SECCOMP_FILTER) + +#include /* prctl(2), PR_* */ +#include /* struct sock_*, */ +#include /* SECCOMP_MODE_FILTER, */ +#include /* struct sock_*, */ +#include /* AUDIT_, */ +#include /* LIST_FOREACH, */ +#include /* size_t, */ +#include /* talloc_*, */ +#include /* E*, */ +#include /* memcpy(3), */ +#include /* offsetof(3), */ +#include /* uint*_t, UINT*_MAX, */ +#include /* assert(3), */ + +#include "syscall/seccomp.h" +#include "tracee/tracee.h" +#include "syscall/syscall.h" +#include "syscall/sysnum.h" +#include "extension/extension.h" +#include "cli/note.h" + +#include "compat.h" +#include "attribute.h" + +#define DEBUG_FILTER(...) /* fprintf(stderr, __VA_ARGS__) */ + +/** + * Allocate an empty @program->filter. This function returns -errno + * if an error occurred, otherwise 0. + */ +static int new_program_filter(struct sock_fprog *program) +{ + program->filter = talloc_array(NULL, struct sock_filter, 0); + if (program->filter == NULL) + return -ENOMEM; + + program->len = 0; + return 0; +} + +/** + * Append to @program->filter the given @statements (@nb_statements + * items). This function returns -errno if an error occurred, + * otherwise 0. + */ +static int add_statements(struct sock_fprog *program, size_t nb_statements, + struct sock_filter *statements) +{ + size_t length; + void *tmp; + size_t i; + + length = talloc_array_length(program->filter); + tmp = talloc_realloc(NULL, program->filter, struct sock_filter, length + nb_statements); + if (tmp == NULL) + return -ENOMEM; + program->filter = tmp; + + for (i = 0; i < nb_statements; i++, length++) + memcpy(&program->filter[length], &statements[i], sizeof(struct sock_filter)); + + return 0; +} + +/** + * Append to @program->filter the statements required to notify PRoot + * about the given @syscall made by a tracee, with the given @flag. + * This function returns -errno if an error occurred, otherwise 0. + */ +static int add_trace_syscall(struct sock_fprog *program, word_t syscall, int flag) +{ + int status; + + /* Sanity check. */ + if (syscall > UINT32_MAX) + return -ERANGE; + + #define LENGTH_TRACE_SYSCALL 2 + struct sock_filter statements[LENGTH_TRACE_SYSCALL] = { + /* Compare the accumulator with the expected syscall: + * skip the next statement if not equal. */ + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, syscall, 0, 1), + + /* Notify the tracer. */ + BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_TRACE + flag) + }; + + DEBUG_FILTER("FILTER: trace if syscall == %ld\n", syscall); + + status = add_statements(program, LENGTH_TRACE_SYSCALL, statements); + if (status < 0) + return status; + + return 0; +} + +/** + * Append to @program->filter the statements that allow anything (if + * unfiltered). Note that @nb_traced_syscalls is used to make a + * sanity check. This function returns -errno if an error occurred, + * otherwise 0. + */ +static int end_arch_section(struct sock_fprog *program, size_t nb_traced_syscalls) +{ + int status; + + #define LENGTH_END_SECTION 1 + struct sock_filter statements[LENGTH_END_SECTION] = { + BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW) + }; + + DEBUG_FILTER("FILTER: allow\n"); + + status = add_statements(program, LENGTH_END_SECTION, statements); + if (status < 0) + return status; + + /* Sanity check, see start_arch_section(). */ + if ( talloc_array_length(program->filter) - program->len + != LENGTH_END_SECTION + nb_traced_syscalls * LENGTH_TRACE_SYSCALL) + return -ERANGE; + + return 0; +} + +/** + * Append to @program->filter the statements that check the current + * @architecture. Note that @nb_traced_syscalls is used to make a + * sanity check. This function returns -errno if an error occurred, + * otherwise 0. + */ +static int start_arch_section(struct sock_fprog *program, uint32_t arch, size_t nb_traced_syscalls) +{ + const size_t arch_offset = offsetof(struct seccomp_data, arch); + const size_t syscall_offset = offsetof(struct seccomp_data, nr); + const size_t section_length = LENGTH_END_SECTION + + nb_traced_syscalls * LENGTH_TRACE_SYSCALL; + int status; + + /* Sanity checks. */ + if ( arch_offset > UINT32_MAX + || syscall_offset > UINT32_MAX + || section_length > UINT32_MAX - 1) + return -ERANGE; + + #define LENGTH_START_SECTION 4 + struct sock_filter statements[LENGTH_START_SECTION] = { + /* Load the current architecture into the + * accumulator. */ + BPF_STMT(BPF_LD + BPF_W + BPF_ABS, arch_offset), + + /* Compare the accumulator with the expected + * architecture: skip the following statement if + * equal. */ + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, arch, 1, 0), + + /* This is not the expected architecture, so jump + * unconditionally to the end of this section. */ + BPF_STMT(BPF_JMP + BPF_JA + BPF_K, section_length + 1), + + /* This is the expected architecture, so load the + * current syscall into the accumulator. */ + BPF_STMT(BPF_LD + BPF_W + BPF_ABS, syscall_offset) + }; + + DEBUG_FILTER("FILTER: if arch == %ld, up to %zdth statement\n", + arch, nb_traced_syscalls); + + status = add_statements(program, LENGTH_START_SECTION, statements); + if (status < 0) + return status; + + /* See the sanity check in end_arch_section(). */ + program->len = talloc_array_length(program->filter); + + return 0; +} + +/** + * Append to @program->filter the statements that forbid anything (if + * unfiltered) and update @program->len. This function returns -errno + * if an error occurred, otherwise 0. + */ +static int finalize_program_filter(struct sock_fprog *program) +{ + int status; + + #define LENGTH_FINALIZE 1 + struct sock_filter statements[LENGTH_FINALIZE] = { + BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_KILL) + }; + + DEBUG_FILTER("FILTER: kill\n"); + + status = add_statements(program, LENGTH_FINALIZE, statements); + if (status < 0) + return status; + + program->len = talloc_array_length(program->filter); + + return 0; +} + +/** + * Free @program->filter and set @program->len to 0. + */ +static void free_program_filter(struct sock_fprog *program) +{ + TALLOC_FREE(program->filter); + program->len = 0; +} + +/** + * Convert the given @sysnums into BPF filters according to the + * following pseudo-code, then enabled them for the given @tracee and + * all of its future children: + * + * for each handled architectures + * for each filtered syscall + * trace + * allow + * kill + * + * This function returns -errno if an error occurred, otherwise 0. + */ +static int set_seccomp_filters(const FilteredSysnum *sysnums) +{ + SeccompArch seccomp_archs[] = SECCOMP_ARCHS; + size_t nb_archs = sizeof(seccomp_archs) / sizeof(SeccompArch); + + struct sock_fprog program = { .len = 0, .filter = NULL }; + size_t nb_traced_syscalls; + size_t i, j, k; + int status; + + status = new_program_filter(&program); + if (status < 0) + goto end; + + /* For each handled architectures */ + for (i = 0; i < nb_archs; i++) { + word_t syscall; + + nb_traced_syscalls = 0; + + /* Pre-compute the number of traced syscalls for this architecture. */ + for (j = 0; j < seccomp_archs[i].nb_abis; j++) { + for (k = 0; sysnums[k].value != PR_void; k++) { + syscall = detranslate_sysnum(seccomp_archs[i].abis[j], sysnums[k].value); + if (syscall != SYSCALL_AVOIDER) + nb_traced_syscalls++; + } + } + + /* Filter: if handled architecture */ + status = start_arch_section(&program, seccomp_archs[i].value, nb_traced_syscalls); + if (status < 0) + goto end; + + for (j = 0; j < seccomp_archs[i].nb_abis; j++) { + for (k = 0; sysnums[k].value != PR_void; k++) { + /* Get the architecture specific syscall number. */ + syscall = detranslate_sysnum(seccomp_archs[i].abis[j], sysnums[k].value); + if (syscall == SYSCALL_AVOIDER) + continue; + + /* Filter: trace if handled syscall */ + status = add_trace_syscall(&program, syscall, sysnums[k].flags); + if (status < 0) + goto end; + } + } + + /* Filter: allow untraced syscalls for this architecture */ + status = end_arch_section(&program, nb_traced_syscalls); + if (status < 0) + goto end; + } + + status = finalize_program_filter(&program); + if (status < 0) + goto end; + + status = prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); + if (status < 0) + goto end; + + /* To output this BPF program for debug purpose: + * + * write(2, program.filter, program.len * sizeof(struct sock_filter)); + */ + + status = prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &program); + if (status < 0) + goto end; + + status = 0; +end: + free_program_filter(&program); + return status; +} + +/* List of sysnums handled by PRoot. */ +static FilteredSysnum proot_sysnums[] = { + { PR_accept, FILTER_SYSEXIT }, + { PR_accept4, FILTER_SYSEXIT }, + { PR_access, 0 }, + { PR_acct, 0 }, + { PR_bind, 0 }, + { PR_brk, FILTER_SYSEXIT }, + { PR_chdir, FILTER_SYSEXIT }, + { PR_chmod, 0 }, + { PR_chown, 0 }, + { PR_chown32, 0 }, + { PR_chroot, 0 }, + { PR_clone, 0 }, + { PR_clone3, 0 }, + { PR_close, 0 }, + { PR_connect, 0 }, + { PR_creat, 0 }, + { PR_recvfrom, 0 }, + { PR_recvmsg, 0 }, + { PR_sendmsg, 0 }, + { PR_sendto, 0 }, + { PR_socket, FILTER_SYSEXIT }, + { PR_execve, FILTER_SYSEXIT }, + { PR_execveat, FILTER_SYSEXIT }, + { PR_faccessat, 0 }, + { PR_faccessat2, FILTER_SYSEXIT }, + { PR_fchdir, FILTER_SYSEXIT }, + { PR_fchmodat, 0 }, + { PR_fchownat, 0 }, + { PR_fstatat64, 0 }, + { PR_futimesat, 0 }, + { PR_getcwd, FILTER_SYSEXIT }, + { PR_getpeername, FILTER_SYSEXIT }, + { PR_getsockname, FILTER_SYSEXIT }, + { PR_getxattr, 0 }, + { PR_inotify_add_watch, 0 }, +#ifdef __ANDROID__ + { PR_ioctl, FILTER_SYSEXIT }, +#endif + { PR_lchown, 0 }, + { PR_lchown32, 0 }, + { PR_lgetxattr, 0 }, + { PR_link, 0 }, + { PR_linkat, 0 }, + { PR_listxattr, 0 }, + { PR_llistxattr, 0 }, + { PR_lremovexattr, 0 }, + { PR_lsetxattr, 0 }, + { PR_lstat, 0 }, + { PR_lstat64, 0 }, +#ifdef __ANDROID__ + { PR_memfd_create, 0 }, +#endif + { PR_mkdir, 0 }, + { PR_mkdirat, 0 }, + { PR_mknod, 0 }, + { PR_mknodat, 0 }, + { PR_mount, FILTER_SYSEXIT }, + { PR_name_to_handle_at, 0 }, + { PR_newfstatat, 0 }, + { PR_oldlstat, 0 }, + { PR_oldstat, 0 }, + { PR_open, 0 }, + { PR_openat, 0 }, + { PR_openat2, 0 }, + { PR_pivot_root, FILTER_SYSEXIT }, + { PR_prctl, 0 }, + { PR_prlimit64, FILTER_SYSEXIT }, + { PR_ptrace, FILTER_SYSEXIT }, + { PR_readlink, FILTER_SYSEXIT }, + { PR_readlinkat, FILTER_SYSEXIT }, + { PR_removexattr, 0 }, + { PR_rename, FILTER_SYSEXIT }, + { PR_renameat, FILTER_SYSEXIT }, + { PR_renameat2, FILTER_SYSEXIT }, + { PR_rmdir, 0 }, + { PR_setrlimit, FILTER_SYSEXIT }, + { PR_setxattr, 0 }, + { PR_socketcall, FILTER_SYSEXIT }, + { PR_stat, 0 }, + { PR_stat64, 0 }, + { PR_statfs, FILTER_SYSEXIT }, + { PR_statfs64, FILTER_SYSEXIT }, + { PR_statx, FILTER_SYSEXIT }, + { PR_swapoff, 0 }, + { PR_swapon, 0 }, + { PR_symlink, 0 }, + { PR_symlinkat, 0 }, + { PR_truncate, 0 }, + { PR_truncate64, 0 }, + { PR_umount, FILTER_SYSEXIT }, + { PR_umount2, FILTER_SYSEXIT }, + { PR_uname, FILTER_SYSEXIT }, + { PR_unshare, FILTER_SYSEXIT }, + { PR_setns, FILTER_SYSEXIT }, + { PR_unlink, 0 }, + { PR_unlinkat, 0 }, + { PR_uselib, 0 }, + { PR_utime, FILTER_SYSEXIT }, + { PR_utimensat, 0 }, + { PR_utimes, 0 }, + { PR_wait4, FILTER_SYSEXIT }, + { PR_waitpid, FILTER_SYSEXIT }, + FILTERED_SYSNUM_END, +}; + +/** + * Add the @new_sysnums to the list of filtered @sysnums, using the + * given Talloc @context. This function returns -errno if an error + * occurred, otherwise 0. + */ +static int merge_filtered_sysnums(TALLOC_CTX *context, FilteredSysnum **sysnums, + const FilteredSysnum *new_sysnums) +{ + size_t i, j; + + assert(sysnums != NULL); + + if (*sysnums == NULL) { + /* Start with no sysnums but the terminator. */ + *sysnums = talloc_array(context, FilteredSysnum, 1); + if (*sysnums == NULL) + return -ENOMEM; + + (*sysnums)[0].value = PR_void; + } + + for (i = 0; new_sysnums[i].value != PR_void; i++) { + /* Search for the given sysnum. */ + for (j = 0; (*sysnums)[j].value != PR_void + && (*sysnums)[j].value != new_sysnums[i].value; j++) + ; + + if ((*sysnums)[j].value == PR_void) { + /* No such sysnum, allocate a new entry. */ + (*sysnums) = talloc_realloc(context, (*sysnums), FilteredSysnum, j + 2); + if ((*sysnums) == NULL) + return -ENOMEM; + + (*sysnums)[j] = new_sysnums[i]; + + /* The last item is the terminator. */ + (*sysnums)[j + 1].value = PR_void; + } + else + /* The sysnum is already filtered, merge the + * flags. */ + (*sysnums)[j].flags |= new_sysnums[i].flags; + } + + return 0; +} + +/** + * Tell the kernel to trace only syscalls handled by PRoot and its + * extensions. This filter will be enabled for the given @tracee and + * all of its future children. This function returns -errno if an + * error occurred, otherwise 0. + */ +int enable_syscall_filtering(const Tracee *tracee) +{ + FilteredSysnum *filtered_sysnums = NULL; + Extension *extension; + int status; + + assert(tracee != NULL && tracee->ctx != NULL); + + /* Add the sysnums required by PRoot to the list of filtered + * sysnums. TODO: only if path translation is required. */ + status = merge_filtered_sysnums(tracee->ctx, &filtered_sysnums, proot_sysnums); + if (status < 0) + return status; + + /* Merge the sysnums required by the extensions to the list + * of filtered sysnums. */ + if (tracee->extensions != NULL) { + LIST_FOREACH(extension, tracee->extensions, link) { + if (extension->filtered_sysnums == NULL) + continue; + + status = merge_filtered_sysnums(tracee->ctx, &filtered_sysnums, + extension->filtered_sysnums); + if (status < 0) + return status; + } + } + + status = set_seccomp_filters(filtered_sysnums); + if (status < 0) + return status; + + return 0; +} + +#else + +#include "tracee/tracee.h" +#include "attribute.h" + +int enable_syscall_filtering(const Tracee *tracee UNUSED) +{ + return 0; +} + +#endif /* defined(HAVE_SECCOMP_FILTER) */ diff --git a/core/proot/src/main/cpp/syscall/seccomp.h b/core/proot/src/main/cpp/syscall/seccomp.h new file mode 100644 index 000000000..3d5ba40af --- /dev/null +++ b/core/proot/src/main/cpp/syscall/seccomp.h @@ -0,0 +1,48 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#ifndef SECCOMP_H +#define SECCOMP_H + +#include "syscall/sysnum.h" +#include "tracee/tracee.h" +#include "attribute.h" +#include "arch.h" + +typedef struct { + Sysnum value; + word_t flags; +} FilteredSysnum; + +typedef struct { + unsigned int value; + size_t nb_abis; + Abi abis[NB_MAX_ABIS]; +} SeccompArch; + +#define FILTERED_SYSNUM_END { PR_void, 0 } + +#define FILTER_SYSEXIT 0x1 + +extern int enable_syscall_filtering(const Tracee *tracee); + +#endif /* SECCOMP_H */ diff --git a/core/proot/src/main/cpp/syscall/socket.c b/core/proot/src/main/cpp/syscall/socket.c new file mode 100644 index 000000000..ec7495093 --- /dev/null +++ b/core/proot/src/main/cpp/syscall/socket.c @@ -0,0 +1,216 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#include /* offsetof(3), */ +#include /* bzero(3), */ +#include /* strncpy(3), strlen(3), */ +#include /* assert(3), */ +#include /* E*, */ +#include /* struct sockaddr_un, AF_UNIX, */ +#include /* struct sockaddr_un, */ +#include /* MIN(), MAX(), */ + +#include "syscall/socket.h" +#include "tracee/tracee.h" +#include "tracee/mem.h" +#include "path/binding.h" +#include "path/temp.h" +#include "path/path.h" +#include "arch.h" + +#include "compat.h" + +/* The sockaddr_un structure has exactly the same layout on all + * architectures. */ +static const off_t offsetof_path = offsetof(struct sockaddr_un, sun_path); +extern struct sockaddr_un sockaddr_un__; +static const size_t sizeof_path = sizeof(sockaddr_un__.sun_path); + +/** + * Copy in @sockaddr the struct sockaddr_un stored in the @tracee + * memory at the given @address. Also, its pathname is copied to the + * null-terminated @path. Only @size bytes are read from the @tracee + * memory (should be <= @max_size <= sizeof(struct sockaddr_un)). + * This function returns -errno if an error occurred, 0 if the + * structure was not found (not a sockaddr_un or @size > @max_size), + * otherwise 1. + */ +static int read_sockaddr_un(Tracee *tracee, struct sockaddr_un *sockaddr, word_t max_size, + char path[PATH_MAX], word_t address, int size) +{ + int status; + + assert(max_size <= sizeof(struct sockaddr_un)); + + /* Nothing to do if the sockaddr has an unexpected size. */ + if (size <= offsetof_path || (word_t) size > max_size) + return 0; + + bzero(sockaddr, sizeof(struct sockaddr_un)); + status = read_data(tracee, sockaddr, address, size); + if (status < 0) + return status; + + /* Nothing to do if it's not a named Unix domain socket. */ + if ((sockaddr->sun_family != AF_UNIX) + || sockaddr->sun_path[0] == '\0') + return 0; + + /* Be careful: sun_path doesn't have to be null-terminated. */ + assert(sizeof_path < PATH_MAX - 1); + strncpy(path, sockaddr->sun_path, sizeof_path); + path[sizeof_path] = '\0'; + + return 1; +} + +/** + * Translate the pathname of the struct sockaddr_un currently stored + * in the @tracee memory at the given @address. See the documentation + * of read_sockaddr_un() for the meaning of the @size parameter. + * Also, the new address of the translated sockaddr_un is put in the + * @address parameter. This function returns -errno if an error + * occurred, otherwise 0. + */ +int translate_socketcall_enter(Tracee *tracee, word_t *address, int size) +{ + struct sockaddr_un sockaddr; + char user_path[PATH_MAX]; + char host_path[PATH_MAX]; + int status; + + if (*address == 0) + return 0; + + status = read_sockaddr_un(tracee, &sockaddr, sizeof(sockaddr), user_path, *address, size); + if (status <= 0) + return status; + + status = translate_path(tracee, host_path, AT_FDCWD, user_path, true); + if (status < 0) + return status; + + /* Be careful: sun_path doesn't have to be null-terminated. */ + if (strlen(host_path) > sizeof_path) { + char *shorter_host_path; + Binding *binding; + + /* The translated path is too long to fit the sun_path + * array, so let's bind it to a shorter path. */ + shorter_host_path = create_temp_name(tracee->ctx, "proot"); + if (shorter_host_path == NULL || strlen(shorter_host_path) > sizeof_path) + return -EINVAL; + + (void) mktemp(shorter_host_path); + + if (strlen(shorter_host_path) > sizeof_path) + return -EINVAL; + + /* Ensure the guest path of this new binding is + * canonicalized, as it is always assumed. */ + strcpy(user_path, host_path); + status = detranslate_path(tracee, user_path, NULL); + if (status < 0) + return -EINVAL; + + /* Bing the guest path to a shorter host path. */ + binding = insort_binding3(tracee, tracee->ctx, shorter_host_path, user_path); + if (binding == NULL) + return -EINVAL; + + /* This temporary file (shorter_host_path) will be removed once the + * binding is destroyed. */ + talloc_reparent(tracee->ctx, binding, shorter_host_path); + + /* Let's use this shorter path now. */ + strcpy(host_path, shorter_host_path); + } + strncpy(sockaddr.sun_path, host_path, sizeof_path); + + /* Push the updated sockaddr to a newly allocated space. */ + *address = alloc_mem(tracee, sizeof(sockaddr)); + if (*address == 0) + return -EFAULT; + + status = write_data(tracee, *address, &sockaddr, sizeof(sockaddr)); + if (status < 0) + return status; + + return 1; +} + +/** + * Detranslate the pathname of the struct sockaddr_un currently stored + * in the @tracee memory at the given @sock_addr. See the + * documentation of read_sockaddr_un() for the meaning of the + * @size_addr and @max_size parameters. This function returns -errno + * if an error occurred, otherwise 0. + */ +int translate_socketcall_exit(Tracee *tracee, word_t sock_addr, word_t size_addr, word_t max_size) +{ + struct sockaddr_un sockaddr; + bool is_truncated = false; + char path[PATH_MAX]; + int status; + int size; + + if (sock_addr == 0) + return 0; + + size = peek_int32(tracee, size_addr); + if (errno != 0) + return -errno; + + max_size = MIN(max_size, sizeof(sockaddr)); + status = read_sockaddr_un(tracee, &sockaddr, max_size, path, sock_addr, size); + if (status <= 0) + return status; + + status = detranslate_path(tracee, path, NULL); + if (status < 0) + return status; + + /* Be careful: sun_path doesn't have to be null-terminated. */ + size = offsetof_path + strlen(path) + 1; + if (size < 0 || (word_t) size > max_size) { + size = max_size; + is_truncated = true; + } + strncpy(sockaddr.sun_path, path, sizeof_path); + + /* Overwrite the sockaddr and socklen parameters. */ + status = write_data(tracee, sock_addr, &sockaddr, size); + if (status < 0) + return status; + + /* If sockaddr is truncated (because the buffer provided is + * too small), addrlen will return a value greater than was + * supplied to the call. See man 2 accept. */ + if (is_truncated) + size = max_size + 1; + + poke_int32(tracee, size_addr, size); + if (errno != 0) + return -errno; + + return 0; +} diff --git a/core/proot/src/main/cpp/syscall/socket.h b/core/proot/src/main/cpp/syscall/socket.h new file mode 100644 index 000000000..8c16cb1f9 --- /dev/null +++ b/core/proot/src/main/cpp/syscall/socket.h @@ -0,0 +1,32 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#ifndef SOCKET_H +#define SOCKET_H + +#include "arch.h" /* word_t */ +#include "tracee/tracee.h" + +int translate_socketcall_enter(Tracee *tracee, word_t *sock_addr, int size); +int translate_socketcall_exit(Tracee *tracee, word_t sock_addr, word_t size_addr, word_t max_size); + +#endif /* SOCKET_H */ diff --git a/core/proot/src/main/cpp/syscall/syscall.c b/core/proot/src/main/cpp/syscall/syscall.c new file mode 100644 index 000000000..0761a263b --- /dev/null +++ b/core/proot/src/main/cpp/syscall/syscall.c @@ -0,0 +1,274 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#include /* assert(3), */ +#include /* PATH_MAX, */ +#include /* strlen(3), */ +#include /* errno(3), E* */ + +#include "syscall/syscall.h" +#include "syscall/chain.h" +#include "extension/extension.h" +#include "tracee/tracee.h" +#include "tracee/reg.h" +#include "tracee/mem.h" +#include "cli/note.h" + +/** + * Copy in @path a C string (PATH_MAX bytes max.) from the @tracee's + * memory address space pointed to by the @reg argument of the + * current syscall. This function returns -errno if an error occured, + * otherwise it returns the size in bytes put into the @path. + */ +int get_sysarg_path(const Tracee *tracee, char path[PATH_MAX], Reg reg) +{ + int size; + word_t src; + + src = peek_reg(tracee, CURRENT, reg); + + /* Check if the parameter is not NULL. Technically we should + * not return an -EFAULT for this special value since it is + * allowed for some syscall, utimensat(2) for instance. */ + if (src == 0) { + path[0] = '\0'; + return 0; + } + + /* Get the path from the tracee's memory space. */ + size = read_path(tracee, path, src); + if (size < 0) + return size; + + path[size] = '\0'; + return size; +} + +/** + * Copy @size bytes of the data pointed to by @tracer_ptr into a + * @tracee's memory block and make the @reg argument of the current + * syscall points to this new block. This function returns -errno if + * an error occured, otherwise 0. + */ +int set_sysarg_data(Tracee *tracee, const void *tracer_ptr, word_t size, Reg reg) +{ + word_t tracee_ptr; + int status; + + /* Allocate space into the tracee's memory to host the new data. */ + tracee_ptr = alloc_mem(tracee, size); + if (tracee_ptr == 0) + return -EFAULT; + + /* Copy the new data into the previously allocated space. */ + status = write_data(tracee, tracee_ptr, tracer_ptr, size); + if (status < 0) + return status; + + /* Make this argument point to the new data. */ + poke_reg(tracee, reg, tracee_ptr); + + return 0; +} + +/** + * Copy @path to a @tracee's memory block and make the @reg argument + * of the current syscall points to this new block. This function + * returns -errno if an error occured, otherwise 0. + */ +int set_sysarg_path(Tracee *tracee, const char path[PATH_MAX], Reg reg) +{ + return set_sysarg_data(tracee, path, strlen(path) + 1, reg); +} + +void translate_syscall(Tracee *tracee) +{ + const bool is_enter_stage = IS_IN_SYSENTER(tracee); + int status; + + assert(tracee->exe != NULL); + + status = fetch_regs(tracee); + if (status < 0) + return; + + int suppressed_syscall_status = 0; + + if (is_enter_stage) { + /* Never restore original register values at the end + * of this stage. */ + tracee->restore_original_regs = false; + + print_current_regs(tracee, 3, "sysenter start"); + +#ifdef HAS_POKEDATA_WORKAROUND + /* In case of pokedata workaround has cancelled real enter + * of syscall we've enqueued start of syscall again + * so we won't translate it here again. */ + if (tracee->pokedata_workaround_relaunched_syscall) { + tracee->pokedata_workaround_relaunched_syscall = false; + tracee->status = 1; + tracee->restart_how = PTRACE_SYSCALL; + return; + } +#endif + + /* Translate the syscall only if it was actually + * requested by the tracee, it is not a syscall + * chained by PRoot. */ + if (tracee->chain.syscalls == NULL) { + save_current_regs(tracee, ORIGINAL); + status = translate_syscall_enter(tracee); + save_current_regs(tracee, MODIFIED); + } + else { + if (tracee->chain.sysnum_workaround_state != SYSNUM_WORKAROUND_PROCESS_REPLACED_CALL) { + status = notify_extensions(tracee, SYSCALL_CHAINED_ENTER, 0, 0); + } + tracee->restart_how = PTRACE_SYSCALL; + } + + /* Remember the tracee status for the "exit" stage and + * avoid the actual syscall if an error was reported + * by the translation/extension. */ + if (status < 0) { + set_sysnum(tracee, PR_void); + poke_reg(tracee, SYSARG_RESULT, (word_t) status); + tracee->status = status; +#if defined(ARCH_ARM_EABI) + tracee->restart_how = PTRACE_SYSCALL; +#endif + } + else + tracee->status = 1; + +#ifdef HAS_POKEDATA_WORKAROUND + if (tracee->pokedata_workaround_cancelled_syscall) { + tracee->pokedata_workaround_cancelled_syscall = false; + tracee->pokedata_workaround_relaunched_syscall = true; + tracee->restart_how = PTRACE_SYSCALL; + tracee->status = 0; + poke_reg(tracee, INSTR_POINTER, peek_reg(tracee, CURRENT, INSTR_POINTER) - SYSTRAP_SIZE); + push_specific_regs(tracee, false); + return; + } +#endif + + /* Restore tracee's stack pointer now if it won't hit + * the sysexit stage (i.e. when seccomp is enabled and + * there's nothing else to do). */ + if (tracee->restart_how == PTRACE_CONT) { + suppressed_syscall_status = tracee->status; + tracee->status = 0; + poke_reg(tracee, STACK_POINTER, peek_reg(tracee, ORIGINAL, STACK_POINTER)); + } + } + else { + /* By default, restore original register values at the + * end of this stage. */ + tracee->restore_original_regs = true; + +#ifdef HAS_POKEDATA_WORKAROUND + /* This is exit from syscall that was cancelled + * by pokedata workaround - ignore. */ + if (tracee->pokedata_workaround_relaunched_syscall) + { + return; + } +#endif + + print_current_regs(tracee, 5, "sysexit start"); + + /* Translate the syscall only if it was actually + * requested by the tracee, it is not a syscall + * chained by PRoot. */ + if (tracee->chain.syscalls == NULL || tracee->chain.sysnum_workaround_state == SYSNUM_WORKAROUND_PROCESS_REPLACED_CALL) { + tracee->chain.sysnum_workaround_state = SYSNUM_WORKAROUND_INACTIVE; + translate_syscall_exit(tracee); + } + else if (tracee->chain.sysnum_workaround_state == SYSNUM_WORKAROUND_PROCESS_FAULTY_CALL) { + tracee->chain.sysnum_workaround_state = SYSNUM_WORKAROUND_PROCESS_REPLACED_CALL; + } + else + (void) notify_extensions(tracee, SYSCALL_CHAINED_EXIT, 0, 0); + + /* Reset the tracee's status. */ + tracee->status = 0; +#ifdef HAS_POKEDATA_WORKAROUND + tracee->pokedata_workaround_cancelled_syscall = false; +#endif + + /* Insert the next chained syscall, if any. */ + if (tracee->chain.syscalls != NULL) + chain_next_syscall(tracee); + } + + bool override_sysnum = is_enter_stage && tracee->chain.syscalls == NULL; + int push_regs_status = push_specific_regs(tracee, override_sysnum); + + /* Handle inability to change syscall number */ + if (push_regs_status < 0 && override_sysnum) { + word_t orig_sysnum = peek_reg(tracee, ORIGINAL, SYSARG_NUM); + word_t current_sysnum = peek_reg(tracee, CURRENT, SYSARG_NUM); + print_current_regs(tracee, 4, "pre_push"); + if (orig_sysnum != current_sysnum) { + /* Restart current syscall as chained */ + if (current_sysnum != SYSCALL_AVOIDER) { + restart_current_syscall_as_chained(tracee); + } else if (suppressed_syscall_status) { + /* If we've decided to fail this syscall + * by setting it to no-op and continuing, but turns out + * that we can't just make syscall nop, restore tracee->status + * and intercept syscall exit */ + tracee->status = suppressed_syscall_status; + tracee->restart_how = PTRACE_SYSCALL; + } + + /* Set syscall arguments to make it fail + * TODO: More reliable way to make invalid arguments + * For most syscalls we set all args to -1 + * Hoping there is among them invalid request/address/fd/value that will make syscall fail */ + poke_reg(tracee, SYSARG_1, -1); + poke_reg(tracee, SYSARG_2, -1); + poke_reg(tracee, SYSARG_3, -1); + poke_reg(tracee, SYSARG_4, -1); + poke_reg(tracee, SYSARG_5, -1); + poke_reg(tracee, SYSARG_6, -1); + + if (get_sysnum(tracee, ORIGINAL) == PR_brk) { + /* For brk() we pass 0 as first arg; this is used to query value without changing it */ + poke_reg(tracee, SYSARG_1, 0); + } + + /* Push regs again without changing syscall */ + push_regs_status = push_specific_regs(tracee, false); + if (push_regs_status != 0) { + note(tracee, WARNING, SYSTEM, "can't set tracee registers in workaround"); + } + } + } + + if (is_enter_stage) + print_current_regs(tracee, 5, "sysenter end" ); + else + print_current_regs(tracee, 4, "sysexit end"); +} diff --git a/core/proot/src/main/cpp/syscall/syscall.h b/core/proot/src/main/cpp/syscall/syscall.h new file mode 100644 index 000000000..e0e263c95 --- /dev/null +++ b/core/proot/src/main/cpp/syscall/syscall.h @@ -0,0 +1,43 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#ifndef SYSCALL_H +#define SYSCALL_H + +#include /* PATH_MAX, */ + +#include "tracee/tracee.h" +#include "tracee/reg.h" + +extern int get_sysarg_path(const Tracee *tracee, char path[PATH_MAX], Reg reg); +extern int set_sysarg_path(Tracee *tracee, const char path[PATH_MAX], Reg reg); +extern int set_sysarg_data(Tracee *tracee, const void *tracer_ptr, word_t size, Reg reg); + +extern void translate_syscall(Tracee *tracee); +extern int translate_syscall_enter(Tracee *tracee); +extern void translate_syscall_exit(Tracee *tracee); + +extern void apply_emulated_mount(Tracee *tracee); +extern void apply_emulated_pivot_root(Tracee *tracee); +extern void apply_emulated_umount(Tracee *tracee); + +#endif /* SYSCALL_H */ diff --git a/core/proot/src/main/cpp/syscall/sysnum.c b/core/proot/src/main/cpp/syscall/sysnum.c new file mode 100644 index 000000000..b5440a66b --- /dev/null +++ b/core/proot/src/main/cpp/syscall/sysnum.c @@ -0,0 +1,161 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#include + +#include "syscall/sysnum.h" +#include "tracee/tracee.h" +#include "tracee/abi.h" +#include "tracee/reg.h" +#include "arch.h" +#include "cli/note.h" + +#include SYSNUMS_HEADER1 + +#ifdef SYSNUMS_HEADER2 +#include SYSNUMS_HEADER2 +#endif + +#ifdef SYSNUMS_HEADER3 +#include SYSNUMS_HEADER3 +#endif + +typedef struct { + const Sysnum *table; + word_t offset; + word_t length; +} Sysnums; + +/** + * Update @sysnums' fields with the sysnum table for the given @abi. + */ +static void get_sysnums(Abi abi, Sysnums *sysnums) +{ + switch (abi) { + case ABI_DEFAULT: + sysnums->table = SYSNUMS_ABI1; + sysnums->length = sizeof(SYSNUMS_ABI1) / sizeof(Sysnum); + sysnums->offset = 0; + return; +#ifdef SYSNUMS_ABI2 + case ABI_2: + sysnums->table = SYSNUMS_ABI2; + sysnums->length = sizeof(SYSNUMS_ABI2) / sizeof(Sysnum); + sysnums->offset = 0; + return; +#endif +#ifdef SYSNUMS_ABI3 + case ABI_3: + sysnums->table = SYSNUMS_ABI3; + sysnums->length = sizeof(SYSNUMS_ABI3) / sizeof(Sysnum); + sysnums->offset = 0x40000000; /* x32 */ + return; +#endif + default: + assert(0); + } +} + +/** + * Return the neutral value of @sysnum from the given @abi. + */ +static Sysnum translate_sysnum(Abi abi, word_t sysnum) +{ + Sysnums sysnums; + word_t index; + + get_sysnums(abi, &sysnums); + + /* Sanity checks. */ + if (sysnum < sysnums.offset) + return PR_void; + + index = sysnum - sysnums.offset; + + /* Sanity checks. */ + if (index > sysnums.length) + return PR_void; + + return sysnums.table[index]; +} + +/** + * Return the architecture value of @sysnum for the given @abi. + */ +word_t detranslate_sysnum(Abi abi, Sysnum sysnum) +{ + Sysnums sysnums; + size_t i; + + /* Very special case. */ + if (sysnum == PR_void) + return SYSCALL_AVOIDER; + + get_sysnums(abi, &sysnums); + + for (i = 0; i < sysnums.length; i++) { + if (sysnums.table[i] != sysnum) + continue; + + return i + sysnums.offset; + } + + return SYSCALL_AVOIDER; +} + +/** + * Return the neutral value of the @tracee's current syscall number. + */ +Sysnum get_sysnum(const Tracee *tracee, RegVersion version) +{ + return translate_sysnum(get_abi(tracee), peek_reg(tracee, version, SYSARG_NUM)); +} + +/** + * Overwrite the @tracee's current syscall number with @sysnum. Note: + * this neutral value is automatically converted into the architecture + * value. + */ +void set_sysnum(Tracee *tracee, Sysnum sysnum) +{ + poke_reg(tracee, SYSARG_NUM, detranslate_sysnum(get_abi(tracee), sysnum)); +} + +/** + * Return the human readable name of @sysnum. + */ +const char *stringify_sysnum(Sysnum sysnum) +{ + #define SYSNUM(item) [ PR_ ## item ] = #item, + static const char *names[] = { + #include "syscall/sysnums.list" + }; + #undef SYSNUM + + if (sysnum == 0) + return "void"; + + if (sysnum >= PR_NB_SYSNUM) + return ""; + + return names[sysnum]; +} diff --git a/core/proot/src/main/cpp/syscall/sysnum.h b/core/proot/src/main/cpp/syscall/sysnum.h new file mode 100644 index 000000000..8d56ab3b4 --- /dev/null +++ b/core/proot/src/main/cpp/syscall/sysnum.h @@ -0,0 +1,45 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#ifndef SYSNUM_H +#define SYSNUM_H + +#include + +#include "tracee/tracee.h" +#include "tracee/abi.h" +#include "tracee/reg.h" + +#define SYSNUM(item) PR_ ## item, +typedef enum { + PR_void = 0, + #include "syscall/sysnums.list" + PR_NB_SYSNUM +} Sysnum; +#undef SYSNUM + +extern Sysnum get_sysnum(const Tracee *tracee, RegVersion version); +extern void set_sysnum(Tracee *tracee, Sysnum sysnum); +extern word_t detranslate_sysnum(Abi abi, Sysnum sysnum); +extern const char *stringify_sysnum(Sysnum sysnum); + +#endif /* SYSNUM_H */ diff --git a/core/proot/src/main/cpp/syscall/sysnums-arm.h b/core/proot/src/main/cpp/syscall/sysnums-arm.h new file mode 100644 index 000000000..3d05974b5 --- /dev/null +++ b/core/proot/src/main/cpp/syscall/sysnums-arm.h @@ -0,0 +1,362 @@ +#include "syscall/sysnum.h" + +static const Sysnum sysnums_arm[] = { + [ 0 ] = PR_restart_syscall, + [ 1 ] = PR_exit, + [ 2 ] = PR_fork, + [ 3 ] = PR_read, + [ 4 ] = PR_write, + [ 5 ] = PR_open, + [ 6 ] = PR_close, + [ 8 ] = PR_creat, + [ 9 ] = PR_link, + [ 10 ] = PR_unlink, + [ 11 ] = PR_execve, + [ 12 ] = PR_chdir, + [ 14 ] = PR_mknod, + [ 15 ] = PR_chmod, + [ 16 ] = PR_lchown, + [ 19 ] = PR_lseek, + [ 20 ] = PR_getpid, + [ 21 ] = PR_mount, + [ 23 ] = PR_setuid, + [ 24 ] = PR_getuid, + [ 26 ] = PR_ptrace, + [ 29 ] = PR_pause, + [ 30 ] = PR_utime, + [ 33 ] = PR_access, + [ 34 ] = PR_nice, + [ 36 ] = PR_sync, + [ 37 ] = PR_kill, + [ 38 ] = PR_rename, + [ 39 ] = PR_mkdir, + [ 40 ] = PR_rmdir, + [ 41 ] = PR_dup, + [ 42 ] = PR_pipe, + [ 43 ] = PR_times, + [ 45 ] = PR_brk, + [ 46 ] = PR_setgid, + [ 47 ] = PR_getgid, + [ 49 ] = PR_geteuid, + [ 50 ] = PR_getegid, + [ 51 ] = PR_acct, + [ 52 ] = PR_umount2, + [ 54 ] = PR_ioctl, + [ 55 ] = PR_fcntl, + [ 57 ] = PR_setpgid, + [ 60 ] = PR_umask, + [ 61 ] = PR_chroot, + [ 62 ] = PR_ustat, + [ 63 ] = PR_dup2, + [ 64 ] = PR_getppid, + [ 65 ] = PR_getpgrp, + [ 66 ] = PR_setsid, + [ 67 ] = PR_sigaction, + [ 70 ] = PR_setreuid, + [ 71 ] = PR_setregid, + [ 72 ] = PR_sigsuspend, + [ 73 ] = PR_sigpending, + [ 74 ] = PR_sethostname, + [ 75 ] = PR_setrlimit, + [ 77 ] = PR_getrusage, + [ 78 ] = PR_gettimeofday, + [ 79 ] = PR_settimeofday, + [ 80 ] = PR_getgroups, + [ 81 ] = PR_setgroups, + [ 83 ] = PR_symlink, + [ 85 ] = PR_readlink, + [ 86 ] = PR_uselib, + [ 87 ] = PR_swapon, + [ 88 ] = PR_reboot, + [ 91 ] = PR_munmap, + [ 92 ] = PR_truncate, + [ 93 ] = PR_ftruncate, + [ 94 ] = PR_fchmod, + [ 95 ] = PR_fchown, + [ 96 ] = PR_getpriority, + [ 97 ] = PR_setpriority, + [ 99 ] = PR_statfs, + [ 100 ] = PR_fstatfs, + [ 103 ] = PR_syslog, + [ 104 ] = PR_setitimer, + [ 105 ] = PR_getitimer, + [ 106 ] = PR_stat, + [ 107 ] = PR_lstat, + [ 108 ] = PR_fstat, + [ 111 ] = PR_vhangup, + [ 114 ] = PR_wait4, + [ 115 ] = PR_swapoff, + [ 116 ] = PR_sysinfo, + [ 118 ] = PR_fsync, + [ 119 ] = PR_sigreturn, + [ 120 ] = PR_clone, + [ 121 ] = PR_setdomainname, + [ 122 ] = PR_uname, + [ 124 ] = PR_adjtimex, + [ 125 ] = PR_mprotect, + [ 126 ] = PR_sigprocmask, + [ 128 ] = PR_init_module, + [ 129 ] = PR_delete_module, + [ 131 ] = PR_quotactl, + [ 132 ] = PR_getpgid, + [ 133 ] = PR_fchdir, + [ 134 ] = PR_bdflush, + [ 135 ] = PR_sysfs, + [ 136 ] = PR_personality, + [ 138 ] = PR_setfsuid, + [ 139 ] = PR_setfsgid, + [ 140 ] = PR__llseek, + [ 141 ] = PR_getdents, + [ 142 ] = PR__newselect, + [ 143 ] = PR_flock, + [ 144 ] = PR_msync, + [ 145 ] = PR_readv, + [ 146 ] = PR_writev, + [ 147 ] = PR_getsid, + [ 148 ] = PR_fdatasync, + [ 149 ] = PR__sysctl, + [ 150 ] = PR_mlock, + [ 151 ] = PR_munlock, + [ 152 ] = PR_mlockall, + [ 153 ] = PR_munlockall, + [ 154 ] = PR_sched_setparam, + [ 155 ] = PR_sched_getparam, + [ 156 ] = PR_sched_setscheduler, + [ 157 ] = PR_sched_getscheduler, + [ 158 ] = PR_sched_yield, + [ 159 ] = PR_sched_get_priority_max, + [ 160 ] = PR_sched_get_priority_min, + [ 161 ] = PR_sched_rr_get_interval, + [ 162 ] = PR_nanosleep, + [ 163 ] = PR_mremap, + [ 164 ] = PR_setresuid, + [ 165 ] = PR_getresuid, + [ 168 ] = PR_poll, + [ 169 ] = PR_nfsservctl, + [ 170 ] = PR_setresgid, + [ 171 ] = PR_getresgid, + [ 172 ] = PR_prctl, + [ 173 ] = PR_rt_sigreturn, + [ 174 ] = PR_rt_sigaction, + [ 175 ] = PR_rt_sigprocmask, + [ 176 ] = PR_rt_sigpending, + [ 177 ] = PR_rt_sigtimedwait, + [ 178 ] = PR_rt_sigqueueinfo, + [ 179 ] = PR_rt_sigsuspend, + [ 180 ] = PR_pread64, + [ 181 ] = PR_pwrite64, + [ 182 ] = PR_chown, + [ 183 ] = PR_getcwd, + [ 184 ] = PR_capget, + [ 185 ] = PR_capset, + [ 186 ] = PR_sigaltstack, + [ 187 ] = PR_sendfile, + [ 190 ] = PR_vfork, + [ 191 ] = PR_ugetrlimit, + [ 192 ] = PR_mmap2, + [ 193 ] = PR_truncate64, + [ 194 ] = PR_ftruncate64, + [ 195 ] = PR_stat64, + [ 196 ] = PR_lstat64, + [ 197 ] = PR_fstat64, + [ 198 ] = PR_lchown32, + [ 199 ] = PR_getuid32, + [ 200 ] = PR_getgid32, + [ 201 ] = PR_geteuid32, + [ 202 ] = PR_getegid32, + [ 203 ] = PR_setreuid32, + [ 204 ] = PR_setregid32, + [ 205 ] = PR_getgroups32, + [ 206 ] = PR_setgroups32, + [ 207 ] = PR_fchown32, + [ 208 ] = PR_setresuid32, + [ 209 ] = PR_getresuid32, + [ 210 ] = PR_setresgid32, + [ 211 ] = PR_getresgid32, + [ 212 ] = PR_chown32, + [ 213 ] = PR_setuid32, + [ 214 ] = PR_setgid32, + [ 215 ] = PR_setfsuid32, + [ 216 ] = PR_setfsgid32, + [ 217 ] = PR_getdents64, + [ 218 ] = PR_pivot_root, + [ 219 ] = PR_mincore, + [ 220 ] = PR_madvise, + [ 221 ] = PR_fcntl64, + [ 222 ] = PR_void, + [ 224 ] = PR_gettid, + [ 225 ] = PR_readahead, + [ 226 ] = PR_setxattr, + [ 227 ] = PR_lsetxattr, + [ 228 ] = PR_fsetxattr, + [ 229 ] = PR_getxattr, + [ 230 ] = PR_lgetxattr, + [ 231 ] = PR_fgetxattr, + [ 232 ] = PR_listxattr, + [ 233 ] = PR_llistxattr, + [ 234 ] = PR_flistxattr, + [ 235 ] = PR_removexattr, + [ 236 ] = PR_lremovexattr, + [ 237 ] = PR_fremovexattr, + [ 238 ] = PR_tkill, + [ 239 ] = PR_sendfile64, + [ 240 ] = PR_futex, + [ 241 ] = PR_sched_setaffinity, + [ 242 ] = PR_sched_getaffinity, + [ 243 ] = PR_io_setup, + [ 244 ] = PR_io_destroy, + [ 245 ] = PR_io_getevents, + [ 246 ] = PR_io_submit, + [ 247 ] = PR_io_cancel, + [ 248 ] = PR_exit_group, + [ 249 ] = PR_lookup_dcookie, + [ 250 ] = PR_epoll_create, + [ 251 ] = PR_epoll_ctl, + [ 252 ] = PR_epoll_wait, + [ 253 ] = PR_remap_file_pages, + [ 256 ] = PR_set_tid_address, + [ 257 ] = PR_timer_create, + [ 258 ] = PR_timer_settime, + [ 259 ] = PR_timer_gettime, + [ 260 ] = PR_timer_getoverrun, + [ 261 ] = PR_timer_delete, + [ 262 ] = PR_clock_settime, + [ 263 ] = PR_clock_gettime, + [ 264 ] = PR_clock_getres, + [ 265 ] = PR_clock_nanosleep, + [ 266 ] = PR_statfs64, + [ 267 ] = PR_fstatfs64, + [ 268 ] = PR_tgkill, + [ 269 ] = PR_utimes, + [ 270 ] = PR_arm_fadvise64_64, + [ 271 ] = PR_pciconfig_iobase, + [ 272 ] = PR_pciconfig_read, + [ 273 ] = PR_pciconfig_write, + [ 274 ] = PR_mq_open, + [ 275 ] = PR_mq_unlink, + [ 276 ] = PR_mq_timedsend, + [ 277 ] = PR_mq_timedreceive, + [ 278 ] = PR_mq_notify, + [ 279 ] = PR_mq_getsetattr, + [ 280 ] = PR_waitid, + [ 281 ] = PR_socket, + [ 282 ] = PR_bind, + [ 283 ] = PR_connect, + [ 284 ] = PR_listen, + [ 285 ] = PR_accept, + [ 286 ] = PR_getsockname, + [ 287 ] = PR_getpeername, + [ 288 ] = PR_socketpair, + [ 289 ] = PR_send, + [ 290 ] = PR_sendto, + [ 291 ] = PR_recv, + [ 292 ] = PR_recvfrom, + [ 293 ] = PR_shutdown, + [ 294 ] = PR_setsockopt, + [ 295 ] = PR_getsockopt, + [ 296 ] = PR_sendmsg, + [ 297 ] = PR_recvmsg, + [ 298 ] = PR_semop, + [ 299 ] = PR_semget, + [ 300 ] = PR_semctl, + [ 301 ] = PR_msgsnd, + [ 302 ] = PR_msgrcv, + [ 303 ] = PR_msgget, + [ 304 ] = PR_msgctl, + [ 305 ] = PR_shmat, + [ 306 ] = PR_shmdt, + [ 307 ] = PR_shmget, + [ 308 ] = PR_shmctl, + [ 309 ] = PR_add_key, + [ 310 ] = PR_request_key, + [ 311 ] = PR_keyctl, + [ 312 ] = PR_semtimedop, + [ 313 ] = PR_vserver, + [ 314 ] = PR_ioprio_set, + [ 315 ] = PR_ioprio_get, + [ 316 ] = PR_inotify_init, + [ 317 ] = PR_inotify_add_watch, + [ 318 ] = PR_inotify_rm_watch, + [ 319 ] = PR_mbind, + [ 320 ] = PR_get_mempolicy, + [ 321 ] = PR_set_mempolicy, + [ 322 ] = PR_openat, + [ 323 ] = PR_mkdirat, + [ 324 ] = PR_mknodat, + [ 325 ] = PR_fchownat, + [ 326 ] = PR_futimesat, + [ 327 ] = PR_fstatat64, + [ 328 ] = PR_unlinkat, + [ 329 ] = PR_renameat, + [ 330 ] = PR_linkat, + [ 331 ] = PR_symlinkat, + [ 332 ] = PR_readlinkat, + [ 333 ] = PR_fchmodat, + [ 334 ] = PR_faccessat, + [ 335 ] = PR_pselect6, + [ 336 ] = PR_ppoll, + [ 337 ] = PR_unshare, + [ 338 ] = PR_set_robust_list, + [ 339 ] = PR_get_robust_list, + [ 340 ] = PR_splice, + [ 341 ] = PR_arm_sync_file_range, + [ 342 ] = PR_tee, + [ 343 ] = PR_vmsplice, + [ 344 ] = PR_move_pages, + [ 345 ] = PR_getcpu, + [ 346 ] = PR_epoll_pwait, + [ 347 ] = PR_kexec_load, + [ 348 ] = PR_utimensat, + [ 349 ] = PR_signalfd, + [ 350 ] = PR_timerfd_create, + [ 351 ] = PR_eventfd, + [ 352 ] = PR_fallocate, + [ 353 ] = PR_timerfd_settime, + [ 354 ] = PR_timerfd_gettime, + [ 355 ] = PR_signalfd4, + [ 356 ] = PR_eventfd2, + [ 357 ] = PR_epoll_create1, + [ 358 ] = PR_dup3, + [ 359 ] = PR_pipe2, + [ 360 ] = PR_inotify_init1, + [ 361 ] = PR_preadv, + [ 362 ] = PR_pwritev, + [ 363 ] = PR_rt_tgsigqueueinfo, + [ 364 ] = PR_perf_event_open, + [ 365 ] = PR_recvmmsg, + [ 366 ] = PR_accept4, + [ 367 ] = PR_fanotify_init, + [ 368 ] = PR_fanotify_mark, + [ 369 ] = PR_prlimit64, + [ 370 ] = PR_name_to_handle_at, + [ 371 ] = PR_open_by_handle_at, + [ 372 ] = PR_clock_adjtime, + [ 373 ] = PR_syncfs, + [ 374 ] = PR_sendmmsg, + [ 375 ] = PR_setns, + [ 376 ] = PR_process_vm_readv, + [ 377 ] = PR_process_vm_writev, + [ 378 ] = PR_kcmp, + [ 379 ] = PR_finit_module, + [ 380 ] = PR_sched_setattr, + [ 381 ] = PR_sched_getattr, + [ 382 ] = PR_renameat2, + [ 383 ] = PR_seccomp, + [ 384 ] = PR_getrandom, + [ 385 ] = PR_memfd_create, + [ 386 ] = PR_bpf, + [ 387 ] = PR_execveat, + [ 388 ] = PR_userfaultfd, + [ 389 ] = PR_membarrier, + [ 390 ] = PR_mlock2, + [ 391 ] = PR_copy_file_range, + [ 392 ] = PR_preadv2, + [ 393 ] = PR_pwritev2, + [ 394 ] = PR_pkey_mprotect, + [ 395 ] = PR_pkey_alloc, + [ 396 ] = PR_pkey_free, + [ 397 ] = PR_statx, + [ 439 ] = PR_faccessat2, + [ 435 ] = PR_clone3, + [ 437 ] = PR_openat2, +}; diff --git a/core/proot/src/main/cpp/syscall/sysnums-arm64.h b/core/proot/src/main/cpp/syscall/sysnums-arm64.h new file mode 100644 index 000000000..15cfe4ffb --- /dev/null +++ b/core/proot/src/main/cpp/syscall/sysnums-arm64.h @@ -0,0 +1,284 @@ +#include "syscall/sysnum.h" + +static const Sysnum sysnums_arm64[] = { + [ 0 ] = PR_io_setup, + [ 1 ] = PR_io_destroy, + [ 2 ] = PR_io_submit, + [ 3 ] = PR_io_cancel, + [ 4 ] = PR_io_getevents, + [ 5 ] = PR_setxattr, + [ 6 ] = PR_lsetxattr, + [ 7 ] = PR_fsetxattr, + [ 8 ] = PR_getxattr, + [ 9 ] = PR_lgetxattr, + [ 10 ] = PR_fgetxattr, + [ 11 ] = PR_listxattr, + [ 12 ] = PR_llistxattr, + [ 13 ] = PR_flistxattr, + [ 14 ] = PR_removexattr, + [ 15 ] = PR_lremovexattr, + [ 16 ] = PR_fremovexattr, + [ 17 ] = PR_getcwd, + [ 18 ] = PR_lookup_dcookie, + [ 19 ] = PR_eventfd2, + [ 20 ] = PR_epoll_create1, + [ 21 ] = PR_epoll_ctl, + [ 22 ] = PR_epoll_pwait, + [ 23 ] = PR_dup, + [ 24 ] = PR_dup3, + [ 25 ] = PR_fcntl, + [ 26 ] = PR_inotify_init1, + [ 27 ] = PR_inotify_add_watch, + [ 28 ] = PR_inotify_rm_watch, + [ 29 ] = PR_ioctl, + [ 30 ] = PR_ioprio_set, + [ 31 ] = PR_ioprio_get, + [ 32 ] = PR_flock, + [ 33 ] = PR_mknodat, + [ 34 ] = PR_mkdirat, + [ 35 ] = PR_unlinkat, + [ 36 ] = PR_symlinkat, + [ 37 ] = PR_linkat, + [ 38 ] = PR_renameat, + [ 39 ] = PR_umount2, + [ 40 ] = PR_mount, + [ 41 ] = PR_pivot_root, + [ 42 ] = PR_nfsservctl, + [ 43 ] = PR_statfs, + [ 44 ] = PR_fstatfs, + [ 45 ] = PR_truncate, + [ 46 ] = PR_ftruncate, + [ 47 ] = PR_fallocate, + [ 48 ] = PR_faccessat, + [ 49 ] = PR_chdir, + [ 50 ] = PR_fchdir, + [ 51 ] = PR_chroot, + [ 52 ] = PR_fchmod, + [ 53 ] = PR_fchmodat, + [ 54 ] = PR_fchownat, + [ 55 ] = PR_fchown, + [ 56 ] = PR_openat, + [ 57 ] = PR_close, + [ 58 ] = PR_vhangup, + [ 59 ] = PR_pipe2, + [ 60 ] = PR_quotactl, + [ 61 ] = PR_getdents64, + [ 62 ] = PR_lseek, + [ 63 ] = PR_read, + [ 64 ] = PR_write, + [ 65 ] = PR_readv, + [ 66 ] = PR_writev, + [ 67 ] = PR_pread64, + [ 68 ] = PR_pwrite64, + [ 69 ] = PR_preadv, + [ 70 ] = PR_pwritev, + [ 71 ] = PR_sendfile, + [ 72 ] = PR_pselect6, + [ 73 ] = PR_ppoll, + [ 74 ] = PR_signalfd4, + [ 75 ] = PR_vmsplice, + [ 76 ] = PR_splice, + [ 77 ] = PR_tee, + [ 78 ] = PR_readlinkat, + [ 79 ] = PR_fstatat64, + [ 80 ] = PR_fstat, + [ 81 ] = PR_sync, + [ 82 ] = PR_fsync, + [ 83 ] = PR_fdatasync, + [ 84 ] = PR_sync_file_range, + [ 85 ] = PR_timerfd_create, + [ 86 ] = PR_timerfd_settime, + [ 87 ] = PR_timerfd_gettime, + [ 88 ] = PR_utimensat, + [ 89 ] = PR_acct, + [ 90 ] = PR_capget, + [ 91 ] = PR_capset, + [ 92 ] = PR_personality, + [ 93 ] = PR_exit, + [ 94 ] = PR_exit_group, + [ 95 ] = PR_waitid, + [ 96 ] = PR_set_tid_address, + [ 97 ] = PR_unshare, + [ 98 ] = PR_futex, + [ 99 ] = PR_set_robust_list, + [ 100 ] = PR_get_robust_list, + [ 101 ] = PR_nanosleep, + [ 102 ] = PR_getitimer, + [ 103 ] = PR_setitimer, + [ 104 ] = PR_kexec_load, + [ 105 ] = PR_init_module, + [ 106 ] = PR_delete_module, + [ 107 ] = PR_timer_create, + [ 108 ] = PR_timer_gettime, + [ 109 ] = PR_timer_getoverrun, + [ 110 ] = PR_timer_settime, + [ 111 ] = PR_timer_delete, + [ 112 ] = PR_clock_settime, + [ 113 ] = PR_clock_gettime, + [ 114 ] = PR_clock_getres, + [ 115 ] = PR_clock_nanosleep, + [ 116 ] = PR_syslog, + [ 117 ] = PR_ptrace, + [ 118 ] = PR_sched_setparam, + [ 119 ] = PR_sched_setscheduler, + [ 120 ] = PR_sched_getscheduler, + [ 121 ] = PR_sched_getparam, + [ 122 ] = PR_sched_setaffinity, + [ 123 ] = PR_sched_getaffinity, + [ 124 ] = PR_sched_yield, + [ 125 ] = PR_sched_get_priority_max, + [ 126 ] = PR_sched_get_priority_min, + [ 127 ] = PR_sched_rr_get_interval, + [ 128 ] = PR_restart_syscall, + [ 129 ] = PR_kill, + [ 130 ] = PR_tkill, + [ 131 ] = PR_tgkill, + [ 132 ] = PR_sigaltstack, + [ 133 ] = PR_rt_sigsuspend, + [ 134 ] = PR_rt_sigaction, + [ 135 ] = PR_rt_sigprocmask, + [ 136 ] = PR_rt_sigpending, + [ 137 ] = PR_rt_sigtimedwait, + [ 138 ] = PR_rt_sigqueueinfo, + [ 139 ] = PR_rt_sigreturn, + [ 140 ] = PR_setpriority, + [ 141 ] = PR_getpriority, + [ 142 ] = PR_reboot, + [ 143 ] = PR_setregid, + [ 144 ] = PR_setgid, + [ 145 ] = PR_setreuid, + [ 146 ] = PR_setuid, + [ 147 ] = PR_setresuid, + [ 148 ] = PR_getresuid, + [ 149 ] = PR_setresgid, + [ 150 ] = PR_getresgid, + [ 151 ] = PR_setfsuid, + [ 152 ] = PR_setfsgid, + [ 153 ] = PR_times, + [ 154 ] = PR_setpgid, + [ 155 ] = PR_getpgid, + [ 156 ] = PR_getsid, + [ 157 ] = PR_setsid, + [ 158 ] = PR_getgroups, + [ 159 ] = PR_setgroups, + [ 160 ] = PR_uname, + [ 161 ] = PR_sethostname, + [ 162 ] = PR_setdomainname, + [ 163 ] = PR_getrlimit, + [ 164 ] = PR_setrlimit, + [ 165 ] = PR_getrusage, + [ 166 ] = PR_umask, + [ 167 ] = PR_prctl, + [ 168 ] = PR_getcpu, + [ 169 ] = PR_gettimeofday, + [ 170 ] = PR_settimeofday, + [ 171 ] = PR_adjtimex, + [ 172 ] = PR_getpid, + [ 173 ] = PR_getppid, + [ 174 ] = PR_getuid, + [ 175 ] = PR_geteuid, + [ 176 ] = PR_getgid, + [ 177 ] = PR_getegid, + [ 178 ] = PR_gettid, + [ 179 ] = PR_sysinfo, + [ 180 ] = PR_mq_open, + [ 181 ] = PR_mq_unlink, + [ 182 ] = PR_mq_timedsend, + [ 183 ] = PR_mq_timedreceive, + [ 184 ] = PR_mq_notify, + [ 185 ] = PR_mq_getsetattr, + [ 186 ] = PR_msgget, + [ 187 ] = PR_msgctl, + [ 188 ] = PR_msgrcv, + [ 189 ] = PR_msgsnd, + [ 190 ] = PR_semget, + [ 191 ] = PR_semctl, + [ 192 ] = PR_semtimedop, + [ 193 ] = PR_semop, + [ 194 ] = PR_shmget, + [ 195 ] = PR_shmctl, + [ 196 ] = PR_shmat, + [ 197 ] = PR_shmdt, + [ 198 ] = PR_socket, + [ 199 ] = PR_socketpair, + [ 200 ] = PR_bind, + [ 201 ] = PR_listen, + [ 202 ] = PR_accept, + [ 203 ] = PR_connect, + [ 204 ] = PR_getsockname, + [ 205 ] = PR_getpeername, + [ 206 ] = PR_sendto, + [ 207 ] = PR_recvfrom, + [ 208 ] = PR_setsockopt, + [ 209 ] = PR_getsockopt, + [ 210 ] = PR_shutdown, + [ 211 ] = PR_sendmsg, + [ 212 ] = PR_recvmsg, + [ 213 ] = PR_readahead, + [ 214 ] = PR_brk, + [ 215 ] = PR_munmap, + [ 216 ] = PR_mremap, + [ 217 ] = PR_add_key, + [ 218 ] = PR_request_key, + [ 219 ] = PR_keyctl, + [ 220 ] = PR_clone, + [ 221 ] = PR_execve, + [ 222 ] = PR_mmap, + [ 223 ] = PR_fadvise64, + [ 224 ] = PR_swapon, + [ 225 ] = PR_swapoff, + [ 226 ] = PR_mprotect, + [ 227 ] = PR_msync, + [ 228 ] = PR_mlock, + [ 229 ] = PR_munlock, + [ 230 ] = PR_mlockall, + [ 231 ] = PR_munlockall, + [ 232 ] = PR_mincore, + [ 233 ] = PR_madvise, + [ 234 ] = PR_remap_file_pages, + [ 235 ] = PR_mbind, + [ 236 ] = PR_get_mempolicy, + [ 237 ] = PR_set_mempolicy, + [ 238 ] = PR_migrate_pages, + [ 239 ] = PR_move_pages, + [ 240 ] = PR_rt_tgsigqueueinfo, + [ 241 ] = PR_perf_event_open, + [ 242 ] = PR_accept4, + [ 243 ] = PR_recvmmsg, + [ 244 ] = PR_arch_specific_syscall, + [ 260 ] = PR_wait4, + [ 261 ] = PR_prlimit64, + [ 262 ] = PR_fanotify_init, + [ 263 ] = PR_fanotify_mark, + [ 264 ] = PR_name_to_handle_at, + [ 265 ] = PR_open_by_handle_at, + [ 266 ] = PR_clock_adjtime, + [ 267 ] = PR_syncfs, + [ 268 ] = PR_setns, + [ 269 ] = PR_sendmmsg, + [ 270 ] = PR_process_vm_readv, + [ 271 ] = PR_process_vm_writev, + [ 272 ] = PR_kcmp, + [ 273 ] = PR_finit_module, + [ 274 ] = PR_sched_setattr, + [ 275 ] = PR_sched_getattr, + [ 276 ] = PR_renameat2, + [ 277 ] = PR_seccomp, + [ 278 ] = PR_getrandom, + [ 279 ] = PR_memfd_create, + [ 280 ] = PR_bpf, + [ 281 ] = PR_execveat, + [ 282 ] = PR_userfaultfd, + [ 283 ] = PR_membarrier, + [ 284 ] = PR_mlock2, + [ 285 ] = PR_copy_file_range, + [ 286 ] = PR_preadv2, + [ 287 ] = PR_pwritev2, + [ 288 ] = PR_pkey_mprotect, + [ 289 ] = PR_pkey_alloc, + [ 290 ] = PR_pkey_free, + [ 291 ] = PR_statx, + [ 435 ] = PR_clone3, + [ 437 ] = PR_openat2, + [ 439 ] = PR_faccessat2, +}; diff --git a/core/proot/src/main/cpp/syscall/sysnums-i386.h b/core/proot/src/main/cpp/syscall/sysnums-i386.h new file mode 100644 index 000000000..2dfee95ad --- /dev/null +++ b/core/proot/src/main/cpp/syscall/sysnums-i386.h @@ -0,0 +1,387 @@ +#include "syscall/sysnum.h" + +static const Sysnum sysnums_i386[] = { + [ 0 ] = PR_restart_syscall, + [ 1 ] = PR_exit, + [ 2 ] = PR_fork, + [ 3 ] = PR_read, + [ 4 ] = PR_write, + [ 5 ] = PR_open, + [ 6 ] = PR_close, + [ 7 ] = PR_waitpid, + [ 8 ] = PR_creat, + [ 9 ] = PR_link, + [ 10 ] = PR_unlink, + [ 11 ] = PR_execve, + [ 12 ] = PR_chdir, + [ 13 ] = PR_time, + [ 14 ] = PR_mknod, + [ 15 ] = PR_chmod, + [ 16 ] = PR_lchown, + [ 17 ] = PR_break, + [ 18 ] = PR_oldstat, + [ 19 ] = PR_lseek, + [ 20 ] = PR_getpid, + [ 21 ] = PR_mount, + [ 22 ] = PR_umount, + [ 23 ] = PR_setuid, + [ 24 ] = PR_getuid, + [ 25 ] = PR_stime, + [ 26 ] = PR_ptrace, + [ 27 ] = PR_alarm, + [ 28 ] = PR_oldfstat, + [ 29 ] = PR_pause, + [ 30 ] = PR_utime, + [ 31 ] = PR_stty, + [ 32 ] = PR_gtty, + [ 33 ] = PR_access, + [ 34 ] = PR_nice, + [ 35 ] = PR_ftime, + [ 36 ] = PR_sync, + [ 37 ] = PR_kill, + [ 38 ] = PR_rename, + [ 39 ] = PR_mkdir, + [ 40 ] = PR_rmdir, + [ 41 ] = PR_dup, + [ 42 ] = PR_pipe, + [ 43 ] = PR_times, + [ 44 ] = PR_prof, + [ 45 ] = PR_brk, + [ 46 ] = PR_setgid, + [ 47 ] = PR_getgid, + [ 48 ] = PR_signal, + [ 49 ] = PR_geteuid, + [ 50 ] = PR_getegid, + [ 51 ] = PR_acct, + [ 52 ] = PR_umount2, + [ 53 ] = PR_lock, + [ 54 ] = PR_ioctl, + [ 55 ] = PR_fcntl, + [ 56 ] = PR_mpx, + [ 57 ] = PR_setpgid, + [ 58 ] = PR_ulimit, + [ 59 ] = PR_oldolduname, + [ 60 ] = PR_umask, + [ 61 ] = PR_chroot, + [ 62 ] = PR_ustat, + [ 63 ] = PR_dup2, + [ 64 ] = PR_getppid, + [ 65 ] = PR_getpgrp, + [ 66 ] = PR_setsid, + [ 67 ] = PR_sigaction, + [ 68 ] = PR_sgetmask, + [ 69 ] = PR_ssetmask, + [ 70 ] = PR_setreuid, + [ 71 ] = PR_setregid, + [ 72 ] = PR_sigsuspend, + [ 73 ] = PR_sigpending, + [ 74 ] = PR_sethostname, + [ 75 ] = PR_setrlimit, + [ 76 ] = PR_getrlimit, + [ 77 ] = PR_getrusage, + [ 78 ] = PR_gettimeofday, + [ 79 ] = PR_settimeofday, + [ 80 ] = PR_getgroups, + [ 81 ] = PR_setgroups, + [ 82 ] = PR_select, + [ 83 ] = PR_symlink, + [ 84 ] = PR_oldlstat, + [ 85 ] = PR_readlink, + [ 86 ] = PR_uselib, + [ 87 ] = PR_swapon, + [ 88 ] = PR_reboot, + [ 89 ] = PR_readdir, + [ 90 ] = PR_mmap, + [ 91 ] = PR_munmap, + [ 92 ] = PR_truncate, + [ 93 ] = PR_ftruncate, + [ 94 ] = PR_fchmod, + [ 95 ] = PR_fchown, + [ 96 ] = PR_getpriority, + [ 97 ] = PR_setpriority, + [ 98 ] = PR_profil, + [ 99 ] = PR_statfs, + [ 100 ] = PR_fstatfs, + [ 101 ] = PR_ioperm, + [ 102 ] = PR_socketcall, + [ 103 ] = PR_syslog, + [ 104 ] = PR_setitimer, + [ 105 ] = PR_getitimer, + [ 106 ] = PR_stat, + [ 107 ] = PR_lstat, + [ 108 ] = PR_fstat, + [ 109 ] = PR_olduname, + [ 110 ] = PR_iopl, + [ 111 ] = PR_vhangup, + [ 112 ] = PR_idle, + [ 113 ] = PR_vm86old, + [ 114 ] = PR_wait4, + [ 115 ] = PR_swapoff, + [ 116 ] = PR_sysinfo, + [ 117 ] = PR_ipc, + [ 118 ] = PR_fsync, + [ 119 ] = PR_sigreturn, + [ 120 ] = PR_clone, + [ 121 ] = PR_setdomainname, + [ 122 ] = PR_uname, + [ 123 ] = PR_modify_ldt, + [ 124 ] = PR_adjtimex, + [ 125 ] = PR_mprotect, + [ 126 ] = PR_sigprocmask, + [ 127 ] = PR_create_module, + [ 128 ] = PR_init_module, + [ 129 ] = PR_delete_module, + [ 130 ] = PR_get_kernel_syms, + [ 131 ] = PR_quotactl, + [ 132 ] = PR_getpgid, + [ 133 ] = PR_fchdir, + [ 134 ] = PR_bdflush, + [ 135 ] = PR_sysfs, + [ 136 ] = PR_personality, + [ 137 ] = PR_afs_syscall, + [ 138 ] = PR_setfsuid, + [ 139 ] = PR_setfsgid, + [ 140 ] = PR__llseek, + [ 141 ] = PR_getdents, + [ 142 ] = PR__newselect, + [ 143 ] = PR_flock, + [ 144 ] = PR_msync, + [ 145 ] = PR_readv, + [ 146 ] = PR_writev, + [ 147 ] = PR_getsid, + [ 148 ] = PR_fdatasync, + [ 149 ] = PR__sysctl, + [ 150 ] = PR_mlock, + [ 151 ] = PR_munlock, + [ 152 ] = PR_mlockall, + [ 153 ] = PR_munlockall, + [ 154 ] = PR_sched_setparam, + [ 155 ] = PR_sched_getparam, + [ 156 ] = PR_sched_setscheduler, + [ 157 ] = PR_sched_getscheduler, + [ 158 ] = PR_sched_yield, + [ 159 ] = PR_sched_get_priority_max, + [ 160 ] = PR_sched_get_priority_min, + [ 161 ] = PR_sched_rr_get_interval, + [ 162 ] = PR_nanosleep, + [ 163 ] = PR_mremap, + [ 164 ] = PR_setresuid, + [ 165 ] = PR_getresuid, + [ 166 ] = PR_vm86, + [ 167 ] = PR_query_module, + [ 168 ] = PR_poll, + [ 169 ] = PR_nfsservctl, + [ 170 ] = PR_setresgid, + [ 171 ] = PR_getresgid, + [ 172 ] = PR_prctl, + [ 173 ] = PR_rt_sigreturn, + [ 174 ] = PR_rt_sigaction, + [ 175 ] = PR_rt_sigprocmask, + [ 176 ] = PR_rt_sigpending, + [ 177 ] = PR_rt_sigtimedwait, + [ 178 ] = PR_rt_sigqueueinfo, + [ 179 ] = PR_rt_sigsuspend, + [ 180 ] = PR_pread64, + [ 181 ] = PR_pwrite64, + [ 182 ] = PR_chown, + [ 183 ] = PR_getcwd, + [ 184 ] = PR_capget, + [ 185 ] = PR_capset, + [ 186 ] = PR_sigaltstack, + [ 187 ] = PR_sendfile, + [ 188 ] = PR_getpmsg, + [ 189 ] = PR_putpmsg, + [ 190 ] = PR_vfork, + [ 191 ] = PR_ugetrlimit, + [ 192 ] = PR_mmap2, + [ 193 ] = PR_truncate64, + [ 194 ] = PR_ftruncate64, + [ 195 ] = PR_stat64, + [ 196 ] = PR_lstat64, + [ 197 ] = PR_fstat64, + [ 198 ] = PR_lchown32, + [ 199 ] = PR_getuid32, + [ 200 ] = PR_getgid32, + [ 201 ] = PR_geteuid32, + [ 202 ] = PR_getegid32, + [ 203 ] = PR_setreuid32, + [ 204 ] = PR_setregid32, + [ 205 ] = PR_getgroups32, + [ 206 ] = PR_setgroups32, + [ 207 ] = PR_fchown32, + [ 208 ] = PR_setresuid32, + [ 209 ] = PR_getresuid32, + [ 210 ] = PR_setresgid32, + [ 211 ] = PR_getresgid32, + [ 212 ] = PR_chown32, + [ 213 ] = PR_setuid32, + [ 214 ] = PR_setgid32, + [ 215 ] = PR_setfsuid32, + [ 216 ] = PR_setfsgid32, + [ 217 ] = PR_pivot_root, + [ 218 ] = PR_mincore, + [ 219 ] = PR_madvise, + [ 220 ] = PR_getdents64, + [ 221 ] = PR_fcntl64, + [ 224 ] = PR_gettid, + [ 225 ] = PR_readahead, + [ 226 ] = PR_setxattr, + [ 227 ] = PR_lsetxattr, + [ 228 ] = PR_fsetxattr, + [ 229 ] = PR_getxattr, + [ 230 ] = PR_lgetxattr, + [ 231 ] = PR_fgetxattr, + [ 232 ] = PR_listxattr, + [ 233 ] = PR_llistxattr, + [ 234 ] = PR_flistxattr, + [ 235 ] = PR_removexattr, + [ 236 ] = PR_lremovexattr, + [ 237 ] = PR_fremovexattr, + [ 238 ] = PR_tkill, + [ 239 ] = PR_sendfile64, + [ 240 ] = PR_futex, + [ 241 ] = PR_sched_setaffinity, + [ 242 ] = PR_sched_getaffinity, + [ 243 ] = PR_set_thread_area, + [ 244 ] = PR_get_thread_area, + [ 245 ] = PR_io_setup, + [ 246 ] = PR_io_destroy, + [ 247 ] = PR_io_getevents, + [ 248 ] = PR_io_submit, + [ 249 ] = PR_io_cancel, + [ 250 ] = PR_fadvise64, + [ 252 ] = PR_exit_group, + [ 253 ] = PR_lookup_dcookie, + [ 254 ] = PR_epoll_create, + [ 255 ] = PR_epoll_ctl, + [ 256 ] = PR_epoll_wait, + [ 257 ] = PR_remap_file_pages, + [ 258 ] = PR_set_tid_address, + [ 259 ] = PR_timer_create, + [ 260 ] = PR_timer_settime, + [ 261 ] = PR_timer_gettime, + [ 262 ] = PR_timer_getoverrun, + [ 263 ] = PR_timer_delete, + [ 264 ] = PR_clock_settime, + [ 265 ] = PR_clock_gettime, + [ 266 ] = PR_clock_getres, + [ 267 ] = PR_clock_nanosleep, + [ 268 ] = PR_statfs64, + [ 269 ] = PR_fstatfs64, + [ 270 ] = PR_tgkill, + [ 271 ] = PR_utimes, + [ 272 ] = PR_fadvise64_64, + [ 273 ] = PR_vserver, + [ 274 ] = PR_mbind, + [ 275 ] = PR_get_mempolicy, + [ 276 ] = PR_set_mempolicy, + [ 277 ] = PR_mq_open, + [ 278 ] = PR_mq_unlink, + [ 279 ] = PR_mq_timedsend, + [ 280 ] = PR_mq_timedreceive, + [ 281 ] = PR_mq_notify, + [ 282 ] = PR_mq_getsetattr, + [ 283 ] = PR_kexec_load, + [ 284 ] = PR_waitid, + [ 286 ] = PR_add_key, + [ 287 ] = PR_request_key, + [ 288 ] = PR_keyctl, + [ 289 ] = PR_ioprio_set, + [ 290 ] = PR_ioprio_get, + [ 291 ] = PR_inotify_init, + [ 292 ] = PR_inotify_add_watch, + [ 293 ] = PR_inotify_rm_watch, + [ 294 ] = PR_migrate_pages, + [ 295 ] = PR_openat, + [ 296 ] = PR_mkdirat, + [ 297 ] = PR_mknodat, + [ 298 ] = PR_fchownat, + [ 299 ] = PR_futimesat, + [ 300 ] = PR_fstatat64, + [ 301 ] = PR_unlinkat, + [ 302 ] = PR_renameat, + [ 303 ] = PR_linkat, + [ 304 ] = PR_symlinkat, + [ 305 ] = PR_readlinkat, + [ 306 ] = PR_fchmodat, + [ 307 ] = PR_faccessat, + [ 308 ] = PR_pselect6, + [ 309 ] = PR_ppoll, + [ 310 ] = PR_unshare, + [ 311 ] = PR_set_robust_list, + [ 312 ] = PR_get_robust_list, + [ 313 ] = PR_splice, + [ 314 ] = PR_sync_file_range, + [ 315 ] = PR_tee, + [ 316 ] = PR_vmsplice, + [ 317 ] = PR_move_pages, + [ 318 ] = PR_getcpu, + [ 319 ] = PR_epoll_pwait, + [ 320 ] = PR_utimensat, + [ 321 ] = PR_signalfd, + [ 322 ] = PR_timerfd_create, + [ 323 ] = PR_eventfd, + [ 324 ] = PR_fallocate, + [ 325 ] = PR_timerfd_settime, + [ 326 ] = PR_timerfd_gettime, + [ 327 ] = PR_signalfd4, + [ 328 ] = PR_eventfd2, + [ 329 ] = PR_epoll_create1, + [ 330 ] = PR_dup3, + [ 331 ] = PR_pipe2, + [ 332 ] = PR_inotify_init1, + [ 333 ] = PR_preadv, + [ 334 ] = PR_pwritev, + [ 335 ] = PR_rt_tgsigqueueinfo, + [ 336 ] = PR_perf_event_open, + [ 337 ] = PR_recvmmsg, + [ 338 ] = PR_fanotify_init, + [ 339 ] = PR_fanotify_mark, + [ 340 ] = PR_prlimit64, + [ 341 ] = PR_name_to_handle_at, + [ 342 ] = PR_open_by_handle_at, + [ 343 ] = PR_clock_adjtime, + [ 344 ] = PR_syncfs, + [ 345 ] = PR_sendmmsg, + [ 346 ] = PR_setns, + [ 347 ] = PR_process_vm_readv, + [ 348 ] = PR_process_vm_writev, + [ 349 ] = PR_kcmp, + [ 350 ] = PR_finit_module, + [ 351 ] = PR_sched_setattr, + [ 352 ] = PR_sched_getattr, + [ 353 ] = PR_renameat2, + [ 354 ] = PR_seccomp, + [ 355 ] = PR_getrandom, + [ 356 ] = PR_memfd_create, + [ 357 ] = PR_bpf, + [ 358 ] = PR_execveat, + [ 359 ] = PR_socket, + [ 360 ] = PR_socketpair, + [ 361 ] = PR_bind, + [ 362 ] = PR_connect, + [ 363 ] = PR_listen, + [ 364 ] = PR_accept4, + [ 365 ] = PR_getsockopt, + [ 366 ] = PR_setsockopt, + [ 367 ] = PR_getsockname, + [ 368 ] = PR_getpeername, + [ 369 ] = PR_sendto, + [ 370 ] = PR_sendmsg, + [ 371 ] = PR_recvfrom, + [ 372 ] = PR_recvmsg, + [ 373 ] = PR_shutdown, + [ 374 ] = PR_userfaultfd, + [ 375 ] = PR_membarrier, + [ 376 ] = PR_mlock2, + [ 377 ] = PR_copy_file_range, + [ 378 ] = PR_preadv2, + [ 379 ] = PR_pwritev2, + [ 380 ] = PR_pkey_mprotect, + [ 381 ] = PR_pkey_alloc, + [ 382 ] = PR_pkey_free, + [ 383 ] = PR_statx, + [ 435 ] = PR_clone3, + [ 437 ] = PR_openat2, + [ 439 ] = PR_faccessat2, +}; diff --git a/core/proot/src/main/cpp/syscall/sysnums-sh4.h b/core/proot/src/main/cpp/syscall/sysnums-sh4.h new file mode 100644 index 000000000..5081c8ab7 --- /dev/null +++ b/core/proot/src/main/cpp/syscall/sysnums-sh4.h @@ -0,0 +1,345 @@ +#include "syscall/sysnum.h" + +static const Sysnum sysnums_sh4[] = { + [ 0 ] = PR_restart_syscall, + [ 1 ] = PR_exit, + [ 2 ] = PR_fork, + [ 3 ] = PR_read, + [ 4 ] = PR_write, + [ 5 ] = PR_open, + [ 6 ] = PR_close, + [ 7 ] = PR_waitpid, + [ 8 ] = PR_creat, + [ 9 ] = PR_link, + [ 10 ] = PR_unlink, + [ 11 ] = PR_execve, + [ 12 ] = PR_chdir, + [ 13 ] = PR_time, + [ 14 ] = PR_mknod, + [ 15 ] = PR_chmod, + [ 16 ] = PR_lchown, + [ 18 ] = PR_oldstat, + [ 19 ] = PR_lseek, + [ 20 ] = PR_getpid, + [ 21 ] = PR_mount, + [ 22 ] = PR_umount, + [ 23 ] = PR_setuid, + [ 24 ] = PR_getuid, + [ 25 ] = PR_stime, + [ 26 ] = PR_ptrace, + [ 27 ] = PR_alarm, + [ 28 ] = PR_oldfstat, + [ 29 ] = PR_pause, + [ 30 ] = PR_utime, + [ 33 ] = PR_access, + [ 34 ] = PR_nice, + [ 36 ] = PR_sync, + [ 37 ] = PR_kill, + [ 38 ] = PR_rename, + [ 39 ] = PR_mkdir, + [ 40 ] = PR_rmdir, + [ 41 ] = PR_dup, + [ 42 ] = PR_pipe, + [ 43 ] = PR_times, + [ 45 ] = PR_brk, + [ 46 ] = PR_setgid, + [ 47 ] = PR_getgid, + [ 48 ] = PR_signal, + [ 49 ] = PR_geteuid, + [ 50 ] = PR_getegid, + [ 51 ] = PR_acct, + [ 52 ] = PR_umount2, + [ 54 ] = PR_ioctl, + [ 55 ] = PR_fcntl, + [ 57 ] = PR_setpgid, + [ 60 ] = PR_umask, + [ 61 ] = PR_chroot, + [ 62 ] = PR_ustat, + [ 63 ] = PR_dup2, + [ 64 ] = PR_getppid, + [ 65 ] = PR_getpgrp, + [ 66 ] = PR_setsid, + [ 67 ] = PR_sigaction, + [ 68 ] = PR_sgetmask, + [ 69 ] = PR_ssetmask, + [ 70 ] = PR_setreuid, + [ 71 ] = PR_setregid, + [ 72 ] = PR_sigsuspend, + [ 73 ] = PR_sigpending, + [ 74 ] = PR_sethostname, + [ 75 ] = PR_setrlimit, + [ 76 ] = PR_getrlimit, + [ 77 ] = PR_getrusage, + [ 78 ] = PR_gettimeofday, + [ 79 ] = PR_settimeofday, + [ 80 ] = PR_getgroups, + [ 81 ] = PR_setgroups, + [ 83 ] = PR_symlink, + [ 84 ] = PR_oldlstat, + [ 85 ] = PR_readlink, + [ 86 ] = PR_uselib, + [ 87 ] = PR_swapon, + [ 88 ] = PR_reboot, + [ 89 ] = PR_readdir, + [ 90 ] = PR_mmap, + [ 91 ] = PR_munmap, + [ 92 ] = PR_truncate, + [ 93 ] = PR_ftruncate, + [ 94 ] = PR_fchmod, + [ 95 ] = PR_fchown, + [ 96 ] = PR_getpriority, + [ 97 ] = PR_setpriority, + [ 99 ] = PR_statfs, + [ 100 ] = PR_fstatfs, + [ 102 ] = PR_socketcall, + [ 103 ] = PR_syslog, + [ 104 ] = PR_setitimer, + [ 105 ] = PR_getitimer, + [ 106 ] = PR_stat, + [ 107 ] = PR_lstat, + [ 108 ] = PR_fstat, + [ 109 ] = PR_olduname, + [ 111 ] = PR_vhangup, + [ 114 ] = PR_wait4, + [ 115 ] = PR_swapoff, + [ 116 ] = PR_sysinfo, + [ 117 ] = PR_ipc, + [ 118 ] = PR_fsync, + [ 119 ] = PR_sigreturn, + [ 120 ] = PR_clone, + [ 121 ] = PR_setdomainname, + [ 122 ] = PR_uname, + [ 123 ] = PR_cacheflush, + [ 124 ] = PR_adjtimex, + [ 125 ] = PR_mprotect, + [ 126 ] = PR_sigprocmask, + [ 128 ] = PR_init_module, + [ 129 ] = PR_delete_module, + [ 131 ] = PR_quotactl, + [ 132 ] = PR_getpgid, + [ 133 ] = PR_fchdir, + [ 134 ] = PR_bdflush, + [ 135 ] = PR_sysfs, + [ 136 ] = PR_personality, + [ 138 ] = PR_setfsuid, + [ 139 ] = PR_setfsgid, + [ 140 ] = PR__llseek, + [ 141 ] = PR_getdents, + [ 142 ] = PR__newselect, + [ 143 ] = PR_flock, + [ 144 ] = PR_msync, + [ 145 ] = PR_readv, + [ 146 ] = PR_writev, + [ 147 ] = PR_getsid, + [ 148 ] = PR_fdatasync, + [ 149 ] = PR__sysctl, + [ 150 ] = PR_mlock, + [ 151 ] = PR_munlock, + [ 152 ] = PR_mlockall, + [ 153 ] = PR_munlockall, + [ 154 ] = PR_sched_setparam, + [ 155 ] = PR_sched_getparam, + [ 156 ] = PR_sched_setscheduler, + [ 157 ] = PR_sched_getscheduler, + [ 158 ] = PR_sched_yield, + [ 159 ] = PR_sched_get_priority_max, + [ 160 ] = PR_sched_get_priority_min, + [ 161 ] = PR_sched_rr_get_interval, + [ 162 ] = PR_nanosleep, + [ 163 ] = PR_mremap, + [ 164 ] = PR_setresuid, + [ 165 ] = PR_getresuid, + [ 168 ] = PR_poll, + [ 169 ] = PR_nfsservctl, + [ 170 ] = PR_setresgid, + [ 171 ] = PR_getresgid, + [ 172 ] = PR_prctl, + [ 173 ] = PR_rt_sigreturn, + [ 174 ] = PR_rt_sigaction, + [ 175 ] = PR_rt_sigprocmask, + [ 176 ] = PR_rt_sigpending, + [ 177 ] = PR_rt_sigtimedwait, + [ 178 ] = PR_rt_sigqueueinfo, + [ 179 ] = PR_rt_sigsuspend, + [ 180 ] = PR_pread64, + [ 181 ] = PR_pwrite64, + [ 182 ] = PR_chown, + [ 183 ] = PR_getcwd, + [ 184 ] = PR_capget, + [ 185 ] = PR_capset, + [ 186 ] = PR_sigaltstack, + [ 187 ] = PR_sendfile, + [ 190 ] = PR_vfork, + [ 191 ] = PR_ugetrlimit, + [ 192 ] = PR_mmap2, + [ 193 ] = PR_truncate64, + [ 194 ] = PR_ftruncate64, + [ 195 ] = PR_stat64, + [ 196 ] = PR_lstat64, + [ 197 ] = PR_fstat64, + [ 198 ] = PR_lchown32, + [ 199 ] = PR_getuid32, + [ 200 ] = PR_getgid32, + [ 201 ] = PR_geteuid32, + [ 202 ] = PR_getegid32, + [ 203 ] = PR_setreuid32, + [ 204 ] = PR_setregid32, + [ 205 ] = PR_getgroups32, + [ 206 ] = PR_setgroups32, + [ 207 ] = PR_fchown32, + [ 208 ] = PR_setresuid32, + [ 209 ] = PR_getresuid32, + [ 210 ] = PR_setresgid32, + [ 211 ] = PR_getresgid32, + [ 212 ] = PR_chown32, + [ 213 ] = PR_setuid32, + [ 214 ] = PR_setgid32, + [ 215 ] = PR_setfsuid32, + [ 216 ] = PR_setfsgid32, + [ 217 ] = PR_pivot_root, + [ 218 ] = PR_mincore, + [ 219 ] = PR_madvise, + [ 220 ] = PR_getdents64, + [ 221 ] = PR_fcntl64, + [ 224 ] = PR_gettid, + [ 225 ] = PR_readahead, + [ 226 ] = PR_setxattr, + [ 227 ] = PR_lsetxattr, + [ 228 ] = PR_fsetxattr, + [ 229 ] = PR_getxattr, + [ 230 ] = PR_lgetxattr, + [ 231 ] = PR_fgetxattr, + [ 232 ] = PR_listxattr, + [ 233 ] = PR_llistxattr, + [ 234 ] = PR_flistxattr, + [ 235 ] = PR_removexattr, + [ 236 ] = PR_lremovexattr, + [ 237 ] = PR_fremovexattr, + [ 238 ] = PR_tkill, + [ 239 ] = PR_sendfile64, + [ 240 ] = PR_futex, + [ 241 ] = PR_sched_setaffinity, + [ 242 ] = PR_sched_getaffinity, + [ 245 ] = PR_io_setup, + [ 246 ] = PR_io_destroy, + [ 247 ] = PR_io_getevents, + [ 248 ] = PR_io_submit, + [ 249 ] = PR_io_cancel, + [ 250 ] = PR_fadvise64, + [ 252 ] = PR_exit_group, + [ 253 ] = PR_lookup_dcookie, + [ 254 ] = PR_epoll_create, + [ 255 ] = PR_epoll_ctl, + [ 256 ] = PR_epoll_wait, + [ 257 ] = PR_remap_file_pages, + [ 258 ] = PR_set_tid_address, + [ 259 ] = PR_timer_create, + [ 260 ] = PR_timer_settime, + [ 261 ] = PR_timer_gettime, + [ 262 ] = PR_timer_getoverrun, + [ 263 ] = PR_timer_delete, + [ 264 ] = PR_clock_settime, + [ 265 ] = PR_clock_gettime, + [ 266 ] = PR_clock_getres, + [ 267 ] = PR_clock_nanosleep, + [ 268 ] = PR_statfs64, + [ 269 ] = PR_fstatfs64, + [ 270 ] = PR_tgkill, + [ 271 ] = PR_utimes, + [ 272 ] = PR_fadvise64_64, + [ 274 ] = PR_mbind, + [ 275 ] = PR_get_mempolicy, + [ 276 ] = PR_set_mempolicy, + [ 277 ] = PR_mq_open, + [ 278 ] = PR_mq_unlink, + [ 279 ] = PR_mq_timedsend, + [ 280 ] = PR_mq_timedreceive, + [ 281 ] = PR_mq_notify, + [ 282 ] = PR_mq_getsetattr, + [ 283 ] = PR_kexec_load, + [ 284 ] = PR_waitid, + [ 285 ] = PR_add_key, + [ 286 ] = PR_request_key, + [ 287 ] = PR_keyctl, + [ 288 ] = PR_ioprio_set, + [ 289 ] = PR_ioprio_get, + [ 290 ] = PR_inotify_init, + [ 291 ] = PR_inotify_add_watch, + [ 292 ] = PR_inotify_rm_watch, + [ 294 ] = PR_migrate_pages, + [ 295 ] = PR_openat, + [ 296 ] = PR_mkdirat, + [ 297 ] = PR_mknodat, + [ 298 ] = PR_fchownat, + [ 299 ] = PR_futimesat, + [ 300 ] = PR_fstatat64, + [ 301 ] = PR_unlinkat, + [ 302 ] = PR_renameat, + [ 303 ] = PR_linkat, + [ 304 ] = PR_symlinkat, + [ 305 ] = PR_readlinkat, + [ 306 ] = PR_fchmodat, + [ 307 ] = PR_faccessat, + [ 308 ] = PR_pselect6, + [ 309 ] = PR_ppoll, + [ 310 ] = PR_unshare, + [ 311 ] = PR_set_robust_list, + [ 312 ] = PR_get_robust_list, + [ 313 ] = PR_splice, + [ 314 ] = PR_sync_file_range, + [ 315 ] = PR_tee, + [ 316 ] = PR_vmsplice, + [ 317 ] = PR_move_pages, + [ 318 ] = PR_getcpu, + [ 319 ] = PR_epoll_pwait, + [ 320 ] = PR_utimensat, + [ 321 ] = PR_signalfd, + [ 322 ] = PR_timerfd_create, + [ 323 ] = PR_eventfd, + [ 324 ] = PR_fallocate, + [ 325 ] = PR_timerfd_settime, + [ 326 ] = PR_timerfd_gettime, + [ 327 ] = PR_signalfd4, + [ 328 ] = PR_eventfd2, + [ 329 ] = PR_epoll_create1, + [ 330 ] = PR_dup3, + [ 331 ] = PR_pipe2, + [ 332 ] = PR_inotify_init1, + [ 333 ] = PR_preadv, + [ 334 ] = PR_pwritev, + [ 335 ] = PR_rt_tgsigqueueinfo, + [ 336 ] = PR_perf_event_open, + [ 337 ] = PR_fanotify_init, + [ 338 ] = PR_fanotify_mark, + [ 339 ] = PR_prlimit64, + [ 340 ] = PR_socket, + [ 341 ] = PR_bind, + [ 342 ] = PR_connect, + [ 343 ] = PR_listen, + [ 344 ] = PR_accept, + [ 345 ] = PR_getsockname, + [ 346 ] = PR_getpeername, + [ 347 ] = PR_socketpair, + [ 348 ] = PR_send, + [ 349 ] = PR_sendto, + [ 350 ] = PR_recv, + [ 351 ] = PR_recvfrom, + [ 352 ] = PR_shutdown, + [ 353 ] = PR_setsockopt, + [ 354 ] = PR_getsockopt, + [ 355 ] = PR_sendmsg, + [ 356 ] = PR_recvmsg, + [ 357 ] = PR_recvmmsg, + [ 358 ] = PR_accept4, + [ 359 ] = PR_name_to_handle_at, + [ 360 ] = PR_open_by_handle_at, + [ 361 ] = PR_clock_adjtime, + [ 362 ] = PR_syncfs, + [ 363 ] = PR_sendmmsg, + [ 364 ] = PR_setns, + [ 365 ] = PR_process_vm_readv, + [ 366 ] = PR_process_vm_writev, + [ 435 ] = PR_clone3, + [ 437 ] = PR_openat2, + [ 439 ] = PR_faccessat2, +}; diff --git a/core/proot/src/main/cpp/syscall/sysnums-x32.h b/core/proot/src/main/cpp/syscall/sysnums-x32.h new file mode 100644 index 000000000..524b7c0bb --- /dev/null +++ b/core/proot/src/main/cpp/syscall/sysnums-x32.h @@ -0,0 +1,309 @@ +#include "syscall/sysnum.h" + +static const Sysnum sysnums_x32[] = { + [ 0 ] = PR_read, + [ 1 ] = PR_write, + [ 2 ] = PR_open, + [ 3 ] = PR_close, + [ 4 ] = PR_stat, + [ 5 ] = PR_fstat, + [ 6 ] = PR_lstat, + [ 7 ] = PR_poll, + [ 8 ] = PR_lseek, + [ 9 ] = PR_mmap, + [ 10 ] = PR_mprotect, + [ 11 ] = PR_munmap, + [ 12 ] = PR_brk, + [ 14 ] = PR_rt_sigprocmask, + [ 17 ] = PR_pread64, + [ 18 ] = PR_pwrite64, + [ 21 ] = PR_access, + [ 22 ] = PR_pipe, + [ 23 ] = PR_select, + [ 24 ] = PR_sched_yield, + [ 25 ] = PR_mremap, + [ 26 ] = PR_msync, + [ 27 ] = PR_mincore, + [ 28 ] = PR_madvise, + [ 29 ] = PR_shmget, + [ 30 ] = PR_shmat, + [ 31 ] = PR_shmctl, + [ 32 ] = PR_dup, + [ 33 ] = PR_dup2, + [ 34 ] = PR_pause, + [ 35 ] = PR_nanosleep, + [ 36 ] = PR_getitimer, + [ 37 ] = PR_alarm, + [ 38 ] = PR_setitimer, + [ 39 ] = PR_getpid, + [ 40 ] = PR_sendfile, + [ 41 ] = PR_socket, + [ 42 ] = PR_connect, + [ 43 ] = PR_accept, + [ 44 ] = PR_sendto, + [ 48 ] = PR_shutdown, + [ 49 ] = PR_bind, + [ 50 ] = PR_listen, + [ 51 ] = PR_getsockname, + [ 52 ] = PR_getpeername, + [ 53 ] = PR_socketpair, + [ 56 ] = PR_clone, + [ 57 ] = PR_fork, + [ 58 ] = PR_vfork, + [ 60 ] = PR_exit, + [ 61 ] = PR_wait4, + [ 62 ] = PR_kill, + [ 63 ] = PR_uname, + [ 64 ] = PR_semget, + [ 65 ] = PR_semop, + [ 66 ] = PR_semctl, + [ 67 ] = PR_shmdt, + [ 68 ] = PR_msgget, + [ 69 ] = PR_msgsnd, + [ 70 ] = PR_msgrcv, + [ 71 ] = PR_msgctl, + [ 72 ] = PR_fcntl, + [ 73 ] = PR_flock, + [ 74 ] = PR_fsync, + [ 75 ] = PR_fdatasync, + [ 76 ] = PR_truncate, + [ 77 ] = PR_ftruncate, + [ 78 ] = PR_getdents, + [ 79 ] = PR_getcwd, + [ 80 ] = PR_chdir, + [ 81 ] = PR_fchdir, + [ 82 ] = PR_rename, + [ 83 ] = PR_mkdir, + [ 84 ] = PR_rmdir, + [ 85 ] = PR_creat, + [ 86 ] = PR_link, + [ 87 ] = PR_unlink, + [ 88 ] = PR_symlink, + [ 89 ] = PR_readlink, + [ 90 ] = PR_chmod, + [ 91 ] = PR_fchmod, + [ 92 ] = PR_chown, + [ 93 ] = PR_fchown, + [ 94 ] = PR_lchown, + [ 95 ] = PR_umask, + [ 96 ] = PR_gettimeofday, + [ 97 ] = PR_getrlimit, + [ 98 ] = PR_getrusage, + [ 99 ] = PR_sysinfo, + [ 100 ] = PR_times, + [ 102 ] = PR_getuid, + [ 103 ] = PR_syslog, + [ 104 ] = PR_getgid, + [ 105 ] = PR_setuid, + [ 106 ] = PR_setgid, + [ 107 ] = PR_geteuid, + [ 108 ] = PR_getegid, + [ 109 ] = PR_setpgid, + [ 110 ] = PR_getppid, + [ 111 ] = PR_getpgrp, + [ 112 ] = PR_setsid, + [ 113 ] = PR_setreuid, + [ 114 ] = PR_setregid, + [ 115 ] = PR_getgroups, + [ 116 ] = PR_setgroups, + [ 117 ] = PR_setresuid, + [ 118 ] = PR_getresuid, + [ 119 ] = PR_setresgid, + [ 120 ] = PR_getresgid, + [ 121 ] = PR_getpgid, + [ 122 ] = PR_setfsuid, + [ 123 ] = PR_setfsgid, + [ 124 ] = PR_getsid, + [ 125 ] = PR_capget, + [ 126 ] = PR_capset, + [ 130 ] = PR_rt_sigsuspend, + [ 132 ] = PR_utime, + [ 133 ] = PR_mknod, + [ 135 ] = PR_personality, + [ 136 ] = PR_ustat, + [ 137 ] = PR_statfs, + [ 138 ] = PR_fstatfs, + [ 139 ] = PR_sysfs, + [ 140 ] = PR_getpriority, + [ 141 ] = PR_setpriority, + [ 142 ] = PR_sched_setparam, + [ 143 ] = PR_sched_getparam, + [ 144 ] = PR_sched_setscheduler, + [ 145 ] = PR_sched_getscheduler, + [ 146 ] = PR_sched_get_priority_max, + [ 147 ] = PR_sched_get_priority_min, + [ 148 ] = PR_sched_rr_get_interval, + [ 149 ] = PR_mlock, + [ 150 ] = PR_munlock, + [ 151 ] = PR_mlockall, + [ 152 ] = PR_munlockall, + [ 153 ] = PR_vhangup, + [ 154 ] = PR_modify_ldt, + [ 155 ] = PR_pivot_root, + [ 157 ] = PR_prctl, + [ 158 ] = PR_arch_prctl, + [ 159 ] = PR_adjtimex, + [ 160 ] = PR_setrlimit, + [ 161 ] = PR_chroot, + [ 162 ] = PR_sync, + [ 163 ] = PR_acct, + [ 164 ] = PR_settimeofday, + [ 165 ] = PR_mount, + [ 166 ] = PR_umount2, + [ 167 ] = PR_swapon, + [ 168 ] = PR_swapoff, + [ 169 ] = PR_reboot, + [ 170 ] = PR_sethostname, + [ 171 ] = PR_setdomainname, + [ 172 ] = PR_iopl, + [ 173 ] = PR_ioperm, + [ 175 ] = PR_init_module, + [ 176 ] = PR_delete_module, + [ 179 ] = PR_quotactl, + [ 181 ] = PR_getpmsg, + [ 182 ] = PR_putpmsg, + [ 183 ] = PR_afs_syscall, + [ 184 ] = PR_tuxcall, + [ 185 ] = PR_security, + [ 186 ] = PR_gettid, + [ 187 ] = PR_readahead, + [ 188 ] = PR_setxattr, + [ 189 ] = PR_lsetxattr, + [ 190 ] = PR_fsetxattr, + [ 191 ] = PR_getxattr, + [ 192 ] = PR_lgetxattr, + [ 193 ] = PR_fgetxattr, + [ 194 ] = PR_listxattr, + [ 195 ] = PR_llistxattr, + [ 196 ] = PR_flistxattr, + [ 197 ] = PR_removexattr, + [ 198 ] = PR_lremovexattr, + [ 199 ] = PR_fremovexattr, + [ 200 ] = PR_tkill, + [ 201 ] = PR_time, + [ 202 ] = PR_futex, + [ 203 ] = PR_sched_setaffinity, + [ 204 ] = PR_sched_getaffinity, + [ 206 ] = PR_io_setup, + [ 207 ] = PR_io_destroy, + [ 208 ] = PR_io_getevents, + [ 209 ] = PR_io_submit, + [ 210 ] = PR_io_cancel, + [ 212 ] = PR_lookup_dcookie, + [ 213 ] = PR_epoll_create, + [ 216 ] = PR_remap_file_pages, + [ 217 ] = PR_getdents64, + [ 218 ] = PR_set_tid_address, + [ 219 ] = PR_restart_syscall, + [ 220 ] = PR_semtimedop, + [ 221 ] = PR_fadvise64, + [ 223 ] = PR_timer_settime, + [ 224 ] = PR_timer_gettime, + [ 225 ] = PR_timer_getoverrun, + [ 226 ] = PR_timer_delete, + [ 227 ] = PR_clock_settime, + [ 228 ] = PR_clock_gettime, + [ 229 ] = PR_clock_getres, + [ 230 ] = PR_clock_nanosleep, + [ 231 ] = PR_exit_group, + [ 232 ] = PR_epoll_wait, + [ 233 ] = PR_epoll_ctl, + [ 234 ] = PR_tgkill, + [ 235 ] = PR_utimes, + [ 237 ] = PR_mbind, + [ 238 ] = PR_set_mempolicy, + [ 239 ] = PR_get_mempolicy, + [ 240 ] = PR_mq_open, + [ 241 ] = PR_mq_unlink, + [ 242 ] = PR_mq_timedsend, + [ 243 ] = PR_mq_timedreceive, + [ 245 ] = PR_mq_getsetattr, + [ 248 ] = PR_add_key, + [ 249 ] = PR_request_key, + [ 250 ] = PR_keyctl, + [ 251 ] = PR_ioprio_set, + [ 252 ] = PR_ioprio_get, + [ 253 ] = PR_inotify_init, + [ 254 ] = PR_inotify_add_watch, + [ 255 ] = PR_inotify_rm_watch, + [ 256 ] = PR_migrate_pages, + [ 257 ] = PR_openat, + [ 258 ] = PR_mkdirat, + [ 259 ] = PR_mknodat, + [ 260 ] = PR_fchownat, + [ 261 ] = PR_futimesat, + [ 262 ] = PR_newfstatat, + [ 263 ] = PR_unlinkat, + [ 264 ] = PR_renameat, + [ 265 ] = PR_linkat, + [ 266 ] = PR_symlinkat, + [ 267 ] = PR_readlinkat, + [ 268 ] = PR_fchmodat, + [ 269 ] = PR_faccessat, + [ 270 ] = PR_pselect6, + [ 271 ] = PR_ppoll, + [ 272 ] = PR_unshare, + [ 275 ] = PR_splice, + [ 276 ] = PR_tee, + [ 277 ] = PR_sync_file_range, + [ 280 ] = PR_utimensat, + [ 281 ] = PR_epoll_pwait, + [ 282 ] = PR_signalfd, + [ 283 ] = PR_timerfd_create, + [ 284 ] = PR_eventfd, + [ 285 ] = PR_fallocate, + [ 286 ] = PR_timerfd_settime, + [ 287 ] = PR_timerfd_gettime, + [ 288 ] = PR_accept4, + [ 289 ] = PR_signalfd4, + [ 290 ] = PR_eventfd2, + [ 291 ] = PR_epoll_create1, + [ 292 ] = PR_dup3, + [ 293 ] = PR_pipe2, + [ 294 ] = PR_inotify_init1, + [ 298 ] = PR_perf_event_open, + [ 300 ] = PR_fanotify_init, + [ 301 ] = PR_fanotify_mark, + [ 302 ] = PR_prlimit64, + [ 303 ] = PR_name_to_handle_at, + [ 304 ] = PR_open_by_handle_at, + [ 305 ] = PR_clock_adjtime, + [ 306 ] = PR_syncfs, + [ 308 ] = PR_setns, + [ 309 ] = PR_getcpu, + [ 312 ] = PR_kcmp, + [ 316 ] = PR_renameat2, + [ 435 ] = PR_clone3, + [ 437 ] = PR_openat2, + [ 512 ] = PR_rt_sigaction, + [ 513 ] = PR_rt_sigreturn, + [ 514 ] = PR_ioctl, + [ 515 ] = PR_readv, + [ 516 ] = PR_writev, + [ 517 ] = PR_recvfrom, + [ 518 ] = PR_sendmsg, + [ 519 ] = PR_recvmsg, + [ 520 ] = PR_execve, + [ 521 ] = PR_ptrace, + [ 522 ] = PR_rt_sigpending, + [ 523 ] = PR_rt_sigtimedwait, + [ 524 ] = PR_rt_sigqueueinfo, + [ 525 ] = PR_sigaltstack, + [ 526 ] = PR_timer_create, + [ 527 ] = PR_mq_notify, + [ 528 ] = PR_kexec_load, + [ 529 ] = PR_waitid, + [ 530 ] = PR_set_robust_list, + [ 531 ] = PR_get_robust_list, + [ 532 ] = PR_vmsplice, + [ 533 ] = PR_move_pages, + [ 534 ] = PR_preadv, + [ 535 ] = PR_pwritev, + [ 536 ] = PR_rt_tgsigqueueinfo, + [ 537 ] = PR_recvmmsg, + [ 538 ] = PR_sendmmsg, + [ 539 ] = PR_process_vm_readv, + [ 540 ] = PR_process_vm_writev, + [ 541 ] = PR_setsockopt, + [ 542 ] = PR_getsockopt, +}; diff --git a/core/proot/src/main/cpp/syscall/sysnums-x86_64.h b/core/proot/src/main/cpp/syscall/sysnums-x86_64.h new file mode 100644 index 000000000..b4601c108 --- /dev/null +++ b/core/proot/src/main/cpp/syscall/sysnums-x86_64.h @@ -0,0 +1,340 @@ +#include "syscall/sysnum.h" + +static const Sysnum sysnums_x86_64[] = { + [ 0 ] = PR_read, + [ 1 ] = PR_write, + [ 2 ] = PR_open, + [ 3 ] = PR_close, + [ 4 ] = PR_stat, + [ 5 ] = PR_fstat, + [ 6 ] = PR_lstat, + [ 7 ] = PR_poll, + [ 8 ] = PR_lseek, + [ 9 ] = PR_mmap, + [ 10 ] = PR_mprotect, + [ 11 ] = PR_munmap, + [ 12 ] = PR_brk, + [ 13 ] = PR_rt_sigaction, + [ 14 ] = PR_rt_sigprocmask, + [ 15 ] = PR_rt_sigreturn, + [ 16 ] = PR_ioctl, + [ 17 ] = PR_pread64, + [ 18 ] = PR_pwrite64, + [ 19 ] = PR_readv, + [ 20 ] = PR_writev, + [ 21 ] = PR_access, + [ 22 ] = PR_pipe, + [ 23 ] = PR_select, + [ 24 ] = PR_sched_yield, + [ 25 ] = PR_mremap, + [ 26 ] = PR_msync, + [ 27 ] = PR_mincore, + [ 28 ] = PR_madvise, + [ 29 ] = PR_shmget, + [ 30 ] = PR_shmat, + [ 31 ] = PR_shmctl, + [ 32 ] = PR_dup, + [ 33 ] = PR_dup2, + [ 34 ] = PR_pause, + [ 35 ] = PR_nanosleep, + [ 36 ] = PR_getitimer, + [ 37 ] = PR_alarm, + [ 38 ] = PR_setitimer, + [ 39 ] = PR_getpid, + [ 40 ] = PR_sendfile, + [ 41 ] = PR_socket, + [ 42 ] = PR_connect, + [ 43 ] = PR_accept, + [ 44 ] = PR_sendto, + [ 45 ] = PR_recvfrom, + [ 46 ] = PR_sendmsg, + [ 47 ] = PR_recvmsg, + [ 48 ] = PR_shutdown, + [ 49 ] = PR_bind, + [ 50 ] = PR_listen, + [ 51 ] = PR_getsockname, + [ 52 ] = PR_getpeername, + [ 53 ] = PR_socketpair, + [ 54 ] = PR_setsockopt, + [ 55 ] = PR_getsockopt, + [ 56 ] = PR_clone, + [ 57 ] = PR_fork, + [ 58 ] = PR_vfork, + [ 59 ] = PR_execve, + [ 60 ] = PR_exit, + [ 61 ] = PR_wait4, + [ 62 ] = PR_kill, + [ 63 ] = PR_uname, + [ 64 ] = PR_semget, + [ 65 ] = PR_semop, + [ 66 ] = PR_semctl, + [ 67 ] = PR_shmdt, + [ 68 ] = PR_msgget, + [ 69 ] = PR_msgsnd, + [ 70 ] = PR_msgrcv, + [ 71 ] = PR_msgctl, + [ 72 ] = PR_fcntl, + [ 73 ] = PR_flock, + [ 74 ] = PR_fsync, + [ 75 ] = PR_fdatasync, + [ 76 ] = PR_truncate, + [ 77 ] = PR_ftruncate, + [ 78 ] = PR_getdents, + [ 79 ] = PR_getcwd, + [ 80 ] = PR_chdir, + [ 81 ] = PR_fchdir, + [ 82 ] = PR_rename, + [ 83 ] = PR_mkdir, + [ 84 ] = PR_rmdir, + [ 85 ] = PR_creat, + [ 86 ] = PR_link, + [ 87 ] = PR_unlink, + [ 88 ] = PR_symlink, + [ 89 ] = PR_readlink, + [ 90 ] = PR_chmod, + [ 91 ] = PR_fchmod, + [ 92 ] = PR_chown, + [ 93 ] = PR_fchown, + [ 94 ] = PR_lchown, + [ 95 ] = PR_umask, + [ 96 ] = PR_gettimeofday, + [ 97 ] = PR_getrlimit, + [ 98 ] = PR_getrusage, + [ 99 ] = PR_sysinfo, + [ 100 ] = PR_times, + [ 101 ] = PR_ptrace, + [ 102 ] = PR_getuid, + [ 103 ] = PR_syslog, + [ 104 ] = PR_getgid, + [ 105 ] = PR_setuid, + [ 106 ] = PR_setgid, + [ 107 ] = PR_geteuid, + [ 108 ] = PR_getegid, + [ 109 ] = PR_setpgid, + [ 110 ] = PR_getppid, + [ 111 ] = PR_getpgrp, + [ 112 ] = PR_setsid, + [ 113 ] = PR_setreuid, + [ 114 ] = PR_setregid, + [ 115 ] = PR_getgroups, + [ 116 ] = PR_setgroups, + [ 117 ] = PR_setresuid, + [ 118 ] = PR_getresuid, + [ 119 ] = PR_setresgid, + [ 120 ] = PR_getresgid, + [ 121 ] = PR_getpgid, + [ 122 ] = PR_setfsuid, + [ 123 ] = PR_setfsgid, + [ 124 ] = PR_getsid, + [ 125 ] = PR_capget, + [ 126 ] = PR_capset, + [ 127 ] = PR_rt_sigpending, + [ 128 ] = PR_rt_sigtimedwait, + [ 129 ] = PR_rt_sigqueueinfo, + [ 130 ] = PR_rt_sigsuspend, + [ 131 ] = PR_sigaltstack, + [ 132 ] = PR_utime, + [ 133 ] = PR_mknod, + [ 134 ] = PR_uselib, + [ 135 ] = PR_personality, + [ 136 ] = PR_ustat, + [ 137 ] = PR_statfs, + [ 138 ] = PR_fstatfs, + [ 139 ] = PR_sysfs, + [ 140 ] = PR_getpriority, + [ 141 ] = PR_setpriority, + [ 142 ] = PR_sched_setparam, + [ 143 ] = PR_sched_getparam, + [ 144 ] = PR_sched_setscheduler, + [ 145 ] = PR_sched_getscheduler, + [ 146 ] = PR_sched_get_priority_max, + [ 147 ] = PR_sched_get_priority_min, + [ 148 ] = PR_sched_rr_get_interval, + [ 149 ] = PR_mlock, + [ 150 ] = PR_munlock, + [ 151 ] = PR_mlockall, + [ 152 ] = PR_munlockall, + [ 153 ] = PR_vhangup, + [ 154 ] = PR_modify_ldt, + [ 155 ] = PR_pivot_root, + [ 156 ] = PR__sysctl, + [ 157 ] = PR_prctl, + [ 158 ] = PR_arch_prctl, + [ 159 ] = PR_adjtimex, + [ 160 ] = PR_setrlimit, + [ 161 ] = PR_chroot, + [ 162 ] = PR_sync, + [ 163 ] = PR_acct, + [ 164 ] = PR_settimeofday, + [ 165 ] = PR_mount, + [ 166 ] = PR_umount2, + [ 167 ] = PR_swapon, + [ 168 ] = PR_swapoff, + [ 169 ] = PR_reboot, + [ 170 ] = PR_sethostname, + [ 171 ] = PR_setdomainname, + [ 172 ] = PR_iopl, + [ 173 ] = PR_ioperm, + [ 174 ] = PR_create_module, + [ 175 ] = PR_init_module, + [ 176 ] = PR_delete_module, + [ 177 ] = PR_get_kernel_syms, + [ 178 ] = PR_query_module, + [ 179 ] = PR_quotactl, + [ 180 ] = PR_nfsservctl, + [ 181 ] = PR_getpmsg, + [ 182 ] = PR_putpmsg, + [ 183 ] = PR_afs_syscall, + [ 184 ] = PR_tuxcall, + [ 185 ] = PR_security, + [ 186 ] = PR_gettid, + [ 187 ] = PR_readahead, + [ 188 ] = PR_setxattr, + [ 189 ] = PR_lsetxattr, + [ 190 ] = PR_fsetxattr, + [ 191 ] = PR_getxattr, + [ 192 ] = PR_lgetxattr, + [ 193 ] = PR_fgetxattr, + [ 194 ] = PR_listxattr, + [ 195 ] = PR_llistxattr, + [ 196 ] = PR_flistxattr, + [ 197 ] = PR_removexattr, + [ 198 ] = PR_lremovexattr, + [ 199 ] = PR_fremovexattr, + [ 200 ] = PR_tkill, + [ 201 ] = PR_time, + [ 202 ] = PR_futex, + [ 203 ] = PR_sched_setaffinity, + [ 204 ] = PR_sched_getaffinity, + [ 205 ] = PR_set_thread_area, + [ 206 ] = PR_io_setup, + [ 207 ] = PR_io_destroy, + [ 208 ] = PR_io_getevents, + [ 209 ] = PR_io_submit, + [ 210 ] = PR_io_cancel, + [ 211 ] = PR_get_thread_area, + [ 212 ] = PR_lookup_dcookie, + [ 213 ] = PR_epoll_create, + [ 214 ] = PR_epoll_ctl_old, + [ 215 ] = PR_epoll_wait_old, + [ 216 ] = PR_remap_file_pages, + [ 217 ] = PR_getdents64, + [ 218 ] = PR_set_tid_address, + [ 219 ] = PR_restart_syscall, + [ 220 ] = PR_semtimedop, + [ 221 ] = PR_fadvise64, + [ 222 ] = PR_timer_create, + [ 223 ] = PR_timer_settime, + [ 224 ] = PR_timer_gettime, + [ 225 ] = PR_timer_getoverrun, + [ 226 ] = PR_timer_delete, + [ 227 ] = PR_clock_settime, + [ 228 ] = PR_clock_gettime, + [ 229 ] = PR_clock_getres, + [ 230 ] = PR_clock_nanosleep, + [ 231 ] = PR_exit_group, + [ 232 ] = PR_epoll_wait, + [ 233 ] = PR_epoll_ctl, + [ 234 ] = PR_tgkill, + [ 235 ] = PR_utimes, + [ 236 ] = PR_vserver, + [ 237 ] = PR_mbind, + [ 238 ] = PR_set_mempolicy, + [ 239 ] = PR_get_mempolicy, + [ 240 ] = PR_mq_open, + [ 241 ] = PR_mq_unlink, + [ 242 ] = PR_mq_timedsend, + [ 243 ] = PR_mq_timedreceive, + [ 244 ] = PR_mq_notify, + [ 245 ] = PR_mq_getsetattr, + [ 246 ] = PR_kexec_load, + [ 247 ] = PR_waitid, + [ 248 ] = PR_add_key, + [ 249 ] = PR_request_key, + [ 250 ] = PR_keyctl, + [ 251 ] = PR_ioprio_set, + [ 252 ] = PR_ioprio_get, + [ 253 ] = PR_inotify_init, + [ 254 ] = PR_inotify_add_watch, + [ 255 ] = PR_inotify_rm_watch, + [ 256 ] = PR_migrate_pages, + [ 257 ] = PR_openat, + [ 258 ] = PR_mkdirat, + [ 259 ] = PR_mknodat, + [ 260 ] = PR_fchownat, + [ 261 ] = PR_futimesat, + [ 262 ] = PR_newfstatat, + [ 263 ] = PR_unlinkat, + [ 264 ] = PR_renameat, + [ 265 ] = PR_linkat, + [ 266 ] = PR_symlinkat, + [ 267 ] = PR_readlinkat, + [ 268 ] = PR_fchmodat, + [ 269 ] = PR_faccessat, + [ 270 ] = PR_pselect6, + [ 271 ] = PR_ppoll, + [ 272 ] = PR_unshare, + [ 273 ] = PR_set_robust_list, + [ 274 ] = PR_get_robust_list, + [ 275 ] = PR_splice, + [ 276 ] = PR_tee, + [ 277 ] = PR_sync_file_range, + [ 278 ] = PR_vmsplice, + [ 279 ] = PR_move_pages, + [ 280 ] = PR_utimensat, + [ 281 ] = PR_epoll_pwait, + [ 282 ] = PR_signalfd, + [ 283 ] = PR_timerfd_create, + [ 284 ] = PR_eventfd, + [ 285 ] = PR_fallocate, + [ 286 ] = PR_timerfd_settime, + [ 287 ] = PR_timerfd_gettime, + [ 288 ] = PR_accept4, + [ 289 ] = PR_signalfd4, + [ 290 ] = PR_eventfd2, + [ 291 ] = PR_epoll_create1, + [ 292 ] = PR_dup3, + [ 293 ] = PR_pipe2, + [ 294 ] = PR_inotify_init1, + [ 295 ] = PR_preadv, + [ 296 ] = PR_pwritev, + [ 297 ] = PR_rt_tgsigqueueinfo, + [ 298 ] = PR_perf_event_open, + [ 299 ] = PR_recvmmsg, + [ 300 ] = PR_fanotify_init, + [ 301 ] = PR_fanotify_mark, + [ 302 ] = PR_prlimit64, + [ 303 ] = PR_name_to_handle_at, + [ 304 ] = PR_open_by_handle_at, + [ 305 ] = PR_clock_adjtime, + [ 306 ] = PR_syncfs, + [ 307 ] = PR_sendmmsg, + [ 308 ] = PR_setns, + [ 309 ] = PR_getcpu, + [ 310 ] = PR_process_vm_readv, + [ 311 ] = PR_process_vm_writev, + [ 312 ] = PR_kcmp, + [ 313 ] = PR_finit_module, + [ 314 ] = PR_sched_setattr, + [ 315 ] = PR_sched_getattr, + [ 316 ] = PR_renameat2, + [ 317 ] = PR_seccomp, + [ 318 ] = PR_getrandom, + [ 319 ] = PR_memfd_create, + [ 320 ] = PR_kexec_file_load, + [ 321 ] = PR_bpf, + [ 322 ] = PR_execveat, + [ 323 ] = PR_userfaultfd, + [ 324 ] = PR_membarrier, + [ 325 ] = PR_mlock2, + [ 326 ] = PR_copy_file_range, + [ 327 ] = PR_preadv2, + [ 328 ] = PR_pwritev2, + [ 329 ] = PR_pkey_mprotect, + [ 330 ] = PR_pkey_alloc, + [ 331 ] = PR_pkey_free, + [ 332 ] = PR_statx, + [ 435 ] = PR_clone3, + [ 437 ] = PR_openat2, + [ 439 ] = PR_faccessat2, +}; diff --git a/core/proot/src/main/cpp/syscall/sysnums.list b/core/proot/src/main/cpp/syscall/sysnums.list new file mode 100644 index 000000000..d99e23185 --- /dev/null +++ b/core/proot/src/main/cpp/syscall/sysnums.list @@ -0,0 +1,449 @@ +SYSNUM(ARM_BASE) +SYSNUM(ARM_breakpoint) +SYSNUM(ARM_cacheflush) +SYSNUM(ARM_set_tls) +SYSNUM(ARM_usr26) +SYSNUM(ARM_usr32) +SYSNUM(X32_SYSCALL_BIT) +SYSNUM(_llseek) +SYSNUM(_newselect) +SYSNUM(_sysctl) +SYSNUM(accept) +SYSNUM(accept4) +SYSNUM(access) +SYSNUM(acct) +SYSNUM(add_key) +SYSNUM(adjtimex) +SYSNUM(afs_syscall) +SYSNUM(alarm) +SYSNUM(arch_prctl) +SYSNUM(arch_specific_syscall) +SYSNUM(arm_fadvise64_64) +SYSNUM(arm_sync_file_range) +SYSNUM(bdflush) +SYSNUM(bind) +SYSNUM(bpf) +SYSNUM(break) +SYSNUM(brk) +SYSNUM(cacheflush) +SYSNUM(capget) +SYSNUM(capset) +SYSNUM(chdir) +SYSNUM(chmod) +SYSNUM(chown) +SYSNUM(chown32) +SYSNUM(chroot) +SYSNUM(clock_adjtime) +SYSNUM(clock_getres) +SYSNUM(clock_gettime) +SYSNUM(clock_nanosleep) +SYSNUM(clock_settime) +SYSNUM(clone) +SYSNUM(clone3) +SYSNUM(close) +SYSNUM(connect) +SYSNUM(copy_file_range) +SYSNUM(creat) +SYSNUM(create_module) +SYSNUM(delete_module) +SYSNUM(dup) +SYSNUM(dup2) +SYSNUM(dup3) +SYSNUM(epoll_create) +SYSNUM(epoll_create1) +SYSNUM(epoll_ctl) +SYSNUM(epoll_ctl_old) +SYSNUM(epoll_pwait) +SYSNUM(epoll_wait) +SYSNUM(epoll_wait_old) +SYSNUM(eventfd) +SYSNUM(eventfd2) +SYSNUM(execve) +SYSNUM(execveat) +SYSNUM(exit) +SYSNUM(exit_group) +SYSNUM(faccessat) +SYSNUM(faccessat2) +SYSNUM(fadvise64) +SYSNUM(fadvise64_64) +SYSNUM(fallocate) +SYSNUM(fanotify_init) +SYSNUM(fanotify_mark) +SYSNUM(fchdir) +SYSNUM(fchmod) +SYSNUM(fchmodat) +SYSNUM(fchown) +SYSNUM(fchown32) +SYSNUM(fchownat) +SYSNUM(fcntl) +SYSNUM(fcntl64) +SYSNUM(fdatasync) +SYSNUM(fgetxattr) +SYSNUM(finit_module) +SYSNUM(flistxattr) +SYSNUM(flock) +SYSNUM(fork) +SYSNUM(fremovexattr) +SYSNUM(fsetxattr) +SYSNUM(fstat) +SYSNUM(fstat64) +SYSNUM(fstatat64) +SYSNUM(fstatfs) +SYSNUM(fstatfs64) +SYSNUM(fsync) +SYSNUM(ftime) +SYSNUM(ftruncate) +SYSNUM(ftruncate64) +SYSNUM(futex) +SYSNUM(futimesat) +SYSNUM(get_kernel_syms) +SYSNUM(get_mempolicy) +SYSNUM(get_robust_list) +SYSNUM(get_thread_area) +SYSNUM(getcpu) +SYSNUM(getcwd) +SYSNUM(getdents) +SYSNUM(getdents64) +SYSNUM(getegid) +SYSNUM(getegid32) +SYSNUM(geteuid) +SYSNUM(geteuid32) +SYSNUM(getgid) +SYSNUM(getgid32) +SYSNUM(getgroups) +SYSNUM(getgroups32) +SYSNUM(getitimer) +SYSNUM(getpeername) +SYSNUM(getpgid) +SYSNUM(getpgrp) +SYSNUM(getpid) +SYSNUM(getpmsg) +SYSNUM(getppid) +SYSNUM(getpriority) +SYSNUM(getrandom) +SYSNUM(getresgid) +SYSNUM(getresgid32) +SYSNUM(getresuid) +SYSNUM(getresuid32) +SYSNUM(getrlimit) +SYSNUM(getrusage) +SYSNUM(getsid) +SYSNUM(getsockname) +SYSNUM(getsockopt) +SYSNUM(gettid) +SYSNUM(gettimeofday) +SYSNUM(getuid) +SYSNUM(getuid32) +SYSNUM(getxattr) +SYSNUM(gtty) +SYSNUM(idle) +SYSNUM(init_module) +SYSNUM(inotify_add_watch) +SYSNUM(inotify_init) +SYSNUM(inotify_init1) +SYSNUM(inotify_rm_watch) +SYSNUM(io_cancel) +SYSNUM(io_destroy) +SYSNUM(io_getevents) +SYSNUM(io_setup) +SYSNUM(io_submit) +SYSNUM(ioctl) +SYSNUM(ioperm) +SYSNUM(iopl) +SYSNUM(ioprio_get) +SYSNUM(ioprio_set) +SYSNUM(ipc) +SYSNUM(kcmp) +SYSNUM(kexec_file_load) +SYSNUM(kexec_load) +SYSNUM(keyctl) +SYSNUM(kill) +SYSNUM(lchown) +SYSNUM(lchown32) +SYSNUM(lgetxattr) +SYSNUM(link) +SYSNUM(linkat) +SYSNUM(listen) +SYSNUM(listxattr) +SYSNUM(llistxattr) +SYSNUM(lock) +SYSNUM(lookup_dcookie) +SYSNUM(lremovexattr) +SYSNUM(lseek) +SYSNUM(lsetxattr) +SYSNUM(lstat) +SYSNUM(lstat64) +SYSNUM(madvise) +SYSNUM(mbind) +SYSNUM(membarrier) +SYSNUM(memfd_create) +SYSNUM(migrate_pages) +SYSNUM(mincore) +SYSNUM(mkdir) +SYSNUM(mkdirat) +SYSNUM(mknod) +SYSNUM(mknodat) +SYSNUM(mlock) +SYSNUM(mlock2) +SYSNUM(mlockall) +SYSNUM(mmap) +SYSNUM(mmap2) +SYSNUM(modify_ldt) +SYSNUM(mount) +SYSNUM(move_pages) +SYSNUM(mprotect) +SYSNUM(mpx) +SYSNUM(mq_getsetattr) +SYSNUM(mq_notify) +SYSNUM(mq_open) +SYSNUM(mq_timedreceive) +SYSNUM(mq_timedsend) +SYSNUM(mq_unlink) +SYSNUM(mremap) +SYSNUM(msgctl) +SYSNUM(msgget) +SYSNUM(msgrcv) +SYSNUM(msgsnd) +SYSNUM(msync) +SYSNUM(munlock) +SYSNUM(munlockall) +SYSNUM(munmap) +SYSNUM(name_to_handle_at) +SYSNUM(nanosleep) +SYSNUM(newfstatat) +SYSNUM(nfsservctl) +SYSNUM(nice) +SYSNUM(oldfstat) +SYSNUM(oldlstat) +SYSNUM(oldolduname) +SYSNUM(oldstat) +SYSNUM(olduname) +SYSNUM(open) +SYSNUM(open_by_handle_at) +SYSNUM(openat) +SYSNUM(openat2) +SYSNUM(pause) +SYSNUM(pciconfig_iobase) +SYSNUM(pciconfig_read) +SYSNUM(pciconfig_write) +SYSNUM(perf_event_open) +SYSNUM(personality) +SYSNUM(pipe) +SYSNUM(pipe2) +SYSNUM(pivot_root) +SYSNUM(pkey_alloc) +SYSNUM(pkey_free) +SYSNUM(pkey_mprotect) +SYSNUM(poll) +SYSNUM(ppoll) +SYSNUM(prctl) +SYSNUM(pread64) +SYSNUM(preadv) +SYSNUM(preadv2) +SYSNUM(prlimit64) +SYSNUM(process_vm_readv) +SYSNUM(process_vm_writev) +SYSNUM(prof) +SYSNUM(profil) +SYSNUM(pselect6) +SYSNUM(ptrace) +SYSNUM(putpmsg) +SYSNUM(pwrite64) +SYSNUM(pwritev) +SYSNUM(pwritev2) +SYSNUM(query_module) +SYSNUM(quotactl) +SYSNUM(read) +SYSNUM(readahead) +SYSNUM(readdir) +SYSNUM(readlink) +SYSNUM(readlinkat) +SYSNUM(readv) +SYSNUM(reboot) +SYSNUM(recv) +SYSNUM(recvfrom) +SYSNUM(recvmmsg) +SYSNUM(recvmsg) +SYSNUM(remap_file_pages) +SYSNUM(removexattr) +SYSNUM(rename) +SYSNUM(renameat) +SYSNUM(renameat2) +SYSNUM(request_key) +SYSNUM(restart_syscall) +SYSNUM(rmdir) +SYSNUM(rt_sigaction) +SYSNUM(rt_sigpending) +SYSNUM(rt_sigprocmask) +SYSNUM(rt_sigqueueinfo) +SYSNUM(rt_sigreturn) +SYSNUM(rt_sigsuspend) +SYSNUM(rt_sigtimedwait) +SYSNUM(rt_tgsigqueueinfo) +SYSNUM(sched_get_priority_max) +SYSNUM(sched_get_priority_min) +SYSNUM(sched_getaffinity) +SYSNUM(sched_getattr) +SYSNUM(sched_getparam) +SYSNUM(sched_getscheduler) +SYSNUM(sched_rr_get_interval) +SYSNUM(sched_setaffinity) +SYSNUM(sched_setattr) +SYSNUM(sched_setparam) +SYSNUM(sched_setscheduler) +SYSNUM(sched_yield) +SYSNUM(seccomp) +SYSNUM(security) +SYSNUM(select) +SYSNUM(semctl) +SYSNUM(semget) +SYSNUM(semop) +SYSNUM(semtimedop) +SYSNUM(send) +SYSNUM(sendfile) +SYSNUM(sendfile64) +SYSNUM(sendmmsg) +SYSNUM(sendmsg) +SYSNUM(sendto) +SYSNUM(set_mempolicy) +SYSNUM(set_robust_list) +SYSNUM(set_thread_area) +SYSNUM(set_tid_address) +SYSNUM(setdomainname) +SYSNUM(setfsgid) +SYSNUM(setfsgid32) +SYSNUM(setfsuid) +SYSNUM(setfsuid32) +SYSNUM(setgid) +SYSNUM(setgid32) +SYSNUM(setgroups) +SYSNUM(setgroups32) +SYSNUM(sethostname) +SYSNUM(setitimer) +SYSNUM(setns) +SYSNUM(setpgid) +SYSNUM(setpriority) +SYSNUM(setregid) +SYSNUM(setregid32) +SYSNUM(setresgid) +SYSNUM(setresgid32) +SYSNUM(setresuid) +SYSNUM(setresuid32) +SYSNUM(setreuid) +SYSNUM(setreuid32) +SYSNUM(setrlimit) +SYSNUM(setsid) +SYSNUM(setsockopt) +SYSNUM(settimeofday) +SYSNUM(setuid) +SYSNUM(setuid32) +SYSNUM(setxattr) +SYSNUM(sgetmask) +SYSNUM(shmat) +SYSNUM(shmctl) +SYSNUM(shmdt) +SYSNUM(shmget) +SYSNUM(shutdown) +SYSNUM(sigaction) +SYSNUM(sigaltstack) +SYSNUM(signal) +SYSNUM(signalfd) +SYSNUM(signalfd4) +SYSNUM(sigpending) +SYSNUM(sigprocmask) +SYSNUM(sigreturn) +SYSNUM(sigsuspend) +SYSNUM(socket) +SYSNUM(socketcall) +SYSNUM(socketpair) +SYSNUM(splice) +SYSNUM(ssetmask) +SYSNUM(stat) +SYSNUM(stat64) +SYSNUM(statfs) +SYSNUM(statfs64) +SYSNUM(statx) +SYSNUM(stime) +SYSNUM(stty) +SYSNUM(swapoff) +SYSNUM(swapon) +SYSNUM(symlink) +SYSNUM(symlinkat) +SYSNUM(sync) +SYSNUM(sync_file_range) +SYSNUM(sync_file_range2) +SYSNUM(syncfs) +SYSNUM(sysfs) +SYSNUM(sysinfo) +SYSNUM(syslog) +SYSNUM(tee) +SYSNUM(tgkill) +SYSNUM(time) +SYSNUM(timer_create) +SYSNUM(timer_delete) +SYSNUM(timer_getoverrun) +SYSNUM(timer_gettime) +SYSNUM(timer_settime) +SYSNUM(timerfd_create) +SYSNUM(timerfd_gettime) +SYSNUM(timerfd_settime) +SYSNUM(times) +SYSNUM(tkill) +SYSNUM(truncate) +SYSNUM(truncate64) +SYSNUM(tuxcall) +SYSNUM(ugetrlimit) +SYSNUM(ulimit) +SYSNUM(umask) +SYSNUM(umount) +SYSNUM(umount2) +SYSNUM(uname) +SYSNUM(unlink) +SYSNUM(unlinkat) +SYSNUM(unshare) +SYSNUM(uselib) +SYSNUM(userfaultfd) +SYSNUM(ustat) +SYSNUM(utime) +SYSNUM(utimensat) +SYSNUM(utimes) +SYSNUM(vfork) +SYSNUM(vhangup) +SYSNUM(vm86) +SYSNUM(vm86old) +SYSNUM(vmsplice) +SYSNUM(vserver) +SYSNUM(wait4) +SYSNUM(waitid) +SYSNUM(waitpid) +SYSNUM(write) +SYSNUM(writev) +SYSNUM(x32_execve) +SYSNUM(x32_get_robust_list) +SYSNUM(x32_ioctl) +SYSNUM(x32_kexec_load) +SYSNUM(x32_move_pages) +SYSNUM(x32_mq_notify) +SYSNUM(x32_preadv) +SYSNUM(x32_process_vm_readv) +SYSNUM(x32_process_vm_writev) +SYSNUM(x32_ptrace) +SYSNUM(x32_pwritev) +SYSNUM(x32_readv) +SYSNUM(x32_recvfrom) +SYSNUM(x32_recvmmsg) +SYSNUM(x32_recvmsg) +SYSNUM(x32_rt_sigaction) +SYSNUM(x32_rt_sigpending) +SYSNUM(x32_rt_sigqueueinfo) +SYSNUM(x32_rt_sigreturn) +SYSNUM(x32_rt_sigtimedwait) +SYSNUM(x32_rt_tgsigqueueinfo) +SYSNUM(x32_sendmmsg) +SYSNUM(x32_sendmsg) +SYSNUM(x32_set_robust_list) +SYSNUM(x32_sigaltstack) +SYSNUM(x32_timer_create) +SYSNUM(x32_vmsplice) +SYSNUM(x32_waitid) +SYSNUM(x32_writev) diff --git a/core/proot/src/main/cpp/talloc/closefrom.c b/core/proot/src/main/cpp/talloc/closefrom.c new file mode 100644 index 000000000..bef53e427 --- /dev/null +++ b/core/proot/src/main/cpp/talloc/closefrom.c @@ -0,0 +1,138 @@ +/* + * Unix SMB/CIFS implementation. + * Samba utility functions + * Copyright (C) Volker Lendecke 2016 + * + * ** NOTE! The following LGPL license applies to the replace + * ** library. This does NOT imply that all of Samba is released + * ** under the LGPL + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see . + */ + +#include "replace.h" +#include +#include +#include + +static int closefrom_sysconf(int lower) +{ + long max_files, fd; + + max_files = sysconf(_SC_OPEN_MAX); + if (max_files == -1) { + max_files = 65536; + } + + for (fd=lower; fdd_name, &endptr, 10); + if ((fd == 0) && (errno == EINVAL)) { + continue; + } + if ((fd == ULLONG_MAX) && (errno == ERANGE)) { + continue; + } + if (*endptr != '\0') { + continue; + } + if (fd == dir_fd) { + continue; + } + if (fd > INT_MAX) { + continue; + } + if (fd < lower) { + continue; + } + + if (num_fds >= (fd_array_size / sizeof(int))) { + void *tmp; + + if (fd_array_size == 0) { + fd_array_size = 16 * sizeof(int); + } else { + if (fd_array_size + fd_array_size < + fd_array_size) { + /* overflow */ + goto fail; + } + fd_array_size = fd_array_size + fd_array_size; + } + + tmp = realloc(fds, fd_array_size); + if (tmp == NULL) { + goto fail; + } + fds = tmp; + } + + fds[num_fds++] = fd; + } + + for (i=0; i. +*/ + +#include "replace.h" + +#include "system/filesys.h" +#include "system/time.h" +#include "system/network.h" +#include "system/passwd.h" +#include "system/syslog.h" +#include "system/locale.h" +#include "system/wait.h" + +#ifdef HAVE_SYS_SYSCALL_H +#include +#endif + +#ifdef _WIN32 +#define mkdir(d,m) _mkdir(d) +#endif + +void replace_dummy(void); +void replace_dummy(void) {} + +#ifndef HAVE_FTRUNCATE + /******************************************************************* +ftruncate for operating systems that don't have it +********************************************************************/ +int rep_ftruncate(int f, off_t l) +{ +#ifdef HAVE_CHSIZE + return chsize(f,l); +#elif defined(F_FREESP) + struct flock fl; + + fl.l_whence = 0; + fl.l_len = 0; + fl.l_start = l; + fl.l_type = F_WRLCK; + return fcntl(f, F_FREESP, &fl); +#else +#error "you must have a ftruncate function" +#endif +} +#endif /* HAVE_FTRUNCATE */ + + +#ifndef HAVE_STRLCPY +/* + * Like strncpy but does not 0 fill the buffer and always null + * terminates. bufsize is the size of the destination buffer. + * Returns the length of s. + */ +size_t rep_strlcpy(char *d, const char *s, size_t bufsize) +{ + size_t len = strlen(s); + size_t ret = len; + + if (bufsize <= 0) { + return 0; + } + if (len >= bufsize) { + len = bufsize - 1; + } + memcpy(d, s, len); + d[len] = 0; + return ret; +} +#endif + +#ifndef HAVE_STRLCAT +/* like strncat but does not 0 fill the buffer and always null + terminates. bufsize is the length of the buffer, which should + be one more than the maximum resulting string length */ +size_t rep_strlcat(char *d, const char *s, size_t bufsize) +{ + size_t len1 = strnlen(d, bufsize); + size_t len2 = strlen(s); + size_t ret = len1 + len2; + + if (len1+len2 >= bufsize) { + if (bufsize < (len1+1)) { + return ret; + } + len2 = bufsize - (len1+1); + } + if (len2 > 0) { + memcpy(d+len1, s, len2); + d[len1+len2] = 0; + } + return ret; +} +#endif + +#ifndef HAVE_MKTIME +/******************************************************************* +a mktime() replacement for those who don't have it - contributed by +C.A. Lademann +Corrections by richard.kettlewell@kewill.com +********************************************************************/ + +#define MINUTE 60 +#define HOUR 60*MINUTE +#define DAY 24*HOUR +#define YEAR 365*DAY +time_t rep_mktime(struct tm *t) +{ + struct tm *u; + time_t epoch = 0; + int n; + int mon [] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }, + y, m, i; + + if(t->tm_year < 70) + return((time_t)-1); + + n = t->tm_year + 1900 - 1; + epoch = (t->tm_year - 70) * YEAR + + ((n / 4 - n / 100 + n / 400) - (1969 / 4 - 1969 / 100 + 1969 / 400)) * DAY; + + y = t->tm_year + 1900; + m = 0; + + for(i = 0; i < t->tm_mon; i++) { + epoch += mon [m] * DAY; + if(m == 1 && y % 4 == 0 && (y % 100 != 0 || y % 400 == 0)) + epoch += DAY; + + if(++m > 11) { + m = 0; + y++; + } + } + + epoch += (t->tm_mday - 1) * DAY; + epoch += t->tm_hour * HOUR + t->tm_min * MINUTE + t->tm_sec; + + if((u = localtime(&epoch)) != NULL) { + t->tm_sec = u->tm_sec; + t->tm_min = u->tm_min; + t->tm_hour = u->tm_hour; + t->tm_mday = u->tm_mday; + t->tm_mon = u->tm_mon; + t->tm_year = u->tm_year; + t->tm_wday = u->tm_wday; + t->tm_yday = u->tm_yday; + t->tm_isdst = u->tm_isdst; + } + + return(epoch); +} +#endif /* !HAVE_MKTIME */ + + +#ifndef HAVE_INITGROUPS +/**************************************************************************** + some systems don't have an initgroups call +****************************************************************************/ +int rep_initgroups(char *name, gid_t id) +{ +#ifndef HAVE_SETGROUPS + /* yikes! no SETGROUPS or INITGROUPS? how can this work? */ + errno = ENOSYS; + return -1; +#else /* HAVE_SETGROUPS */ + +#include + + gid_t *grouplst = NULL; + int max_gr = NGROUPS_MAX; + int ret; + int i,j; + struct group *g; + char *gr; + + if((grouplst = malloc(sizeof(gid_t) * max_gr)) == NULL) { + errno = ENOMEM; + return -1; + } + + grouplst[0] = id; + i = 1; + while (i < max_gr && ((g = (struct group *)getgrent()) != (struct group *)NULL)) { + if (g->gr_gid == id) + continue; + j = 0; + gr = g->gr_mem[0]; + while (gr && (*gr != (char)NULL)) { + if (strcmp(name,gr) == 0) { + grouplst[i] = g->gr_gid; + i++; + gr = (char *)NULL; + break; + } + gr = g->gr_mem[++j]; + } + } + endgrent(); + ret = setgroups(i, grouplst); + free(grouplst); + return ret; +#endif /* HAVE_SETGROUPS */ +} +#endif /* HAVE_INITGROUPS */ + + +#ifndef HAVE_MEMMOVE +/******************************************************************* +safely copies memory, ensuring no overlap problems. +this is only used if the machine does not have its own memmove(). +this is not the fastest algorithm in town, but it will do for our +needs. +********************************************************************/ +void *rep_memmove(void *dest,const void *src,int size) +{ + unsigned long d,s; + int i; + if (dest==src || !size) return(dest); + + d = (unsigned long)dest; + s = (unsigned long)src; + + if ((d >= (s+size)) || (s >= (d+size))) { + /* no overlap */ + memcpy(dest,src,size); + return(dest); + } + + if (d < s) { + /* we can forward copy */ + if (s-d >= sizeof(int) && + !(s%sizeof(int)) && + !(d%sizeof(int)) && + !(size%sizeof(int))) { + /* do it all as words */ + int *idest = (int *)dest; + int *isrc = (int *)src; + size /= sizeof(int); + for (i=0;i= sizeof(int) && + !(s%sizeof(int)) && + !(d%sizeof(int)) && + !(size%sizeof(int))) { + /* do it all as words */ + int *idest = (int *)dest; + int *isrc = (int *)src; + size /= sizeof(int); + for (i=size-1;i>=0;i--) idest[i] = isrc[i]; + } else { + /* simplest */ + char *cdest = (char *)dest; + char *csrc = (char *)src; + for (i=size-1;i>=0;i--) cdest[i] = csrc[i]; + } + } + return(dest); +} +#endif /* HAVE_MEMMOVE */ + +#ifndef HAVE_STRDUP +/**************************************************************************** +duplicate a string +****************************************************************************/ +char *rep_strdup(const char *s) +{ + size_t len; + char *ret; + + if (!s) return(NULL); + + len = strlen(s)+1; + ret = (char *)malloc(len); + if (!ret) return(NULL); + memcpy(ret,s,len); + return(ret); +} +#endif /* HAVE_STRDUP */ + +#ifndef HAVE_SETLINEBUF +void rep_setlinebuf(FILE *stream) +{ + setvbuf(stream, (char *)NULL, _IOLBF, 0); +} +#endif /* HAVE_SETLINEBUF */ + +#ifndef HAVE_VSYSLOG +#ifdef HAVE_SYSLOG +void rep_vsyslog (int facility_priority, const char *format, va_list arglist) +{ + char *msg = NULL; + vasprintf(&msg, format, arglist); + if (!msg) + return; + syslog(facility_priority, "%s", msg); + free(msg); +} +#endif /* HAVE_SYSLOG */ +#endif /* HAVE_VSYSLOG */ + +#ifndef HAVE_STRNLEN +/** + Some platforms don't have strnlen +**/ + size_t rep_strnlen(const char *s, size_t max) +{ + size_t len; + + for (len = 0; len < max; len++) { + if (s[len] == '\0') { + break; + } + } + return len; +} +#endif + +#ifndef HAVE_STRNDUP +/** + Some platforms don't have strndup. +**/ +char *rep_strndup(const char *s, size_t n) +{ + char *ret; + + n = strnlen(s, n); + ret = malloc(n+1); + if (!ret) + return NULL; + memcpy(ret, s, n); + ret[n] = 0; + + return ret; +} +#endif + +#if !defined(HAVE_WAITPID) && defined(HAVE_WAIT4) +int rep_waitpid(pid_t pid,int *status,int options) +{ + return wait4(pid, status, options, NULL); +} +#endif + +#ifndef HAVE_SETEUID +int rep_seteuid(uid_t euid) +{ +#ifdef HAVE_SETRESUID + return setresuid(-1, euid, -1); +#else + errno = ENOSYS; + return -1; +#endif +} +#endif + +#ifndef HAVE_SETEGID +int rep_setegid(gid_t egid) +{ +#ifdef HAVE_SETRESGID + return setresgid(-1, egid, -1); +#else + errno = ENOSYS; + return -1; +#endif +} +#endif + +/******************************************************************* +os/2 also doesn't have chroot +********************************************************************/ +#ifndef HAVE_CHROOT +int rep_chroot(const char *dname) +{ + errno = ENOSYS; + return -1; +} +#endif + +/***************************************************************** + Possibly replace mkstemp if it is broken. +*****************************************************************/ + +#ifndef HAVE_SECURE_MKSTEMP +int rep_mkstemp(char *template) +{ + /* have a reasonable go at emulating it. Hope that + the system mktemp() isn't completely hopeless */ + mktemp(template); + if (template[0] == 0) + return -1; + return open(template, O_CREAT|O_EXCL|O_RDWR, 0600); +} +#endif + +#ifndef HAVE_MKDTEMP +char *rep_mkdtemp(char *template) +{ + char *dname; + + if ((dname = mktemp(template))) { + if (mkdir(dname, 0700) >= 0) { + return dname; + } + } + + return NULL; +} +#endif + +/***************************************************************** + Watch out: this is not thread safe. +*****************************************************************/ + +#ifndef HAVE_PREAD +ssize_t rep_pread(int __fd, void *__buf, size_t __nbytes, off_t __offset) +{ + if (lseek(__fd, __offset, SEEK_SET) != __offset) { + return -1; + } + return read(__fd, __buf, __nbytes); +} +#endif + +/***************************************************************** + Watch out: this is not thread safe. +*****************************************************************/ + +#ifndef HAVE_PWRITE +ssize_t rep_pwrite(int __fd, const void *__buf, size_t __nbytes, off_t __offset) +{ + if (lseek(__fd, __offset, SEEK_SET) != __offset) { + return -1; + } + return write(__fd, __buf, __nbytes); +} +#endif + +#ifndef HAVE_STRCASESTR +char *rep_strcasestr(const char *haystack, const char *needle) +{ + const char *s; + size_t nlen = strlen(needle); + for (s=haystack;*s;s++) { + if (toupper(*needle) == toupper(*s) && + strncasecmp(s, needle, nlen) == 0) { + return (char *)((uintptr_t)s); + } + } + return NULL; +} +#endif + +#ifndef HAVE_STRSEP +char *rep_strsep(char **pps, const char *delim) +{ + char *ret = *pps; + char *p = *pps; + + if (p == NULL) { + return NULL; + } + p += strcspn(p, delim); + if (*p == '\0') { + *pps = NULL; + } else { + *p = '\0'; + *pps = p + 1; + } + return ret; +} +#endif + +#ifndef HAVE_STRTOK_R +/* based on GLIBC version, copyright Free Software Foundation */ +char *rep_strtok_r(char *s, const char *delim, char **save_ptr) +{ + char *token; + + if (s == NULL) s = *save_ptr; + + s += strspn(s, delim); + if (*s == '\0') { + *save_ptr = s; + return NULL; + } + + token = s; + s = strpbrk(token, delim); + if (s == NULL) { + *save_ptr = token + strlen(token); + } else { + *s = '\0'; + *save_ptr = s + 1; + } + + return token; +} +#endif + + +#ifndef HAVE_STRTOLL +long long int rep_strtoll(const char *str, char **endptr, int base) +{ +#ifdef HAVE_STRTOQ + return strtoq(str, endptr, base); +#elif defined(HAVE___STRTOLL) + return __strtoll(str, endptr, base); +#elif SIZEOF_LONG == SIZEOF_LONG_LONG + return (long long int) strtol(str, endptr, base); +#else +# error "You need a strtoll function" +#endif +} +#else +#ifdef HAVE_BSD_STRTOLL +#undef strtoll +long long int rep_strtoll(const char *str, char **endptr, int base) +{ + int saved_errno = errno; + long long int nb = strtoll(str, endptr, base); + /* With glibc EINVAL is only returned if base is not ok */ + if (errno == EINVAL) { + if (base == 0 || (base >1 && base <37)) { + /* Base was ok so it's because we were not + * able to make the conversion. + * Let's reset errno. + */ + errno = saved_errno; + } + } + return nb; +} +#endif /* HAVE_BSD_STRTOLL */ +#endif /* HAVE_STRTOLL */ + + +#ifndef HAVE_STRTOULL +unsigned long long int rep_strtoull(const char *str, char **endptr, int base) +{ +#ifdef HAVE_STRTOUQ + return strtouq(str, endptr, base); +#elif defined(HAVE___STRTOULL) + return __strtoull(str, endptr, base); +#elif SIZEOF_LONG == SIZEOF_LONG_LONG + return (unsigned long long int) strtoul(str, endptr, base); +#else +# error "You need a strtoull function" +#endif +} +#else +#ifdef HAVE_BSD_STRTOLL +#undef strtoull +unsigned long long int rep_strtoull(const char *str, char **endptr, int base) +{ + int saved_errno = errno; + unsigned long long int nb = strtoull(str, endptr, base); + /* With glibc EINVAL is only returned if base is not ok */ + if (errno == EINVAL) { + if (base == 0 || (base >1 && base <37)) { + /* Base was ok so it's because we were not + * able to make the conversion. + * Let's reset errno. + */ + errno = saved_errno; + } + } + return nb; +} +#endif /* HAVE_BSD_STRTOLL */ +#endif /* HAVE_STRTOULL */ + +#ifndef HAVE_SETENV +int rep_setenv(const char *name, const char *value, int overwrite) +{ + char *p; + size_t l1, l2; + int ret; + + if (!overwrite && getenv(name)) { + return 0; + } + + l1 = strlen(name); + l2 = strlen(value); + + p = malloc(l1+l2+2); + if (p == NULL) { + return -1; + } + memcpy(p, name, l1); + p[l1] = '='; + memcpy(p+l1+1, value, l2); + p[l1+l2+1] = 0; + + ret = putenv(p); + if (ret != 0) { + free(p); + } + + return ret; +} +#endif + +#ifndef HAVE_UNSETENV +int rep_unsetenv(const char *name) +{ + extern char **environ; + size_t len = strlen(name); + size_t i, count; + + if (environ == NULL || getenv(name) == NULL) { + return 0; + } + + for (i=0;environ[i];i++) /* noop */ ; + + count=i; + + for (i=0;i= needlelen) { + char *p = (char *)memchr(haystack, *(const char *)needle, + haystacklen-(needlelen-1)); + if (!p) return NULL; + if (memcmp(p, needle, needlelen) == 0) { + return p; + } + haystack = p+1; + haystacklen -= (p - (const char *)haystack) + 1; + } + return NULL; +} +#endif + +#if !defined(HAVE_VDPRINTF) || !defined(HAVE_C99_VSNPRINTF) +int rep_vdprintf(int fd, const char *format, va_list ap) +{ + char *s = NULL; + int ret; + + vasprintf(&s, format, ap); + if (s == NULL) { + errno = ENOMEM; + return -1; + } + ret = write(fd, s, strlen(s)); + free(s); + return ret; +} +#endif + +#if !defined(HAVE_DPRINTF) || !defined(HAVE_C99_VSNPRINTF) +int rep_dprintf(int fd, const char *format, ...) +{ + int ret; + va_list ap; + + va_start(ap, format); + ret = vdprintf(fd, format, ap); + va_end(ap); + + return ret; +} +#endif + +#ifndef HAVE_GET_CURRENT_DIR_NAME +char *rep_get_current_dir_name(void) +{ + char buf[PATH_MAX+1]; + char *p; + p = getcwd(buf, sizeof(buf)); + if (p == NULL) { + return NULL; + } + return strdup(p); +} +#endif + +#ifndef HAVE_STRERROR_R +int rep_strerror_r(int errnum, char *buf, size_t buflen) +{ + char *s = strerror(errnum); + if (strlen(s)+1 > buflen) { + errno = ERANGE; + return -1; + } + strncpy(buf, s, buflen); + return 0; +} +#elif (!defined(STRERROR_R_XSI_NOT_GNU)) +#undef strerror_r +int rep_strerror_r(int errnum, char *buf, size_t buflen) +{ + char *s = strerror_r(errnum, buf, buflen); + if (s == NULL) { + /* Shouldn't happen, should always get a string */ + return EINVAL; + } + if (s != buf) { + strlcpy(buf, s, buflen); + if (strlen(s) > buflen - 1) { + return ERANGE; + } + } + return 0; + +} +#endif + +#ifndef HAVE_CLOCK_GETTIME +int rep_clock_gettime(clockid_t clk_id, struct timespec *tp) +{ + struct timeval tval; + switch (clk_id) { + case 0: /* CLOCK_REALTIME :*/ +#if defined(HAVE_GETTIMEOFDAY_TZ) || defined(HAVE_GETTIMEOFDAY_TZ_VOID) + gettimeofday(&tval,NULL); +#else + gettimeofday(&tval); +#endif + tp->tv_sec = tval.tv_sec; + tp->tv_nsec = tval.tv_usec * 1000; + break; + default: + errno = EINVAL; + return -1; + } + return 0; +} +#endif + +#ifndef HAVE_MEMALIGN +void *rep_memalign( size_t align, size_t size ) +{ +#if defined(HAVE_POSIX_MEMALIGN) + void *p = NULL; + int ret = posix_memalign( &p, align, size ); + if ( ret == 0 ) + return p; + + return NULL; +#else + /* On *BSD systems memaligns doesn't exist, but memory will + * be aligned on allocations of > pagesize. */ +#if defined(SYSCONF_SC_PAGESIZE) + size_t pagesize = (size_t)sysconf(_SC_PAGESIZE); +#elif defined(HAVE_GETPAGESIZE) + size_t pagesize = (size_t)getpagesize(); +#else + size_t pagesize = (size_t)-1; +#endif + if (pagesize == (size_t)-1) { + errno = ENOSYS; + return NULL; + } + if (size < pagesize) { + size = pagesize; + } + return malloc(size); +#endif +} +#endif + +#ifndef HAVE_GETPEEREID +int rep_getpeereid(int s, uid_t *uid, gid_t *gid) +{ +#if defined(HAVE_PEERCRED) + struct ucred cred; + socklen_t cred_len = sizeof(struct ucred); + int ret; + +#undef getsockopt + ret = getsockopt(s, SOL_SOCKET, SO_PEERCRED, (void *)&cred, &cred_len); + if (ret != 0) { + return -1; + } + + if (cred_len != sizeof(struct ucred)) { + errno = EINVAL; + return -1; + } + + *uid = cred.uid; + *gid = cred.gid; + return 0; +#else + errno = ENOSYS; + return -1; +#endif +} +#endif + +#ifndef HAVE_USLEEP +int rep_usleep(useconds_t sec) +{ + struct timeval tval; + /* + * Fake it with select... + */ + tval.tv_sec = 0; + tval.tv_usec = usecs/1000; + select(0,NULL,NULL,NULL,&tval); + return 0; +} +#endif /* HAVE_USLEEP */ + +#ifndef HAVE_SETPROCTITLE +void rep_setproctitle(const char *fmt, ...) +{ +} +#endif +#ifndef HAVE_SETPROCTITLE_INIT +void rep_setproctitle_init(int argc, char *argv[], char *envp[]) +{ +} +#endif + +#ifndef HAVE_MEMSET_S +# ifndef RSIZE_MAX +# define RSIZE_MAX (SIZE_MAX >> 1) +# endif + +int rep_memset_s(void *dest, size_t destsz, int ch, size_t count) +{ + if (dest == NULL) { + return EINVAL; + } + + if (destsz > RSIZE_MAX || + count > RSIZE_MAX || + count > destsz) { + return ERANGE; + } + +#if defined(HAVE_MEMSET_EXPLICIT) + memset_explicit(dest, ch, count); +#else /* HAVE_MEMSET_EXPLICIT */ + memset(dest, ch, count); +# if defined(HAVE_GCC_VOLATILE_MEMORY_PROTECTION) + /* See http://llvm.org/bugs/show_bug.cgi?id=15495 */ + __asm__ volatile("" : : "g"(dest) : "memory"); +# endif /* HAVE_GCC_VOLATILE_MEMORY_PROTECTION */ +#endif /* HAVE_MEMSET_EXPLICIT */ + + return 0; +} +#endif /* HAVE_MEMSET_S */ + +#ifndef HAVE_GETPROGNAME +# ifndef HAVE_PROGRAM_INVOCATION_SHORT_NAME +# define PROGNAME_SIZE 32 +static char rep_progname[PROGNAME_SIZE]; +# endif /* HAVE_PROGRAM_INVOCATION_SHORT_NAME */ + +const char *rep_getprogname(void) +{ +#ifdef HAVE_PROGRAM_INVOCATION_SHORT_NAME + return program_invocation_short_name; +#else /* HAVE_PROGRAM_INVOCATION_SHORT_NAME */ + FILE *fp = NULL; + char cmdline[4096] = {0}; + char *p = NULL; + pid_t pid; + size_t nread; + int len; + int rc; + + if (rep_progname[0] != '\0') { + return rep_progname; + } + + len = snprintf(rep_progname, sizeof(rep_progname), "%s", ""); + if (len <= 0) { + return NULL; + } + + pid = getpid(); + if (pid <= 1 || pid == (pid_t)-1) { + return NULL; + } + + len = snprintf(cmdline, + sizeof(cmdline), + "/proc/%u/cmdline", + (unsigned int)pid); + if (len <= 0 || len == sizeof(cmdline)) { + return NULL; + } + + fp = fopen(cmdline, "r"); + if (fp == NULL) { + return NULL; + } + + nread = fread(cmdline, 1, sizeof(cmdline) - 1, fp); + + rc = fclose(fp); + if (rc != 0) { + return NULL; + } + + if (nread == 0) { + return NULL; + } + + cmdline[nread] = '\0'; + + p = strrchr(cmdline, '/'); + if (p != NULL) { + p++; + } else { + p = cmdline; + } + + len = strlen(p); + if (len > PROGNAME_SIZE) { + p[PROGNAME_SIZE - 1] = '\0'; + } + + (void)snprintf(rep_progname, sizeof(rep_progname), "%s", p); + + return rep_progname; +#endif /* HAVE_PROGRAM_INVOCATION_SHORT_NAME */ +} +#endif /* HAVE_GETPROGNAME */ + +#ifndef HAVE_COPY_FILE_RANGE +ssize_t rep_copy_file_range(int fd_in, + loff_t *off_in, + int fd_out, + loff_t *off_out, + size_t len, + unsigned int flags) +{ +# ifdef HAVE_SYSCALL_COPY_FILE_RANGE + return syscall(__NR_copy_file_range, + fd_in, + off_in, + fd_out, + off_out, + len, + flags); +# endif /* HAVE_SYSCALL_COPY_FILE_RANGE */ + errno = ENOSYS; + return -1; +} +#endif /* HAVE_COPY_FILE_RANGE */ + +#ifdef HAVE_LINUX_IOCTL +# include +# include +#endif + +ssize_t rep_copy_reflink(int src_fd, + off_t src_off, + int dst_fd, + off_t dst_off, + off_t to_copy) +{ +#ifdef HAVE_LINUX_IOCTL + struct file_clone_range cr; + + cr = (struct file_clone_range) { + .src_fd = src_fd, + .src_offset = (uint64_t)src_off, + .dest_offset = (uint64_t)dst_off, + .src_length = (uint64_t)to_copy, + }; + + return ioctl(dst_fd, FICLONERANGE, &cr); +#else + errno = ENOSYS; + return -1; +#endif +} + +#ifndef HAVE_OPENAT2 + +/* fallback known wellknown __NR_openat2 values */ +#ifndef __NR_openat2 +# if defined(LINUX) && defined(HAVE_SYS_SYSCALL_H) +# if defined(__i386__) +# define __NR_openat2 437 +# elif defined(__x86_64__) && defined(__LP64__) +# define __NR_openat2 437 /* 437 0x1B5 */ +# elif defined(__x86_64__) && defined(__ILP32__) +# define __NR_openat2 1073742261 /* 1073742261 0x400001B5 */ +# elif defined(__aarch64__) +# define __NR_openat2 437 +# elif defined(__arm__) +# define __NR_openat2 437 +# elif defined(__sparc__) +# define __NR_openat2 437 +# endif +# endif /* defined(LINUX) && defined(HAVE_SYS_SYSCALL_H) */ +#endif /* !__NR_openat2 */ + +#ifdef DISABLE_OPATH +/* + * systems without O_PATH also don't have openat2, + * so make sure we at a realistic combination. + */ +#undef __NR_openat2 +#endif /* DISABLE_OPATH */ + +long rep_openat2(int dirfd, const char *pathname, + struct open_how *how, size_t size) +{ +#ifdef __NR_openat2 +#if _FILE_OFFSET_BITS == 64 && SIZE_MAX == 0xffffffffUL && defined(O_LARGEFILE) + struct open_how __how; + +#if defined(O_PATH) && ! defined(DISABLE_OPATH) + if ((how->flags & O_PATH) == 0) +#endif + { + if (sizeof(__how) == size) { + __how = *how; + + __how.flags |= O_LARGEFILE; + how = &__how; + } + } +#endif + + return syscall(__NR_openat2, + dirfd, + pathname, + how, + size); +#else + errno = ENOSYS; + return -1; +#endif +} +#endif /* !HAVE_OPENAT2 */ + +#ifndef HAVE_RENAMEAT2 + +/* fallback to wellknown __NR_renameat2 values */ +#ifndef __NR_renameat2 +# if defined(LINUX) && defined(HAVE_SYS_SYSCALL_H) +# if defined(__i386__) +# define __NR_renameat2 353 +# elif defined(__x86_64__) && defined(__LP64__) +# define __NR_renameat2 316 /* 316 0x13C */ +# elif defined(__x86_64__) && defined(__ILP32__) +# define __NR_renameat2 1073742140 /* 1073742140 0x4000013C */ +# elif defined(__aarch64__) +# define __NR_renameat2 276 +# elif defined(__arm__) +# define __NR_renameat2 382 +# elif defined(__sparc__) +# define __NR_renameat2 345 +# endif +# endif /* defined(LINUX) && defined(HAVE_SYS_SYSCALL_H) */ +#endif /* !__NR_renameat2 */ + +#ifdef DISABLE_OPATH +/* + * systems without O_PATH also don't have renameat2, + * so make sure we at a realistic combination. + */ +#undef __NR_renameat2 +#endif /* DISABLE_OPATH */ + +int rep_renameat2(int __oldfd, const char *__old, int __newfd, + const char *__new, unsigned int __flags) +{ + if (__flags != 0) { +#ifdef __NR_renameat2 + int ret; + + ret = syscall(__NR_renameat2, + __oldfd, + __old, + __newfd, + __new, + __flags); + if (ret != -1 || errno != ENOSYS) { + /* + * if it's ENOSYS, we fallback + * to EINVAL below, otherwise + * we return what the kernel + * did. + */ + return ret; + } +#endif + errno = EINVAL; + return -1; + } + + return renameat(__oldfd, __old, __newfd, __new); +} +#endif /* ! HAVE_RENAMEAT2 */ + +const char hexchars_lower[] = "0123456789abcdef"; +const char hexchars_upper[] = "0123456789ABCDEF"; diff --git a/core/proot/src/main/cpp/talloc/replace.h b/core/proot/src/main/cpp/talloc/replace.h new file mode 100644 index 000000000..4923e1f30 --- /dev/null +++ b/core/proot/src/main/cpp/talloc/replace.h @@ -0,0 +1,1120 @@ +/* + Unix SMB/CIFS implementation. + + macros to go along with the lib/replace/ portability layer code + + Copyright (C) Andrew Tridgell 2005 + Copyright (C) Jelmer Vernooij 2006-2008 + Copyright (C) Jeremy Allison 2007. + + ** NOTE! The following LGPL license applies to the replace + ** library. This does NOT imply that all of Samba is released + ** under the LGPL + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 3 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see . +*/ + +#ifndef _LIBREPLACE_REPLACE_H +#define _LIBREPLACE_REPLACE_H + +#ifndef NO_CONFIG_H +#include "config.h" +#endif + +#ifdef HAVE_STANDARDS_H +#include +#endif + +/* + * Needs to be defined before std*.h and string*.h are included + * As it's also needed when Python.h is the first header we + * require a global -D__STDC_WANT_LIB_EXT1__=1 + */ +#if __STDC_WANT_LIB_EXT1__ != 1 +#error -D__STDC_WANT_LIB_EXT1__=1 required +#endif + +#include +#include +#include +#include + +#ifndef HAVE_DECL_EWOULDBLOCK +#define EWOULDBLOCK EAGAIN +#endif + +#if defined(_MSC_VER) || defined(__MINGW32__) +#include "win32_replace.h" +#endif + + +#ifdef HAVE_INTTYPES_H +#define __STDC_FORMAT_MACROS +#include +#elif defined(HAVE_STDINT_H) +#include +/* force off HAVE_INTTYPES_H so that roken doesn't try to include both, + which causes a warning storm on irix */ +#undef HAVE_INTTYPES_H +#endif + +#ifdef HAVE_MALLOC_H +#include +#endif + +#ifndef __PRI64_PREFIX +# if __WORDSIZE == 64 && ! defined __APPLE__ +# define __PRI64_PREFIX "l" +# else +# define __PRI64_PREFIX "ll" +# endif +#endif + +/* Decimal notation. */ +#ifndef PRId8 +# define PRId8 "d" +#endif +#ifndef PRId16 +# define PRId16 "d" +#endif +#ifndef PRId32 +# define PRId32 "d" +#endif +#ifndef PRId64 +# define PRId64 __PRI64_PREFIX "d" +#endif + +#ifndef PRIi8 +# define PRIi8 "i" +#endif +#ifndef PRIi16 +# define PRIi16 "i" +#endif +#ifndef PRIi32 +# define PRIi32 "i" +#endif +#ifndef PRIi64 +# define PRIi64 __PRI64_PREFIX "i" +#endif + +#ifndef PRIu8 +# define PRIu8 "u" +#endif +#ifndef PRIu16 +# define PRIu16 "u" +#endif +#ifndef PRIu32 +# define PRIu32 "u" +#endif +#ifndef PRIu64 +# define PRIu64 __PRI64_PREFIX "u" +#endif + +#ifndef SCNd8 +# define SCNd8 "hhd" +#endif +#ifndef SCNd16 +# define SCNd16 "hd" +#endif +#ifndef SCNd32 +# define SCNd32 "d" +#endif +#ifndef SCNd64 +# define SCNd64 __PRI64_PREFIX "d" +#endif + +#ifndef SCNi8 +# define SCNi8 "hhi" +#endif +#ifndef SCNi16 +# define SCNi16 "hi" +#endif +#ifndef SCNi32 +# define SCNi32 "i" +#endif +#ifndef SCNi64 +# define SCNi64 __PRI64_PREFIX "i" +#endif + +#ifndef SCNu8 +# define SCNu8 "hhu" +#endif +#ifndef SCNu16 +# define SCNu16 "hu" +#endif +#ifndef SCNu32 +# define SCNu32 "u" +#endif +#ifndef SCNu64 +# define SCNu64 __PRI64_PREFIX "u" +#endif + +#ifdef HAVE_BSD_STRING_H +#include +#endif + +#ifdef HAVE_BSD_UNISTD_H +#include +#endif + +#ifdef HAVE_UNISTD_H +#include +#endif + +#ifdef HAVE_STRING_H +#include +#endif + +#ifdef HAVE_STRINGS_H +#include +#endif + +#ifdef HAVE_SYS_TYPES_H +#include +#endif + +#ifdef HAVE_SYS_SYSMACROS_H +#include +#endif + +#ifdef HAVE_SETPROCTITLE_H +#include +#endif + +#if STDC_HEADERS +#include +#include +#endif + +#ifdef HAVE_LINUX_TYPES_H +/* + * This is needed as some broken header files require this to be included early + */ +#include +#endif + +#ifndef HAVE_STRERROR +extern const char *const sys_errlist[]; +#define strerror(i) sys_errlist[i] +#endif + +#ifndef HAVE_ERRNO_DECL +extern int errno; +#endif + +#ifndef HAVE_STRDUP +#define strdup rep_strdup +char *rep_strdup(const char *s); +#endif + +#ifndef HAVE_MEMMOVE +#define memmove rep_memmove +void *rep_memmove(void *dest,const void *src,int size); +#endif + +#ifndef HAVE_MEMMEM +#define memmem rep_memmem +void *rep_memmem(const void *haystack, size_t haystacklen, + const void *needle, size_t needlelen); +#endif + +#ifndef HAVE_MEMALIGN +#define memalign rep_memalign +void *rep_memalign(size_t boundary, size_t size); +#endif + +#ifndef HAVE_MKTIME +#define mktime rep_mktime +/* prototype is in "system/time.h" */ +#endif + +#ifndef HAVE_TIMEGM +#define timegm rep_timegm +/* prototype is in "system/time.h" */ +#endif + +#ifndef HAVE_UTIME +#define utime rep_utime +/* prototype is in "system/time.h" */ +#endif + +#ifndef HAVE_UTIMES +#define utimes rep_utimes +/* prototype is in "system/time.h" */ +#endif + +#ifndef HAVE_STRLCPY +#define strlcpy rep_strlcpy +size_t rep_strlcpy(char *d, const char *s, size_t bufsize); +#endif + +#ifndef HAVE_STRLCAT +#define strlcat rep_strlcat +size_t rep_strlcat(char *d, const char *s, size_t bufsize); +#endif + +#ifndef HAVE_CLOSEFROM +#define closefrom rep_closefrom +int rep_closefrom(int lower); +#endif + + +#if (defined(BROKEN_STRNDUP) || !defined(HAVE_STRNDUP)) +#undef HAVE_STRNDUP +#define strndup rep_strndup +char *rep_strndup(const char *s, size_t n); +#endif + +#if (defined(BROKEN_STRNLEN) || !defined(HAVE_STRNLEN)) +#undef HAVE_STRNLEN +#define strnlen rep_strnlen +size_t rep_strnlen(const char *s, size_t n); +#endif + +#if !defined(HAVE_DECL_ENVIRON) +# ifdef __APPLE__ +# include +# define environ (*_NSGetEnviron()) +# else /* __APPLE__ */ +extern char **environ; +# endif /* __APPLE */ +#endif /* !defined(HAVE_DECL_ENVIRON) */ + +#ifndef HAVE_SETENV +#define setenv rep_setenv +int rep_setenv(const char *name, const char *value, int overwrite); +#else +#ifndef HAVE_SETENV_DECL +int setenv(const char *name, const char *value, int overwrite); +#endif +#endif + +#ifndef HAVE_UNSETENV +#define unsetenv rep_unsetenv +int rep_unsetenv(const char *name); +#endif + +#ifndef HAVE_SETEUID +#define seteuid rep_seteuid +int rep_seteuid(uid_t); +#endif + +#ifndef HAVE_SETEGID +#define setegid rep_setegid +int rep_setegid(gid_t); +#endif + +#if (defined(USE_SETRESUID) && !defined(HAVE_SETRESUID_DECL)) +/* stupid glibc */ +int setresuid(uid_t ruid, uid_t euid, uid_t suid); +#endif +#if (defined(USE_SETRESUID) && !defined(HAVE_SETRESGID_DECL)) +int setresgid(gid_t rgid, gid_t egid, gid_t sgid); +#endif + +#ifndef HAVE_CHOWN +#define chown rep_chown +int rep_chown(const char *path, uid_t uid, gid_t gid); +#endif + +#ifndef HAVE_CHROOT +#define chroot rep_chroot +int rep_chroot(const char *dirname); +#endif + +#ifndef HAVE_LINK +#define link rep_link +int rep_link(const char *oldpath, const char *newpath); +#endif + +#ifndef HAVE_READLINK +#define readlink rep_readlink +ssize_t rep_readlink(const char *path, char *buf, size_t bufsize); +#endif + +#ifndef HAVE_SYMLINK +#define symlink rep_symlink +int rep_symlink(const char *oldpath, const char *newpath); +#endif + +#ifndef HAVE_REALPATH +#define realpath rep_realpath +char *rep_realpath(const char *path, char *resolved_path); +#endif + +#ifndef HAVE_LCHOWN +#define lchown rep_lchown +int rep_lchown(const char *fname,uid_t uid,gid_t gid); +#endif + +#ifdef HAVE_UNIX_H +#include +#endif + +#ifndef HAVE_SETLINEBUF +#define setlinebuf rep_setlinebuf +void rep_setlinebuf(FILE *); +#endif + +#ifndef HAVE_STRCASESTR +#define strcasestr rep_strcasestr +char *rep_strcasestr(const char *haystack, const char *needle); +#endif + +#ifndef HAVE_STRSEP +#define strsep rep_strsep +char *rep_strsep(char **pps, const char *delim); +#endif + +#ifndef HAVE_STRTOK_R +#define strtok_r rep_strtok_r +char *rep_strtok_r(char *s, const char *delim, char **save_ptr); +#endif + + + +#ifndef HAVE_STRTOLL +#define strtoll rep_strtoll +long long int rep_strtoll(const char *str, char **endptr, int base); +#else +#ifdef HAVE_BSD_STRTOLL +#define strtoll rep_strtoll +long long int rep_strtoll(const char *str, char **endptr, int base); +#endif +#endif + +#ifndef HAVE_STRTOULL +#define strtoull rep_strtoull +unsigned long long int rep_strtoull(const char *str, char **endptr, int base); +#else +#ifdef HAVE_BSD_STRTOLL /* yes, it's not HAVE_BSD_STRTOULL */ +#define strtoull rep_strtoull +unsigned long long int rep_strtoull(const char *str, char **endptr, int base); +#endif +#endif + +#ifndef HAVE_FTRUNCATE +#define ftruncate rep_ftruncate +int rep_ftruncate(int,off_t); +#endif + +#ifndef HAVE_INITGROUPS +#define initgroups rep_initgroups +int rep_initgroups(char *name, gid_t id); +#endif + +#ifndef HAVE_DLERROR +#define dlerror rep_dlerror +char *rep_dlerror(void); +#endif + +#ifndef HAVE_DLOPEN +#define dlopen rep_dlopen +#ifdef DLOPEN_TAKES_UNSIGNED_FLAGS +void *rep_dlopen(const char *name, unsigned int flags); +#else +void *rep_dlopen(const char *name, int flags); +#endif +#endif + +#ifndef HAVE_DLSYM +#define dlsym rep_dlsym +void *rep_dlsym(void *handle, const char *symbol); +#endif + +#ifndef HAVE_DLCLOSE +#define dlclose rep_dlclose +int rep_dlclose(void *handle); +#endif + +#ifndef HAVE_SOCKETPAIR +#define socketpair rep_socketpair +/* prototype is in system/network.h */ +#endif + +/* for old gcc releases that don't have the feature test macro __has_attribute */ +#ifndef __has_attribute +#define __has_attribute(x) 0 +#endif + +#ifndef PRINTF_ATTRIBUTE +#if __has_attribute(format) || (__GNUC__ >= 3) +/** Use gcc attribute to check printf fns. a1 is the 1-based index of + * the parameter containing the format, and a2 the index of the first + * argument. Note that some gcc 2.x versions don't handle this + * properly **/ +#define PRINTF_ATTRIBUTE(a1, a2) __attribute__ ((format (__printf__, a1, a2))) +#else +#define PRINTF_ATTRIBUTE(a1, a2) +#endif +#endif + +#ifndef _DEPRECATED_ +#if __has_attribute(deprecated) || (__GNUC__ >= 3) +#define _DEPRECATED_ __attribute__ ((deprecated)) +#else +#define _DEPRECATED_ +#endif +#endif + +#if !defined(HAVE_VDPRINTF) || !defined(HAVE_C99_VSNPRINTF) +#define vdprintf rep_vdprintf +int rep_vdprintf(int fd, const char *format, va_list ap) PRINTF_ATTRIBUTE(2,0); +#endif + +#if !defined(HAVE_DPRINTF) || !defined(HAVE_C99_VSNPRINTF) +#define dprintf rep_dprintf +int rep_dprintf(int fd, const char *format, ...) PRINTF_ATTRIBUTE(2,3); +#endif + +#if !defined(HAVE_VASPRINTF) || !defined(HAVE_C99_VSNPRINTF) +#define vasprintf rep_vasprintf +int rep_vasprintf(char **ptr, const char *format, va_list ap) PRINTF_ATTRIBUTE(2,0); +#endif + +#if !defined(HAVE_SNPRINTF) || !defined(HAVE_C99_VSNPRINTF) +#define snprintf rep_snprintf +int rep_snprintf(char *,size_t ,const char *, ...) PRINTF_ATTRIBUTE(3,4); +#endif + +#if !defined(HAVE_VSNPRINTF) || !defined(HAVE_C99_VSNPRINTF) +#define vsnprintf rep_vsnprintf +int rep_vsnprintf(char *,size_t ,const char *, va_list ap) PRINTF_ATTRIBUTE(3,0); +#endif + +#if !defined(HAVE_ASPRINTF) || !defined(HAVE_C99_VSNPRINTF) +#define asprintf rep_asprintf +int rep_asprintf(char **,const char *, ...) PRINTF_ATTRIBUTE(2,3); +#endif + +#if !defined(HAVE_C99_VSNPRINTF) +#ifdef REPLACE_BROKEN_PRINTF +/* + * We do not redefine printf by default + * as it breaks the build if system headers + * use __attribute__((format(printf, 3, 0))) + * instead of __attribute__((format(__printf__, 3, 0))) + */ +#define printf rep_printf +#endif +int rep_printf(const char *, ...) PRINTF_ATTRIBUTE(1,2); +#endif + +#if !defined(HAVE_C99_VSNPRINTF) +#define fprintf rep_fprintf +int rep_fprintf(FILE *stream, const char *, ...) PRINTF_ATTRIBUTE(2,3); +#endif + +#ifndef HAVE_VSYSLOG +#ifdef HAVE_SYSLOG +#define vsyslog rep_vsyslog +void rep_vsyslog (int facility_priority, const char *format, va_list arglist) PRINTF_ATTRIBUTE(2,0); +#endif +#endif + +/* we used to use these fns, but now we have good replacements + for snprintf and vsnprintf */ +#define slprintf snprintf + + +#ifndef HAVE_VA_COPY +#undef va_copy +#ifdef HAVE___VA_COPY +#define va_copy(dest, src) __va_copy(dest, src) +#else +#define va_copy(dest, src) (dest) = (src) +#endif +#endif + +#ifndef HAVE_VOLATILE +#define volatile +#endif + +#ifndef HAVE_COMPARISON_FN_T +typedef int (*comparison_fn_t)(const void *, const void *); +#endif + +#ifndef HAVE_WORKING_STRPTIME +#define strptime rep_strptime +struct tm; +char *rep_strptime(const char *buf, const char *format, struct tm *tm); +#endif + +#ifndef HAVE_DUP2 +#define dup2 rep_dup2 +int rep_dup2(int oldfd, int newfd); +#endif + +/* Load header file for dynamic linking stuff */ +#ifdef HAVE_DLFCN_H +#include +#endif + +#ifndef RTLD_LAZY +#define RTLD_LAZY 0 +#endif +#ifndef RTLD_NOW +#define RTLD_NOW 0 +#endif +#ifndef RTLD_GLOBAL +#define RTLD_GLOBAL 0 +#endif + +#ifndef HAVE_SECURE_MKSTEMP +#define mkstemp(path) rep_mkstemp(path) +int rep_mkstemp(char *temp); +#endif + +#ifndef HAVE_MKDTEMP +#define mkdtemp rep_mkdtemp +char *rep_mkdtemp(char *template); +#endif + +#ifndef HAVE_PREAD +#define pread rep_pread +ssize_t rep_pread(int __fd, void *__buf, size_t __nbytes, off_t __offset); +#define LIBREPLACE_PREAD_REPLACED 1 +#else +#define LIBREPLACE_PREAD_NOT_REPLACED 1 +#endif + +#ifndef HAVE_PWRITE +#define pwrite rep_pwrite +ssize_t rep_pwrite(int __fd, const void *__buf, size_t __nbytes, off_t __offset); +#define LIBREPLACE_PWRITE_REPLACED 1 +#else +#define LIBREPLACE_PWRITE_NOT_REPLACED 1 +#endif + +#if !defined(HAVE_INET_NTOA) || defined(REPLACE_INET_NTOA) +#define inet_ntoa rep_inet_ntoa +/* prototype is in "system/network.h" */ +#endif + +#ifndef HAVE_INET_PTON +#define inet_pton rep_inet_pton +/* prototype is in "system/network.h" */ +#endif + +#ifndef HAVE_INET_NTOP +#define inet_ntop rep_inet_ntop +/* prototype is in "system/network.h" */ +#endif + +#ifndef HAVE_INET_ATON +#define inet_aton rep_inet_aton +/* prototype is in "system/network.h" */ +#endif + +#ifndef HAVE_CONNECT +#define connect rep_connect +/* prototype is in "system/network.h" */ +#endif + +#ifndef HAVE_GETHOSTBYNAME +#define gethostbyname rep_gethostbyname +/* prototype is in "system/network.h" */ +#endif + +#ifndef HAVE_GETIFADDRS +#define getifaddrs rep_getifaddrs +/* prototype is in "system/network.h" */ +#endif + +#ifndef HAVE_FREEIFADDRS +#define freeifaddrs rep_freeifaddrs +/* prototype is in "system/network.h" */ +#endif + +#ifndef HAVE_GET_CURRENT_DIR_NAME +#define get_current_dir_name rep_get_current_dir_name +char *rep_get_current_dir_name(void); +#endif + +#if (!defined(HAVE_STRERROR_R) || !defined(STRERROR_R_XSI_NOT_GNU)) +#define strerror_r rep_strerror_r +int rep_strerror_r(int errnum, char *buf, size_t buflen); +#endif + +#if !defined(HAVE_CLOCK_GETTIME) +#define clock_gettime rep_clock_gettime +#endif + +#ifdef HAVE_LIMITS_H +#include +#endif + +#ifdef HAVE_SYS_PARAM_H +#include +#endif + +/* The extra casts work around common compiler bugs. */ +#define _TYPE_SIGNED(t) (! ((t) 0 < (t) -1)) +/* The outer cast is needed to work around a bug in Cray C 5.0.3.0. + It is necessary at least when t == time_t. */ +#define _TYPE_MINIMUM(t) ((t) (_TYPE_SIGNED (t) \ + ? ~ (t) 0 << (sizeof (t) * CHAR_BIT - 1) : (t) 0)) +#define _TYPE_MAXIMUM(t) ((t) (~ (t) 0 - _TYPE_MINIMUM (t))) + +#ifndef UINT16_MAX +#define UINT16_MAX 65535 +#endif + +#ifndef UINT32_MAX +#define UINT32_MAX (4294967295U) +#endif + +#ifndef UINT64_MAX +#define UINT64_MAX ((uint64_t)-1) +#endif + +#ifndef INT64_MAX +#define INT64_MAX 9223372036854775807LL +#endif + +#ifndef CHAR_BIT +#define CHAR_BIT 8 +#endif + +#ifndef INT32_MAX +#define INT32_MAX _TYPE_MAXIMUM(int32_t) +#endif + +#ifdef HAVE_STDBOOL_H +#include +#endif + +#ifndef HAVE_BOOL +#error Need a real boolean type +#endif + +#if !defined(HAVE_INTPTR_T) +typedef long long intptr_t ; +#define __intptr_t_defined +#endif + +#if !defined(HAVE_UINTPTR_T) +typedef unsigned long long uintptr_t ; +#define __uintptr_t_defined +#endif + +#if !defined(HAVE_PTRDIFF_T) +typedef unsigned long long ptrdiff_t ; +#endif + +/* + * to prevent from doing a redefine of 'bool' + * + * IRIX, HPUX, MacOS 10 and Solaris need BOOL_DEFINED + * Tru64 needs _BOOL_EXISTS + * AIX needs _BOOL,_TRUE,_FALSE + */ +#ifndef BOOL_DEFINED +#define BOOL_DEFINED +#endif +#ifndef _BOOL_EXISTS +#define _BOOL_EXISTS +#endif +#ifndef _BOOL +#define _BOOL +#endif + +#ifndef __bool_true_false_are_defined +#define __bool_true_false_are_defined +#endif + +#ifndef true +#define true (1) +#endif +#ifndef false +#define false (0) +#endif + +#ifndef _TRUE +#define _TRUE true +#endif +#ifndef _FALSE +#define _FALSE false +#endif + +#ifndef HAVE_FUNCTION_MACRO +#ifdef HAVE_func_MACRO +#define __FUNCTION__ __func__ +#else +#define __FUNCTION__ ("") +#endif +#endif + + +#ifndef MIN +#define MIN(a,b) ((a)<(b)?(a):(b)) +#endif + +#ifndef MAX +#define MAX(a,b) ((a)>(b)?(a):(b)) +#endif + +#if !defined(HAVE_VOLATILE) +#define volatile +#endif + +/** + this is a warning hack. The idea is to use this everywhere that we + get the "discarding const" warning from gcc. That doesn't actually + fix the problem of course, but it means that when we do get to + cleaning them up we can do it by searching the code for + discard_const. + + It also means that other error types aren't as swamped by the noise + of hundreds of const warnings, so we are more likely to notice when + we get new errors. + + Please only add more uses of this macro when you find it + _really_ hard to fix const warnings. Our aim is to eventually use + this function in only a very few places. + + Also, please call this via the discard_const_p() macro interface, as that + makes the return type safe. +*/ +#define discard_const(ptr) ((void *)((uintptr_t)(ptr))) + +/** Type-safe version of discard_const */ +#define discard_const_p(type, ptr) ((type *)discard_const(ptr)) + +#ifndef __STRING +#define __STRING(x) #x +#endif + +#ifndef __STRINGSTRING +#define __STRINGSTRING(x) __STRING(x) +#endif + +#ifndef __LINESTR__ +#define __LINESTR__ __STRINGSTRING(__LINE__) +#endif + +#ifndef __location__ +#define __location__ __FILE__ ":" __LINESTR__ +#endif + +/** + * Zero a structure. + */ +#define ZERO_STRUCT(x) memset_s((char *)&(x), sizeof(x), 0, sizeof(x)) + +/** + * Zero a structure given a pointer to the structure. + */ +#define ZERO_STRUCTP(x) do { \ + if ((x) != NULL) { \ + memset_s((char *)(x), sizeof(*(x)), 0, sizeof(*(x))); \ + } \ +} while(0) + +/** + * Zero a structure given a pointer to the structure - no zero check + */ +#define ZERO_STRUCTPN(x) memset_s((char *)(x), sizeof(*(x)), 0, sizeof(*(x))) + +/** + * Zero an array - note that sizeof(array) must work - ie. it must not be a + * pointer + */ +#define ZERO_ARRAY(x) memset_s((char *)(x), sizeof(x), 0, sizeof(x)) + +/** + * Zero a given len of an array + */ +#define ZERO_ARRAY_LEN(x, l) memset_s((char *)(x), (l), 0, (l)) + +/** + * Explicitly zero data from memory. This is guaranteed to be not optimized + * away. + */ +#define BURN_DATA(x) memset_s((char *)&(x), sizeof(x), 0, sizeof(x)) + +/** + * Explicitly zero data from memory. This is guaranteed to be not optimized + * away. + */ +#define BURN_DATA_SIZE(x, s) memset_s((char *)&(x), (s), 0, (s)) + +/** + * Explicitly zero data from memory. This is guaranteed to be not optimized + * away. + */ +#define BURN_PTR_SIZE(x, s) memset_s((x), (s), 0, (s)) + +/** + * Explicitly zero data in string. This is guaranteed to be not optimized + * away. + */ +#define BURN_STR(x) do { \ + if ((x) != NULL) { \ + size_t s = strlen(x); \ + memset_s((x), s, 0, s); \ + } \ + } while(0) + +/** + * Work out how many elements there are in a static array. + */ +#ifdef ARRAY_SIZE +#undef ARRAY_SIZE +#endif +#define ARRAY_SIZE(a) (sizeof(a)/sizeof(a[0])) + +/** + * Remove an array element by moving the rest one down + */ +#define ARRAY_DEL_ELEMENT(a,i,n) \ +if((i)<((n)-1)){memmove(&((a)[(i)]),&((a)[(i)+1]),(sizeof(*(a))*((n)-(i)-1)));} + +/** + * Insert an array element by moving the rest one up + * + */ +#define ARRAY_INSERT_ELEMENT(__array,__old_last_idx,__new_elem,__new_idx) do { \ + if ((__new_idx) < (__old_last_idx)) { \ + const void *__src = &((__array)[(__new_idx)]); \ + void *__dst = &((__array)[(__new_idx)+1]); \ + size_t __num = (__old_last_idx)-(__new_idx); \ + size_t __len = sizeof(*(__array)) * __num; \ + memmove(__dst, __src, __len); \ + } \ + (__array)[(__new_idx)] = (__new_elem); \ +} while(0) + +/** + * Pointer difference macro + */ +#define PTR_DIFF(p1,p2) ((ptrdiff_t)(((const char *)(p1)) - (const char *)(p2))) + +#ifdef __COMPAR_FN_T +#define QSORT_CAST (__compar_fn_t) +#endif + +#ifndef QSORT_CAST +#define QSORT_CAST (int (*)(const void *, const void *)) +#endif + +#ifndef PATH_MAX +#define PATH_MAX 1024 +#endif + +#ifndef MAX_DNS_NAME_LENGTH +#define MAX_DNS_NAME_LENGTH 256 /* Actually 255 but +1 for terminating null. */ +#endif + +#ifdef HAVE_CRYPT_H +#include +#endif + +/* these macros gain us a few percent of speed on gcc */ +#if (__GNUC__ >= 3) +/* the strange !! is to ensure that __builtin_expect() takes either 0 or 1 + as its first argument */ +#ifndef likely +#define likely(x) __builtin_expect(!!(x), 1) +#endif +#ifndef unlikely +#define unlikely(x) __builtin_expect(!!(x), 0) +#endif +#else +#ifndef likely +#define likely(x) (x) +#endif +#ifndef unlikely +#define unlikely(x) (x) +#endif +#endif + +#ifndef HAVE_FDATASYNC +#define fdatasync(fd) fsync(fd) +#elif !defined(HAVE_DECL_FDATASYNC) +int fdatasync(int ); +#endif + +/* these are used to mark symbols as local to a shared lib, or + * publicly available via the shared lib API */ +#ifndef _PUBLIC_ +#ifdef HAVE_VISIBILITY_ATTR +#define _PUBLIC_ __attribute__((visibility("default"))) +#else +#define _PUBLIC_ +#endif +#endif + +#ifndef _PRIVATE_ +#ifdef HAVE_VISIBILITY_ATTR +# define _PRIVATE_ __attribute__((visibility("hidden"))) +#else +# define _PRIVATE_ +#endif +#endif + +#ifndef HAVE_POLL +#define poll rep_poll +/* prototype is in "system/network.h" */ +#endif + +#ifndef HAVE_GETPEEREID +#define getpeereid rep_getpeereid +int rep_getpeereid(int s, uid_t *uid, gid_t *gid); +#endif + +#ifndef HAVE_USLEEP +#define usleep rep_usleep +typedef long useconds_t; +int usleep(useconds_t); +#endif + +#ifndef HAVE_SETPROCTITLE +#define setproctitle rep_setproctitle +void rep_setproctitle(const char *fmt, ...) PRINTF_ATTRIBUTE(1, 2); +#endif + +#ifndef HAVE_SETPROCTITLE_INIT +#define setproctitle_init rep_setproctitle_init +void rep_setproctitle_init(int argc, char *argv[], char *envp[]); +#endif + +#ifndef HAVE_MEMSET_S +#define memset_s rep_memset_s +int rep_memset_s(void *dest, size_t destsz, int ch, size_t count); +#endif + +#ifndef HAVE_GETPROGNAME +#define getprogname rep_getprogname +const char *rep_getprogname(void); +#endif + +#ifndef HAVE_COPY_FILE_RANGE +#define copy_file_range rep_copy_file_range +ssize_t rep_copy_file_range(int fd_in, + loff_t *off_in, + int fd_out, + loff_t *off_out, + size_t len, + unsigned int flags); +#endif /* HAVE_COPY_FILE_RANGE */ + + +#define copy_reflink rep_copy_reflink + +ssize_t rep_copy_reflink(int src_fd, + off_t src_off, + int dst_fd, + off_t dst_off, + off_t to_copy); + +#ifndef FALL_THROUGH +# ifdef HAVE_FALLTHROUGH_ATTRIBUTE +# define FALL_THROUGH __attribute__ ((fallthrough)) +# else /* HAVE_FALLTHROUGH_ATTRIBUTE */ +# define FALL_THROUGH ((void)0) +# endif /* HAVE_FALLTHROUGH_ATTRIBUTE */ +#endif /* FALL_THROUGH */ + +struct __rep_cwrap_enabled_state { + const char *fnname; + bool cached; + bool retval; +}; + +static inline bool __rep_cwrap_enabled_fn(struct __rep_cwrap_enabled_state *state) +{ + bool (*__wrapper_enabled_fn)(void) = NULL; + + if (state->cached) { + return state->retval; + } + state->retval = false; + state->cached = true; + + __wrapper_enabled_fn = (bool (*)(void))dlsym(RTLD_DEFAULT, state->fnname); + if (__wrapper_enabled_fn == NULL) { + return state->retval; + } + + state->retval = __wrapper_enabled_fn(); + return state->retval; +} + +static inline bool nss_wrapper_enabled(void) +{ + struct __rep_cwrap_enabled_state state = { + .fnname = "nss_wrapper_enabled", + }; + return __rep_cwrap_enabled_fn(&state); +} +static inline bool nss_wrapper_hosts_enabled(void) +{ + struct __rep_cwrap_enabled_state state = { + .fnname = "nss_wrapper_hosts_enabled", + }; + return __rep_cwrap_enabled_fn(&state); +} +static inline bool socket_wrapper_enabled(void) +{ + struct __rep_cwrap_enabled_state state = { + .fnname = "socket_wrapper_enabled", + }; + return __rep_cwrap_enabled_fn(&state); +} +static inline bool uid_wrapper_enabled(void) +{ + struct __rep_cwrap_enabled_state state = { + .fnname = "uid_wrapper_enabled", + }; + return __rep_cwrap_enabled_fn(&state); +} + +static inline bool _hexcharval(char c, uint8_t *val) +{ + if ((c >= '0') && (c <= '9')) { *val = c - '0'; return true; } + c &= 0xDF; /* map lower to upper case -- thanks libnfs :-) */ + if ((c >= 'A') && (c <= 'F')) { *val = c - 'A' + 10; return true; } + return false; +} + +static inline bool hex_byte(const char *in, uint8_t *out) +{ + uint8_t hi=0, lo=0; + bool ok = _hexcharval(in[0], &hi) && _hexcharval(in[1], &lo); + *out = (hi<<4)+lo; + return ok; +} + +extern const char hexchars_lower[]; +extern const char hexchars_upper[]; + +/* Needed for Solaris atomic_add_XX functions. */ +#if defined(HAVE_SYS_ATOMIC_H) +#include +#endif + +/* + * This handles the case of missing pthread support and ensures code can use + * __thread unconditionally, such that when built on a platform without pthread + * support, the __thread qualifier is an empty define. + */ +#ifndef HAVE___THREAD +# ifdef HAVE_PTHREAD +# error Configure failed to detect pthread library with missing TLS support +# endif +#define HAVE___THREAD +#endif + +#endif /* _LIBREPLACE_REPLACE_H */ diff --git a/core/proot/src/main/cpp/talloc/system/README b/core/proot/src/main/cpp/talloc/system/README new file mode 100644 index 000000000..69a2b80b5 --- /dev/null +++ b/core/proot/src/main/cpp/talloc/system/README @@ -0,0 +1,4 @@ +This directory contains wrappers around logical groups of system +include files. The idea is to avoid #ifdef blocks in the main code, +and instead put all the necessary conditional includes in subsystem +specific header files in this directory. diff --git a/core/proot/src/main/cpp/talloc/system/capability.h b/core/proot/src/main/cpp/talloc/system/capability.h new file mode 100644 index 000000000..44b8d5127 --- /dev/null +++ b/core/proot/src/main/cpp/talloc/system/capability.h @@ -0,0 +1,57 @@ +#ifndef _system_capability_h +#define _system_capability_h +/* + Unix SMB/CIFS implementation. + + capability system include wrappers + + Copyright (C) Andrew Tridgell 2004 + + ** NOTE! The following LGPL license applies to the replace + ** library. This does NOT imply that all of Samba is released + ** under the LGPL + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 3 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see . +*/ + +#ifdef HAVE_SYS_CAPABILITY_H + +#if defined(BROKEN_REDHAT_7_SYSTEM_HEADERS) && !defined(_I386_STATFS_H) && !defined(_PPC_STATFS_H) +#define _I386_STATFS_H +#define _PPC_STATFS_H +#define BROKEN_REDHAT_7_STATFS_WORKAROUND +#endif + +#if defined(BROKEN_RHEL5_SYS_CAP_HEADER) && !defined(_LINUX_TYPES_H) +#define BROKEN_RHEL5_SYS_CAP_HEADER_WORKAROUND +#endif + +#ifdef HAVE_POSIX_CAPABILITIES +#include +#endif + +#ifdef BROKEN_RHEL5_SYS_CAP_HEADER_WORKAROUND +#undef _LINUX_TYPES_H +#undef BROKEN_RHEL5_SYS_CAP_HEADER_WORKAROUND +#endif + +#ifdef BROKEN_REDHAT_7_STATFS_WORKAROUND +#undef _PPC_STATFS_H +#undef _I386_STATFS_H +#undef BROKEN_REDHAT_7_STATFS_WORKAROUND +#endif + +#endif + +#endif diff --git a/core/proot/src/main/cpp/talloc/system/dir.h b/core/proot/src/main/cpp/talloc/system/dir.h new file mode 100644 index 000000000..497b5a7c0 --- /dev/null +++ b/core/proot/src/main/cpp/talloc/system/dir.h @@ -0,0 +1,71 @@ +#ifndef _system_dir_h +#define _system_dir_h +/* + Unix SMB/CIFS implementation. + + directory system include wrappers + + Copyright (C) Andrew Tridgell 2004 + + ** NOTE! The following LGPL license applies to the replace + ** library. This does NOT imply that all of Samba is released + ** under the LGPL + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 3 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see . +*/ + +#ifdef HAVE_DIRENT_H +# include +# define NAMLEN(dirent) strlen((dirent)->d_name) +#else +# define dirent direct +# define NAMLEN(dirent) (dirent)->d_namlen +# if HAVE_SYS_NDIR_H +# include +# endif +# if HAVE_SYS_DIR_H +# include +# endif +# if HAVE_NDIR_H +# include +# endif +#endif + +#ifndef HAVE_MKDIR_MODE +#define mkdir(dir, mode) mkdir(dir) +#endif + +#ifdef HAVE_LIBGEN_H +# include +#endif + +/* Test whether a file name is the "." or ".." directory entries. + * These really should be inline functions. + */ +#ifndef ISDOT +#define ISDOT(path) ( \ + *((const char *)(path)) == '.' && \ + *(((const char *)(path)) + 1) == '\0' \ + ) +#endif + +#ifndef ISDOTDOT +#define ISDOTDOT(path) ( \ + *((const char *)(path)) == '.' && \ + *(((const char *)(path)) + 1) == '.' && \ + *(((const char *)(path)) + 2) == '\0' \ + ) +#endif + +#endif diff --git a/core/proot/src/main/cpp/talloc/system/filesys.h b/core/proot/src/main/cpp/talloc/system/filesys.h new file mode 100644 index 000000000..9738ad529 --- /dev/null +++ b/core/proot/src/main/cpp/talloc/system/filesys.h @@ -0,0 +1,310 @@ +#ifndef _system_filesys_h +#define _system_filesys_h +/* + Unix SMB/CIFS implementation. + + filesystem system include wrappers + + Copyright (C) Andrew Tridgell 2004 + + ** NOTE! The following LGPL license applies to the replace + ** library. This does NOT imply that all of Samba is released + ** under the LGPL + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 3 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see . + +*/ + +#ifdef HAVE_UNISTD_H +#include +#endif + +#include + +#ifdef HAVE_SYS_PARAM_H +#include +#endif + +/* This include is required on UNIX (*BSD, AIX, ...) for statfs() */ +#if !defined(LINUX) && defined(HAVE_SYS_MOUNT_H) +#include +#endif + +#ifdef HAVE_MNTENT_H +#include +#endif + +/* This include is required on Linux for statfs() */ +#ifdef HAVE_SYS_VFS_H +#include +#endif + +#ifdef HAVE_SYS_ACL_H +#include +#endif + +#ifdef HAVE_ACL_LIBACL_H +#include +#endif + +#ifdef HAVE_SYS_FS_S5PARAM_H +#include +#endif + +#if defined (HAVE_SYS_FILSYS_H) && !defined (_CRAY) +#include +#endif + +#ifdef HAVE_SYS_STATFS_H +# include +#endif + +#ifdef HAVE_DUSTAT_H +#include +#endif + +#ifdef HAVE_SYS_STATVFS_H +#include +#endif + +#ifdef HAVE_SYS_FILIO_H +#include +#endif + +#ifdef HAVE_SYS_FILE_H +#include +#endif + +#ifdef HAVE_FCNTL_H +#include +#else +#ifdef HAVE_SYS_FCNTL_H +#include +#endif +#endif + +#ifdef HAVE_SYS_MODE_H +/* apparently AIX needs this for S_ISLNK */ +#ifndef S_ISLNK +#include +#endif +#endif + +#ifdef HAVE_SYS_IOCTL_H +#include +#endif + +#ifdef HAVE_SYS_UIO_H +#include +#endif + +/* mutually exclusive (SuSE 8.2) */ +#if defined(HAVE_SYS_XATTR_H) +#include +#elif defined(HAVE_ATTR_XATTR_H) +#include +#elif defined(HAVE_SYS_ATTRIBUTES_H) +#include +#elif defined(HAVE_ATTR_ATTRIBUTES_H) +#include +#endif + +#ifdef HAVE_SYS_EA_H +#include +#endif + +#ifdef HAVE_SYS_EXTATTR_H +#include +#endif + +#ifdef HAVE_SYS_RESOURCE_H +#include +#endif + +#ifndef XATTR_CREATE +#define XATTR_CREATE 0x1 /* set value, fail if attr already exists */ +#endif + +#ifndef XATTR_REPLACE +#define XATTR_REPLACE 0x2 /* set value, fail if attr does not exist */ +#endif + +/* Some POSIX definitions for those without */ + +#ifndef S_IFDIR +#define S_IFDIR 0x4000 +#endif +#ifndef S_ISDIR +#define S_ISDIR(mode) ((mode & 0xF000) == S_IFDIR) +#endif +#ifndef S_IRWXU +#define S_IRWXU 00700 /* read, write, execute: owner */ +#endif +#ifndef S_IRUSR +#define S_IRUSR 00400 /* read permission: owner */ +#endif +#ifndef S_IWUSR +#define S_IWUSR 00200 /* write permission: owner */ +#endif +#ifndef S_IXUSR +#define S_IXUSR 00100 /* execute permission: owner */ +#endif +#ifndef S_IRWXG +#define S_IRWXG 00070 /* read, write, execute: group */ +#endif +#ifndef S_IRGRP +#define S_IRGRP 00040 /* read permission: group */ +#endif +#ifndef S_IWGRP +#define S_IWGRP 00020 /* write permission: group */ +#endif +#ifndef S_IXGRP +#define S_IXGRP 00010 /* execute permission: group */ +#endif +#ifndef S_IRWXO +#define S_IRWXO 00007 /* read, write, execute: other */ +#endif +#ifndef S_IROTH +#define S_IROTH 00004 /* read permission: other */ +#endif +#ifndef S_IWOTH +#define S_IWOTH 00002 /* write permission: other */ +#endif +#ifndef S_IXOTH +#define S_IXOTH 00001 /* execute permission: other */ +#endif + +#ifndef O_ACCMODE +#define O_ACCMODE (O_RDONLY | O_WRONLY | O_RDWR) +#endif + +#ifndef MAXPATHLEN +#define MAXPATHLEN 256 +#endif + +#ifndef SEEK_SET +#define SEEK_SET 0 +#endif + +#ifdef _WIN32 +#define mkdir(d,m) _mkdir(d) +#endif + +#ifdef DISABLE_OPATH +#undef O_PATH +#endif + +/* + this allows us to use a uniform error handling for our xattr + wrappers +*/ +#ifndef ENOATTR +#define ENOATTR ENODATA +#endif + + +#if !defined(HAVE_XATTR_XATTR) || defined(XATTR_ADDITIONAL_OPTIONS) + +ssize_t rep_getxattr (const char *path, const char *name, void *value, size_t size); +#define getxattr(path, name, value, size) rep_getxattr(path, name, value, size) +/* define is in "replace.h" */ +ssize_t rep_fgetxattr (int filedes, const char *name, void *value, size_t size); +#define fgetxattr(filedes, name, value, size) rep_fgetxattr(filedes, name, value, size) +/* define is in "replace.h" */ +ssize_t rep_listxattr (const char *path, char *list, size_t size); +#define listxattr(path, list, size) rep_listxattr(path, list, size) +/* define is in "replace.h" */ +ssize_t rep_flistxattr (int filedes, char *list, size_t size); +#define flistxattr(filedes, value, size) rep_flistxattr(filedes, value, size) +/* define is in "replace.h" */ +int rep_removexattr (const char *path, const char *name); +#define removexattr(path, name) rep_removexattr(path, name) +/* define is in "replace.h" */ +int rep_fremovexattr (int filedes, const char *name); +#define fremovexattr(filedes, name) rep_fremovexattr(filedes, name) +/* define is in "replace.h" */ +int rep_setxattr (const char *path, const char *name, const void *value, size_t size, int flags); +#define setxattr(path, name, value, size, flags) rep_setxattr(path, name, value, size, flags) +/* define is in "replace.h" */ +int rep_fsetxattr (int filedes, const char *name, const void *value, size_t size, int flags); +#define fsetxattr(filedes, name, value, size, flags) rep_fsetxattr(filedes, name, value, size, flags) +/* define is in "replace.h" */ + +#endif /* !defined(HAVE_XATTR_XATTR) || defined(XATTR_ADDITIONAL_OPTIONS) */ + +#ifdef HAVE_LINUX_OPENAT2_H +#include +#else /* ! HAVE_LINUX_OPENAT2_H */ +/* how->resolve flags for openat2(2). */ +#define RESOLVE_NO_XDEV 0x01 /* Block mount-point crossings + (includes bind-mounts). */ +#define RESOLVE_NO_MAGICLINKS 0x02 /* Block traversal through procfs-style + "magic-links". */ +#define RESOLVE_NO_SYMLINKS 0x04 /* Block traversal through all symlinks + (implies OEXT_NO_MAGICLINKS) */ +#define RESOLVE_BENEATH 0x08 /* Block "lexical" trickery like + "..", symlinks, and absolute + paths which escape the dirfd. */ +#define RESOLVE_IN_ROOT 0x10 /* Make all jumps to "/" and ".." + be scoped inside the dirfd + (similar to chroot(2)). */ +#define RESOLVE_CACHED 0x20 /* Only complete if resolution can be + completed through cached lookup. May + return -EAGAIN if that's not + possible. */ +struct __rep_open_how { + uint64_t flags; + uint64_t mode; + uint64_t resolve; +}; +#define open_how __rep_open_how +#endif /* ! HAVE_LINUX_OPENAT2_H */ + +#ifndef HAVE_OPENAT2 +long rep_openat2(int dirfd, const char *pathname, + struct open_how *how, size_t size); +#define openat2(dirfd, pathname, how, size) \ + rep_openat2(dirfd, pathname, how, size) +#endif /* !HAVE_OPENAT2 */ + +#ifdef DISABLE_OPATH +/* + * Without O_PATH, the kernel + * most likely doesn't have renameat2() too + * and we should test the fallback code + */ +#undef HAVE_RENAMEAT2 +#endif + +#ifndef HAVE_RENAMEAT2 + +#ifndef RENAME_NOREPLACE +# define RENAME_NOREPLACE (1 << 0) +#endif + +#ifndef RENAME_EXCHANGE +# define RENAME_EXCHANGE (1 << 1) +#endif + +#ifndef RENAME_WHITEOUT +# define RENAME_WHITEOUT (1 << 2) +#endif + +int rep_renameat2(int __oldfd, const char *__old, int __newfd, + const char *__new, unsigned int __flags); +#define renameat2(__oldfd, __old, __newfd, __new, __flags) \ + rep_renameat2(__oldfd, __old, __newfd, __new, __flags) +#endif /* !HAVE_RENAMEAT2 */ + +#endif diff --git a/core/proot/src/main/cpp/talloc/system/glob.h b/core/proot/src/main/cpp/talloc/system/glob.h new file mode 100644 index 000000000..3e23db682 --- /dev/null +++ b/core/proot/src/main/cpp/talloc/system/glob.h @@ -0,0 +1,37 @@ +#ifndef _system_glob_h +#define _system_glob_h +/* + Unix SMB/CIFS implementation. + + glob system include wrappers + + Copyright (C) Andrew Tridgell 2004 + + ** NOTE! The following LGPL license applies to the replace + ** library. This does NOT imply that all of Samba is released + ** under the LGPL + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 3 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see . + +*/ + +#ifdef HAVE_GLOB_H +#include +#endif + +#ifdef HAVE_FNMATCH_H +#include +#endif + +#endif diff --git a/core/proot/src/main/cpp/talloc/system/gssapi.h b/core/proot/src/main/cpp/talloc/system/gssapi.h new file mode 100644 index 000000000..d8632d778 --- /dev/null +++ b/core/proot/src/main/cpp/talloc/system/gssapi.h @@ -0,0 +1,53 @@ +#ifndef _system_gssapi_h +#define _system_gssapi_h + +/* + Unix SMB/CIFS implementation. + + GSSAPI system include wrappers + + Copyright (C) Andrew Tridgell 2004 + + ** NOTE! The following LGPL license applies to the replace + ** library. This does NOT imply that all of Samba is released + ** under the LGPL + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 3 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see . + +*/ + +#ifdef HAVE_GSSAPI + +#ifdef HAVE_GSSAPI_GSSAPI_EXT_H +#include +#elif defined(HAVE_GSSAPI_GSSAPI_H) +#include +#elif defined(HAVE_GSSAPI_GSSAPI_GENERIC_H) +#include +#elif defined(HAVE_GSSAPI_H) +#include +#endif + +#ifdef HAVE_GSSAPI_GSSAPI_KRB5_H +#include +#endif + +#ifdef HAVE_GSSAPI_GSSAPI_SPNEGO_H +#include +#elif defined(HAVE_GSSAPI_SPNEGO_H) +#include +#endif + +#endif +#endif diff --git a/core/proot/src/main/cpp/talloc/system/iconv.h b/core/proot/src/main/cpp/talloc/system/iconv.h new file mode 100644 index 000000000..3c8a71f2f --- /dev/null +++ b/core/proot/src/main/cpp/talloc/system/iconv.h @@ -0,0 +1,57 @@ +#ifndef _system_iconv_h +#define _system_iconv_h +/* + Unix SMB/CIFS implementation. + + iconv memory system include wrappers + + Copyright (C) Andrew Tridgell 2004 + + ** NOTE! The following LGPL license applies to the replace + ** library. This does NOT imply that all of Samba is released + ** under the LGPL + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 3 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see . + +*/ + +#if !defined(HAVE_ICONV) && defined(HAVE_ICONV_H) +#define HAVE_ICONV +#endif + +#if !defined(HAVE_GICONV) && defined(HAVE_GICONV_H) +#define HAVE_GICONV +#endif + +#if !defined(HAVE_BICONV) && defined(HAVE_BICONV_H) +#define HAVE_BICONV +#endif + +#ifdef HAVE_NATIVE_ICONV +#if defined(HAVE_ICONV) +#include +#elif defined(HAVE_GICONV) +#include +#elif defined(HAVE_BICONV) +#include +#endif +#endif /* HAVE_NATIVE_ICONV */ + +/* needed for some systems without iconv. Doesn't really matter + what error code we use */ +#ifndef EILSEQ +#define EILSEQ EIO +#endif + +#endif diff --git a/core/proot/src/main/cpp/talloc/system/kerberos.h b/core/proot/src/main/cpp/talloc/system/kerberos.h new file mode 100644 index 000000000..f68eff501 --- /dev/null +++ b/core/proot/src/main/cpp/talloc/system/kerberos.h @@ -0,0 +1,44 @@ +#ifndef _system_kerberos_h +#define _system_kerberos_h + +/* + Unix SMB/CIFS implementation. + + kerberos system include wrappers + + Copyright (C) Andrew Tridgell 2004 + + ** NOTE! The following LGPL license applies to the replace + ** library. This does NOT imply that all of Samba is released + ** under the LGPL + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 3 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see . + +*/ + +#ifdef HAVE_KRB5 + +#ifdef HAVE_KRB5_H +#include +#endif + +#ifdef HAVE_COM_ERR_H +#include +#endif + +#define krb5_cc_default __ERROR__XX__NEVER_USE_krb5_cc_default__; +#define krb5_cc_default_name __ERROR__XX__NEVER_USE_krb5_cc_default_name__; + +#endif +#endif diff --git a/core/proot/src/main/cpp/talloc/system/locale.h b/core/proot/src/main/cpp/talloc/system/locale.h new file mode 100644 index 000000000..504a3bb47 --- /dev/null +++ b/core/proot/src/main/cpp/talloc/system/locale.h @@ -0,0 +1,42 @@ +#ifndef _system_locale_h +#define _system_locale_h + +/* + Unix SMB/CIFS implementation. + + locale include wrappers + + Copyright (C) Andrew Tridgell 2004 + + ** NOTE! The following LGPL license applies to the replace + ** library. This does NOT imply that all of Samba is released + ** under the LGPL + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 3 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see . + +*/ + +#ifdef HAVE_CTYPE_H +#include +#endif + +#ifdef HAVE_LOCALE_H +#include +#endif + +#ifdef HAVE_LANGINFO_H +#include +#endif + +#endif diff --git a/core/proot/src/main/cpp/talloc/system/network.h b/core/proot/src/main/cpp/talloc/system/network.h new file mode 100644 index 000000000..1721d65a5 --- /dev/null +++ b/core/proot/src/main/cpp/talloc/system/network.h @@ -0,0 +1,391 @@ +#ifndef _system_network_h +#define _system_network_h +/* + Unix SMB/CIFS implementation. + + networking system include wrappers + + Copyright (C) Andrew Tridgell 2004 + Copyright (C) Jelmer Vernooij 2007 + + ** NOTE! The following LGPL license applies to the replace + ** library. This does NOT imply that all of Samba is released + ** under the LGPL + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 3 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see . + +*/ + +#ifndef LIBREPLACE_NETWORK_CHECKS +#error "AC_LIBREPLACE_NETWORK_CHECKS missing in configure" +#endif + +#ifdef HAVE_UNISTD_H +#include +#endif + +#ifdef HAVE_SYS_SOCKET_H +#include +#endif + +#ifdef HAVE_UNIXSOCKET +#include +#endif + +#ifdef HAVE_NETINET_IN_H +#include +#endif +#ifdef HAVE_ARPA_INET_H +#include +#endif + +#ifdef HAVE_NETDB_H +#include +#endif + +#ifdef HAVE_NETINET_TCP_H +#include +#endif + +/* + * The next three defines are needed to access the IPTOS_* options + * on some systems. + */ + +#ifdef HAVE_NETINET_IN_SYSTM_H +#include +#endif + +#ifdef HAVE_NETINET_IN_IP_H +#include +#endif + +#ifdef HAVE_NETINET_IP_H +#include +#endif + +#ifdef HAVE_NET_IF_H +#include +#endif + +#ifdef HAVE_SYS_IOCTL_H +#include +#endif + +#ifdef HAVE_SYS_UIO_H +#include +#endif + +#ifdef HAVE_STROPTS_H +#include +#endif + +#include + +#ifndef HAVE_SOCKLEN_T +#define HAVE_SOCKLEN_T +typedef int socklen_t; +#endif + +#if !defined (HAVE_INET_NTOA) || defined(REPLACE_INET_NTOA) +/* define is in "replace.h" */ +char *rep_inet_ntoa(struct in_addr ip); +#endif + +#ifndef HAVE_INET_PTON +/* define is in "replace.h" */ +int rep_inet_pton(int af, const char *src, void *dst); +#endif + +#ifndef HAVE_INET_NTOP +/* define is in "replace.h" */ +const char *rep_inet_ntop(int af, const void *src, char *dst, socklen_t size); +#endif + +#ifndef HAVE_INET_ATON +/* define is in "replace.h" */ +int rep_inet_aton(const char *src, struct in_addr *dst); +#endif + +#ifndef HAVE_CONNECT +/* define is in "replace.h" */ +int rep_connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); +#endif + +#ifndef HAVE_GETHOSTBYNAME +/* define is in "replace.h" */ +struct hostent *rep_gethostbyname(const char *name); +#endif + +#ifdef HAVE_IFADDRS_H +#include +#endif + +#ifndef HAVE_STRUCT_IFADDRS +struct ifaddrs { + struct ifaddrs *ifa_next; /* Pointer to next struct */ + char *ifa_name; /* Interface name */ + unsigned int ifa_flags; /* Interface flags */ + struct sockaddr *ifa_addr; /* Interface address */ + struct sockaddr *ifa_netmask; /* Interface netmask */ +#undef ifa_dstaddr + struct sockaddr *ifa_dstaddr; /* P2P interface destination */ + void *ifa_data; /* Address specific data */ +}; +#endif + +#ifndef HAVE_GETIFADDRS +int rep_getifaddrs(struct ifaddrs **); +#endif + +#ifndef HAVE_FREEIFADDRS +void rep_freeifaddrs(struct ifaddrs *); +#endif + +#ifndef HAVE_SOCKETPAIR +/* define is in "replace.h" */ +int rep_socketpair(int d, int type, int protocol, int sv[2]); +#endif + +/* + * Some systems have getaddrinfo but not the + * defines needed to use it. + */ + +/* Various macros that ought to be in , but might not be */ + +#ifndef EAI_FAIL +#define EAI_BADFLAGS (-1) +#define EAI_NONAME (-2) +#define EAI_AGAIN (-3) +#define EAI_FAIL (-4) +#define EAI_FAMILY (-6) +#define EAI_SOCKTYPE (-7) +#define EAI_SERVICE (-8) +#define EAI_MEMORY (-10) +#define EAI_SYSTEM (-11) +#endif /* !EAI_FAIL */ + +#ifndef AI_PASSIVE +#define AI_PASSIVE 0x0001 +#endif + +#ifndef AI_CANONNAME +#define AI_CANONNAME 0x0002 +#endif + +#ifndef AI_NUMERICHOST +/* + * some platforms don't support AI_NUMERICHOST; define as zero if using + * the system version of getaddrinfo... + */ +#if defined(HAVE_STRUCT_ADDRINFO) && defined(HAVE_GETADDRINFO) +#define AI_NUMERICHOST 0 +#else +#define AI_NUMERICHOST 0x0004 +#endif +#endif + +/* + * Some of the functions in source3/lib/util_sock.c use AI_ADDRCONFIG. On QNX + * 6.3.0, this macro is defined but, if it's used, getaddrinfo will fail. This + * prevents smbd from opening any sockets. + * + * If I undefine AI_ADDRCONFIG on such systems and define it to be 0, + * this works around the issue. + */ +#ifdef __QNX__ +#include +#if _NTO_VERSION == 630 +#undef AI_ADDRCONFIG +#endif +#endif +#ifndef AI_ADDRCONFIG +/* + * logic copied from AI_NUMERICHOST + */ +#if defined(HAVE_STRUCT_ADDRINFO) && defined(HAVE_GETADDRINFO) +#define AI_ADDRCONFIG 0 +#else +#define AI_ADDRCONFIG 0x0020 +#endif +#endif + +#ifndef AI_NUMERICSERV +/* + * logic copied from AI_NUMERICHOST + */ +#if defined(HAVE_STRUCT_ADDRINFO) && defined(HAVE_GETADDRINFO) +#define AI_NUMERICSERV 0 +#else +#define AI_NUMERICSERV 0x0400 +#endif +#endif + +#ifndef NI_NUMERICHOST +#define NI_NUMERICHOST 1 +#endif + +#ifndef NI_NUMERICSERV +#define NI_NUMERICSERV 2 +#endif + +#ifndef NI_NOFQDN +#define NI_NOFQDN 4 +#endif + +#ifndef NI_NAMEREQD +#define NI_NAMEREQD 8 +#endif + +#ifndef NI_DGRAM +#define NI_DGRAM 16 +#endif + + +#ifndef NI_MAXHOST +#define NI_MAXHOST 1025 +#endif + +#ifndef NI_MAXSERV +#define NI_MAXSERV 32 +#endif + +/* + * glibc on linux doesn't seem to have MSG_WAITALL + * defined. I think the kernel has it though.. + */ +#ifndef MSG_WAITALL +#define MSG_WAITALL 0 +#endif + +#ifndef INADDR_LOOPBACK +#define INADDR_LOOPBACK 0x7f000001 +#endif + +#ifndef INADDR_NONE +#define INADDR_NONE 0xffffffff +#endif + +#ifndef EAFNOSUPPORT +#define EAFNOSUPPORT EINVAL +#endif + +#ifndef INET_ADDRSTRLEN +#define INET_ADDRSTRLEN 16 +#endif + +#ifndef INET6_ADDRSTRLEN +#define INET6_ADDRSTRLEN 46 +#endif + +#ifndef HOST_NAME_MAX +#define HOST_NAME_MAX 255 +#endif + +#ifndef MAXHOSTNAMELEN +#define MAXHOSTNAMELEN HOST_NAME_MAX +#endif + +#ifndef HAVE_SA_FAMILY_T +#define HAVE_SA_FAMILY_T +typedef unsigned short int sa_family_t; +#endif + +#ifndef HAVE_STRUCT_SOCKADDR_STORAGE +#define HAVE_STRUCT_SOCKADDR_STORAGE +#ifdef HAVE_STRUCT_SOCKADDR_IN6 +#define sockaddr_storage sockaddr_in6 +#define ss_family sin6_family +#define HAVE_SS_FAMILY 1 +#else /*HAVE_STRUCT_SOCKADDR_IN6*/ +#define sockaddr_storage sockaddr_in +#define ss_family sin_family +#define HAVE_SS_FAMILY 1 +#endif /*HAVE_STRUCT_SOCKADDR_IN6*/ +#endif /*HAVE_STRUCT_SOCKADDR_STORAGE*/ + +#ifndef HAVE_SS_FAMILY +#ifdef HAVE___SS_FAMILY +#define ss_family __ss_family +#define HAVE_SS_FAMILY 1 +#endif +#endif + +#ifndef IOV_MAX +# ifdef UIO_MAXIOV +# define IOV_MAX UIO_MAXIOV +# else +# ifdef __sgi + /* + * IRIX 6.5 has sysconf(_SC_IOV_MAX) + * which might return 512 or bigger + */ +# define IOV_MAX 512 +# endif +# ifdef __GNU__ + /* + * GNU/Hurd does not have such hardcoded limitations. Use a reasonable + * amount. + */ +# define IOV_MAX 512 +# endif +# endif +#endif + +#ifndef PIPE_BUF +# ifdef __GNU__ + /* + * GNU/Hurd does not have such hardcoded limitations. But it has to support + * the minimum POSIX value anyway. + */ +# define PIPE_BUF 512 +# endif +#endif + +#ifndef HAVE_STRUCT_ADDRINFO +#define HAVE_STRUCT_ADDRINFO +struct addrinfo { + int ai_flags; + int ai_family; + int ai_socktype; + int ai_protocol; + socklen_t ai_addrlen; + struct sockaddr *ai_addr; + char *ai_canonname; + struct addrinfo *ai_next; +}; +#endif /* HAVE_STRUCT_ADDRINFO */ + +#if !defined(HAVE_GETADDRINFO) +#include "getaddrinfo.h" +#endif + +/* Needed for some systems that don't define it (Solaris). */ +#ifndef ifr_netmask +#define ifr_netmask ifr_addr +#endif + +/* Some old Linux systems have broken header files */ +#ifdef HAVE_IPV6 +#ifdef HAVE_LINUX_IPV6_V6ONLY_26 +#define IPV6_V6ONLY 26 +#endif /* HAVE_LINUX_IPV6_V6ONLY_26 */ +#endif /* HAVE_IPV6 */ + +#ifndef SCOPE_DELIMITER +#define SCOPE_DELIMITER '%' +#endif + +#endif diff --git a/core/proot/src/main/cpp/talloc/system/passwd.h b/core/proot/src/main/cpp/talloc/system/passwd.h new file mode 100644 index 000000000..ecc9f603d --- /dev/null +++ b/core/proot/src/main/cpp/talloc/system/passwd.h @@ -0,0 +1,92 @@ +#ifndef _system_passwd_h +#define _system_passwd_h + +/* + Unix SMB/CIFS implementation. + + passwd system include wrappers + + Copyright (C) Andrew Tridgell 2004 + + ** NOTE! The following LGPL license applies to the replace + ** library. This does NOT imply that all of Samba is released + ** under the LGPL + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 3 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see . + +*/ + +#ifdef HAVE_UNISTD_H +#include +#endif + +#ifdef HAVE_PWD_H +#include +#endif +#ifdef HAVE_GRP_H +#include +#endif +#ifdef HAVE_SYS_PRIV_H +#include +#endif +#ifdef HAVE_SYS_ID_H +#include +#endif + +#ifdef HAVE_CRYPT_H +#include +#endif + +#ifdef HAVE_SHADOW_H +#include +#endif + +#ifdef HAVE_SYS_SECURITY_H +#include +#include +#define PASSWORD_LENGTH 16 +#endif /* HAVE_SYS_SECURITY_H */ + +#ifdef HAVE_GETPWANAM +#include +#include +#include +#endif + +#ifdef HAVE_COMPAT_H +#include +#endif + +#ifndef NGROUPS_MAX +#define NGROUPS_MAX 32 /* Guess... */ +#endif + +/* what is the longest significant password available on your system? + Knowing this speeds up password searches a lot */ +#ifndef PASSWORD_LENGTH +#define PASSWORD_LENGTH 8 +#endif + + +#ifndef ALLOW_CHANGE_PASSWORD +#if (defined(HAVE_TERMIOS_H) && defined(HAVE_DUP2) && defined(HAVE_SETSID)) +#define ALLOW_CHANGE_PASSWORD 1 +#endif +#endif + +#if defined(HAVE_CRYPT16) && defined(HAVE_GETAUTHUID) +#define ULTRIX_AUTH 1 +#endif + +#endif diff --git a/core/proot/src/main/cpp/talloc/system/python.h b/core/proot/src/main/cpp/talloc/system/python.h new file mode 100644 index 000000000..b242baecb --- /dev/null +++ b/core/proot/src/main/cpp/talloc/system/python.h @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023 Andreas Schneider + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef _SAMBA_PYTHON_H +#define _SAMBA_PYTHON_H + +/* + * With Python 3.6 Cpython started to require C99. With Python 3.12 they + * started to mix code and variable declarations so disable the warnings. + */ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeclaration-after-statement" +#include +#pragma GCC diagnostic pop + +#endif /* _SAMBA_PYTHON_H */ diff --git a/core/proot/src/main/cpp/talloc/system/readline.h b/core/proot/src/main/cpp/talloc/system/readline.h new file mode 100644 index 000000000..ac3604fc1 --- /dev/null +++ b/core/proot/src/main/cpp/talloc/system/readline.h @@ -0,0 +1,63 @@ +#ifndef _system_readline_h +#define _system_readline_h +/* + Unix SMB/CIFS implementation. + + Readline wrappers + + ** NOTE! The following LGPL license applies to the replace + ** library. This does NOT imply that all of Samba is released + ** under the LGPL + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 3 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see . + +*/ + +#ifdef HAVE_LIBREADLINE +# ifdef HAVE_READLINE_READLINE_H +# ifdef HAVE_READLINE_READLINE_WORKAROUND +# define _FUNCTION_DEF +# endif +# include +# ifdef HAVE_READLINE_HISTORY_H +# include +# endif +# else +# ifdef HAVE_READLINE_H +# include +# ifdef HAVE_HISTORY_H +# include +# endif +# else +# undef HAVE_LIBREADLINE +# endif +# endif +#endif + +#ifdef HAVE_NEW_LIBREADLINE +#if defined(HAVE_RL_COMPLETION_FUNC_T) +# define RL_COMPLETION_CAST (rl_completion_func_t *) +#elif defined(HAVE_CPPFUNCTION) +# define RL_COMPLETION_CAST (CPPFunction *) +#elif defined(HAVE_RL_COMPLETION_T) +# define RL_COMPLETION_CAST (rl_completion_t *) +#else +# define RL_COMPLETION_CAST +#endif +#else +/* This type is missing from libreadline<4.0 (approximately) */ +# define RL_COMPLETION_CAST +#endif /* HAVE_NEW_LIBREADLINE */ + +#endif diff --git a/core/proot/src/main/cpp/talloc/system/select.h b/core/proot/src/main/cpp/talloc/system/select.h new file mode 100644 index 000000000..11c5390d9 --- /dev/null +++ b/core/proot/src/main/cpp/talloc/system/select.h @@ -0,0 +1,77 @@ +#ifndef _system_select_h +#define _system_select_h +/* + Unix SMB/CIFS implementation. + + select system include wrappers + + Copyright (C) Andrew Tridgell 2004 + + ** NOTE! The following LGPL license applies to the replace + ** library. This does NOT imply that all of Samba is released + ** under the LGPL + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 3 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see . + +*/ + +#ifdef HAVE_SYS_SELECT_H +#include +#endif + +#ifdef HAVE_SYS_EPOLL_H +#include +#endif + +#ifndef SELECT_CAST +#define SELECT_CAST +#endif + +#ifdef HAVE_POLL + +#include + +#else + +/* Type used for the number of file descriptors. */ +typedef unsigned long int nfds_t; + +/* Data structure describing a polling request. */ +struct pollfd +{ + int fd; /* File descriptor to poll. */ + short int events; /* Types of events poller cares about. */ + short int revents; /* Types of events that actually occurred. */ +}; + +/* Event types that can be polled for. These bits may be set in `events' + to indicate the interesting event types; they will appear in `revents' + to indicate the status of the file descriptor. */ +#define POLLIN 0x001 /* There is data to read. */ +#define POLLPRI 0x002 /* There is urgent data to read. */ +#define POLLOUT 0x004 /* Writing now will not block. */ +#define POLLRDNORM 0x040 /* Normal data may be read. */ +#define POLLRDBAND 0x080 /* Priority data may be read. */ +#define POLLWRNORM 0x100 /* Writing now will not block. */ +#define POLLWRBAND 0x200 /* Priority data may be written. */ +#define POLLERR 0x008 /* Error condition. */ +#define POLLHUP 0x010 /* Hung up. */ +#define POLLNVAL 0x020 /* Invalid polling request. */ + +/* define is in "replace.h" */ +int rep_poll(struct pollfd *fds, nfds_t nfds, int timeout); + +#endif + +#endif diff --git a/core/proot/src/main/cpp/talloc/system/shmem.h b/core/proot/src/main/cpp/talloc/system/shmem.h new file mode 100644 index 000000000..64fe39b6c --- /dev/null +++ b/core/proot/src/main/cpp/talloc/system/shmem.h @@ -0,0 +1,59 @@ +#ifndef _system_shmem_h +#define _system_shmem_h +/* + Unix SMB/CIFS implementation. + + shared memory system include wrappers + + Copyright (C) Andrew Tridgell 2004 + + ** NOTE! The following LGPL license applies to the replace + ** library. This does NOT imply that all of Samba is released + ** under the LGPL + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 3 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see . + +*/ + +#if defined(HAVE_SYS_IPC_H) +#include +#endif /* HAVE_SYS_IPC_H */ + +#if defined(HAVE_SYS_SHM_H) +#include +#endif /* HAVE_SYS_SHM_H */ + +#ifdef HAVE_SYS_MMAN_H +#include +#endif + +/* NetBSD doesn't have these */ +#ifndef SHM_R +#define SHM_R 0400 +#endif + +#ifndef SHM_W +#define SHM_W 0200 +#endif + + +#ifndef MAP_FILE +#define MAP_FILE 0 +#endif + +#ifndef MAP_FAILED +#define MAP_FAILED ((void *)-1) +#endif + +#endif diff --git a/core/proot/src/main/cpp/talloc/system/syslog.h b/core/proot/src/main/cpp/talloc/system/syslog.h new file mode 100644 index 000000000..104be1df8 --- /dev/null +++ b/core/proot/src/main/cpp/talloc/system/syslog.h @@ -0,0 +1,70 @@ +#ifndef _system_syslog_h +#define _system_syslog_h +/* + Unix SMB/CIFS implementation. + + syslog system include wrappers + + Copyright (C) Andrew Tridgell 2004 + + ** NOTE! The following LGPL license applies to the replace + ** library. This does NOT imply that all of Samba is released + ** under the LGPL + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 3 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see . + +*/ + +#ifdef HAVE_SYSLOG_H +#include +#else +#ifdef HAVE_SYS_SYSLOG_H +#include +#endif +#endif + +/* For sys_adminlog(). */ +#ifndef LOG_EMERG +#define LOG_EMERG 0 /* system is unusable */ +#endif + +#ifndef LOG_ALERT +#define LOG_ALERT 1 /* action must be taken immediately */ +#endif + +#ifndef LOG_CRIT +#define LOG_CRIT 2 /* critical conditions */ +#endif + +#ifndef LOG_ERR +#define LOG_ERR 3 /* error conditions */ +#endif + +#ifndef LOG_WARNING +#define LOG_WARNING 4 /* warning conditions */ +#endif + +#ifndef LOG_NOTICE +#define LOG_NOTICE 5 /* normal but significant condition */ +#endif + +#ifndef LOG_INFO +#define LOG_INFO 6 /* informational */ +#endif + +#ifndef LOG_DEBUG +#define LOG_DEBUG 7 /* debug-level messages */ +#endif + +#endif diff --git a/core/proot/src/main/cpp/talloc/system/terminal.h b/core/proot/src/main/cpp/talloc/system/terminal.h new file mode 100644 index 000000000..9ad601ace --- /dev/null +++ b/core/proot/src/main/cpp/talloc/system/terminal.h @@ -0,0 +1,46 @@ +#ifndef _system_terminal_h +#define _system_terminal_h +/* + Unix SMB/CIFS implementation. + + terminal system include wrappers + + Copyright (C) Andrew Tridgell 2004 + + ** NOTE! The following LGPL license applies to the replace + ** library. This does NOT imply that all of Samba is released + ** under the LGPL + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 3 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see . + +*/ + +#ifdef SUNOS4 +/* on SUNOS4 termios.h conflicts with sys/ioctl.h */ +#undef HAVE_TERMIOS_H +#endif + + +#if defined(HAVE_TERMIOS_H) +/* POSIX terminal handling. */ +#include +#elif defined(HAVE_TERMIO_H) +/* Older SYSV terminal handling - don't use if we can avoid it. */ +#include +#elif defined(HAVE_SYS_TERMIO_H) +/* Older SYSV terminal handling - don't use if we can avoid it. */ +#include +#endif + +#endif diff --git a/core/proot/src/main/cpp/talloc/system/threads.h b/core/proot/src/main/cpp/talloc/system/threads.h new file mode 100644 index 000000000..d189ed620 --- /dev/null +++ b/core/proot/src/main/cpp/talloc/system/threads.h @@ -0,0 +1,72 @@ +#ifndef _system_threads_h +#define _system_threads_h +/* + Unix SMB/CIFS implementation. + + macros to go along with the lib/replace/ portability layer code + + Copyright (C) Volker Lendecke 2012 + + ** NOTE! The following LGPL license applies to the replace + ** library. This does NOT imply that all of Samba is released + ** under the LGPL + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 3 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see . +*/ + +#include + +#if defined(HAVE_PTHREAD_MUTEXATTR_SETROBUST_NP) && \ + !defined(HAVE_PTHREAD_MUTEXATTR_SETROBUST) +#define pthread_mutexattr_setrobust pthread_mutexattr_setrobust_np +#endif + +#if defined(HAVE_DECL_PTHREAD_MUTEX_ROBUST_NP) && \ + !defined(HAVE_DECL_PTHREAD_MUTEX_ROBUST) +#define PTHREAD_MUTEX_ROBUST PTHREAD_MUTEX_ROBUST_NP +#endif + +#if defined(HAVE_PTHREAD_MUTEX_CONSISTENT_NP) && \ + !defined(HAVE_PTHREAD_MUTEX_CONSISTENT) +#define pthread_mutex_consistent pthread_mutex_consistent_np +#endif + +#ifdef HAVE_STDATOMIC_H +#include +#endif + +#ifndef HAVE_ATOMIC_THREAD_FENCE +#ifdef HAVE___ATOMIC_THREAD_FENCE +#define atomic_thread_fence(__ignore_order) __atomic_thread_fence(__ATOMIC_SEQ_CST) +#define HAVE_ATOMIC_THREAD_FENCE 1 +#endif /* HAVE___ATOMIC_THREAD_FENCE */ +#endif /* not HAVE_ATOMIC_THREAD_FENCE */ + +#ifndef HAVE_ATOMIC_THREAD_FENCE +#ifdef HAVE___SYNC_SYNCHRONIZE +#define atomic_thread_fence(__ignore_order) __sync_synchronize() +#define HAVE_ATOMIC_THREAD_FENCE 1 +#endif /* HAVE___SYNC_SYNCHRONIZE */ +#endif /* not HAVE_ATOMIC_THREAD_FENCE */ + +#ifndef HAVE_ATOMIC_THREAD_FENCE +#ifdef HAVE_ATOMIC_THREAD_FENCE_SUPPORT +#error mismatch_error_between_configure_test_and_header +#endif +/* make sure the build fails if someone uses it without checking the define */ +#define atomic_thread_fence(__order) \ + __function__atomic_thread_fence_not_available_on_this_platform__() +#endif /* not HAVE_ATOMIC_THREAD_FENCE */ + +#endif diff --git a/core/proot/src/main/cpp/talloc/system/time.h b/core/proot/src/main/cpp/talloc/system/time.h new file mode 100644 index 000000000..272fe84fc --- /dev/null +++ b/core/proot/src/main/cpp/talloc/system/time.h @@ -0,0 +1,106 @@ +#ifndef _system_time_h +#define _system_time_h +/* + Unix SMB/CIFS implementation. + + time system include wrappers + + Copyright (C) Andrew Tridgell 2004 + + ** NOTE! The following LGPL license applies to the replace + ** library. This does NOT imply that all of Samba is released + ** under the LGPL + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 3 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see . + +*/ + +#ifdef TIME_WITH_SYS_TIME +#include +#include +#else +#ifdef HAVE_SYS_TIME_H +#include +#else +#include +#endif +#endif + +#ifdef HAVE_UTIME_H +#include +#else +struct utimbuf { + time_t actime; /* access time */ + time_t modtime; /* modification time */ +}; +#endif + +#ifndef HAVE_STRUCT_TIMESPEC +struct timespec { + time_t tv_sec; /* Seconds. */ + long tv_nsec; /* Nanoseconds. */ +}; +#endif + +#ifndef HAVE_MKTIME +/* define is in "replace.h" */ +time_t rep_mktime(struct tm *t); +#endif + +#ifndef HAVE_TIMEGM +/* define is in "replace.h" */ +time_t rep_timegm(struct tm *tm); +#endif + +#ifndef HAVE_UTIME +/* define is in "replace.h" */ +int rep_utime(const char *filename, const struct utimbuf *buf); +#endif + +#ifndef HAVE_UTIMES +/* define is in "replace.h" */ +int rep_utimes(const char *filename, const struct timeval tv[2]); +#endif + +#ifndef HAVE_CLOCK_GETTIME +/* CLOCK_REALTIME is required by POSIX */ +#define CLOCK_REALTIME 0 +typedef int clockid_t; +int rep_clock_gettime(clockid_t clk_id, struct timespec *tp); +#endif +/* make sure we have a best effort CUSTOM_CLOCK_MONOTONIC we can rely on. + * + * on AIX the values of CLOCK_* are cast expressions, not integer constants, + * this prevents them from being compared against in a preprocessor directive. + * The following ...IS_* macros can be used to check which clock is in use. + */ +#if defined(CLOCK_MONOTONIC) +#define CUSTOM_CLOCK_MONOTONIC CLOCK_MONOTONIC +#define CUSTOM_CLOCK_MONOTONIC_IS_MONOTONIC +#elif defined(CLOCK_HIGHRES) +#define CUSTOM_CLOCK_MONOTONIC CLOCK_HIGHRES +#define CUSTOM_CLOCK_MONOTONIC_IS_HIGHRES +#else +#define CUSTOM_CLOCK_MONOTONIC CLOCK_REALTIME +#define CUSTOM_CLOCK_MONOTONIC_IS_REALTIME +#endif + +#ifndef UTIME_NOW +#define UTIME_NOW ((1l << 30) - 1l) +#endif +#ifndef UTIME_OMIT +#define UTIME_OMIT ((1l << 30) - 2l) +#endif + +#endif diff --git a/core/proot/src/main/cpp/talloc/system/wait.h b/core/proot/src/main/cpp/talloc/system/wait.h new file mode 100644 index 000000000..1f5fcd99b --- /dev/null +++ b/core/proot/src/main/cpp/talloc/system/wait.h @@ -0,0 +1,55 @@ +#ifndef _system_wait_h +#define _system_wait_h +/* + Unix SMB/CIFS implementation. + + waitpid system include wrappers + + Copyright (C) Andrew Tridgell 2004 + + ** NOTE! The following LGPL license applies to the replace + ** library. This does NOT imply that all of Samba is released + ** under the LGPL + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 3 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see . + +*/ + +#ifdef HAVE_SYS_WAIT_H +#include +#endif + +#include + +#ifndef SIGCLD +#define SIGCLD SIGCHLD +#endif + +#ifdef HAVE_SETJMP_H +#include +#endif + +#ifdef HAVE_SYS_UCONTEXT_H +#include +#endif + +#if !defined(HAVE_SIG_ATOMIC_T_TYPE) +typedef int sig_atomic_t; +#endif + +#if !defined(HAVE_WAITPID) && defined(HAVE_WAIT4) +int rep_waitpid(pid_t pid,int *status,int options); +#endif + +#endif diff --git a/core/proot/src/main/cpp/talloc/system/wscript_configure b/core/proot/src/main/cpp/talloc/system/wscript_configure new file mode 100644 index 000000000..b51d9179c --- /dev/null +++ b/core/proot/src/main/cpp/talloc/system/wscript_configure @@ -0,0 +1,18 @@ +#!/usr/bin/env python + +# solaris variants of getXXent_r +conf.CHECK_C_PROTOTYPE('getpwent_r', + 'struct passwd *getpwent_r(struct passwd *src, char *buf, int buflen)', + define='SOLARIS_GETPWENT_R', headers='pwd.h') +conf.CHECK_C_PROTOTYPE('getgrent_r', + 'struct group *getgrent_r(struct group *src, char *buf, int buflen)', + define='SOLARIS_GETGRENT_R', headers='grp.h') + +# the irix variants +conf.CHECK_C_PROTOTYPE('getpwent_r', + 'struct passwd *getpwent_r(struct passwd *src, char *buf, size_t buflen)', + define='SOLARIS_GETPWENT_R', headers='pwd.h') +conf.CHECK_C_PROTOTYPE('getgrent_r', + 'struct group *getgrent_r(struct group *src, char *buf, size_t buflen)', + define='SOLARIS_GETGRENT_R', headers='grp.h') + diff --git a/core/proot/src/main/cpp/talloc/talloc.c b/core/proot/src/main/cpp/talloc/talloc.c new file mode 100644 index 000000000..727abe77a --- /dev/null +++ b/core/proot/src/main/cpp/talloc/talloc.c @@ -0,0 +1,3074 @@ +/* + Samba Unix SMB/CIFS implementation. + + Samba trivial allocation library - new interface + + NOTE: Please read talloc_guide.txt for full documentation + + Copyright (C) Andrew Tridgell 2004 + Copyright (C) Stefan Metzmacher 2006 + + ** NOTE! The following LGPL license applies to the talloc + ** library. This does NOT imply that all of Samba is released + ** under the LGPL + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 3 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see . +*/ + +/* + inspired by http://swapped.cc/halloc/ +*/ + +#include "replace.h" +#include "talloc.h" + +#ifdef HAVE_SYS_AUXV_H +#include +#endif + +#if (TALLOC_VERSION_MAJOR != TALLOC_BUILD_VERSION_MAJOR) +#error "TALLOC_VERSION_MAJOR != TALLOC_BUILD_VERSION_MAJOR" +#endif + +#if (TALLOC_VERSION_MINOR != TALLOC_BUILD_VERSION_MINOR) +#error "TALLOC_VERSION_MINOR != TALLOC_BUILD_VERSION_MINOR" +#endif + +/* Special macros that are no-ops except when run under Valgrind on + * x86. They've moved a little bit from valgrind 1.0.4 to 1.9.4 */ +#ifdef HAVE_VALGRIND_MEMCHECK_H + /* memcheck.h includes valgrind.h */ +#include +#elif defined(HAVE_VALGRIND_H) +#include +#endif + +#define MAX_TALLOC_SIZE 0x10000000 + +#define TALLOC_FLAG_FREE 0x01 +#define TALLOC_FLAG_LOOP 0x02 +#define TALLOC_FLAG_POOL 0x04 /* This is a talloc pool */ +#define TALLOC_FLAG_POOLMEM 0x08 /* This is allocated in a pool */ + +/* + * Bits above this are random, used to make it harder to fake talloc + * headers during an attack. Try not to change this without good reason. + */ +#define TALLOC_FLAG_MASK 0x0F + +#define TALLOC_MAGIC_REFERENCE ((const char *)1) + +#define TALLOC_MAGIC_BASE 0xe814ec70 +#define TALLOC_MAGIC_NON_RANDOM ( \ + ~TALLOC_FLAG_MASK & ( \ + TALLOC_MAGIC_BASE + \ + (TALLOC_BUILD_VERSION_MAJOR << 24) + \ + (TALLOC_BUILD_VERSION_MINOR << 16) + \ + (TALLOC_BUILD_VERSION_RELEASE << 8))) +static unsigned int talloc_magic = TALLOC_MAGIC_NON_RANDOM; + +/* by default we abort when given a bad pointer (such as when talloc_free() is called + on a pointer that came from malloc() */ +#ifndef TALLOC_ABORT +#define TALLOC_ABORT(reason) abort() +#endif + +#ifndef discard_const_p +#if defined(__intptr_t_defined) || defined(HAVE_INTPTR_T) +# define discard_const_p(type, ptr) ((type *)((intptr_t)(ptr))) +#else +# define discard_const_p(type, ptr) ((type *)(ptr)) +#endif +#endif + +/* these macros gain us a few percent of speed on gcc */ +#if (__GNUC__ >= 3) +/* the strange !! is to ensure that __builtin_expect() takes either 0 or 1 + as its first argument */ +#ifndef likely +#define likely(x) __builtin_expect(!!(x), 1) +#endif +#ifndef unlikely +#define unlikely(x) __builtin_expect(!!(x), 0) +#endif +#else +#ifndef likely +#define likely(x) (x) +#endif +#ifndef unlikely +#define unlikely(x) (x) +#endif +#endif + +/* this null_context is only used if talloc_enable_leak_report() or + talloc_enable_leak_report_full() is called, otherwise it remains + NULL +*/ +static void *null_context; +static bool talloc_report_null; +static bool talloc_report_null_full; +static void *autofree_context; + +static void talloc_setup_atexit(void); + +/* used to enable fill of memory on free, which can be useful for + * catching use after free errors when valgrind is too slow + */ +static struct { + bool initialised; + bool enabled; + uint8_t fill_value; +} talloc_fill; + +#define TALLOC_FILL_ENV "TALLOC_FREE_FILL" + +/* + * do not wipe the header, to allow the + * double-free logic to still work + */ +#define TC_INVALIDATE_FULL_FILL_CHUNK(_tc) do { \ + if (unlikely(talloc_fill.enabled)) { \ + size_t _flen = (_tc)->size; \ + char *_fptr = (char *)TC_PTR_FROM_CHUNK(_tc); \ + memset(_fptr, talloc_fill.fill_value, _flen); \ + } \ +} while (0) + +#if defined(DEVELOPER) && defined(VALGRIND_MAKE_MEM_NOACCESS) +/* Mark the whole chunk as not accessible */ +#define TC_INVALIDATE_FULL_VALGRIND_CHUNK(_tc) do { \ + size_t _flen = TC_HDR_SIZE + (_tc)->size; \ + char *_fptr = (char *)(_tc); \ + VALGRIND_MAKE_MEM_NOACCESS(_fptr, _flen); \ +} while(0) +#else +#define TC_INVALIDATE_FULL_VALGRIND_CHUNK(_tc) do { } while (0) +#endif + +#define TC_INVALIDATE_FULL_CHUNK(_tc) do { \ + TC_INVALIDATE_FULL_FILL_CHUNK(_tc); \ + TC_INVALIDATE_FULL_VALGRIND_CHUNK(_tc); \ +} while (0) + +#define TC_INVALIDATE_SHRINK_FILL_CHUNK(_tc, _new_size) do { \ + if (unlikely(talloc_fill.enabled)) { \ + size_t _flen = (_tc)->size - (_new_size); \ + char *_fptr = (char *)TC_PTR_FROM_CHUNK(_tc); \ + _fptr += (_new_size); \ + memset(_fptr, talloc_fill.fill_value, _flen); \ + } \ +} while (0) + +#if defined(DEVELOPER) && defined(VALGRIND_MAKE_MEM_NOACCESS) +/* Mark the unused bytes not accessible */ +#define TC_INVALIDATE_SHRINK_VALGRIND_CHUNK(_tc, _new_size) do { \ + size_t _flen = (_tc)->size - (_new_size); \ + char *_fptr = (char *)TC_PTR_FROM_CHUNK(_tc); \ + _fptr += (_new_size); \ + VALGRIND_MAKE_MEM_NOACCESS(_fptr, _flen); \ +} while (0) +#else +#define TC_INVALIDATE_SHRINK_VALGRIND_CHUNK(_tc, _new_size) do { } while (0) +#endif + +#define TC_INVALIDATE_SHRINK_CHUNK(_tc, _new_size) do { \ + TC_INVALIDATE_SHRINK_FILL_CHUNK(_tc, _new_size); \ + TC_INVALIDATE_SHRINK_VALGRIND_CHUNK(_tc, _new_size); \ +} while (0) + +#define TC_UNDEFINE_SHRINK_FILL_CHUNK(_tc, _new_size) do { \ + if (unlikely(talloc_fill.enabled)) { \ + size_t _flen = (_tc)->size - (_new_size); \ + char *_fptr = (char *)TC_PTR_FROM_CHUNK(_tc); \ + _fptr += (_new_size); \ + memset(_fptr, talloc_fill.fill_value, _flen); \ + } \ +} while (0) + +#if defined(DEVELOPER) && defined(VALGRIND_MAKE_MEM_UNDEFINED) +/* Mark the unused bytes as undefined */ +#define TC_UNDEFINE_SHRINK_VALGRIND_CHUNK(_tc, _new_size) do { \ + size_t _flen = (_tc)->size - (_new_size); \ + char *_fptr = (char *)TC_PTR_FROM_CHUNK(_tc); \ + _fptr += (_new_size); \ + VALGRIND_MAKE_MEM_UNDEFINED(_fptr, _flen); \ +} while (0) +#else +#define TC_UNDEFINE_SHRINK_VALGRIND_CHUNK(_tc, _new_size) do { } while (0) +#endif + +#define TC_UNDEFINE_SHRINK_CHUNK(_tc, _new_size) do { \ + TC_UNDEFINE_SHRINK_FILL_CHUNK(_tc, _new_size); \ + TC_UNDEFINE_SHRINK_VALGRIND_CHUNK(_tc, _new_size); \ +} while (0) + +#if defined(DEVELOPER) && defined(VALGRIND_MAKE_MEM_UNDEFINED) +/* Mark the new bytes as undefined */ +#define TC_UNDEFINE_GROW_VALGRIND_CHUNK(_tc, _new_size) do { \ + size_t _old_used = TC_HDR_SIZE + (_tc)->size; \ + size_t _new_used = TC_HDR_SIZE + (_new_size); \ + size_t _flen = _new_used - _old_used; \ + char *_fptr = _old_used + (char *)(_tc); \ + VALGRIND_MAKE_MEM_UNDEFINED(_fptr, _flen); \ +} while (0) +#else +#define TC_UNDEFINE_GROW_VALGRIND_CHUNK(_tc, _new_size) do { } while (0) +#endif + +#define TC_UNDEFINE_GROW_CHUNK(_tc, _new_size) do { \ + TC_UNDEFINE_GROW_VALGRIND_CHUNK(_tc, _new_size); \ +} while (0) + +struct talloc_reference_handle { + struct talloc_reference_handle *next, *prev; + void *ptr; + const char *location; +}; + +struct talloc_memlimit { + struct talloc_chunk *parent; + struct talloc_memlimit *upper; + size_t max_size; + size_t cur_size; +}; + +static inline bool talloc_memlimit_check(struct talloc_memlimit *limit, size_t size); +static inline void talloc_memlimit_grow(struct talloc_memlimit *limit, + size_t size); +static inline void talloc_memlimit_shrink(struct talloc_memlimit *limit, + size_t size); +static inline void tc_memlimit_update_on_free(struct talloc_chunk *tc); + +static inline void _tc_set_name_const(struct talloc_chunk *tc, + const char *name); +static struct talloc_chunk *_vasprintf_tc(const void *t, + const char *fmt, + va_list ap); + +typedef int (*talloc_destructor_t)(void *); + +struct talloc_pool_hdr; + +struct talloc_chunk { + /* + * flags includes the talloc magic, which is randomised to + * make overwrite attacks harder + */ + unsigned flags; + + /* + * If you have a logical tree like: + * + * + * / | \ + * / | \ + * / | \ + * + * + * The actual talloc tree is: + * + * + * | + * - - + * + * The children are linked with next/prev pointers, and + * child 1 is linked to the parent with parent/child + * pointers. + */ + + struct talloc_chunk *next, *prev; + struct talloc_chunk *parent, *child; + struct talloc_reference_handle *refs; + talloc_destructor_t destructor; + const char *name; + size_t size; + + /* + * limit semantics: + * if 'limit' is set it means all *new* children of the context will + * be limited to a total aggregate size ox max_size for memory + * allocations. + * cur_size is used to keep track of the current use + */ + struct talloc_memlimit *limit; + + /* + * For members of a pool (i.e. TALLOC_FLAG_POOLMEM is set), "pool" + * is a pointer to the struct talloc_chunk of the pool that it was + * allocated from. This way children can quickly find the pool to chew + * from. + */ + struct talloc_pool_hdr *pool; +}; + +union talloc_chunk_cast_u { + uint8_t *ptr; + struct talloc_chunk *chunk; +}; + +/* 16 byte alignment seems to keep everyone happy */ +#define TC_ALIGN16(s) (((s)+15)&~15) +#define TC_HDR_SIZE TC_ALIGN16(sizeof(struct talloc_chunk)) +#define TC_PTR_FROM_CHUNK(tc) ((void *)(TC_HDR_SIZE + (char*)tc)) + +_PUBLIC_ int talloc_version_major(void) +{ + return TALLOC_VERSION_MAJOR; +} + +_PUBLIC_ int talloc_version_minor(void) +{ + return TALLOC_VERSION_MINOR; +} + +_PUBLIC_ int talloc_test_get_magic(void) +{ + return talloc_magic; +} + +static inline void _talloc_chunk_set_free(struct talloc_chunk *tc, + const char *location) +{ + /* + * Mark this memory as free, and also over-stamp the talloc + * magic with the old-style magic. + * + * Why? This tries to avoid a memory read use-after-free from + * disclosing our talloc magic, which would then allow an + * attacker to prepare a valid header and so run a destructor. + * + */ + tc->flags = TALLOC_MAGIC_NON_RANDOM | TALLOC_FLAG_FREE + | (tc->flags & TALLOC_FLAG_MASK); + + /* we mark the freed memory with where we called the free + * from. This means on a double free error we can report where + * the first free came from + */ + if (location) { + tc->name = location; + } +} + +static inline void _talloc_chunk_set_not_free(struct talloc_chunk *tc) +{ + /* + * Mark this memory as not free. + * + * Why? This is memory either in a pool (and so available for + * talloc's re-use or after the realloc(). We need to mark + * the memory as free() before any realloc() call as we can't + * write to the memory after that. + * + * We put back the normal magic instead of the 'not random' + * magic. + */ + + tc->flags = talloc_magic | + ((tc->flags & TALLOC_FLAG_MASK) & ~TALLOC_FLAG_FREE); +} + +static void (*talloc_log_fn)(const char *message); + +_PUBLIC_ void talloc_set_log_fn(void (*log_fn)(const char *message)) +{ + talloc_log_fn = log_fn; +} + +#ifdef HAVE_CONSTRUCTOR_ATTRIBUTE +#define CONSTRUCTOR __attribute__((constructor)) +#elif defined(HAVE_PRAGMA_INIT) +#define CONSTRUCTOR +#pragma init (talloc_lib_init) +#endif +#if defined(HAVE_CONSTRUCTOR_ATTRIBUTE) || defined(HAVE_PRAGMA_INIT) +void talloc_lib_init(void) CONSTRUCTOR; +void talloc_lib_init(void) +{ + uint32_t random_value; +#if defined(HAVE_GETAUXVAL) && defined(AT_RANDOM) + uint8_t *p; + /* + * Use the kernel-provided random values used for + * ASLR. This won't change per-exec, which is ideal for us + */ + p = (uint8_t *) getauxval(AT_RANDOM); + if (p) { + /* + * We get 16 bytes from getauxval. By calling rand(), + * a totally insecure PRNG, but one that will + * deterministically have a different value when called + * twice, we ensure that if two talloc-like libraries + * are somehow loaded in the same address space, that + * because we choose different bytes, we will keep the + * protection against collision of multiple talloc + * libs. + * + * This protection is important because the effects of + * passing a talloc pointer from one to the other may + * be very hard to determine. + */ + int offset = rand() % (16 - sizeof(random_value)); + memcpy(&random_value, p + offset, sizeof(random_value)); + } else +#endif + { + /* + * Otherwise, hope the location we are loaded in + * memory is randomised by someone else + */ + random_value = ((uintptr_t)talloc_lib_init & 0xFFFFFFFF); + } + talloc_magic = random_value & ~TALLOC_FLAG_MASK; +} +#else +#warning "No __attribute__((constructor)) support found on this platform, additional talloc security measures not available" +#endif + +static void talloc_lib_atexit(void) +{ + TALLOC_FREE(autofree_context); + + if (talloc_total_size(null_context) == 0) { + return; + } + + if (talloc_report_null_full) { + talloc_report_full(null_context, stderr); + } else if (talloc_report_null) { + talloc_report(null_context, stderr); + } +} + +static void talloc_setup_atexit(void) +{ + static bool done; + + if (done) { + return; + } + + atexit(talloc_lib_atexit); + done = true; +} + +static void talloc_log(const char *fmt, ...) PRINTF_ATTRIBUTE(1,2); +static void talloc_log(const char *fmt, ...) +{ + va_list ap; + char *message; + + if (!talloc_log_fn) { + return; + } + + va_start(ap, fmt); + message = talloc_vasprintf(NULL, fmt, ap); + va_end(ap); + + talloc_log_fn(message); + talloc_free(message); +} + +static void talloc_log_stderr(const char *message) +{ + fprintf(stderr, "%s", message); +} + +_PUBLIC_ void talloc_set_log_stderr(void) +{ + talloc_set_log_fn(talloc_log_stderr); +} + +static void (*talloc_abort_fn)(const char *reason); + +_PUBLIC_ void talloc_set_abort_fn(void (*abort_fn)(const char *reason)) +{ + talloc_abort_fn = abort_fn; +} + +static void talloc_abort(const char *reason) +{ + talloc_log("%s\n", reason); + + if (!talloc_abort_fn) { + TALLOC_ABORT(reason); + } + + talloc_abort_fn(reason); +} + +static void talloc_abort_access_after_free(void) +{ + talloc_abort("Bad talloc magic value - access after free"); +} + +static void talloc_abort_unknown_value(void) +{ + talloc_abort("Bad talloc magic value - unknown value"); +} + +/* panic if we get a bad magic value */ +static inline struct talloc_chunk *talloc_chunk_from_ptr(const void *ptr) +{ + const char *pp = (const char *)ptr; + struct talloc_chunk *tc = discard_const_p(struct talloc_chunk, pp - TC_HDR_SIZE); + if (unlikely((tc->flags & (TALLOC_FLAG_FREE | ~TALLOC_FLAG_MASK)) != talloc_magic)) { + if ((tc->flags & (TALLOC_FLAG_FREE | ~TALLOC_FLAG_MASK)) + == (TALLOC_MAGIC_NON_RANDOM | TALLOC_FLAG_FREE)) { + talloc_log("talloc: access after free error - first free may be at %s\n", tc->name); + talloc_abort_access_after_free(); + return NULL; + } + + talloc_abort_unknown_value(); + return NULL; + } + return tc; +} + +/* hook into the front of the list */ +#define _TLIST_ADD(list, p) \ +do { \ + if (!(list)) { \ + (list) = (p); \ + (p)->next = (p)->prev = NULL; \ + } else { \ + (list)->prev = (p); \ + (p)->next = (list); \ + (p)->prev = NULL; \ + (list) = (p); \ + }\ +} while (0) + +/* remove an element from a list - element doesn't have to be in list. */ +#define _TLIST_REMOVE(list, p) \ +do { \ + if ((p) == (list)) { \ + (list) = (p)->next; \ + if (list) (list)->prev = NULL; \ + } else { \ + if ((p)->prev) (p)->prev->next = (p)->next; \ + if ((p)->next) (p)->next->prev = (p)->prev; \ + } \ + if ((p) && ((p) != (list))) (p)->next = (p)->prev = NULL; \ +} while (0) + + +/* + return the parent chunk of a pointer +*/ +static inline struct talloc_chunk *talloc_parent_chunk(const void *ptr) +{ + struct talloc_chunk *tc; + + if (unlikely(ptr == NULL)) { + return NULL; + } + + tc = talloc_chunk_from_ptr(ptr); + while (tc->prev) tc=tc->prev; + + return tc->parent; +} + +_PUBLIC_ void *talloc_parent(const void *ptr) +{ + struct talloc_chunk *tc = talloc_parent_chunk(ptr); + return tc? TC_PTR_FROM_CHUNK(tc) : NULL; +} + +/* + find parents name +*/ +_PUBLIC_ const char *talloc_parent_name(const void *ptr) +{ + struct talloc_chunk *tc = talloc_parent_chunk(ptr); + return tc? tc->name : NULL; +} + +/* + A pool carries an in-pool object count count in the first 16 bytes. + bytes. This is done to support talloc_steal() to a parent outside of the + pool. The count includes the pool itself, so a talloc_free() on a pool will + only destroy the pool if the count has dropped to zero. A talloc_free() of a + pool member will reduce the count, and eventually also call free(3) on the + pool memory. + + The object count is not put into "struct talloc_chunk" because it is only + relevant for talloc pools and the alignment to 16 bytes would increase the + memory footprint of each talloc chunk by those 16 bytes. +*/ + +struct talloc_pool_hdr { + void *end; + unsigned int object_count; + size_t poolsize; +}; + +union talloc_pool_hdr_cast_u { + uint8_t *ptr; + struct talloc_pool_hdr *hdr; +}; + +#define TP_HDR_SIZE TC_ALIGN16(sizeof(struct talloc_pool_hdr)) + +static inline struct talloc_pool_hdr *talloc_pool_from_chunk(struct talloc_chunk *c) +{ + union talloc_chunk_cast_u tcc = { .chunk = c }; + union talloc_pool_hdr_cast_u tphc = { tcc.ptr - TP_HDR_SIZE }; + return tphc.hdr; +} + +static inline struct talloc_chunk *talloc_chunk_from_pool(struct talloc_pool_hdr *h) +{ + union talloc_pool_hdr_cast_u tphc = { .hdr = h }; + union talloc_chunk_cast_u tcc = { .ptr = tphc.ptr + TP_HDR_SIZE }; + return tcc.chunk; +} + +static inline void *tc_pool_end(struct talloc_pool_hdr *pool_hdr) +{ + struct talloc_chunk *tc = talloc_chunk_from_pool(pool_hdr); + return (char *)tc + TC_HDR_SIZE + pool_hdr->poolsize; +} + +static inline size_t tc_pool_space_left(struct talloc_pool_hdr *pool_hdr) +{ + return (char *)tc_pool_end(pool_hdr) - (char *)pool_hdr->end; +} + +/* If tc is inside a pool, this gives the next neighbour. */ +static inline void *tc_next_chunk(struct talloc_chunk *tc) +{ + return (char *)tc + TC_ALIGN16(TC_HDR_SIZE + tc->size); +} + +static inline void *tc_pool_first_chunk(struct talloc_pool_hdr *pool_hdr) +{ + struct talloc_chunk *tc = talloc_chunk_from_pool(pool_hdr); + return tc_next_chunk(tc); +} + +/* Mark the whole remaining pool as not accessible */ +static inline void tc_invalidate_pool(struct talloc_pool_hdr *pool_hdr) +{ + size_t flen = tc_pool_space_left(pool_hdr); + + if (unlikely(talloc_fill.enabled)) { + memset(pool_hdr->end, talloc_fill.fill_value, flen); + } + +#if defined(DEVELOPER) && defined(VALGRIND_MAKE_MEM_NOACCESS) + VALGRIND_MAKE_MEM_NOACCESS(pool_hdr->end, flen); +#endif +} + +/* + Allocate from a pool +*/ + +static inline struct talloc_chunk *tc_alloc_pool(struct talloc_chunk *parent, + size_t size, size_t prefix_len) +{ + struct talloc_pool_hdr *pool_hdr = NULL; + union talloc_chunk_cast_u tcc; + size_t space_left; + struct talloc_chunk *result; + size_t chunk_size; + + if (parent == NULL) { + return NULL; + } + + if (parent->flags & TALLOC_FLAG_POOL) { + pool_hdr = talloc_pool_from_chunk(parent); + } + else if (parent->flags & TALLOC_FLAG_POOLMEM) { + pool_hdr = parent->pool; + } + + if (pool_hdr == NULL) { + return NULL; + } + + space_left = tc_pool_space_left(pool_hdr); + + /* + * Align size to 16 bytes + */ + chunk_size = TC_ALIGN16(size + prefix_len); + + if (space_left < chunk_size) { + return NULL; + } + + tcc = (union talloc_chunk_cast_u) { + .ptr = ((uint8_t *)pool_hdr->end) + prefix_len + }; + result = tcc.chunk; + +#if defined(DEVELOPER) && defined(VALGRIND_MAKE_MEM_UNDEFINED) + VALGRIND_MAKE_MEM_UNDEFINED(pool_hdr->end, chunk_size); +#endif + + pool_hdr->end = (void *)((char *)pool_hdr->end + chunk_size); + + result->flags = talloc_magic | TALLOC_FLAG_POOLMEM; + result->pool = pool_hdr; + + pool_hdr->object_count++; + + return result; +} + +/* + Allocate a bit of memory as a child of an existing pointer +*/ +static inline void *__talloc_with_prefix(const void *context, + size_t size, + size_t prefix_len, + struct talloc_chunk **tc_ret) +{ + struct talloc_chunk *tc = NULL; + struct talloc_memlimit *limit = NULL; + size_t total_len = TC_HDR_SIZE + size + prefix_len; + struct talloc_chunk *parent = NULL; + + if (unlikely(context == NULL)) { + context = null_context; + } + + if (unlikely(size >= MAX_TALLOC_SIZE)) { + return NULL; + } + + if (unlikely(total_len < TC_HDR_SIZE)) { + return NULL; + } + + if (likely(context != NULL)) { + parent = talloc_chunk_from_ptr(context); + + if (parent->limit != NULL) { + limit = parent->limit; + } + + tc = tc_alloc_pool(parent, TC_HDR_SIZE+size, prefix_len); + } + + if (tc == NULL) { + uint8_t *ptr = NULL; + union talloc_chunk_cast_u tcc; + + /* + * Only do the memlimit check/update on actual allocation. + */ + if (!talloc_memlimit_check(limit, total_len)) { + errno = ENOMEM; + return NULL; + } + + ptr = malloc(total_len); + if (unlikely(ptr == NULL)) { + return NULL; + } + tcc = (union talloc_chunk_cast_u) { .ptr = ptr + prefix_len }; + tc = tcc.chunk; + tc->flags = talloc_magic; + tc->pool = NULL; + + talloc_memlimit_grow(limit, total_len); + } + + tc->limit = limit; + tc->size = size; + tc->destructor = NULL; + tc->child = NULL; + tc->name = NULL; + tc->refs = NULL; + + if (likely(context != NULL)) { + if (parent->child) { + parent->child->parent = NULL; + tc->next = parent->child; + tc->next->prev = tc; + } else { + tc->next = NULL; + } + tc->parent = parent; + tc->prev = NULL; + parent->child = tc; + } else { + tc->next = tc->prev = tc->parent = NULL; + } + + *tc_ret = tc; + return TC_PTR_FROM_CHUNK(tc); +} + +static inline void *__talloc(const void *context, + size_t size, + struct talloc_chunk **tc) +{ + return __talloc_with_prefix(context, size, 0, tc); +} + +/* + * Create a talloc pool + */ + +static inline void *_talloc_pool(const void *context, size_t size) +{ + struct talloc_chunk *tc = NULL; + struct talloc_pool_hdr *pool_hdr; + void *result; + + result = __talloc_with_prefix(context, size, TP_HDR_SIZE, &tc); + + if (unlikely(result == NULL)) { + return NULL; + } + + pool_hdr = talloc_pool_from_chunk(tc); + + tc->flags |= TALLOC_FLAG_POOL; + tc->size = 0; + + pool_hdr->object_count = 1; + pool_hdr->end = result; + pool_hdr->poolsize = size; + + tc_invalidate_pool(pool_hdr); + + return result; +} + +_PUBLIC_ void *talloc_pool(const void *context, size_t size) +{ + return _talloc_pool(context, size); +} + +/* + * Create a talloc pool correctly sized for a basic size plus + * a number of subobjects whose total size is given. Essentially + * a custom allocator for talloc to reduce fragmentation. + */ + +_PUBLIC_ void *_talloc_pooled_object(const void *ctx, + size_t type_size, + const char *type_name, + unsigned num_subobjects, + size_t total_subobjects_size) +{ + size_t poolsize, subobjects_slack, tmp; + struct talloc_chunk *tc; + struct talloc_pool_hdr *pool_hdr; + void *ret; + + poolsize = type_size + total_subobjects_size; + + if ((poolsize < type_size) || (poolsize < total_subobjects_size)) { + goto overflow; + } + + if (num_subobjects == UINT_MAX) { + goto overflow; + } + num_subobjects += 1; /* the object body itself */ + + /* + * Alignment can increase the pool size by at most 15 bytes per object + * plus alignment for the object itself + */ + subobjects_slack = (TC_HDR_SIZE + TP_HDR_SIZE + 15) * num_subobjects; + if (subobjects_slack < num_subobjects) { + goto overflow; + } + + tmp = poolsize + subobjects_slack; + if ((tmp < poolsize) || (tmp < subobjects_slack)) { + goto overflow; + } + poolsize = tmp; + + ret = _talloc_pool(ctx, poolsize); + if (ret == NULL) { + return NULL; + } + + tc = talloc_chunk_from_ptr(ret); + tc->size = type_size; + + pool_hdr = talloc_pool_from_chunk(tc); + +#if defined(DEVELOPER) && defined(VALGRIND_MAKE_MEM_UNDEFINED) + VALGRIND_MAKE_MEM_UNDEFINED(pool_hdr->end, type_size); +#endif + + pool_hdr->end = ((char *)pool_hdr->end + TC_ALIGN16(type_size)); + + _tc_set_name_const(tc, type_name); + return ret; + +overflow: + return NULL; +} + +/* + setup a destructor to be called on free of a pointer + the destructor should return 0 on success, or -1 on failure. + if the destructor fails then the free is failed, and the memory can + be continued to be used +*/ +_PUBLIC_ void _talloc_set_destructor(const void *ptr, int (*destructor)(void *)) +{ + struct talloc_chunk *tc = talloc_chunk_from_ptr(ptr); + tc->destructor = destructor; +} + +/* + increase the reference count on a piece of memory. +*/ +_PUBLIC_ int talloc_increase_ref_count(const void *ptr) +{ + if (unlikely(!talloc_reference(null_context, ptr))) { + return -1; + } + return 0; +} + +/* + helper for talloc_reference() + + this is referenced by a function pointer and should not be inline +*/ +static int talloc_reference_destructor(struct talloc_reference_handle *handle) +{ + struct talloc_chunk *ptr_tc = talloc_chunk_from_ptr(handle->ptr); + _TLIST_REMOVE(ptr_tc->refs, handle); + return 0; +} + +/* + more efficient way to add a name to a pointer - the name must point to a + true string constant +*/ +static inline void _tc_set_name_const(struct talloc_chunk *tc, + const char *name) +{ + tc->name = name; +} + +/* + internal talloc_named_const() +*/ +static inline void *_talloc_named_const(const void *context, size_t size, const char *name) +{ + void *ptr; + struct talloc_chunk *tc = NULL; + + ptr = __talloc(context, size, &tc); + if (unlikely(ptr == NULL)) { + return NULL; + } + + _tc_set_name_const(tc, name); + + return ptr; +} + +/* + make a secondary reference to a pointer, hanging off the given context. + the pointer remains valid until both the original caller and this given + context are freed. + + the major use for this is when two different structures need to reference the + same underlying data, and you want to be able to free the two instances separately, + and in either order +*/ +_PUBLIC_ void *_talloc_reference_loc(const void *context, const void *ptr, const char *location) +{ + struct talloc_chunk *tc; + struct talloc_reference_handle *handle; + if (unlikely(ptr == NULL)) return NULL; + + tc = talloc_chunk_from_ptr(ptr); + handle = (struct talloc_reference_handle *)_talloc_named_const(context, + sizeof(struct talloc_reference_handle), + TALLOC_MAGIC_REFERENCE); + if (unlikely(handle == NULL)) return NULL; + + /* note that we hang the destructor off the handle, not the + main context as that allows the caller to still setup their + own destructor on the context if they want to */ + talloc_set_destructor(handle, talloc_reference_destructor); + handle->ptr = discard_const_p(void, ptr); + handle->location = location; + _TLIST_ADD(tc->refs, handle); + return handle->ptr; +} + +static void *_talloc_steal_internal(const void *new_ctx, const void *ptr); + +static inline void _tc_free_poolmem(struct talloc_chunk *tc, + const char *location) +{ + struct talloc_pool_hdr *pool; + struct talloc_chunk *pool_tc; + void *next_tc; + + pool = tc->pool; + pool_tc = talloc_chunk_from_pool(pool); + next_tc = tc_next_chunk(tc); + + _talloc_chunk_set_free(tc, location); + + TC_INVALIDATE_FULL_CHUNK(tc); + + if (unlikely(pool->object_count == 0)) { + talloc_abort("Pool object count zero!"); + return; + } + + pool->object_count--; + + if (unlikely(pool->object_count == 1 + && !(pool_tc->flags & TALLOC_FLAG_FREE))) { + /* + * if there is just one object left in the pool + * and pool->flags does not have TALLOC_FLAG_FREE, + * it means this is the pool itself and + * the rest is available for new objects + * again. + */ + pool->end = tc_pool_first_chunk(pool); + tc_invalidate_pool(pool); + return; + } + + if (unlikely(pool->object_count == 0)) { + /* + * we mark the freed memory with where we called the free + * from. This means on a double free error we can report where + * the first free came from + */ + pool_tc->name = location; + + if (pool_tc->flags & TALLOC_FLAG_POOLMEM) { + _tc_free_poolmem(pool_tc, location); + } else { + /* + * The tc_memlimit_update_on_free() + * call takes into account the + * prefix TP_HDR_SIZE allocated before + * the pool talloc_chunk. + */ + tc_memlimit_update_on_free(pool_tc); + TC_INVALIDATE_FULL_CHUNK(pool_tc); + free(pool); + } + return; + } + + if (pool->end == next_tc) { + /* + * if pool->pool still points to end of + * 'tc' (which is stored in the 'next_tc' variable), + * we can reclaim the memory of 'tc'. + */ + pool->end = tc; + return; + } + + /* + * Do nothing. The memory is just "wasted", waiting for the pool + * itself to be freed. + */ +} + +static inline void _tc_free_children_internal(struct talloc_chunk *tc, + void *ptr, + const char *location); + +static inline int _talloc_free_internal(void *ptr, const char *location); + +/* + internal free call that takes a struct talloc_chunk *. +*/ +static inline int _tc_free_internal(struct talloc_chunk *tc, + const char *location) +{ + void *ptr_to_free; + void *ptr = TC_PTR_FROM_CHUNK(tc); + + if (unlikely(tc->refs)) { + int is_child; + /* check if this is a reference from a child or + * grandchild back to it's parent or grandparent + * + * in that case we need to remove the reference and + * call another instance of talloc_free() on the current + * pointer. + */ + is_child = talloc_is_parent(tc->refs, ptr); + _talloc_free_internal(tc->refs, location); + if (is_child) { + return _talloc_free_internal(ptr, location); + } + return -1; + } + + if (unlikely(tc->flags & TALLOC_FLAG_LOOP)) { + /* we have a free loop - stop looping */ + return 0; + } + + if (unlikely(tc->destructor)) { + talloc_destructor_t d = tc->destructor; + + /* + * Protect the destructor against some overwrite + * attacks, by explicitly checking it has the right + * magic here. + */ + if (talloc_chunk_from_ptr(ptr) != tc) { + /* + * This can't actually happen, the + * call itself will panic. + */ + TALLOC_ABORT("talloc_chunk_from_ptr failed!"); + } + + if (d == (talloc_destructor_t)-1) { + return -1; + } + tc->destructor = (talloc_destructor_t)-1; + if (d(ptr) == -1) { + /* + * Only replace the destructor pointer if + * calling the destructor didn't modify it. + */ + if (tc->destructor == (talloc_destructor_t)-1) { + tc->destructor = d; + } + return -1; + } + tc->destructor = NULL; + } + + if (tc->parent) { + _TLIST_REMOVE(tc->parent->child, tc); + if (tc->parent->child) { + tc->parent->child->parent = tc->parent; + } + } else { + if (tc->prev) tc->prev->next = tc->next; + if (tc->next) tc->next->prev = tc->prev; + tc->prev = tc->next = NULL; + } + + tc->flags |= TALLOC_FLAG_LOOP; + + _tc_free_children_internal(tc, ptr, location); + + _talloc_chunk_set_free(tc, location); + + if (tc->flags & TALLOC_FLAG_POOL) { + struct talloc_pool_hdr *pool; + + pool = talloc_pool_from_chunk(tc); + + if (unlikely(pool->object_count == 0)) { + talloc_abort("Pool object count zero!"); + return 0; + } + + pool->object_count--; + + if (likely(pool->object_count != 0)) { + return 0; + } + + /* + * With object_count==0, a pool becomes a normal piece of + * memory to free. If it's allocated inside a pool, it needs + * to be freed as poolmem, else it needs to be just freed. + */ + ptr_to_free = pool; + } else { + ptr_to_free = tc; + } + + if (tc->flags & TALLOC_FLAG_POOLMEM) { + _tc_free_poolmem(tc, location); + return 0; + } + + tc_memlimit_update_on_free(tc); + + TC_INVALIDATE_FULL_CHUNK(tc); + free(ptr_to_free); + return 0; +} + +/* + internal talloc_free call +*/ +static inline int _talloc_free_internal(void *ptr, const char *location) +{ + struct talloc_chunk *tc; + + if (unlikely(ptr == NULL)) { + return -1; + } + + /* possibly initialised the talloc fill value */ + if (unlikely(!talloc_fill.initialised)) { + const char *fill = getenv(TALLOC_FILL_ENV); + if (fill != NULL) { + talloc_fill.enabled = true; + talloc_fill.fill_value = strtoul(fill, NULL, 0); + } + talloc_fill.initialised = true; + } + + tc = talloc_chunk_from_ptr(ptr); + return _tc_free_internal(tc, location); +} + +static inline size_t _talloc_total_limit_size(const void *ptr, + struct talloc_memlimit *old_limit, + struct talloc_memlimit *new_limit); + +/* + move a lump of memory from one talloc context to another returning the + ptr on success, or NULL if it could not be transferred. + passing NULL as ptr will always return NULL with no side effects. +*/ +static void *_talloc_steal_internal(const void *new_ctx, const void *ptr) +{ + struct talloc_chunk *tc, *new_tc; + size_t ctx_size = 0; + + if (unlikely(!ptr)) { + return NULL; + } + + if (unlikely(new_ctx == NULL)) { + new_ctx = null_context; + } + + tc = talloc_chunk_from_ptr(ptr); + + if (tc->limit != NULL) { + + ctx_size = _talloc_total_limit_size(ptr, NULL, NULL); + + /* Decrement the memory limit from the source .. */ + talloc_memlimit_shrink(tc->limit->upper, ctx_size); + + if (tc->limit->parent == tc) { + tc->limit->upper = NULL; + } else { + tc->limit = NULL; + } + } + + if (unlikely(new_ctx == NULL)) { + if (tc->parent) { + _TLIST_REMOVE(tc->parent->child, tc); + if (tc->parent->child) { + tc->parent->child->parent = tc->parent; + } + } else { + if (tc->prev) tc->prev->next = tc->next; + if (tc->next) tc->next->prev = tc->prev; + } + + tc->parent = tc->next = tc->prev = NULL; + return discard_const_p(void, ptr); + } + + new_tc = talloc_chunk_from_ptr(new_ctx); + + if (unlikely(tc == new_tc || tc->parent == new_tc)) { + return discard_const_p(void, ptr); + } + + if (tc->parent) { + _TLIST_REMOVE(tc->parent->child, tc); + if (tc->parent->child) { + tc->parent->child->parent = tc->parent; + } + } else { + if (tc->prev) tc->prev->next = tc->next; + if (tc->next) tc->next->prev = tc->prev; + tc->prev = tc->next = NULL; + } + + tc->parent = new_tc; + if (new_tc->child) new_tc->child->parent = NULL; + _TLIST_ADD(new_tc->child, tc); + + if (tc->limit || new_tc->limit) { + ctx_size = _talloc_total_limit_size(ptr, tc->limit, + new_tc->limit); + /* .. and increment it in the destination. */ + if (new_tc->limit) { + talloc_memlimit_grow(new_tc->limit, ctx_size); + } + } + + return discard_const_p(void, ptr); +} + +/* + move a lump of memory from one talloc context to another returning the + ptr on success, or NULL if it could not be transferred. + passing NULL as ptr will always return NULL with no side effects. +*/ +_PUBLIC_ void *_talloc_steal_loc(const void *new_ctx, const void *ptr, const char *location) +{ + struct talloc_chunk *tc; + + if (unlikely(ptr == NULL)) { + return NULL; + } + + tc = talloc_chunk_from_ptr(ptr); + + if (unlikely(tc->refs != NULL) && talloc_parent(ptr) != new_ctx) { + struct talloc_reference_handle *h; + + talloc_log("WARNING: talloc_steal with references at %s\n", + location); + + for (h=tc->refs; h; h=h->next) { + talloc_log("\treference at %s\n", + h->location); + } + } + +#if 0 + /* this test is probably too expensive to have on in the + normal build, but it useful for debugging */ + if (talloc_is_parent(new_ctx, ptr)) { + talloc_log("WARNING: stealing into talloc child at %s\n", location); + } +#endif + + return _talloc_steal_internal(new_ctx, ptr); +} + +/* + this is like a talloc_steal(), but you must supply the old + parent. This resolves the ambiguity in a talloc_steal() which is + called on a context that has more than one parent (via references) + + The old parent can be either a reference or a parent +*/ +_PUBLIC_ void *talloc_reparent(const void *old_parent, const void *new_parent, const void *ptr) +{ + struct talloc_chunk *tc; + struct talloc_reference_handle *h; + + if (unlikely(ptr == NULL)) { + return NULL; + } + + if (old_parent == talloc_parent(ptr)) { + return _talloc_steal_internal(new_parent, ptr); + } + + tc = talloc_chunk_from_ptr(ptr); + for (h=tc->refs;h;h=h->next) { + if (talloc_parent(h) == old_parent) { + if (_talloc_steal_internal(new_parent, h) != h) { + return NULL; + } + return discard_const_p(void, ptr); + } + } + + /* it wasn't a parent */ + return NULL; +} + +/* + remove a secondary reference to a pointer. This undo's what + talloc_reference() has done. The context and pointer arguments + must match those given to a talloc_reference() +*/ +static inline int talloc_unreference(const void *context, const void *ptr) +{ + struct talloc_chunk *tc = talloc_chunk_from_ptr(ptr); + struct talloc_reference_handle *h; + + if (unlikely(context == NULL)) { + context = null_context; + } + + for (h=tc->refs;h;h=h->next) { + struct talloc_chunk *p = talloc_parent_chunk(h); + if (p == NULL) { + if (context == NULL) break; + } else if (TC_PTR_FROM_CHUNK(p) == context) { + break; + } + } + if (h == NULL) { + return -1; + } + + return _talloc_free_internal(h, __location__); +} + +/* + remove a specific parent context from a pointer. This is a more + controlled variant of talloc_free() +*/ + +/* coverity[ -tainted_data_sink : arg-1 ] */ +_PUBLIC_ int talloc_unlink(const void *context, void *ptr) +{ + struct talloc_chunk *tc_p, *new_p, *tc_c; + void *new_parent; + + if (ptr == NULL) { + return -1; + } + + if (context == NULL) { + context = null_context; + } + + if (talloc_unreference(context, ptr) == 0) { + return 0; + } + + if (context != NULL) { + tc_c = talloc_chunk_from_ptr(context); + } else { + tc_c = NULL; + } + if (tc_c != talloc_parent_chunk(ptr)) { + return -1; + } + + tc_p = talloc_chunk_from_ptr(ptr); + + if (tc_p->refs == NULL) { + return _talloc_free_internal(ptr, __location__); + } + + new_p = talloc_parent_chunk(tc_p->refs); + if (new_p) { + new_parent = TC_PTR_FROM_CHUNK(new_p); + } else { + new_parent = NULL; + } + + if (talloc_unreference(new_parent, ptr) != 0) { + return -1; + } + + _talloc_steal_internal(new_parent, ptr); + + return 0; +} + +/* + add a name to an existing pointer - va_list version +*/ +static inline const char *tc_set_name_v(struct talloc_chunk *tc, + const char *fmt, + va_list ap) PRINTF_ATTRIBUTE(2,0); + +static inline const char *tc_set_name_v(struct talloc_chunk *tc, + const char *fmt, + va_list ap) +{ + struct talloc_chunk *name_tc = _vasprintf_tc(TC_PTR_FROM_CHUNK(tc), + fmt, + ap); + if (likely(name_tc)) { + tc->name = TC_PTR_FROM_CHUNK(name_tc); + _tc_set_name_const(name_tc, ".name"); + } else { + tc->name = NULL; + } + return tc->name; +} + +/* + add a name to an existing pointer +*/ +_PUBLIC_ const char *talloc_set_name(const void *ptr, const char *fmt, ...) +{ + struct talloc_chunk *tc = talloc_chunk_from_ptr(ptr); + const char *name; + va_list ap; + va_start(ap, fmt); + name = tc_set_name_v(tc, fmt, ap); + va_end(ap); + return name; +} + + +/* + create a named talloc pointer. Any talloc pointer can be named, and + talloc_named() operates just like talloc() except that it allows you + to name the pointer. +*/ +_PUBLIC_ void *talloc_named(const void *context, size_t size, const char *fmt, ...) +{ + va_list ap; + void *ptr; + const char *name; + struct talloc_chunk *tc = NULL; + + ptr = __talloc(context, size, &tc); + if (unlikely(ptr == NULL)) return NULL; + + va_start(ap, fmt); + name = tc_set_name_v(tc, fmt, ap); + va_end(ap); + + if (unlikely(name == NULL)) { + _talloc_free_internal(ptr, __location__); + return NULL; + } + + return ptr; +} + +/* + return the name of a talloc ptr, or "UNNAMED" +*/ +static inline const char *__talloc_get_name(const void *ptr) +{ + struct talloc_chunk *tc = talloc_chunk_from_ptr(ptr); + if (unlikely(tc->name == TALLOC_MAGIC_REFERENCE)) { + return ".reference"; + } + if (likely(tc->name)) { + return tc->name; + } + return "UNNAMED"; +} + +_PUBLIC_ const char *talloc_get_name(const void *ptr) +{ + return __talloc_get_name(ptr); +} + +/* + check if a pointer has the given name. If it does, return the pointer, + otherwise return NULL +*/ +_PUBLIC_ void *talloc_check_name(const void *ptr, const char *name) +{ + const char *pname; + if (unlikely(ptr == NULL)) return NULL; + pname = __talloc_get_name(ptr); + if (likely(pname == name || strcmp(pname, name) == 0)) { + return discard_const_p(void, ptr); + } + return NULL; +} + +static void talloc_abort_type_mismatch(const char *location, + const char *name, + const char *expected) +{ + const char *reason; + + reason = talloc_asprintf(NULL, + "%s: Type mismatch: name[%s] expected[%s]", + location, + name?name:"NULL", + expected); + if (!reason) { + reason = "Type mismatch"; + } + + talloc_abort(reason); +} + +_PUBLIC_ void *_talloc_get_type_abort(const void *ptr, const char *name, const char *location) +{ + const char *pname; + + if (unlikely(ptr == NULL)) { + talloc_abort_type_mismatch(location, NULL, name); + return NULL; + } + + pname = __talloc_get_name(ptr); + if (likely(pname == name || strcmp(pname, name) == 0)) { + return discard_const_p(void, ptr); + } + + talloc_abort_type_mismatch(location, pname, name); + return NULL; +} + +/* + this is for compatibility with older versions of talloc +*/ +_PUBLIC_ void *talloc_init(const char *fmt, ...) +{ + va_list ap; + void *ptr; + const char *name; + struct talloc_chunk *tc = NULL; + + ptr = __talloc(NULL, 0, &tc); + if (unlikely(ptr == NULL)) return NULL; + + va_start(ap, fmt); + name = tc_set_name_v(tc, fmt, ap); + va_end(ap); + + if (unlikely(name == NULL)) { + _talloc_free_internal(ptr, __location__); + return NULL; + } + + return ptr; +} + +static inline void _tc_free_children_internal(struct talloc_chunk *tc, + void *ptr, + const char *location) +{ + while (tc->child) { + /* we need to work out who will own an abandoned child + if it cannot be freed. In priority order, the first + choice is owner of any remaining reference to this + pointer, the second choice is our parent, and the + final choice is the null context. */ + void *child = TC_PTR_FROM_CHUNK(tc->child); + const void *new_parent = null_context; + if (unlikely(tc->child->refs)) { + struct talloc_chunk *p = talloc_parent_chunk(tc->child->refs); + if (p) new_parent = TC_PTR_FROM_CHUNK(p); + } + if (unlikely(_tc_free_internal(tc->child, location) == -1)) { + if (talloc_parent_chunk(child) != tc) { + /* + * Destructor already reparented this child. + * No further reparenting needed. + */ + continue; + } + if (new_parent == null_context) { + struct talloc_chunk *p = talloc_parent_chunk(ptr); + if (p) new_parent = TC_PTR_FROM_CHUNK(p); + } + _talloc_steal_internal(new_parent, child); + } + } +} + +/* + this is a replacement for the Samba3 talloc_destroy_pool functionality. It + should probably not be used in new code. It's in here to keep the talloc + code consistent across Samba 3 and 4. +*/ +_PUBLIC_ void talloc_free_children(void *ptr) +{ + struct talloc_chunk *tc_name = NULL; + struct talloc_chunk *tc; + + if (unlikely(ptr == NULL)) { + return; + } + + tc = talloc_chunk_from_ptr(ptr); + + /* we do not want to free the context name if it is a child .. */ + if (likely(tc->child)) { + for (tc_name = tc->child; tc_name; tc_name = tc_name->next) { + if (tc->name == TC_PTR_FROM_CHUNK(tc_name)) break; + } + if (tc_name) { + _TLIST_REMOVE(tc->child, tc_name); + if (tc->child) { + tc->child->parent = tc; + } + } + } + + _tc_free_children_internal(tc, ptr, __location__); + + /* .. so we put it back after all other children have been freed */ + if (tc_name) { + if (tc->child) { + tc->child->parent = NULL; + } + tc_name->parent = tc; + _TLIST_ADD(tc->child, tc_name); + } +} + +/* + Allocate a bit of memory as a child of an existing pointer +*/ +_PUBLIC_ void *_talloc(const void *context, size_t size) +{ + struct talloc_chunk *tc; + return __talloc(context, size, &tc); +} + +/* + externally callable talloc_set_name_const() +*/ +_PUBLIC_ void talloc_set_name_const(const void *ptr, const char *name) +{ + _tc_set_name_const(talloc_chunk_from_ptr(ptr), name); +} + +/* + create a named talloc pointer. Any talloc pointer can be named, and + talloc_named() operates just like talloc() except that it allows you + to name the pointer. +*/ +_PUBLIC_ void *talloc_named_const(const void *context, size_t size, const char *name) +{ + return _talloc_named_const(context, size, name); +} + +/* + free a talloc pointer. This also frees all child pointers of this + pointer recursively + + return 0 if the memory is actually freed, otherwise -1. The memory + will not be freed if the ref_count is > 1 or the destructor (if + any) returns non-zero +*/ +_PUBLIC_ int _talloc_free(void *ptr, const char *location) +{ + struct talloc_chunk *tc; + + if (unlikely(ptr == NULL)) { + return -1; + } + + tc = talloc_chunk_from_ptr(ptr); + + if (unlikely(tc->refs != NULL)) { + struct talloc_reference_handle *h; + + if (talloc_parent(ptr) == null_context && tc->refs->next == NULL) { + /* in this case we do know which parent should + get this pointer, as there is really only + one parent */ + return talloc_unlink(null_context, ptr); + } + + talloc_log("ERROR: talloc_free with references at %s\n", + location); + + for (h=tc->refs; h; h=h->next) { + talloc_log("\treference at %s\n", + h->location); + } + return -1; + } + + return _talloc_free_internal(ptr, location); +} + + + +/* + A talloc version of realloc. The context argument is only used if + ptr is NULL +*/ +_PUBLIC_ void *_talloc_realloc(const void *context, void *ptr, size_t size, const char *name) +{ + struct talloc_chunk *tc; + void *new_ptr; + bool malloced = false; + struct talloc_pool_hdr *pool_hdr = NULL; + size_t old_size = 0; + size_t new_size = 0; + + /* size zero is equivalent to free() */ + if (unlikely(size == 0)) { + talloc_unlink(context, ptr); + return NULL; + } + + if (unlikely(size >= MAX_TALLOC_SIZE)) { + return NULL; + } + + /* realloc(NULL) is equivalent to malloc() */ + if (ptr == NULL) { + return _talloc_named_const(context, size, name); + } + + tc = talloc_chunk_from_ptr(ptr); + + /* don't allow realloc on referenced pointers */ + if (unlikely(tc->refs)) { + return NULL; + } + + /* don't let anybody try to realloc a talloc_pool */ + if (unlikely(tc->flags & TALLOC_FLAG_POOL)) { + return NULL; + } + + /* handle realloc inside a talloc_pool */ + if (unlikely(tc->flags & TALLOC_FLAG_POOLMEM)) { + pool_hdr = tc->pool; + } + + /* don't shrink if we have less than 1k to gain */ + if (size < tc->size && tc->limit == NULL) { + if (pool_hdr) { + void *next_tc = tc_next_chunk(tc); + TC_INVALIDATE_SHRINK_CHUNK(tc, size); + tc->size = size; + if (next_tc == pool_hdr->end) { + /* note: tc->size has changed, so this works */ + pool_hdr->end = tc_next_chunk(tc); + } + return ptr; + } else if ((tc->size - size) < 1024) { + /* + * if we call TC_INVALIDATE_SHRINK_CHUNK() here + * we would need to call TC_UNDEFINE_GROW_CHUNK() + * after each realloc call, which slows down + * testing a lot :-(. + * + * That is why we only mark memory as undefined here. + */ + TC_UNDEFINE_SHRINK_CHUNK(tc, size); + + /* do not shrink if we have less than 1k to gain */ + tc->size = size; + return ptr; + } + } else if (tc->size == size) { + /* + * do not change the pointer if it is exactly + * the same size. + */ + return ptr; + } + + /* + * by resetting magic we catch users of the old memory + * + * We mark this memory as free, and also over-stamp the talloc + * magic with the old-style magic. + * + * Why? This tries to avoid a memory read use-after-free from + * disclosing our talloc magic, which would then allow an + * attacker to prepare a valid header and so run a destructor. + * + * What else? We have to re-stamp back a valid normal magic + * on this memory once realloc() is done, as it will have done + * a memcpy() into the new valid memory. We can't do this in + * reverse as that would be a real use-after-free. + */ + _talloc_chunk_set_free(tc, NULL); + + if (pool_hdr) { + struct talloc_chunk *pool_tc; + void *next_tc = tc_next_chunk(tc); + size_t old_chunk_size = TC_ALIGN16(TC_HDR_SIZE + tc->size); + size_t new_chunk_size = TC_ALIGN16(TC_HDR_SIZE + size); + size_t space_needed; + size_t space_left; + unsigned int chunk_count = pool_hdr->object_count; + + pool_tc = talloc_chunk_from_pool(pool_hdr); + if (!(pool_tc->flags & TALLOC_FLAG_FREE)) { + chunk_count -= 1; + } + + if (chunk_count == 1) { + /* + * optimize for the case where 'tc' is the only + * chunk in the pool. + */ + char *start = tc_pool_first_chunk(pool_hdr); + space_needed = new_chunk_size; + space_left = (char *)tc_pool_end(pool_hdr) - start; + + if (space_left >= space_needed) { + size_t old_used = TC_HDR_SIZE + tc->size; + size_t new_used = TC_HDR_SIZE + size; + new_ptr = start; + +#if defined(DEVELOPER) && defined(VALGRIND_MAKE_MEM_UNDEFINED) + { + /* + * The area from + * start -> tc may have + * been freed and thus been marked as + * VALGRIND_MEM_NOACCESS. Set it to + * VALGRIND_MEM_UNDEFINED so we can + * copy into it without valgrind errors. + * We can't just mark + * new_ptr -> new_ptr + old_used + * as this may overlap on top of tc, + * (which is why we use memmove, not + * memcpy below) hence the MIN. + */ + size_t undef_len = MIN((((char *)tc) - ((char *)new_ptr)),old_used); + VALGRIND_MAKE_MEM_UNDEFINED(new_ptr, undef_len); + } +#endif + + memmove(new_ptr, tc, old_used); + + tc = (struct talloc_chunk *)new_ptr; + TC_UNDEFINE_GROW_CHUNK(tc, size); + + /* + * first we do not align the pool pointer + * because we want to invalidate the padding + * too. + */ + pool_hdr->end = new_used + (char *)new_ptr; + tc_invalidate_pool(pool_hdr); + + /* now the aligned pointer */ + pool_hdr->end = new_chunk_size + (char *)new_ptr; + goto got_new_ptr; + } + + next_tc = NULL; + } + + if (new_chunk_size == old_chunk_size) { + TC_UNDEFINE_GROW_CHUNK(tc, size); + _talloc_chunk_set_not_free(tc); + tc->size = size; + return ptr; + } + + if (next_tc == pool_hdr->end) { + /* + * optimize for the case where 'tc' is the last + * chunk in the pool. + */ + space_needed = new_chunk_size - old_chunk_size; + space_left = tc_pool_space_left(pool_hdr); + + if (space_left >= space_needed) { + TC_UNDEFINE_GROW_CHUNK(tc, size); + _talloc_chunk_set_not_free(tc); + tc->size = size; + pool_hdr->end = tc_next_chunk(tc); + return ptr; + } + } + + new_ptr = tc_alloc_pool(tc, size + TC_HDR_SIZE, 0); + + if (new_ptr == NULL) { + /* + * Couldn't allocate from pool (pool size + * counts as already allocated for memlimit + * purposes). We must check memory limit + * before any real malloc. + */ + if (tc->limit) { + /* + * Note we're doing an extra malloc, + * on top of the pool size, so account + * for size only, not the difference + * between old and new size. + */ + if (!talloc_memlimit_check(tc->limit, size)) { + _talloc_chunk_set_not_free(tc); + errno = ENOMEM; + return NULL; + } + } + new_ptr = malloc(TC_HDR_SIZE+size); + malloced = true; + new_size = size; + } + + if (new_ptr) { + memcpy(new_ptr, tc, MIN(tc->size,size) + TC_HDR_SIZE); + + _tc_free_poolmem(tc, __location__ "_talloc_realloc"); + } + } + else { + /* We're doing realloc here, so record the difference. */ + old_size = tc->size; + new_size = size; + /* + * We must check memory limit + * before any real realloc. + */ + if (tc->limit && (size > old_size)) { + if (!talloc_memlimit_check(tc->limit, + (size - old_size))) { + _talloc_chunk_set_not_free(tc); + errno = ENOMEM; + return NULL; + } + } + new_ptr = realloc(tc, size + TC_HDR_SIZE); + } +got_new_ptr: + + if (unlikely(!new_ptr)) { + /* + * Ok, this is a strange spot. We have to put back + * the old talloc_magic and any flags, except the + * TALLOC_FLAG_FREE as this was not free'ed by the + * realloc() call after all + */ + _talloc_chunk_set_not_free(tc); + return NULL; + } + + /* + * tc is now the new value from realloc(), the old memory we + * can't access any more and was preemptively marked as + * TALLOC_FLAG_FREE before the call. Now we mark it as not + * free again + */ + tc = (struct talloc_chunk *)new_ptr; + _talloc_chunk_set_not_free(tc); + if (malloced) { + tc->flags &= ~TALLOC_FLAG_POOLMEM; + } + if (tc->parent) { + tc->parent->child = tc; + } + if (tc->child) { + tc->child->parent = tc; + } + + if (tc->prev) { + tc->prev->next = tc; + } + if (tc->next) { + tc->next->prev = tc; + } + + if (new_size > old_size) { + talloc_memlimit_grow(tc->limit, new_size - old_size); + } else if (new_size < old_size) { + talloc_memlimit_shrink(tc->limit, old_size - new_size); + } + + tc->size = size; + _tc_set_name_const(tc, name); + + return TC_PTR_FROM_CHUNK(tc); +} + +/* + a wrapper around talloc_steal() for situations where you are moving a pointer + between two structures, and want the old pointer to be set to NULL +*/ +_PUBLIC_ void *_talloc_move(const void *new_ctx, const void *_pptr) +{ + const void **pptr = discard_const_p(const void *,_pptr); + void *ret = talloc_steal(new_ctx, discard_const_p(void, *pptr)); + (*pptr) = NULL; + return ret; +} + +enum talloc_mem_count_type { + TOTAL_MEM_SIZE, + TOTAL_MEM_BLOCKS, + TOTAL_MEM_LIMIT, +}; + +static inline size_t _talloc_total_mem_internal(const void *ptr, + enum talloc_mem_count_type type, + struct talloc_memlimit *old_limit, + struct talloc_memlimit *new_limit) +{ + size_t total = 0; + struct talloc_chunk *c, *tc; + + if (ptr == NULL) { + ptr = null_context; + } + if (ptr == NULL) { + return 0; + } + + tc = talloc_chunk_from_ptr(ptr); + + if (old_limit || new_limit) { + if (tc->limit && tc->limit->upper == old_limit) { + tc->limit->upper = new_limit; + } + } + + /* optimize in the memlimits case */ + if (type == TOTAL_MEM_LIMIT && + tc->limit != NULL && + tc->limit != old_limit && + tc->limit->parent == tc) { + return tc->limit->cur_size; + } + + if (tc->flags & TALLOC_FLAG_LOOP) { + return 0; + } + + tc->flags |= TALLOC_FLAG_LOOP; + + if (old_limit || new_limit) { + if (old_limit == tc->limit) { + tc->limit = new_limit; + } + } + + switch (type) { + case TOTAL_MEM_SIZE: + if (likely(tc->name != TALLOC_MAGIC_REFERENCE)) { + total = tc->size; + } + break; + case TOTAL_MEM_BLOCKS: + total++; + break; + case TOTAL_MEM_LIMIT: + if (likely(tc->name != TALLOC_MAGIC_REFERENCE)) { + /* + * Don't count memory allocated from a pool + * when calculating limits. Only count the + * pool itself. + */ + if (!(tc->flags & TALLOC_FLAG_POOLMEM)) { + if (tc->flags & TALLOC_FLAG_POOL) { + /* + * If this is a pool, the allocated + * size is in the pool header, and + * remember to add in the prefix + * length. + */ + struct talloc_pool_hdr *pool_hdr + = talloc_pool_from_chunk(tc); + total = pool_hdr->poolsize + + TC_HDR_SIZE + + TP_HDR_SIZE; + } else { + total = tc->size + TC_HDR_SIZE; + } + } + } + break; + } + for (c = tc->child; c; c = c->next) { + total += _talloc_total_mem_internal(TC_PTR_FROM_CHUNK(c), type, + old_limit, new_limit); + } + + tc->flags &= ~TALLOC_FLAG_LOOP; + + return total; +} + +/* + return the total size of a talloc pool (subtree) +*/ +_PUBLIC_ size_t talloc_total_size(const void *ptr) +{ + return _talloc_total_mem_internal(ptr, TOTAL_MEM_SIZE, NULL, NULL); +} + +/* + return the total number of blocks in a talloc pool (subtree) +*/ +_PUBLIC_ size_t talloc_total_blocks(const void *ptr) +{ + return _talloc_total_mem_internal(ptr, TOTAL_MEM_BLOCKS, NULL, NULL); +} + +/* + return the number of external references to a pointer +*/ +_PUBLIC_ size_t talloc_reference_count(const void *ptr) +{ + struct talloc_chunk *tc = talloc_chunk_from_ptr(ptr); + struct talloc_reference_handle *h; + size_t ret = 0; + + for (h=tc->refs;h;h=h->next) { + ret++; + } + return ret; +} + +/* + report on memory usage by all children of a pointer, giving a full tree view +*/ +_PUBLIC_ void talloc_report_depth_cb(const void *ptr, int depth, int max_depth, + void (*callback)(const void *ptr, + int depth, int max_depth, + int is_ref, + void *private_data), + void *private_data) +{ + struct talloc_chunk *c, *tc; + + if (ptr == NULL) { + ptr = null_context; + } + if (ptr == NULL) return; + + tc = talloc_chunk_from_ptr(ptr); + + if (tc->flags & TALLOC_FLAG_LOOP) { + return; + } + + callback(ptr, depth, max_depth, 0, private_data); + + if (max_depth >= 0 && depth >= max_depth) { + return; + } + + tc->flags |= TALLOC_FLAG_LOOP; + for (c=tc->child;c;c=c->next) { + if (c->name == TALLOC_MAGIC_REFERENCE) { + struct talloc_reference_handle *h = (struct talloc_reference_handle *)TC_PTR_FROM_CHUNK(c); + callback(h->ptr, depth + 1, max_depth, 1, private_data); + } else { + talloc_report_depth_cb(TC_PTR_FROM_CHUNK(c), depth + 1, max_depth, callback, private_data); + } + } + tc->flags &= ~TALLOC_FLAG_LOOP; +} + +static void talloc_report_depth_FILE_helper(const void *ptr, int depth, int max_depth, int is_ref, void *_f) +{ + const char *name = __talloc_get_name(ptr); + struct talloc_chunk *tc; + FILE *f = (FILE *)_f; + + if (is_ref) { + fprintf(f, "%*sreference to: %s\n", depth*4, "", name); + return; + } + + tc = talloc_chunk_from_ptr(ptr); + if (tc->limit && tc->limit->parent == tc) { + fprintf(f, "%*s%-30s is a memlimit context" + " (max_size = %lu bytes, cur_size = %lu bytes)\n", + depth*4, "", + name, + (unsigned long)tc->limit->max_size, + (unsigned long)tc->limit->cur_size); + } + + if (depth == 0) { + fprintf(f,"%stalloc report on '%s' (total %6lu bytes in %3lu blocks)\n", + (max_depth < 0 ? "full " :""), name, + (unsigned long)talloc_total_size(ptr), + (unsigned long)talloc_total_blocks(ptr)); + return; + } + + fprintf(f, "%*s%-30s contains %6lu bytes in %3lu blocks (ref %d) %p\n", + depth*4, "", + name, + (unsigned long)talloc_total_size(ptr), + (unsigned long)talloc_total_blocks(ptr), + (int)talloc_reference_count(ptr), ptr); + +#if 0 + fprintf(f, "content: "); + if (talloc_total_size(ptr)) { + int tot = talloc_total_size(ptr); + int i; + + for (i = 0; i < tot; i++) { + if ((((char *)ptr)[i] > 31) && (((char *)ptr)[i] < 126)) { + fprintf(f, "%c", ((char *)ptr)[i]); + } else { + fprintf(f, "~%02x", ((char *)ptr)[i]); + } + } + } + fprintf(f, "\n"); +#endif +} + +/* + report on memory usage by all children of a pointer, giving a full tree view +*/ +_PUBLIC_ void talloc_report_depth_file(const void *ptr, int depth, int max_depth, FILE *f) +{ + if (f) { + talloc_report_depth_cb(ptr, depth, max_depth, talloc_report_depth_FILE_helper, f); + fflush(f); + } +} + +/* + report on memory usage by all children of a pointer, giving a full tree view +*/ +_PUBLIC_ void talloc_report_full(const void *ptr, FILE *f) +{ + talloc_report_depth_file(ptr, 0, -1, f); +} + +/* + report on memory usage by all children of a pointer +*/ +_PUBLIC_ void talloc_report(const void *ptr, FILE *f) +{ + talloc_report_depth_file(ptr, 0, 1, f); +} + +/* + enable tracking of the NULL context +*/ +_PUBLIC_ void talloc_enable_null_tracking(void) +{ + if (null_context == NULL) { + null_context = _talloc_named_const(NULL, 0, "null_context"); + if (autofree_context != NULL) { + talloc_reparent(NULL, null_context, autofree_context); + } + } +} + +/* + enable tracking of the NULL context, not moving the autofree context + into the NULL context. This is needed for the talloc testsuite +*/ +_PUBLIC_ void talloc_enable_null_tracking_no_autofree(void) +{ + if (null_context == NULL) { + null_context = _talloc_named_const(NULL, 0, "null_context"); + } +} + +/* + disable tracking of the NULL context +*/ +_PUBLIC_ void talloc_disable_null_tracking(void) +{ + if (null_context != NULL) { + /* we have to move any children onto the real NULL + context */ + struct talloc_chunk *tc, *tc2; + tc = talloc_chunk_from_ptr(null_context); + for (tc2 = tc->child; tc2; tc2=tc2->next) { + if (tc2->parent == tc) tc2->parent = NULL; + if (tc2->prev == tc) tc2->prev = NULL; + } + for (tc2 = tc->next; tc2; tc2=tc2->next) { + if (tc2->parent == tc) tc2->parent = NULL; + if (tc2->prev == tc) tc2->prev = NULL; + } + tc->child = NULL; + tc->next = NULL; + } + talloc_free(null_context); + null_context = NULL; +} + +/* + enable leak reporting on exit +*/ +_PUBLIC_ void talloc_enable_leak_report(void) +{ + talloc_enable_null_tracking(); + talloc_report_null = true; + talloc_setup_atexit(); +} + +/* + enable full leak reporting on exit +*/ +_PUBLIC_ void talloc_enable_leak_report_full(void) +{ + talloc_enable_null_tracking(); + talloc_report_null_full = true; + talloc_setup_atexit(); +} + +/* + talloc and zero memory. +*/ +_PUBLIC_ void *_talloc_zero(const void *ctx, size_t size, const char *name) +{ + void *p = _talloc_named_const(ctx, size, name); + + if (p) { + memset(p, '\0', size); + } + + return p; +} + +/* + memdup with a talloc. +*/ +_PUBLIC_ void *_talloc_memdup(const void *t, const void *p, size_t size, const char *name) +{ + void *newp = NULL; + + if (likely(size > 0) && unlikely(p == NULL)) { + return NULL; + } + + newp = _talloc_named_const(t, size, name); + if (likely(newp != NULL) && likely(size > 0)) { + memcpy(newp, p, size); + } + + return newp; +} + +static inline char *__talloc_strlendup(const void *t, const char *p, size_t len) +{ + char *ret; + struct talloc_chunk *tc = NULL; + + ret = (char *)__talloc(t, len + 1, &tc); + if (unlikely(!ret)) return NULL; + + memcpy(ret, p, len); + ret[len] = 0; + + _tc_set_name_const(tc, ret); + return ret; +} + +/* + strdup with a talloc +*/ +_PUBLIC_ char *talloc_strdup(const void *t, const char *p) +{ + if (unlikely(!p)) return NULL; + return __talloc_strlendup(t, p, strlen(p)); +} + +/* + strndup with a talloc +*/ +_PUBLIC_ char *talloc_strndup(const void *t, const char *p, size_t n) +{ + if (unlikely(!p)) return NULL; + return __talloc_strlendup(t, p, strnlen(p, n)); +} + +static inline char *__talloc_strlendup_append(char *s, size_t slen, + const char *a, size_t alen) +{ + char *ret; + + ret = talloc_realloc(NULL, s, char, slen + alen + 1); + if (unlikely(!ret)) return NULL; + + /* append the string and the trailing \0 */ + memcpy(&ret[slen], a, alen); + ret[slen+alen] = 0; + + _tc_set_name_const(talloc_chunk_from_ptr(ret), ret); + return ret; +} + +/* + * Appends at the end of the string. + */ +_PUBLIC_ char *talloc_strdup_append(char *s, const char *a) +{ + if (unlikely(!s)) { + return talloc_strdup(NULL, a); + } + + if (unlikely(!a)) { + return s; + } + + return __talloc_strlendup_append(s, strlen(s), a, strlen(a)); +} + +/* + * Appends at the end of the talloc'ed buffer, + * not the end of the string. + */ +_PUBLIC_ char *talloc_strdup_append_buffer(char *s, const char *a) +{ + size_t slen; + + if (unlikely(!s)) { + return talloc_strdup(NULL, a); + } + + if (unlikely(!a)) { + return s; + } + + slen = talloc_get_size(s); + if (likely(slen > 0)) { + slen--; + } + + return __talloc_strlendup_append(s, slen, a, strlen(a)); +} + +/* + * Appends at the end of the string. + */ +_PUBLIC_ char *talloc_strndup_append(char *s, const char *a, size_t n) +{ + if (unlikely(!s)) { + return talloc_strndup(NULL, a, n); + } + + if (unlikely(!a)) { + return s; + } + + return __talloc_strlendup_append(s, strlen(s), a, strnlen(a, n)); +} + +/* + * Appends at the end of the talloc'ed buffer, + * not the end of the string. + */ +_PUBLIC_ char *talloc_strndup_append_buffer(char *s, const char *a, size_t n) +{ + size_t slen; + + if (unlikely(!s)) { + return talloc_strndup(NULL, a, n); + } + + if (unlikely(!a)) { + return s; + } + + slen = talloc_get_size(s); + if (likely(slen > 0)) { + slen--; + } + + return __talloc_strlendup_append(s, slen, a, strnlen(a, n)); +} + +#ifndef HAVE_VA_COPY +#ifdef HAVE___VA_COPY +#define va_copy(dest, src) __va_copy(dest, src) +#else +#define va_copy(dest, src) (dest) = (src) +#endif +#endif + +static struct talloc_chunk *_vasprintf_tc(const void *t, + const char *fmt, + va_list ap) PRINTF_ATTRIBUTE(2,0); + +static struct talloc_chunk *_vasprintf_tc(const void *t, + const char *fmt, + va_list ap) +{ + int vlen; + size_t len; + char *ret; + va_list ap2; + struct talloc_chunk *tc = NULL; + char buf[1024]; + + va_copy(ap2, ap); + vlen = vsnprintf(buf, sizeof(buf), fmt, ap2); + va_end(ap2); + if (unlikely(vlen < 0)) { + return NULL; + } + len = vlen; + if (unlikely(len + 1 < len)) { + return NULL; + } + + ret = (char *)__talloc(t, len+1, &tc); + if (unlikely(!ret)) return NULL; + + if (len < sizeof(buf)) { + memcpy(ret, buf, len+1); + } else { + va_copy(ap2, ap); + vsnprintf(ret, len+1, fmt, ap2); + va_end(ap2); + } + + _tc_set_name_const(tc, ret); + return tc; +} + +_PUBLIC_ char *talloc_vasprintf(const void *t, const char *fmt, va_list ap) +{ + struct talloc_chunk *tc = _vasprintf_tc(t, fmt, ap); + if (tc == NULL) { + return NULL; + } + return TC_PTR_FROM_CHUNK(tc); +} + + +/* + Perform string formatting, and return a pointer to newly allocated + memory holding the result, inside a memory pool. + */ +_PUBLIC_ char *talloc_asprintf(const void *t, const char *fmt, ...) +{ + va_list ap; + char *ret; + + va_start(ap, fmt); + ret = talloc_vasprintf(t, fmt, ap); + va_end(ap); + return ret; +} + +static inline char *__talloc_vaslenprintf_append(char *s, size_t slen, + const char *fmt, va_list ap) + PRINTF_ATTRIBUTE(3,0); + +static inline char *__talloc_vaslenprintf_append(char *s, size_t slen, + const char *fmt, va_list ap) +{ + ssize_t alen; + va_list ap2; + char c; + + va_copy(ap2, ap); + /* this call looks strange, but it makes it work on older solaris boxes */ + alen = vsnprintf(&c, 1, fmt, ap2); + va_end(ap2); + + if (alen <= 0) { + /* Either the vsnprintf failed or the format resulted in + * no characters being formatted. In the former case, we + * ought to return NULL, in the latter we ought to return + * the original string. Most current callers of this + * function expect it to never return NULL. + */ + return s; + } + + s = talloc_realloc(NULL, s, char, slen + alen + 1); + if (!s) return NULL; + + vsnprintf(s + slen, alen + 1, fmt, ap); + + _tc_set_name_const(talloc_chunk_from_ptr(s), s); + return s; +} + +/** + * Realloc @p s to append the formatted result of @p fmt and @p ap, + * and return @p s, which may have moved. Good for gradually + * accumulating output into a string buffer. Appends at the end + * of the string. + **/ +_PUBLIC_ char *talloc_vasprintf_append(char *s, const char *fmt, va_list ap) +{ + if (unlikely(!s)) { + return talloc_vasprintf(NULL, fmt, ap); + } + + return __talloc_vaslenprintf_append(s, strlen(s), fmt, ap); +} + +/** + * Realloc @p s to append the formatted result of @p fmt and @p ap, + * and return @p s, which may have moved. Always appends at the + * end of the talloc'ed buffer, not the end of the string. + **/ +_PUBLIC_ char *talloc_vasprintf_append_buffer(char *s, const char *fmt, va_list ap) +{ + size_t slen; + + if (unlikely(!s)) { + return talloc_vasprintf(NULL, fmt, ap); + } + + slen = talloc_get_size(s); + if (likely(slen > 0)) { + slen--; + } + + return __talloc_vaslenprintf_append(s, slen, fmt, ap); +} + +/* + Realloc @p s to append the formatted result of @p fmt and return @p + s, which may have moved. Good for gradually accumulating output + into a string buffer. + */ +_PUBLIC_ char *talloc_asprintf_append(char *s, const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + s = talloc_vasprintf_append(s, fmt, ap); + va_end(ap); + return s; +} + +/* + Realloc @p s to append the formatted result of @p fmt and return @p + s, which may have moved. Good for gradually accumulating output + into a buffer. + */ +_PUBLIC_ char *talloc_asprintf_append_buffer(char *s, const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + s = talloc_vasprintf_append_buffer(s, fmt, ap); + va_end(ap); + return s; +} + +/* + * Function to make string-building simple by handling intermediate + * realloc failures. See for example commit a37ea9d750e1. + */ +_PUBLIC_ void talloc_asprintf_addbuf(char **ps, const char *fmt, ...) +{ + va_list ap; + char *s = *ps; + char *t = NULL; + + if (s == NULL) { + return; + } + + va_start(ap, fmt); + t = talloc_vasprintf_append_buffer(s, fmt, ap); + va_end(ap); + + if (t == NULL) { + /* signal failure to the next caller */ + TALLOC_FREE(s); + *ps = NULL; + } else { + *ps = t; + } +} + +/* + alloc an array, checking for integer overflow in the array size +*/ +_PUBLIC_ void *_talloc_array(const void *ctx, size_t el_size, unsigned count, const char *name) +{ + if (count >= MAX_TALLOC_SIZE/el_size) { + return NULL; + } + return _talloc_named_const(ctx, el_size * count, name); +} + +/* + alloc an zero array, checking for integer overflow in the array size +*/ +_PUBLIC_ void *_talloc_zero_array(const void *ctx, size_t el_size, unsigned count, const char *name) +{ + if (count >= MAX_TALLOC_SIZE/el_size) { + return NULL; + } + return _talloc_zero(ctx, el_size * count, name); +} + +/* + realloc an array, checking for integer overflow in the array size +*/ +_PUBLIC_ void *_talloc_realloc_array(const void *ctx, void *ptr, size_t el_size, unsigned count, const char *name) +{ + if (count >= MAX_TALLOC_SIZE/el_size) { + return NULL; + } + return _talloc_realloc(ctx, ptr, el_size * count, name); +} + +/* + a function version of talloc_realloc(), so it can be passed as a function pointer + to libraries that want a realloc function (a realloc function encapsulates + all the basic capabilities of an allocation library, which is why this is useful) +*/ +_PUBLIC_ void *talloc_realloc_fn(const void *context, void *ptr, size_t size) +{ + return _talloc_realloc(context, ptr, size, NULL); +} + + +static int talloc_autofree_destructor(void *ptr) +{ + autofree_context = NULL; + return 0; +} + +/* + return a context which will be auto-freed on exit + this is useful for reducing the noise in leak reports +*/ +_PUBLIC_ void *talloc_autofree_context(void) +{ + if (autofree_context == NULL) { + autofree_context = _talloc_named_const(NULL, 0, "autofree_context"); + talloc_set_destructor(autofree_context, talloc_autofree_destructor); + talloc_setup_atexit(); + } + return autofree_context; +} + +_PUBLIC_ size_t talloc_get_size(const void *context) +{ + struct talloc_chunk *tc; + + if (context == NULL) { + return 0; + } + + tc = talloc_chunk_from_ptr(context); + + return tc->size; +} + +/* + find a parent of this context that has the given name, if any +*/ +_PUBLIC_ void *talloc_find_parent_byname(const void *context, const char *name) +{ + struct talloc_chunk *tc; + + if (context == NULL) { + return NULL; + } + + tc = talloc_chunk_from_ptr(context); + while (tc) { + if (tc->name && strcmp(tc->name, name) == 0) { + return TC_PTR_FROM_CHUNK(tc); + } + while (tc && tc->prev) tc = tc->prev; + if (tc) { + tc = tc->parent; + } + } + return NULL; +} + +/* + show the parentage of a context +*/ +_PUBLIC_ void talloc_show_parents(const void *context, FILE *file) +{ + struct talloc_chunk *tc; + + if (context == NULL) { + fprintf(file, "talloc no parents for NULL\n"); + return; + } + + tc = talloc_chunk_from_ptr(context); + fprintf(file, "talloc parents of '%s'\n", __talloc_get_name(context)); + while (tc) { + fprintf(file, "\t'%s'\n", __talloc_get_name(TC_PTR_FROM_CHUNK(tc))); + while (tc && tc->prev) tc = tc->prev; + if (tc) { + tc = tc->parent; + } + } + fflush(file); +} + +/* + return 1 if ptr is a parent of context +*/ +static int _talloc_is_parent(const void *context, const void *ptr, int depth) +{ + struct talloc_chunk *tc; + + if (context == NULL) { + return 0; + } + + tc = talloc_chunk_from_ptr(context); + while (tc) { + if (depth <= 0) { + return 0; + } + if (TC_PTR_FROM_CHUNK(tc) == ptr) return 1; + while (tc && tc->prev) tc = tc->prev; + if (tc) { + tc = tc->parent; + depth--; + } + } + return 0; +} + +/* + return 1 if ptr is a parent of context +*/ +_PUBLIC_ int talloc_is_parent(const void *context, const void *ptr) +{ + return _talloc_is_parent(context, ptr, TALLOC_MAX_DEPTH); +} + +/* + return the total size of memory used by this context and all children +*/ +static inline size_t _talloc_total_limit_size(const void *ptr, + struct talloc_memlimit *old_limit, + struct talloc_memlimit *new_limit) +{ + return _talloc_total_mem_internal(ptr, TOTAL_MEM_LIMIT, + old_limit, new_limit); +} + +static inline bool talloc_memlimit_check(struct talloc_memlimit *limit, size_t size) +{ + struct talloc_memlimit *l; + + for (l = limit; l != NULL; l = l->upper) { + if (l->max_size != 0 && + ((l->max_size <= l->cur_size) || + (l->max_size - l->cur_size < size))) { + return false; + } + } + + return true; +} + +/* + Update memory limits when freeing a talloc_chunk. +*/ +static void tc_memlimit_update_on_free(struct talloc_chunk *tc) +{ + size_t limit_shrink_size; + + if (!tc->limit) { + return; + } + + /* + * Pool entries don't count. Only the pools + * themselves are counted as part of the memory + * limits. Note that this also takes care of + * nested pools which have both flags + * TALLOC_FLAG_POOLMEM|TALLOC_FLAG_POOL set. + */ + if (tc->flags & TALLOC_FLAG_POOLMEM) { + return; + } + + /* + * If we are part of a memory limited context hierarchy + * we need to subtract the memory used from the counters + */ + + limit_shrink_size = tc->size+TC_HDR_SIZE; + + /* + * If we're deallocating a pool, take into + * account the prefix size added for the pool. + */ + + if (tc->flags & TALLOC_FLAG_POOL) { + limit_shrink_size += TP_HDR_SIZE; + } + + talloc_memlimit_shrink(tc->limit, limit_shrink_size); + + if (tc->limit->parent == tc) { + free(tc->limit); + } + + tc->limit = NULL; +} + +/* + Increase memory limit accounting after a malloc/realloc. +*/ +static void talloc_memlimit_grow(struct talloc_memlimit *limit, + size_t size) +{ + struct talloc_memlimit *l; + + for (l = limit; l != NULL; l = l->upper) { + size_t new_cur_size = l->cur_size + size; + if (new_cur_size < l->cur_size) { + talloc_abort("logic error in talloc_memlimit_grow\n"); + return; + } + l->cur_size = new_cur_size; + } +} + +/* + Decrease memory limit accounting after a free/realloc. +*/ +static void talloc_memlimit_shrink(struct talloc_memlimit *limit, + size_t size) +{ + struct talloc_memlimit *l; + + for (l = limit; l != NULL; l = l->upper) { + if (l->cur_size < size) { + talloc_abort("logic error in talloc_memlimit_shrink\n"); + return; + } + l->cur_size = l->cur_size - size; + } +} + +_PUBLIC_ int talloc_set_memlimit(const void *ctx, size_t max_size) +{ + struct talloc_chunk *tc = talloc_chunk_from_ptr(ctx); + struct talloc_memlimit *orig_limit; + struct talloc_memlimit *limit = NULL; + + if (tc->limit && tc->limit->parent == tc) { + tc->limit->max_size = max_size; + return 0; + } + orig_limit = tc->limit; + + limit = malloc(sizeof(struct talloc_memlimit)); + if (limit == NULL) { + return 1; + } + limit->parent = tc; + limit->max_size = max_size; + limit->cur_size = _talloc_total_limit_size(ctx, tc->limit, limit); + + if (orig_limit) { + limit->upper = orig_limit; + } else { + limit->upper = NULL; + } + + return 0; +} diff --git a/core/proot/src/main/cpp/talloc/talloc.h b/core/proot/src/main/cpp/talloc/talloc.h new file mode 100644 index 000000000..eef1a701b --- /dev/null +++ b/core/proot/src/main/cpp/talloc/talloc.h @@ -0,0 +1,1972 @@ +#ifndef _TALLOC_H_ +#define _TALLOC_H_ +/* + Unix SMB/CIFS implementation. + Samba temporary memory allocation functions + + Copyright (C) Andrew Tridgell 2004-2005 + Copyright (C) Stefan Metzmacher 2006 + + ** NOTE! The following LGPL license applies to the talloc + ** library. This does NOT imply that all of Samba is released + ** under the LGPL + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 3 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see . +*/ + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* for old gcc releases that don't have the feature test macro __has_attribute */ +#ifndef __has_attribute +#define __has_attribute(x) 0 +#endif + +#ifndef _PUBLIC_ +#if __has_attribute(visibility) +#define _PUBLIC_ __attribute__((visibility("default"))) +#else +#define _PUBLIC_ +#endif +#endif + +/** + * @defgroup talloc The talloc API + * + * talloc is a hierarchical, reference counted memory pool system with + * destructors. It is the core memory allocator used in Samba. + * + * @{ + */ + +#define TALLOC_VERSION_MAJOR 2 +#define TALLOC_VERSION_MINOR 4 + +_PUBLIC_ int talloc_version_major(void); +_PUBLIC_ int talloc_version_minor(void); +/* This is mostly useful only for testing */ +_PUBLIC_ int talloc_test_get_magic(void); + +/** + * @brief Define a talloc parent type + * + * As talloc is a hierarchial memory allocator, every talloc chunk is a + * potential parent to other talloc chunks. So defining a separate type for a + * talloc chunk is not strictly necessary. TALLOC_CTX is defined nevertheless, + * as it provides an indicator for function arguments. You will frequently + * write code like + * + * @code + * struct foo *foo_create(TALLOC_CTX *mem_ctx) + * { + * struct foo *result; + * result = talloc(mem_ctx, struct foo); + * if (result == NULL) return NULL; + * ... initialize foo ... + * return result; + * } + * @endcode + * + * In this type of allocating functions it is handy to have a general + * TALLOC_CTX type to indicate which parent to put allocated structures on. + */ +typedef void TALLOC_CTX; + +/* + this uses a little trick to allow __LINE__ to be stringified +*/ +#ifndef __location__ +#define __TALLOC_STRING_LINE1__(s) #s +#define __TALLOC_STRING_LINE2__(s) __TALLOC_STRING_LINE1__(s) +#define __TALLOC_STRING_LINE3__ __TALLOC_STRING_LINE2__(__LINE__) +#define __location__ __FILE__ ":" __TALLOC_STRING_LINE3__ +#endif + +#ifndef TALLOC_DEPRECATED +#define TALLOC_DEPRECATED 0 +#endif + +#ifndef PRINTF_ATTRIBUTE +#if __has_attribute(format) || (__GNUC__ >= 3) +/** Use gcc attribute to check printf fns. a1 is the 1-based index of + * the parameter containing the format, and a2 the index of the first + * argument. Note that some gcc 2.x versions don't handle this + * properly **/ +#define PRINTF_ATTRIBUTE(a1, a2) __attribute__ ((format (__printf__, a1, a2))) +#else +#define PRINTF_ATTRIBUTE(a1, a2) +#endif +#endif + +#ifndef _DEPRECATED_ +#if __has_attribute(deprecated) || (__GNUC__ >= 3) +#define _DEPRECATED_ __attribute__ ((deprecated)) +#else +#define _DEPRECATED_ +#endif +#endif +#ifdef DOXYGEN + +/** + * @brief Create a new talloc context. + * + * The talloc() macro is the core of the talloc library. It takes a memory + * context and a type, and returns a pointer to a new area of memory of the + * given type. + * + * The returned pointer is itself a talloc context, so you can use it as the + * context argument to more calls to talloc if you wish. + * + * The returned pointer is a "child" of the supplied context. This means that if + * you talloc_free() the context then the new child disappears as well. + * Alternatively you can free just the child. + * + * @param[in] ctx A talloc context to create a new reference on or NULL to + * create a new top level context. + * + * @param[in] type The type of memory to allocate. + * + * @return A type casted talloc context or NULL on error. + * + * @code + * unsigned int *a, *b; + * + * a = talloc(NULL, unsigned int); + * b = talloc(a, unsigned int); + * @endcode + * + * @see talloc_zero + * @see talloc_array + * @see talloc_steal + * @see talloc_free + */ +_PUBLIC_ void *talloc(const void *ctx, #type); +#else +#define talloc(ctx, type) (type *)talloc_named_const(ctx, sizeof(type), #type) +_PUBLIC_ void *_talloc(const void *context, size_t size); +#endif + +/** + * @brief Create a new top level talloc context. + * + * This function creates a zero length named talloc context as a top level + * context. It is equivalent to: + * + * @code + * talloc_named(NULL, 0, fmt, ...); + * @endcode + * @param[in] fmt Format string for the name. + * + * @param[in] ... Additional printf-style arguments. + * + * @return The allocated memory chunk, NULL on error. + * + * @see talloc_named() + */ +_PUBLIC_ void *talloc_init(const char *fmt, ...) PRINTF_ATTRIBUTE(1,2); + +#ifdef DOXYGEN +/** + * @brief Free a chunk of talloc memory. + * + * The talloc_free() function frees a piece of talloc memory, and all its + * children. You can call talloc_free() on any pointer returned by + * talloc(). + * + * The return value of talloc_free() indicates success or failure, with 0 + * returned for success and -1 for failure. A possible failure condition + * is if the pointer had a destructor attached to it and the destructor + * returned -1. See talloc_set_destructor() for details on + * destructors. Likewise, if "ptr" is NULL, then the function will make + * no modifications and return -1. + * + * From version 2.0 and onwards, as a special case, talloc_free() is + * refused on pointers that have more than one parent associated, as talloc + * would have no way of knowing which parent should be removed. This is + * different from older versions in the sense that always the reference to + * the most recently established parent has been destroyed. Hence to free a + * pointer that has more than one parent please use talloc_unlink(). + * + * To help you find problems in your code caused by this behaviour, if + * you do try and free a pointer with more than one parent then the + * talloc logging function will be called to give output like this: + * + * @code + * ERROR: talloc_free with references at some_dir/source/foo.c:123 + * reference at some_dir/source/other.c:325 + * reference at some_dir/source/third.c:121 + * @endcode + * + * Please see the documentation for talloc_set_log_fn() and + * talloc_set_log_stderr() for more information on talloc logging + * functions. + * + * If TALLOC_FREE_FILL environment variable is set, + * the memory occupied by the context is filled with the value of this variable. + * The value should be a numeric representation of the character you want to + * use. + * + * talloc_free() operates recursively on its children. + * + * @param[in] ptr The chunk to be freed. + * + * @return Returns 0 on success and -1 on error. A possible + * failure condition is if the pointer had a destructor + * attached to it and the destructor returned -1. Likewise, + * if "ptr" is NULL, then the function will make no + * modifications and returns -1. + * + * Example: + * @code + * unsigned int *a, *b; + * a = talloc(NULL, unsigned int); + * b = talloc(a, unsigned int); + * + * talloc_free(a); // Frees a and b + * @endcode + * + * @see talloc_set_destructor() + * @see talloc_unlink() + */ +_PUBLIC_ int talloc_free(void *ptr); +#else +#define talloc_free(ctx) _talloc_free(ctx, __location__) +_PUBLIC_ int _talloc_free(void *ptr, const char *location); +#endif + +/** + * @brief Free a talloc chunk's children. + * + * The function walks along the list of all children of a talloc context and + * talloc_free()s only the children, not the context itself. + * + * A NULL argument is handled as no-op. + * + * @param[in] ptr The chunk that you want to free the children of + * (NULL is allowed too) + */ +_PUBLIC_ void talloc_free_children(void *ptr); + +#ifdef DOXYGEN +/** + * @brief Assign a destructor function to be called when a chunk is freed. + * + * The function talloc_set_destructor() sets the "destructor" for the pointer + * "ptr". A destructor is a function that is called when the memory used by a + * pointer is about to be released. The destructor receives the pointer as an + * argument, and should return 0 for success and -1 for failure. + * + * The destructor can do anything it wants to, including freeing other pieces + * of memory. A common use for destructors is to clean up operating system + * resources (such as open file descriptors) contained in the structure the + * destructor is placed on. + * + * You can only place one destructor on a pointer. If you need more than one + * destructor then you can create a zero-length child of the pointer and place + * an additional destructor on that. + * + * To remove a destructor call talloc_set_destructor() with NULL for the + * destructor. + * + * If your destructor attempts to talloc_free() the pointer that it is the + * destructor for then talloc_free() will return -1 and the free will be + * ignored. This would be a pointless operation anyway, as the destructor is + * only called when the memory is just about to go away. + * + * @param[in] ptr The talloc chunk to add a destructor to. + * + * @param[in] destructor The destructor function to be called. NULL to remove + * it. + * + * Example: + * @code + * static int destroy_fd(int *fd) { + * close(*fd); + * return 0; + * } + * + * int *open_file(const char *filename) { + * int *fd = talloc(NULL, int); + * *fd = open(filename, O_RDONLY); + * if (*fd < 0) { + * talloc_free(fd); + * return NULL; + * } + * // Whenever they free this, we close the file. + * talloc_set_destructor(fd, destroy_fd); + * return fd; + * } + * @endcode + * + * @see talloc() + * @see talloc_free() + */ +_PUBLIC_ void talloc_set_destructor(const void *ptr, int (*destructor)(void *)); + +/** + * @brief Change a talloc chunk's parent. + * + * The talloc_steal() function changes the parent context of a talloc + * pointer. It is typically used when the context that the pointer is + * currently a child of is going to be freed and you wish to keep the + * memory for a longer time. + * + * To make the changed hierarchy less error-prone, you might consider to use + * talloc_move(). + * + * If you try and call talloc_steal() on a pointer that has more than one + * parent then the result is ambiguous. Talloc will choose to remove the + * parent that is currently indicated by talloc_parent() and replace it with + * the chosen parent. You will also get a message like this via the talloc + * logging functions: + * + * @code + * WARNING: talloc_steal with references at some_dir/source/foo.c:123 + * reference at some_dir/source/other.c:325 + * reference at some_dir/source/third.c:121 + * @endcode + * + * To unambiguously change the parent of a pointer please see the function + * talloc_reparent(). See the talloc_set_log_fn() documentation for more + * information on talloc logging. + * + * @param[in] new_ctx The new parent context. + * + * @param[in] ptr The talloc chunk to move. + * + * @return Returns the pointer that you pass it. It does not have + * any failure modes. + * + * @note It is possible to produce loops in the parent/child relationship + * if you are not careful with talloc_steal(). No guarantees are provided + * as to your sanity or the safety of your data if you do this. + */ +_PUBLIC_ void *talloc_steal(const void *new_ctx, const void *ptr); +#else /* DOXYGEN */ +/* try to make talloc_set_destructor() and talloc_steal() type safe, + if we have a recent gcc */ +#if (__GNUC__ >= 3) +#define _TALLOC_TYPEOF(ptr) __typeof__(ptr) +#define talloc_set_destructor(ptr, function) \ + do { \ + int (*_talloc_destructor_fn)(_TALLOC_TYPEOF(ptr)) = (function); \ + _talloc_set_destructor((ptr), (int (*)(void *))_talloc_destructor_fn); \ + } while(0) +/* this extremely strange macro is to avoid some braindamaged warning + stupidity in gcc 4.1.x */ +#define talloc_steal(ctx, ptr) ({ _TALLOC_TYPEOF(ptr) __talloc_steal_ret = (_TALLOC_TYPEOF(ptr))_talloc_steal_loc((ctx),(ptr), __location__); __talloc_steal_ret; }) +#else /* __GNUC__ >= 3 */ +#define talloc_set_destructor(ptr, function) \ + _talloc_set_destructor((ptr), (int (*)(void *))(function)) +#define _TALLOC_TYPEOF(ptr) void * +#define talloc_steal(ctx, ptr) (_TALLOC_TYPEOF(ptr))_talloc_steal_loc((ctx),(ptr), __location__) +#endif /* __GNUC__ >= 3 */ +_PUBLIC_ void _talloc_set_destructor(const void *ptr, int (*_destructor)(void *)); +_PUBLIC_ void *_talloc_steal_loc(const void *new_ctx, const void *ptr, const char *location); +#endif /* DOXYGEN */ + +/** + * @brief Assign a name to a talloc chunk. + * + * Each talloc pointer has a "name". The name is used principally for + * debugging purposes, although it is also possible to set and get the name on + * a pointer in as a way of "marking" pointers in your code. + * + * The main use for names on pointer is for "talloc reports". See + * talloc_report() and talloc_report_full() for details. Also see + * talloc_enable_leak_report() and talloc_enable_leak_report_full(). + * + * The talloc_set_name() function allocates memory as a child of the + * pointer. It is logically equivalent to: + * + * @code + * talloc_set_name_const(ptr, talloc_asprintf(ptr, fmt, ...)); + * @endcode + * + * @param[in] ptr The talloc chunk to assign a name to. + * + * @param[in] fmt Format string for the name. + * + * @param[in] ... Add printf-style additional arguments. + * + * @return The assigned name, NULL on error. + * + * @note Multiple calls to talloc_set_name() will allocate more memory without + * releasing the name. All of the memory is released when the ptr is freed + * using talloc_free(). + */ +_PUBLIC_ const char *talloc_set_name(const void *ptr, const char *fmt, ...) PRINTF_ATTRIBUTE(2,3); + +#ifdef DOXYGEN +/** + * @brief Change a talloc chunk's parent. + * + * This function has the same effect as talloc_steal(), and additionally sets + * the source pointer to NULL. You would use it like this: + * + * @code + * struct foo *X = talloc(tmp_ctx, struct foo); + * struct foo *Y; + * Y = talloc_move(new_ctx, &X); + * @endcode + * + * @param[in] new_ctx The new parent context. + * + * @param[in] pptr Pointer to a pointer to the talloc chunk to move. + * + * @return The pointer to the talloc chunk that moved. + * It does not have any failure modes. + * + */ +_PUBLIC_ void *talloc_move(const void *new_ctx, void **pptr); +#else +#define talloc_move(ctx, pptr) (_TALLOC_TYPEOF(*(pptr)))_talloc_move((ctx),(void *)(pptr)) +_PUBLIC_ void *_talloc_move(const void *new_ctx, const void *pptr); +#endif + +/** + * @brief Assign a name to a talloc chunk. + * + * The function is just like talloc_set_name(), but it takes a string constant, + * and is much faster. It is extensively used by the "auto naming" macros, such + * as talloc_p(). + * + * This function does not allocate any memory. It just copies the supplied + * pointer into the internal representation of the talloc ptr. This means you + * must not pass a name pointer to memory that will disappear before the ptr + * is freed with talloc_free(). + * + * @param[in] ptr The talloc chunk to assign a name to. + * + * @param[in] name Format string for the name. + */ +_PUBLIC_ void talloc_set_name_const(const void *ptr, const char *name); + +/** + * @brief Create a named talloc chunk. + * + * The talloc_named() function creates a named talloc pointer. It is + * equivalent to: + * + * @code + * ptr = talloc_size(context, size); + * talloc_set_name(ptr, fmt, ....); + * @endcode + * + * @param[in] context The talloc context to hang the result off. + * + * @param[in] size Number of char's that you want to allocate. + * + * @param[in] fmt Format string for the name. + * + * @param[in] ... Additional printf-style arguments. + * + * @return The allocated memory chunk, NULL on error. + * + * @see talloc_set_name() + */ +_PUBLIC_ void *talloc_named(const void *context, size_t size, + const char *fmt, ...) PRINTF_ATTRIBUTE(3,4); + +/** + * @brief Basic routine to allocate a chunk of memory. + * + * This is equivalent to: + * + * @code + * ptr = talloc_size(context, size); + * talloc_set_name_const(ptr, name); + * @endcode + * + * @param[in] context The parent context. + * + * @param[in] size The number of char's that we want to allocate. + * + * @param[in] name The name the talloc block has. + * + * @return The allocated memory chunk, NULL on error. + */ +_PUBLIC_ void *talloc_named_const(const void *context, size_t size, const char *name); + +#ifdef DOXYGEN +/** + * @brief Untyped allocation. + * + * The function should be used when you don't have a convenient type to pass to + * talloc(). Unlike talloc(), it is not type safe (as it returns a void *), so + * you are on your own for type checking. + * + * Best to use talloc() or talloc_array() instead. + * + * @param[in] ctx The talloc context to hang the result off. + * + * @param[in] size Number of char's that you want to allocate. + * + * @return The allocated memory chunk, NULL on error. + * + * Example: + * @code + * void *mem = talloc_size(NULL, 100); + * @endcode + */ +_PUBLIC_ void *talloc_size(const void *ctx, size_t size); +#else +#define talloc_size(ctx, size) talloc_named_const(ctx, size, __location__) +#endif + +#ifdef DOXYGEN +/** + * @brief Allocate into a typed pointer. + * + * The talloc_ptrtype() macro should be used when you have a pointer and want + * to allocate memory to point at with this pointer. When compiling with + * gcc >= 3 it is typesafe. Note this is a wrapper of talloc_size() and + * talloc_get_name() will return the current location in the source file and + * not the type. + * + * @param[in] ctx The talloc context to hang the result off. + * + * @param[in] type The pointer you want to assign the result to. + * + * @return The properly casted allocated memory chunk, NULL on + * error. + * + * Example: + * @code + * unsigned int *a = talloc_ptrtype(NULL, a); + * @endcode + */ +_PUBLIC_ void *talloc_ptrtype(const void *ctx, #type); +#else +#define talloc_ptrtype(ctx, ptr) (_TALLOC_TYPEOF(ptr))talloc_size(ctx, sizeof(*(ptr))) +#endif + +#ifdef DOXYGEN +/** + * @brief Allocate a new 0-sized talloc chunk. + * + * This is a utility macro that creates a new memory context hanging off an + * existing context, automatically naming it "talloc_new: __location__" where + * __location__ is the source line it is called from. It is particularly + * useful for creating a new temporary working context. + * + * @param[in] ctx The talloc parent context. + * + * @return A new talloc chunk, NULL on error. + */ +_PUBLIC_ void *talloc_new(const void *ctx); +#else +#define talloc_new(ctx) talloc_named_const(ctx, 0, "talloc_new: " __location__) +#endif + +#ifdef DOXYGEN +/** + * @brief Allocate a 0-initizialized structure. + * + * The macro is equivalent to: + * + * @code + * ptr = talloc(ctx, type); + * if (ptr) memset(ptr, 0, sizeof(type)); + * @endcode + * + * @param[in] ctx The talloc context to hang the result off. + * + * @param[in] type The type that we want to allocate. + * + * @return Pointer to a piece of memory, properly cast to 'type *', + * NULL on error. + * + * Example: + * @code + * unsigned int *a, *b; + * a = talloc_zero(NULL, unsigned int); + * b = talloc_zero(a, unsigned int); + * @endcode + * + * @see talloc() + * @see talloc_zero_size() + * @see talloc_zero_array() + */ +_PUBLIC_ void *talloc_zero(const void *ctx, #type); + +/** + * @brief Allocate untyped, 0-initialized memory. + * + * @param[in] ctx The talloc context to hang the result off. + * + * @param[in] size Number of char's that you want to allocate. + * + * @return The allocated memory chunk. + */ +_PUBLIC_ void *talloc_zero_size(const void *ctx, size_t size); +#else +#define talloc_zero(ctx, type) (type *)_talloc_zero(ctx, sizeof(type), #type) +#define talloc_zero_size(ctx, size) _talloc_zero(ctx, size, __location__) +_PUBLIC_ void *_talloc_zero(const void *ctx, size_t size, const char *name); +#endif + +/** + * @brief Return the name of a talloc chunk. + * + * @param[in] ptr The talloc chunk. + * + * @return The current name for the given talloc pointer. + * + * @see talloc_set_name() + */ +_PUBLIC_ const char *talloc_get_name(const void *ptr); + +/** + * @brief Verify that a talloc chunk carries a specified name. + * + * This function checks if a pointer has the specified name. If it does + * then the pointer is returned. + * + * @param[in] ptr The talloc chunk to check. + * + * @param[in] name The name to check against. + * + * @return The pointer if the name matches, NULL if it doesn't. + */ +_PUBLIC_ void *talloc_check_name(const void *ptr, const char *name); + +/** + * @brief Get the parent chunk of a pointer. + * + * @param[in] ptr The talloc pointer to inspect. + * + * @return The talloc parent of ptr, NULL on error. + */ +_PUBLIC_ void *talloc_parent(const void *ptr); + +/** + * @brief Get a talloc chunk's parent name. + * + * @param[in] ptr The talloc pointer to inspect. + * + * @return The name of ptr's parent chunk. + */ +_PUBLIC_ const char *talloc_parent_name(const void *ptr); + +/** + * @brief Get the size of a talloc chunk. + * + * This function lets you know the amount of memory allocated so far by + * this context. It does NOT account for subcontext memory. + * This can be used to calculate the size of an array. + * + * @param[in] ctx The talloc chunk. + * + * @return The size of the talloc chunk. + */ +_PUBLIC_ size_t talloc_get_size(const void *ctx); + +/** + * @brief Get the total size of a talloc chunk including its children. + * + * The function returns the total size in bytes used by this pointer and all + * child pointers. Mostly useful for debugging. + * + * Passing NULL is allowed, but it will only give a meaningful result if + * talloc_enable_leak_report() or talloc_enable_leak_report_full() has + * been called. + * + * @param[in] ptr The talloc chunk. + * + * @return The total size. + */ +_PUBLIC_ size_t talloc_total_size(const void *ptr); + +/** + * @brief Get the number of talloc chunks hanging off a chunk. + * + * The talloc_total_blocks() function returns the total memory block + * count used by this pointer and all child pointers. Mostly useful for + * debugging. + * + * Passing NULL is allowed, but it will only give a meaningful result if + * talloc_enable_leak_report() or talloc_enable_leak_report_full() has + * been called. + * + * @param[in] ptr The talloc chunk. + * + * @return The total size. + */ +_PUBLIC_ size_t talloc_total_blocks(const void *ptr); + +#ifdef DOXYGEN +/** + * @brief Duplicate a memory area into a talloc chunk. + * + * The function is equivalent to: + * + * @code + * ptr = talloc_size(ctx, size); + * if (ptr) memcpy(ptr, p, size); + * @endcode + * + * @param[in] t The talloc context to hang the result off. + * + * @param[in] p The memory chunk you want to duplicate. + * + * @param[in] size Number of chars that you want to copy. + * + * @return The allocated memory chunk. + * + * @see talloc_size() + */ +_PUBLIC_ void *talloc_memdup(const void *t, const void *p, size_t size); +#else +#define talloc_memdup(t, p, size) _talloc_memdup(t, p, size, __location__) +_PUBLIC_ void *_talloc_memdup(const void *t, const void *p, size_t size, const char *name); +#endif + +#ifdef DOXYGEN +/** + * @brief Assign a type to a talloc chunk. + * + * This macro allows you to force the name of a pointer to be of a particular + * type. This can be used in conjunction with talloc_get_type() to do type + * checking on void* pointers. + * + * It is equivalent to this: + * + * @code + * talloc_set_name_const(ptr, #type) + * @endcode + * + * @param[in] ptr The talloc chunk to assign the type to. + * + * @param[in] type The type to assign. + */ +_PUBLIC_ void talloc_set_type(const char *ptr, #type); + +/** + * @brief Get a typed pointer out of a talloc pointer. + * + * This macro allows you to do type checking on talloc pointers. It is + * particularly useful for void* private pointers. It is equivalent to + * this: + * + * @code + * (type *)talloc_check_name(ptr, #type) + * @endcode + * + * @param[in] ptr The talloc pointer to check. + * + * @param[in] type The type to check against. + * + * @return The properly casted pointer given by ptr, NULL on error. + */ +type *talloc_get_type(const void *ptr, #type); +#else +#define talloc_set_type(ptr, type) talloc_set_name_const(ptr, #type) +#define talloc_get_type(ptr, type) (type *)talloc_check_name(ptr, #type) +#endif + +#ifdef DOXYGEN +/** + * @brief Safely turn a void pointer into a typed pointer. + * + * This macro is used together with talloc(mem_ctx, struct foo). If you had to + * assign the talloc chunk pointer to some void pointer variable, + * talloc_get_type_abort() is the recommended way to convert the void + * pointer back to a typed pointer. + * + * @param[in] ptr The void pointer to convert. + * + * @param[in] type The type that this chunk contains + * + * @return The same value as ptr, type-checked and properly cast. + */ +_PUBLIC_ void *talloc_get_type_abort(const void *ptr, #type); +#else +#ifdef TALLOC_GET_TYPE_ABORT_NOOP +#define talloc_get_type_abort(ptr, type) (type *)(ptr) +#else +#define talloc_get_type_abort(ptr, type) (type *)_talloc_get_type_abort(ptr, #type, __location__) +#endif +_PUBLIC_ void *_talloc_get_type_abort(const void *ptr, const char *name, const char *location); +#endif + +/** + * @brief Find a parent context by name. + * + * Find a parent memory context of the current context that has the given + * name. This can be very useful in complex programs where it may be + * difficult to pass all information down to the level you need, but you + * know the structure you want is a parent of another context. + * + * @param[in] ctx The talloc chunk to start from. + * + * @param[in] name The name of the parent we look for. + * + * @return The memory context we are looking for, NULL if not + * found. + */ +_PUBLIC_ void *talloc_find_parent_byname(const void *ctx, const char *name); + +#ifdef DOXYGEN +/** + * @brief Find a parent context by type. + * + * Find a parent memory context of the current context that has the given + * name. This can be very useful in complex programs where it may be + * difficult to pass all information down to the level you need, but you + * know the structure you want is a parent of another context. + * + * Like talloc_find_parent_byname() but takes a type, making it typesafe. + * + * @param[in] ptr The talloc chunk to start from. + * + * @param[in] type The type of the parent to look for. + * + * @return The memory context we are looking for, NULL if not + * found. + */ +_PUBLIC_ void *talloc_find_parent_bytype(const void *ptr, #type); +#else +#define talloc_find_parent_bytype(ptr, type) (type *)talloc_find_parent_byname(ptr, #type) +#endif + +/** + * @brief Allocate a talloc pool. + * + * A talloc pool is a pure optimization for specific situations. In the + * release process for Samba 3.2 we found out that we had become considerably + * slower than Samba 3.0 was. Profiling showed that malloc(3) was a large CPU + * consumer in benchmarks. For Samba 3.2 we have internally converted many + * static buffers to dynamically allocated ones, so malloc(3) being beaten + * more was no surprise. But it made us slower. + * + * talloc_pool() is an optimization to call malloc(3) a lot less for the use + * pattern Samba has: The SMB protocol is mainly a request/response protocol + * where we have to allocate a certain amount of memory per request and free + * that after the SMB reply is sent to the client. + * + * talloc_pool() creates a talloc chunk that you can use as a talloc parent + * exactly as you would use any other ::TALLOC_CTX. The difference is that + * when you talloc a child of this pool, no malloc(3) is done. Instead, talloc + * just increments a pointer inside the talloc_pool. This also works + * recursively. If you use the child of the talloc pool as a parent for + * grand-children, their memory is also taken from the talloc pool. + * + * If there is not enough memory in the pool to allocate the new child, + * it will create a new talloc chunk as if the parent was a normal talloc + * context. + * + * If you talloc_free() children of a talloc pool, the memory is not given + * back to the system. Instead, free(3) is only called if the talloc_pool() + * itself is released with talloc_free(). + * + * The downside of a talloc pool is that if you talloc_move() a child of a + * talloc pool to a talloc parent outside the pool, the whole pool memory is + * not free(3)'ed until that moved chunk is also talloc_free()ed. + * + * @param[in] context The talloc context to hang the result off. + * + * @param[in] size Size of the talloc pool. + * + * @return The allocated talloc pool, NULL on error. + */ +_PUBLIC_ void *talloc_pool(const void *context, size_t size); + +#ifdef DOXYGEN +/** + * @brief Allocate a talloc object as/with an additional pool. + * + * This is like talloc_pool(), but's it's more flexible + * and allows an object to be a pool for its children. + * + * @param[in] ctx The talloc context to hang the result off. + * + * @param[in] type The type that we want to allocate. + * + * @param[in] num_subobjects The expected number of subobjects, which will + * be allocated within the pool. This allocates + * space for talloc_chunk headers. + * + * @param[in] total_subobjects_size The size that all subobjects can use in total. + * + * + * @return The allocated talloc object, NULL on error. + */ +_PUBLIC_ void *talloc_pooled_object(const void *ctx, #type, + unsigned num_subobjects, + size_t total_subobjects_size); +#else +#define talloc_pooled_object(_ctx, _type, \ + _num_subobjects, \ + _total_subobjects_size) \ + (_type *)_talloc_pooled_object((_ctx), sizeof(_type), #_type, \ + (_num_subobjects), \ + (_total_subobjects_size)) +_PUBLIC_ void *_talloc_pooled_object(const void *ctx, + size_t type_size, + const char *type_name, + unsigned num_subobjects, + size_t total_subobjects_size); +#endif + +/** + * @brief Free a talloc chunk and NULL out the pointer. + * + * TALLOC_FREE() frees a pointer and sets it to NULL. Use this if you want + * immediate feedback (i.e. crash) if you use a pointer after having free'ed + * it. + * + * @param[in] ctx The chunk to be freed. + */ +#define TALLOC_FREE(ctx) do { if (ctx != NULL) { talloc_free(ctx); ctx=NULL; } } while(0) + +/* @} ******************************************************************/ + +/** + * \defgroup talloc_ref The talloc reference function. + * @ingroup talloc + * + * This module contains the definitions around talloc references + * + * @{ + */ + +/** + * @brief Increase the reference count of a talloc chunk. + * + * The talloc_increase_ref_count(ptr) function is exactly equivalent to: + * + * @code + * talloc_reference(NULL, ptr); + * @endcode + * + * You can use either syntax, depending on which you think is clearer in + * your code. + * + * @param[in] ptr The pointer to increase the reference count. + * + * @return 0 on success, -1 on error. + */ +_PUBLIC_ int talloc_increase_ref_count(const void *ptr); + +/** + * @brief Get the number of references to a talloc chunk. + * + * @param[in] ptr The pointer to retrieve the reference count from. + * + * @return The number of references. + */ +_PUBLIC_ size_t talloc_reference_count(const void *ptr); + +#ifdef DOXYGEN +/** + * @brief Create an additional talloc parent to a pointer. + * + * The talloc_reference() function makes "context" an additional parent of + * ptr. Each additional reference consumes around 48 bytes of memory on intel + * x86 platforms. + * + * If ptr is NULL, then the function is a no-op, and simply returns NULL. + * + * After creating a reference you can free it in one of the following ways: + * + * - you can talloc_free() any parent of the original pointer. That + * will reduce the number of parents of this pointer by 1, and will + * cause this pointer to be freed if it runs out of parents. + * + * - you can talloc_free() the pointer itself if it has at maximum one + * parent. This behaviour has been changed since the release of version + * 2.0. Further information in the description of "talloc_free". + * + * For more control on which parent to remove, see talloc_unlink() + * @param[in] ctx The additional parent. + * + * @param[in] ptr The pointer you want to create an additional parent for. + * + * @return The original pointer 'ptr', NULL if talloc ran out of + * memory in creating the reference. + * + * @warning You should try to avoid using this interface. It turns a beautiful + * talloc-tree into a graph. It is often really hard to debug if you + * screw something up by accident. + * + * Example: + * @code + * unsigned int *a, *b, *c; + * a = talloc(NULL, unsigned int); + * b = talloc(NULL, unsigned int); + * c = talloc(a, unsigned int); + * // b also serves as a parent of c. + * talloc_reference(b, c); + * @endcode + * + * @see talloc_unlink() + */ +_PUBLIC_ void *talloc_reference(const void *ctx, const void *ptr); +#else +#define talloc_reference(ctx, ptr) (_TALLOC_TYPEOF(ptr))_talloc_reference_loc((ctx),(ptr), __location__) +_PUBLIC_ void *_talloc_reference_loc(const void *context, const void *ptr, const char *location); +#endif + +/** + * @brief Remove a specific parent from a talloc chunk. + * + * The function removes a specific parent from ptr. The context passed must + * either be a context used in talloc_reference() with this pointer, or must be + * a direct parent of ptr. + * + * You can just use talloc_free() instead of talloc_unlink() if there + * is at maximum one parent. This behaviour has been changed since the + * release of version 2.0. Further information in the description of + * "talloc_free". + * + * @param[in] context The talloc parent to remove. + * + * @param[in] ptr The talloc ptr you want to remove the parent from. + * + * @return 0 on success, -1 on error. + * + * @note If the parent has already been removed using talloc_free() then + * this function will fail and will return -1. Likewise, if ptr is NULL, + * then the function will make no modifications and return -1. + * + * @warning You should try to avoid using this interface. It turns a beautiful + * talloc-tree into a graph. It is often really hard to debug if you + * screw something up by accident. + * + * Example: + * @code + * unsigned int *a, *b, *c; + * a = talloc(NULL, unsigned int); + * b = talloc(NULL, unsigned int); + * c = talloc(a, unsigned int); + * // b also serves as a parent of c. + * talloc_reference(b, c); + * talloc_unlink(b, c); + * @endcode + */ +_PUBLIC_ int talloc_unlink(const void *context, void *ptr); + +/** + * @brief Provide a talloc context that is freed at program exit. + * + * This is a handy utility function that returns a talloc context + * which will be automatically freed on program exit. This can be used + * to reduce the noise in memory leak reports. + * + * Never use this in code that might be used in objects loaded with + * dlopen and unloaded with dlclose. talloc_autofree_context() + * internally uses atexit(3). Some platforms like modern Linux handles + * this fine, but for example FreeBSD does not deal well with dlopen() + * and atexit() used simultaneously: dlclose() does not clean up the + * list of atexit-handlers, so when the program exits the code that + * was registered from within talloc_autofree_context() is gone, the + * program crashes at exit. + * + * @return A talloc context, NULL on error. + */ +_PUBLIC_ void *talloc_autofree_context(void) _DEPRECATED_; + +/** + * @brief Show the parentage of a context. + * + * @param[in] context The talloc context to look at. + * + * @param[in] file The output to use, a file, stdout or stderr. + */ +_PUBLIC_ void talloc_show_parents(const void *context, FILE *file); + +/** + * @brief Check if a context is parent of a talloc chunk. + * + * This checks if context is referenced in the talloc hierarchy above ptr. + * + * @param[in] context The assumed talloc context. + * + * @param[in] ptr The talloc chunk to check. + * + * @return Return 1 if this is the case, 0 if not. + */ +_PUBLIC_ int talloc_is_parent(const void *context, const void *ptr); + +/** + * @brief Change the parent context of a talloc pointer. + * + * The function changes the parent context of a talloc pointer. It is typically + * used when the context that the pointer is currently a child of is going to be + * freed and you wish to keep the memory for a longer time. + * + * The difference between talloc_reparent() and talloc_steal() is that + * talloc_reparent() can specify which parent you wish to change. This is + * useful when a pointer has multiple parents via references. + * + * @param[in] old_parent + * @param[in] new_parent + * @param[in] ptr + * + * @return Return the pointer you passed. It does not have any + * failure modes. + */ +_PUBLIC_ void *talloc_reparent(const void *old_parent, const void *new_parent, const void *ptr); + +/* @} ******************************************************************/ + +/** + * @defgroup talloc_array The talloc array functions + * @ingroup talloc + * + * Talloc contains some handy helpers for handling Arrays conveniently + * + * @{ + */ + +#ifdef DOXYGEN +/** + * @brief Allocate an array. + * + * The macro is equivalent to: + * + * @code + * (type *)talloc_size(ctx, sizeof(type) * count); + * @endcode + * + * except that it provides integer overflow protection for the multiply, + * returning NULL if the multiply overflows. + * + * @param[in] ctx The talloc context to hang the result off. + * + * @param[in] type The type that we want to allocate. + * + * @param[in] count The number of 'type' elements you want to allocate. + * + * @return The allocated result, properly cast to 'type *', NULL on + * error. + * + * Example: + * @code + * unsigned int *a, *b; + * a = talloc_zero(NULL, unsigned int); + * b = talloc_array(a, unsigned int, 100); + * @endcode + * + * @see talloc() + * @see talloc_zero_array() + */ +_PUBLIC_ void *talloc_array(const void *ctx, #type, unsigned count); +#else +#define talloc_array(ctx, type, count) (type *)_talloc_array(ctx, sizeof(type), count, #type) +_PUBLIC_ void *_talloc_array(const void *ctx, size_t el_size, unsigned count, const char *name); +#endif + +#ifdef DOXYGEN +/** + * @brief Allocate an array. + * + * @param[in] ctx The talloc context to hang the result off. + * + * @param[in] size The size of an array element. + * + * @param[in] count The number of elements you want to allocate. + * + * @return The allocated result, NULL on error. + */ +_PUBLIC_ void *talloc_array_size(const void *ctx, size_t size, unsigned count); +#else +#define talloc_array_size(ctx, size, count) _talloc_array(ctx, size, count, __location__) +#endif + +#ifdef DOXYGEN +/** + * @brief Allocate an array into a typed pointer. + * + * The macro should be used when you have a pointer to an array and want to + * allocate memory of an array to point at with this pointer. When compiling + * with gcc >= 3 it is typesafe. Note this is a wrapper of talloc_array_size() + * and talloc_get_name() will return the current location in the source file + * and not the type. + * + * @param[in] ctx The talloc context to hang the result off. + * + * @param[in] ptr The pointer you want to assign the result to. + * + * @param[in] count The number of elements you want to allocate. + * + * @return The allocated memory chunk, properly casted. NULL on + * error. + */ +void *talloc_array_ptrtype(const void *ctx, const void *ptr, unsigned count); +#else +#define talloc_array_ptrtype(ctx, ptr, count) (_TALLOC_TYPEOF(ptr))talloc_array_size(ctx, sizeof(*(ptr)), count) +#endif + +#ifdef DOXYGEN +/** + * @brief Get the number of elements in a talloc'ed array. + * + * A talloc chunk carries its own size, so for talloc'ed arrays it is not + * necessary to store the number of elements explicitly. + * + * @param[in] ctx The allocated array. + * + * @return The number of elements in ctx. + */ +size_t talloc_array_length(const void *ctx); +#else +#define talloc_array_length(ctx) (talloc_get_size(ctx)/sizeof(*ctx)) +#endif + +#ifdef DOXYGEN +/** + * @brief Allocate a zero-initialized array + * + * @param[in] ctx The talloc context to hang the result off. + * + * @param[in] type The type that we want to allocate. + * + * @param[in] count The number of "type" elements you want to allocate. + * + * @return The allocated result casted to "type *", NULL on error. + * + * The talloc_zero_array() macro is equivalent to: + * + * @code + * ptr = talloc_array(ctx, type, count); + * if (ptr) memset(ptr, 0, sizeof(type) * count); + * @endcode + */ +void *talloc_zero_array(const void *ctx, #type, unsigned count); +#else +#define talloc_zero_array(ctx, type, count) (type *)_talloc_zero_array(ctx, sizeof(type), count, #type) +_PUBLIC_ void *_talloc_zero_array(const void *ctx, + size_t el_size, + unsigned count, + const char *name); +#endif + +#ifdef DOXYGEN +/** + * @brief Change the size of a talloc array. + * + * The macro changes the size of a talloc pointer. The 'count' argument is the + * number of elements of type 'type' that you want the resulting pointer to + * hold. + * + * talloc_realloc() has the following equivalences: + * + * @code + * talloc_realloc(ctx, NULL, type, 1) ==> talloc(ctx, type); + * talloc_realloc(ctx, NULL, type, N) ==> talloc_array(ctx, type, N); + * talloc_realloc(ctx, ptr, type, 0) ==> talloc_free(ptr); + * @endcode + * + * The "context" argument is only used if "ptr" is NULL, otherwise it is + * ignored. + * + * @param[in] ctx The parent context used if ptr is NULL. + * + * @param[in] ptr The chunk to be resized. + * + * @param[in] type The type of the array element inside ptr. + * + * @param[in] count The intended number of array elements. + * + * @return The new array, NULL on error. The call will fail either + * due to a lack of memory, or because the pointer has more + * than one parent (see talloc_reference()). + */ +_PUBLIC_ void *talloc_realloc(const void *ctx, void *ptr, #type, size_t count); +#else +#define talloc_realloc(ctx, p, type, count) (type *)_talloc_realloc_array(ctx, p, sizeof(type), count, #type) +_PUBLIC_ void *_talloc_realloc_array(const void *ctx, void *ptr, size_t el_size, unsigned count, const char *name); +#endif + +#ifdef DOXYGEN +/** + * @brief Untyped realloc to change the size of a talloc array. + * + * The macro is useful when the type is not known so the typesafe + * talloc_realloc() cannot be used. + * + * @param[in] ctx The parent context used if 'ptr' is NULL. + * + * @param[in] ptr The chunk to be resized. + * + * @param[in] size The new chunk size. + * + * @return The new array, NULL on error. + */ +void *talloc_realloc_size(const void *ctx, void *ptr, size_t size); +#else +#define talloc_realloc_size(ctx, ptr, size) _talloc_realloc(ctx, ptr, size, __location__) +_PUBLIC_ void *_talloc_realloc(const void *context, void *ptr, size_t size, const char *name); +#endif + +/** + * @brief Provide a function version of talloc_realloc_size. + * + * This is a non-macro version of talloc_realloc(), which is useful as + * libraries sometimes want a ralloc function pointer. A realloc() + * implementation encapsulates the functionality of malloc(), free() and + * realloc() in one call, which is why it is useful to be able to pass around + * a single function pointer. + * + * @param[in] context The parent context used if ptr is NULL. + * + * @param[in] ptr The chunk to be resized. + * + * @param[in] size The new chunk size. + * + * @return The new chunk, NULL on error. + */ +_PUBLIC_ void *talloc_realloc_fn(const void *context, void *ptr, size_t size); + +/* @} ******************************************************************/ + +/** + * @defgroup talloc_string The talloc string functions. + * @ingroup talloc + * + * talloc string allocation and manipulation functions. + * @{ + */ + +/** + * @brief Duplicate a string into a talloc chunk. + * + * This function is equivalent to: + * + * @code + * ptr = talloc_size(ctx, strlen(p)+1); + * if (ptr) memcpy(ptr, p, strlen(p)+1); + * @endcode + * + * This functions sets the name of the new pointer to the passed + * string. This is equivalent to: + * + * @code + * talloc_set_name_const(ptr, ptr) + * @endcode + * + * @param[in] t The talloc context to hang the result off. + * + * @param[in] p The string you want to duplicate. + * + * @return The duplicated string, NULL on error. + */ +_PUBLIC_ char *talloc_strdup(const void *t, const char *p); + +/** + * @brief Append a string to given string. + * + * The destination string is reallocated to take + * strlen(s) + strlen(a) + 1 characters. + * + * This functions sets the name of the new pointer to the new + * string. This is equivalent to: + * + * @code + * talloc_set_name_const(ptr, ptr) + * @endcode + * + * If s == NULL then new context is created. + * + * @param[in] s The destination to append to. + * + * @param[in] a The string you want to append. + * + * @return The concatenated strings, NULL on error. + * + * @see talloc_strdup() + * @see talloc_strdup_append_buffer() + */ +_PUBLIC_ char *talloc_strdup_append(char *s, const char *a); + +/** + * @brief Append a string to a given buffer. + * + * This is a more efficient version of talloc_strdup_append(). It determines the + * length of the destination string by the size of the talloc context. + * + * Use this very carefully as it produces a different result than + * talloc_strdup_append() when a zero character is in the middle of the + * destination string. + * + * @code + * char *str_a = talloc_strdup(NULL, "hello world"); + * char *str_b = talloc_strdup(NULL, "hello world"); + * str_a[5] = str_b[5] = '\0' + * + * char *app = talloc_strdup_append(str_a, ", hello"); + * char *buf = talloc_strdup_append_buffer(str_b, ", hello"); + * + * printf("%s\n", app); // hello, hello (app = "hello, hello") + * printf("%s\n", buf); // hello (buf = "hello\0world, hello") + * @endcode + * + * If s == NULL then new context is created. + * + * @param[in] s The destination buffer to append to. + * + * @param[in] a The string you want to append. + * + * @return The concatenated strings, NULL on error. + * + * @see talloc_strdup() + * @see talloc_strdup_append() + * @see talloc_array_length() + */ +_PUBLIC_ char *talloc_strdup_append_buffer(char *s, const char *a); + +/** + * @brief Duplicate a length-limited string into a talloc chunk. + * + * This function is the talloc equivalent of the C library function strndup(3). + * + * This functions sets the name of the new pointer to the passed string. This is + * equivalent to: + * + * @code + * talloc_set_name_const(ptr, ptr) + * @endcode + * + * @param[in] t The talloc context to hang the result off. + * + * @param[in] p The string you want to duplicate. + * + * @param[in] n The maximum string length to duplicate. + * + * @return The duplicated string, NULL on error. + */ +_PUBLIC_ char *talloc_strndup(const void *t, const char *p, size_t n); + +/** + * @brief Append at most n characters of a string to given string. + * + * The destination string is reallocated to take + * strlen(s) + strnlen(a, n) + 1 characters. + * + * This functions sets the name of the new pointer to the new + * string. This is equivalent to: + * + * @code + * talloc_set_name_const(ptr, ptr) + * @endcode + * + * If s == NULL then new context is created. + * + * @param[in] s The destination string to append to. + * + * @param[in] a The source string you want to append. + * + * @param[in] n The number of characters you want to append from the + * string. + * + * @return The concatenated strings, NULL on error. + * + * @see talloc_strndup() + * @see talloc_strndup_append_buffer() + */ +_PUBLIC_ char *talloc_strndup_append(char *s, const char *a, size_t n); + +/** + * @brief Append at most n characters of a string to given buffer + * + * This is a more efficient version of talloc_strndup_append(). It determines + * the length of the destination string by the size of the talloc context. + * + * Use this very carefully as it produces a different result than + * talloc_strndup_append() when a zero character is in the middle of the + * destination string. + * + * @code + * char *str_a = talloc_strdup(NULL, "hello world"); + * char *str_b = talloc_strdup(NULL, "hello world"); + * str_a[5] = str_b[5] = '\0' + * + * char *app = talloc_strndup_append(str_a, ", hello", 7); + * char *buf = talloc_strndup_append_buffer(str_b, ", hello", 7); + * + * printf("%s\n", app); // hello, hello (app = "hello, hello") + * printf("%s\n", buf); // hello (buf = "hello\0world, hello") + * @endcode + * + * If s == NULL then new context is created. + * + * @param[in] s The destination buffer to append to. + * + * @param[in] a The source string you want to append. + * + * @param[in] n The number of characters you want to append from the + * string. + * + * @return The concatenated strings, NULL on error. + * + * @see talloc_strndup() + * @see talloc_strndup_append() + * @see talloc_array_length() + */ +_PUBLIC_ char *talloc_strndup_append_buffer(char *s, const char *a, size_t n); + +/** + * @brief Format a string given a va_list. + * + * This function is the talloc equivalent of the C library function + * vasprintf(3). + * + * This functions sets the name of the new pointer to the new string. This is + * equivalent to: + * + * @code + * talloc_set_name_const(ptr, ptr) + * @endcode + * + * @param[in] t The talloc context to hang the result off. + * + * @param[in] fmt The format string. + * + * @param[in] ap The parameters used to fill fmt. + * + * @return The formatted string, NULL on error. + */ +_PUBLIC_ char *talloc_vasprintf(const void *t, const char *fmt, va_list ap) PRINTF_ATTRIBUTE(2,0); + +/** + * @brief Format a string given a va_list and append it to the given destination + * string. + * + * @param[in] s The destination string to append to. + * + * @param[in] fmt The format string. + * + * @param[in] ap The parameters used to fill fmt. + * + * @return The formatted string, NULL on error. + * + * @see talloc_vasprintf() + */ +_PUBLIC_ char *talloc_vasprintf_append(char *s, const char *fmt, va_list ap) PRINTF_ATTRIBUTE(2,0); + +/** + * @brief Format a string given a va_list and append it to the given destination + * buffer. + * + * @param[in] s The destination buffer to append to. + * + * @param[in] fmt The format string. + * + * @param[in] ap The parameters used to fill fmt. + * + * @return The formatted string, NULL on error. + * + * @see talloc_vasprintf() + */ +_PUBLIC_ char *talloc_vasprintf_append_buffer(char *s, const char *fmt, va_list ap) PRINTF_ATTRIBUTE(2,0); + +/** + * @brief Build up a string buffer, handle allocation failure + * + * @param[in] ps Pointer to the talloc'ed string to be extended + * @param[in] fmt The format string + * @param[in] ... The parameters used to fill fmt. + * + * This does nothing if *ps is NULL and sets *ps to NULL if the + * intermediate reallocation fails. Useful when building up a string + * step by step, no intermediate NULL checks are required. + */ +_PUBLIC_ void talloc_asprintf_addbuf(char **ps, const char *fmt, ...) \ + PRINTF_ATTRIBUTE(2,3); + +/** + * @brief Format a string. + * + * This function is the talloc equivalent of the C library function asprintf(3). + * + * This functions sets the name of the new pointer to the new string. This is + * equivalent to: + * + * @code + * talloc_set_name_const(ptr, ptr) + * @endcode + * + * @param[in] t The talloc context to hang the result off. + * + * @param[in] fmt The format string. + * + * @param[in] ... The parameters used to fill fmt. + * + * @return The formatted string, NULL on error. + */ +_PUBLIC_ char *talloc_asprintf(const void *t, const char *fmt, ...) PRINTF_ATTRIBUTE(2,3); + +/** + * @brief Append a formatted string to another string. + * + * This function appends the given formatted string to the given string. Use + * this variant when the string in the current talloc buffer may have been + * truncated in length. + * + * This functions sets the name of the new pointer to the new + * string. This is equivalent to: + * + * @code + * talloc_set_name_const(ptr, ptr) + * @endcode + * + * If s == NULL then new context is created. + * + * @param[in] s The string to append to. + * + * @param[in] fmt The format string. + * + * @param[in] ... The parameters used to fill fmt. + * + * @return The formatted string, NULL on error. + */ +_PUBLIC_ char *talloc_asprintf_append(char *s, const char *fmt, ...) PRINTF_ATTRIBUTE(2,3); + +/** + * @brief Append a formatted string to another string. + * + * This is a more efficient version of talloc_asprintf_append(). It determines + * the length of the destination string by the size of the talloc context. + * + * Use this very carefully as it produces a different result than + * talloc_asprintf_append() when a zero character is in the middle of the + * destination string. + * + * @code + * char *str_a = talloc_strdup(NULL, "hello world"); + * char *str_b = talloc_strdup(NULL, "hello world"); + * str_a[5] = str_b[5] = '\0' + * + * char *app = talloc_asprintf_append(str_a, "%s", ", hello"); + * char *buf = talloc_strdup_append_buffer(str_b, "%s", ", hello"); + * + * printf("%s\n", app); // hello, hello (app = "hello, hello") + * printf("%s\n", buf); // hello (buf = "hello\0world, hello") + * @endcode + * + * If s == NULL then new context is created. + * + * @param[in] s The string to append to + * + * @param[in] fmt The format string. + * + * @param[in] ... The parameters used to fill fmt. + * + * @return The formatted string, NULL on error. + * + * @see talloc_asprintf() + * @see talloc_asprintf_append() + */ +_PUBLIC_ char *talloc_asprintf_append_buffer(char *s, const char *fmt, ...) PRINTF_ATTRIBUTE(2,3); + +/* @} ******************************************************************/ + +/** + * @defgroup talloc_debug The talloc debugging support functions + * @ingroup talloc + * + * To aid memory debugging, talloc contains routines to inspect the currently + * allocated memory hierarchy. + * + * @{ + */ + +/** + * @brief Walk a complete talloc hierarchy. + * + * This provides a more flexible reports than talloc_report(). It + * will recursively call the callback for the entire tree of memory + * referenced by the pointer. References in the tree are passed with + * is_ref = 1 and the pointer that is referenced. + * + * You can pass NULL for the pointer, in which case a report is + * printed for the top level memory context, but only if + * talloc_enable_leak_report() or talloc_enable_leak_report_full() + * has been called. + * + * The recursion is stopped when depth >= max_depth. + * max_depth = -1 means only stop at leaf nodes. + * + * @param[in] ptr The talloc chunk. + * + * @param[in] depth Internal parameter to control recursion. Call with 0. + * + * @param[in] max_depth Maximum recursion level. + * + * @param[in] callback Function to be called on every chunk. + * + * @param[in] private_data Private pointer passed to callback. + */ +_PUBLIC_ void talloc_report_depth_cb(const void *ptr, int depth, int max_depth, + void (*callback)(const void *ptr, + int depth, int max_depth, + int is_ref, + void *private_data), + void *private_data); + +/** + * @brief Print a talloc hierarchy. + * + * This provides a more flexible reports than talloc_report(). It + * will let you specify the depth and max_depth. + * + * @param[in] ptr The talloc chunk. + * + * @param[in] depth Internal parameter to control recursion. Call with 0. + * + * @param[in] max_depth Maximum recursion level. + * + * @param[in] f The file handle to print to. + */ +_PUBLIC_ void talloc_report_depth_file(const void *ptr, int depth, int max_depth, FILE *f); + +/** + * @brief Print a summary report of all memory used by ptr. + * + * This provides a more detailed report than talloc_report(). It will + * recursively print the entire tree of memory referenced by the + * pointer. References in the tree are shown by giving the name of the + * pointer that is referenced. + * + * You can pass NULL for the pointer, in which case a report is printed + * for the top level memory context, but only if + * talloc_enable_leak_report() or talloc_enable_leak_report_full() has + * been called. + * + * @param[in] ptr The talloc chunk. + * + * @param[in] f The file handle to print to. + * + * Example: + * @code + * unsigned int *a, *b; + * a = talloc(NULL, unsigned int); + * b = talloc(a, unsigned int); + * fprintf(stderr, "Dumping memory tree for a:\n"); + * talloc_report_full(a, stderr); + * @endcode + * + * @see talloc_report() + */ +_PUBLIC_ void talloc_report_full(const void *ptr, FILE *f); + +/** + * @brief Print a summary report of all memory used by ptr. + * + * This function prints a summary report of all memory used by ptr. One line of + * report is printed for each immediate child of ptr, showing the total memory + * and number of blocks used by that child. + * + * You can pass NULL for the pointer, in which case a report is printed + * for the top level memory context, but only if talloc_enable_leak_report() + * or talloc_enable_leak_report_full() has been called. + * + * @param[in] ptr The talloc chunk. + * + * @param[in] f The file handle to print to. + * + * Example: + * @code + * unsigned int *a, *b; + * a = talloc(NULL, unsigned int); + * b = talloc(a, unsigned int); + * fprintf(stderr, "Summary of memory tree for a:\n"); + * talloc_report(a, stderr); + * @endcode + * + * @see talloc_report_full() + */ +_PUBLIC_ void talloc_report(const void *ptr, FILE *f); + +/** + * @brief Enable tracking the use of NULL memory contexts. + * + * This enables tracking of the NULL memory context without enabling leak + * reporting on exit. Useful for when you want to do your own leak + * reporting call via talloc_report_null_full(); + */ +_PUBLIC_ void talloc_enable_null_tracking(void); + +/** + * @brief Enable tracking the use of NULL memory contexts. + * + * This enables tracking of the NULL memory context without enabling leak + * reporting on exit. Useful for when you want to do your own leak + * reporting call via talloc_report_null_full(); + */ +_PUBLIC_ void talloc_enable_null_tracking_no_autofree(void); + +/** + * @brief Disable tracking of the NULL memory context. + * + * This disables tracking of the NULL memory context. + */ +_PUBLIC_ void talloc_disable_null_tracking(void); + +/** + * @brief Enable leak report when a program exits. + * + * This enables calling of talloc_report(NULL, stderr) when the program + * exits. In Samba4 this is enabled by using the --leak-report command + * line option. + * + * For it to be useful, this function must be called before any other + * talloc function as it establishes a "null context" that acts as the + * top of the tree. If you don't call this function first then passing + * NULL to talloc_report() or talloc_report_full() won't give you the + * full tree printout. + * + * Here is a typical talloc report: + * + * @code + * talloc report on 'null_context' (total 267 bytes in 15 blocks) + * libcli/auth/spnego_parse.c:55 contains 31 bytes in 2 blocks + * libcli/auth/spnego_parse.c:55 contains 31 bytes in 2 blocks + * iconv(UTF8,CP850) contains 42 bytes in 2 blocks + * libcli/auth/spnego_parse.c:55 contains 31 bytes in 2 blocks + * iconv(CP850,UTF8) contains 42 bytes in 2 blocks + * iconv(UTF8,UTF-16LE) contains 45 bytes in 2 blocks + * iconv(UTF-16LE,UTF8) contains 45 bytes in 2 blocks + * @endcode + */ +_PUBLIC_ void talloc_enable_leak_report(void); + +/** + * @brief Enable full leak report when a program exits. + * + * This enables calling of talloc_report_full(NULL, stderr) when the + * program exits. In Samba4 this is enabled by using the + * --leak-report-full command line option. + * + * For it to be useful, this function must be called before any other + * talloc function as it establishes a "null context" that acts as the + * top of the tree. If you don't call this function first then passing + * NULL to talloc_report() or talloc_report_full() won't give you the + * full tree printout. + * + * Here is a typical full report: + * + * @code + * full talloc report on 'root' (total 18 bytes in 8 blocks) + * p1 contains 18 bytes in 7 blocks (ref 0) + * r1 contains 13 bytes in 2 blocks (ref 0) + * reference to: p2 + * p2 contains 1 bytes in 1 blocks (ref 1) + * x3 contains 1 bytes in 1 blocks (ref 0) + * x2 contains 1 bytes in 1 blocks (ref 0) + * x1 contains 1 bytes in 1 blocks (ref 0) + * @endcode + */ +_PUBLIC_ void talloc_enable_leak_report_full(void); + +/** + * @brief Set a custom "abort" function that is called on serious error. + * + * The default "abort" function is abort(). + * + * The "abort" function is called when: + * + *
    + *
  • talloc_get_type_abort() fails
  • + *
  • the provided pointer is not a valid talloc context
  • + *
  • when the context meta data are invalid
  • + *
  • when access after free is detected
  • + *
+ * + * Example: + * + * @code + * void my_abort(const char *reason) + * { + * fprintf(stderr, "talloc abort: %s\n", reason); + * abort(); + * } + * + * talloc_set_abort_fn(my_abort); + * @endcode + * + * @param[in] abort_fn The new "abort" function. + * + * @see talloc_set_log_fn() + * @see talloc_get_type() + */ +_PUBLIC_ void talloc_set_abort_fn(void (*abort_fn)(const char *reason)); + +/** + * @brief Set a logging function. + * + * @param[in] log_fn The logging function. + * + * @see talloc_set_log_stderr() + * @see talloc_set_abort_fn() + */ +_PUBLIC_ void talloc_set_log_fn(void (*log_fn)(const char *message)); + +/** + * @brief Set stderr as the output for logs. + * + * @see talloc_set_log_fn() + * @see talloc_set_abort_fn() + */ +_PUBLIC_ void talloc_set_log_stderr(void); + +/** + * @brief Set a max memory limit for the current context hierarchy + * This affects all children of this context and constrain any + * allocation in the hierarchy to never exceed the limit set. + * The limit can be removed by setting 0 (unlimited) as the + * max_size by calling the function again on the same context. + * Memory limits can also be nested, meaning a child can have + * a stricter memory limit than a parent. + * Memory limits are enforced only at memory allocation time. + * Stealing a context into a 'limited' hierarchy properly + * updates memory usage but does *not* cause failure if the + * move causes the new parent to exceed its limits. However + * any further allocation on that hierarchy will then fail. + * + * @warning talloc memlimit functionality is deprecated. Please + * consider using cgroup memory limits instead. + * + * @param[in] ctx The talloc context to set the limit on + * @param[in] max_size The (new) max_size + */ +_PUBLIC_ int talloc_set_memlimit(const void *ctx, size_t max_size) _DEPRECATED_; + +/* @} ******************************************************************/ + +#if TALLOC_DEPRECATED +#define talloc_zero_p(ctx, type) talloc_zero(ctx, type) +#define talloc_p(ctx, type) talloc(ctx, type) +#define talloc_array_p(ctx, type, count) talloc_array(ctx, type, count) +#define talloc_realloc_p(ctx, p, type, count) talloc_realloc(ctx, p, type, count) +#define talloc_destroy(ctx) talloc_free(ctx) +#define talloc_append_string(c, s, a) (s?talloc_strdup_append(s,a):talloc_strdup(c, a)) +#endif + +#ifndef TALLOC_MAX_DEPTH +#define TALLOC_MAX_DEPTH 10000 +#endif + +#ifdef __cplusplus +} /* end of extern "C" */ +#endif + +#endif diff --git a/core/proot/src/main/cpp/talloc/win32_replace.h b/core/proot/src/main/cpp/talloc/win32_replace.h new file mode 100644 index 000000000..9901e72f6 --- /dev/null +++ b/core/proot/src/main/cpp/talloc/win32_replace.h @@ -0,0 +1,159 @@ +#ifndef _WIN32_REPLACE_H +#define _WIN32_REPLACE_H + +#ifdef HAVE_WINSOCK2_H +#include +#endif + +#ifdef HAVE_WS2TCPIP_H +#include +#endif + +#ifdef HAVE_WINDOWS_H +#include +#endif + +/* Map BSD Socket errorcodes to the WSA errorcodes (if possible) */ + +#define EAFNOSUPPORT WSAEAFNOSUPPORT +#define ECONNREFUSED WSAECONNREFUSED +#define EINPROGRESS WSAEINPROGRESS +#define EMSGSIZE WSAEMSGSIZE +#define ENOBUFS WSAENOBUFS +#define ENOTSOCK WSAENOTSOCK +#define ENETUNREACH WSAENETUNREACH +#define ENOPROTOOPT WSAENOPROTOOPT +#define ENOTCONN WSAENOTCONN +#define ENOTSUP 134 + +/* We undefine the following constants due to conflicts with the w32api headers + * and the Windows Platform SDK/DDK. + */ + +#undef interface + +#undef ERROR_INVALID_PARAMETER +#undef ERROR_INSUFFICIENT_BUFFER +#undef ERROR_INVALID_DATATYPE + +#undef FILE_GENERIC_READ +#undef FILE_GENERIC_WRITE +#undef FILE_GENERIC_EXECUTE +#undef FILE_ATTRIBUTE_READONLY +#undef FILE_ATTRIBUTE_HIDDEN +#undef FILE_ATTRIBUTE_SYSTEM +#undef FILE_ATTRIBUTE_DIRECTORY +#undef FILE_ATTRIBUTE_ARCHIVE +#undef FILE_ATTRIBUTE_DEVICE +#undef FILE_ATTRIBUTE_NORMAL +#undef FILE_ATTRIBUTE_TEMPORARY +#undef FILE_ATTRIBUTE_REPARSE_POINT +#undef FILE_ATTRIBUTE_COMPRESSED +#undef FILE_ATTRIBUTE_OFFLINE +#undef FILE_ATTRIBUTE_ENCRYPTED +#undef FILE_FLAG_WRITE_THROUGH +#undef FILE_FLAG_NO_BUFFERING +#undef FILE_FLAG_RANDOM_ACCESS +#undef FILE_FLAG_SEQUENTIAL_SCAN +#undef FILE_FLAG_DELETE_ON_CLOSE +#undef FILE_FLAG_BACKUP_SEMANTICS +#undef FILE_FLAG_POSIX_SEMANTICS +#undef FILE_TYPE_DISK +#undef FILE_TYPE_UNKNOWN +#undef FILE_CASE_SENSITIVE_SEARCH +#undef FILE_CASE_PRESERVED_NAMES +#undef FILE_UNICODE_ON_DISK +#undef FILE_PERSISTENT_ACLS +#undef FILE_FILE_COMPRESSION +#undef FILE_VOLUME_QUOTAS +#undef FILE_VOLUME_IS_COMPRESSED +#undef FILE_NOTIFY_CHANGE_FILE_NAME +#undef FILE_NOTIFY_CHANGE_DIR_NAME +#undef FILE_NOTIFY_CHANGE_ATTRIBUTES +#undef FILE_NOTIFY_CHANGE_SIZE +#undef FILE_NOTIFY_CHANGE_LAST_WRITE +#undef FILE_NOTIFY_CHANGE_LAST_ACCESS +#undef FILE_NOTIFY_CHANGE_CREATION +#undef FILE_NOTIFY_CHANGE_EA +#undef FILE_NOTIFY_CHANGE_SECURITY +#undef FILE_NOTIFY_CHANGE_STREAM_NAME +#undef FILE_NOTIFY_CHANGE_STREAM_SIZE +#undef FILE_NOTIFY_CHANGE_STREAM_WRITE +#undef FILE_NOTIFY_CHANGE_NAME + +#undef PRINTER_ATTRIBUTE_QUEUED +#undef PRINTER_ATTRIBUTE_DIRECT +#undef PRINTER_ATTRIBUTE_DEFAULT +#undef PRINTER_ATTRIBUTE_SHARED +#undef PRINTER_ATTRIBUTE_NETWORK +#undef PRINTER_ATTRIBUTE_HIDDEN +#undef PRINTER_ATTRIBUTE_LOCAL +#undef PRINTER_ATTRIBUTE_ENABLE_DEVQ +#undef PRINTER_ATTRIBUTE_KEEPPRINTEDJOBS +#undef PRINTER_ATTRIBUTE_DO_COMPLETE_FIRST +#undef PRINTER_ATTRIBUTE_WORK_OFFLINE +#undef PRINTER_ATTRIBUTE_ENABLE_BIDI +#undef PRINTER_ATTRIBUTE_RAW_ONLY +#undef PRINTER_ATTRIBUTE_PUBLISHED +#undef PRINTER_ENUM_DEFAULT +#undef PRINTER_ENUM_LOCAL +#undef PRINTER_ENUM_CONNECTIONS +#undef PRINTER_ENUM_FAVORITE +#undef PRINTER_ENUM_NAME +#undef PRINTER_ENUM_REMOTE +#undef PRINTER_ENUM_SHARED +#undef PRINTER_ENUM_NETWORK +#undef PRINTER_ENUM_EXPAND +#undef PRINTER_ENUM_CONTAINER +#undef PRINTER_ENUM_ICON1 +#undef PRINTER_ENUM_ICON2 +#undef PRINTER_ENUM_ICON3 +#undef PRINTER_ENUM_ICON4 +#undef PRINTER_ENUM_ICON5 +#undef PRINTER_ENUM_ICON6 +#undef PRINTER_ENUM_ICON7 +#undef PRINTER_ENUM_ICON8 +#undef PRINTER_STATUS_PAUSED +#undef PRINTER_STATUS_ERROR +#undef PRINTER_STATUS_PENDING_DELETION +#undef PRINTER_STATUS_PAPER_JAM +#undef PRINTER_STATUS_PAPER_OUT +#undef PRINTER_STATUS_MANUAL_FEED +#undef PRINTER_STATUS_PAPER_PROBLEM +#undef PRINTER_STATUS_OFFLINE +#undef PRINTER_STATUS_IO_ACTIVE +#undef PRINTER_STATUS_BUSY +#undef PRINTER_STATUS_PRINTING +#undef PRINTER_STATUS_OUTPUT_BIN_FULL +#undef PRINTER_STATUS_NOT_AVAILABLE +#undef PRINTER_STATUS_WAITING +#undef PRINTER_STATUS_PROCESSING +#undef PRINTER_STATUS_INITIALIZING +#undef PRINTER_STATUS_WARMING_UP +#undef PRINTER_STATUS_TONER_LOW +#undef PRINTER_STATUS_NO_TONER +#undef PRINTER_STATUS_PAGE_PUNT +#undef PRINTER_STATUS_USER_INTERVENTION +#undef PRINTER_STATUS_OUT_OF_MEMORY +#undef PRINTER_STATUS_DOOR_OPEN +#undef PRINTER_STATUS_SERVER_UNKNOWN +#undef PRINTER_STATUS_POWER_SAVE + +#undef DWORD +#undef HKEY_CLASSES_ROOT +#undef HKEY_CURRENT_USER +#undef HKEY_LOCAL_MACHINE +#undef HKEY_USERS +#undef HKEY_PERFORMANCE_DATA +#undef HKEY_CURRENT_CONFIG +#undef HKEY_DYN_DATA +#undef REG_DWORD +#undef REG_QWORD + +#undef SERVICE_STATE_ALL + +#undef SE_GROUP_MANDATORY +#undef SE_GROUP_ENABLED_BY_DEFAULT +#undef SE_GROUP_ENABLED + +#endif /* _WIN32_REPLACE_H */ diff --git a/core/proot/src/main/cpp/tracee/abi.h b/core/proot/src/main/cpp/tracee/abi.h new file mode 100644 index 000000000..87895676f --- /dev/null +++ b/core/proot/src/main/cpp/tracee/abi.h @@ -0,0 +1,149 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#ifndef TRACEE_ABI_H +#define TRACEE_ABI_H + +#include +#include /* offsetof(), */ + +#include "tracee/tracee.h" +#include "tracee/reg.h" +#include "arch.h" + +#include "attribute.h" + +typedef enum { + ABI_DEFAULT = 0, + ABI_2, /* x86_32 on x86_64, ARM EABI on AArch64. */ + ABI_3, /* x32 on x86_64. */ + NB_MAX_ABIS, +} Abi; + +/** + * Return the ABI currently used by the given @tracee. + */ +#if defined(ARCH_X86_64) +static inline Abi get_abi(const Tracee *tracee) +{ + /* The ABI can be changed by a syscall ("execve" typically), + * however the change is only effective once the syscall has + * *fully* returned, hence the use of _regs[ORIGINAL]. */ + switch (tracee->_regs[ORIGINAL].cs) { + case 0x23: + return ABI_2; + + case 0x33: + if (tracee->_regs[ORIGINAL].ds == 0x2B) + return ABI_3; + /* Fall through. */ + default: + return ABI_DEFAULT; + } +} + +/** + * Return true if @tracee is a 32-bit process running on a 64-bit + * kernel. + */ +static inline bool is_32on64_mode(const Tracee *tracee) +{ + /* Unlike the ABI, 32-bit/64-bit mode change is effective + * immediately, hence _regs[CURRENT].cs. */ + switch (tracee->_regs[CURRENT].cs) { + case 0x23: + return true; + + case 0x33: + if (tracee->_regs[CURRENT].ds == 0x2B) + return true; + /* Fall through. */ + default: + return false; + } +} +#elif defined(ARCH_ARM64) +static inline Abi get_abi(const Tracee *tracee) +{ + if (tracee->is_aarch32) { + return ABI_2; + } + + return ABI_DEFAULT; +} + +/** + * Return true if @tracee is a 32-bit process running on a 64-bit + * kernel. + */ +static inline bool is_32on64_mode(const Tracee *tracee) +{ + return tracee->is_aarch32; +} +#else +static inline Abi get_abi(const Tracee *tracee UNUSED) +{ + return ABI_DEFAULT; +} + +static inline bool is_32on64_mode(const Tracee *tracee UNUSED) +{ + return false; +} +#endif + +/** + * Return the size of a word according to the ABI currently used by + * the given @tracee. + */ +static inline size_t sizeof_word(const Tracee *tracee) +{ + return (is_32on64_mode(tracee) + ? sizeof(word_t) / 2 + : sizeof(word_t)); +} + +#include + +/** + * Return the offset of the 'uid' field in a 'stat' structure + * according to the ABI currently used by the given @tracee. + */ +static inline off_t offsetof_stat_uid(const Tracee *tracee) +{ + return (is_32on64_mode(tracee) + ? OFFSETOF_STAT_UID_32 + : offsetof(struct stat, st_uid)); +} + +/** + * Return the offset of the 'gid' field in a 'stat' structure + * according to the ABI currently used by the given @tracee. + */ +static inline off_t offsetof_stat_gid(const Tracee *tracee) +{ + return (is_32on64_mode(tracee) + ? OFFSETOF_STAT_GID_32 + : offsetof(struct stat, st_gid)); +} + +#endif /* TRACEE_ABI_H */ diff --git a/core/proot/src/main/cpp/tracee/event.c b/core/proot/src/main/cpp/tracee/event.c new file mode 100644 index 000000000..6c241b5b7 --- /dev/null +++ b/core/proot/src/main/cpp/tracee/event.c @@ -0,0 +1,780 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#include /* CLONE_*, */ +#include /* pid_t, */ +#include /* ptrace(1), PTRACE_*, */ +#include /* waitpid(2), */ +#include /* waitpid(2), */ +#include /* uname(2), */ +#include /* fork(2), chdir(2), getpid(2), */ +#include /* strcmp(3), */ +#include /* errno(3), */ +#include /* bool, true, false, */ +#include /* assert(3), */ +#include /* atexit(3), getenv(3), */ +#include /* talloc_*, */ +#include /* PRI*, */ + +#include "tracee/event.h" +#include "tracee/seccomp.h" +#include "tracee/mem.h" +#include "cli/note.h" +#include "path/path.h" +#include "path/binding.h" +#include "syscall/syscall.h" +#include "syscall/seccomp.h" +#include "ptrace/wait.h" +#include "extension/extension.h" +#include "execve/elf.h" + +#include "attribute.h" +#include "compat.h" + +static bool seccomp_after_ptrace_enter = false; +static bool seccomp_ptrace_event_supported = false; + +/** + * Return true if the running kernel is new enough to generate + * PTRACE_EVENT_SECCOMP stops (requires Linux >= 3.5). Old Android + * kernels (e.g. 3.1.x nougat) backport SECCOMP_MODE_FILTER but + * silently accept PTRACE_O_TRACESECCOMP option bits without actually + * implementing the event, so we can't rely on PTRACE_SETOPTIONS alone. + */ +static bool kernel_supports_ptrace_event_seccomp(void) +{ + struct utsname uts; + int major = 0, minor = 0; + + if (uname(&uts) < 0) + return true; /* assume supported if uname fails */ + sscanf(uts.release, "%d.%d", &major, &minor); + return (major > 3) || (major == 3 && minor >= 5); +} + +/** + * Start @tracee->exe with the given @argv[]. This function + * returns -errno if an error occurred, otherwise 0. + */ +int launch_process(Tracee *tracee, char *const argv[]) +{ + char *const default_argv[] = { "-sh", NULL }; + long status; + pid_t pid; + + /* Set pokedata workaround stub addr if needed. */ + mem_prepare_before_first_execve(tracee); + + /* Warn about open file descriptors. They won't be + * translated until they are closed. */ + if (tracee->verbose > 0) + list_open_fd(tracee); + + pid = fork(); + switch(pid) { + case -1: + note(tracee, ERROR, SYSTEM, "fork()"); + return -errno; + + case 0: /* child */ + /* Declare myself as ptraceable before executing the + * requested program. */ + status = ptrace(PTRACE_TRACEME, 0, NULL, NULL); + if (status < 0) { + note(tracee, ERROR, SYSTEM, "ptrace(TRACEME)"); + return -errno; + } + + /* Synchronize with the tracer's event loop. Without + * this trick the tracer only sees the "return" from + * the next execve(2) so PRoot wouldn't handle the + * interpreter/runner. I also verified that strace + * does the same thing. */ + kill(getpid(), SIGSTOP); + + /* Improve performance by using seccomp mode 2, unless + * this support is explicitly disabled. */ + if (getenv("PROOT_NO_SECCOMP") == NULL) + (void) enable_syscall_filtering(tracee); + + /* Now process is ptraced, so the current rootfs is already the + * guest rootfs. Note: Valgrind can't handle execve(2) on + * "foreign" binaries (ENOEXEC) but can handle execvp(3) on such + * binaries. */ + execvp(tracee->exe, argv[0] != NULL ? argv : default_argv); + return -errno; + + default: /* parent */ + /* We know the pid of the first tracee now. */ + tracee->pid = pid; + return 0; + } + + /* Never reached. */ + return -ENOSYS; +} + +/* Send the KILL signal to all tracees when PRoot has received a fatal + * signal. */ +static void kill_all_tracees2(int signum, siginfo_t *siginfo UNUSED, void *ucontext UNUSED) +{ + note(NULL, WARNING, INTERNAL, "signal %d received from process %d", + signum, siginfo->si_pid); + kill_all_tracees(); + + /* Exit immediately for system signals (segmentation fault, + * illegal instruction, ...), otherwise exit cleanly through + * the event loop. */ + if (signum != SIGQUIT) + _exit(EXIT_FAILURE); +} + +/** + * Helper for print_talloc_hierarchy(). + */ +static void print_talloc_chunk(const void *ptr, int depth, int max_depth UNUSED, + int is_ref, void *data UNUSED) +{ + const char *name; + size_t count; + size_t size; + + name = talloc_get_name(ptr); + size = talloc_get_size(ptr); + count = talloc_reference_count(ptr); + + if (depth == 0) + return; + + while (depth-- > 1) + fprintf(stderr, "\t"); + + fprintf(stderr, "%-16s ", name); + + if (is_ref) + fprintf(stderr, "-> %-8p", ptr); + else { + fprintf(stderr, "%-8p %zd bytes %zd ref'", ptr, size, count); + + if (name[0] == '$') { + fprintf(stderr, "\t(\"%s\")", (char *)ptr); + } + if (name[0] == '@') { + char **argv; + int i; + + fprintf(stderr, "\t("); + for (i = 0, argv = (char **)ptr; argv[i] != NULL; i++) + fprintf(stderr, "\"%s\", ", argv[i]); + fprintf(stderr, ")"); + } + else if (strcmp(name, "Tracee") == 0) { + fprintf(stderr, "\t(pid = %d, parent = %p)", + ((Tracee *)ptr)->pid, ((Tracee *)ptr)->parent); + } + else if (strcmp(name, "Bindings") == 0) { + Tracee *tracee; + + tracee = TRACEE(ptr); + + if (ptr == tracee->fs->bindings.pending) + fprintf(stderr, "\t(pending)"); + else if (ptr == tracee->fs->bindings.guest) + fprintf(stderr, "\t(guest)"); + else if (ptr == tracee->fs->bindings.host) + fprintf(stderr, "\t(host)"); + } + else if (strcmp(name, "Binding") == 0) { + Binding *binding = (Binding *)ptr; + fprintf(stderr, "\t(%s:%s)", binding->host.path, binding->guest.path); + } + } + + fprintf(stderr, "\n"); +} + +/* Print on stderr the complete talloc hierarchy. */ +static void print_talloc_hierarchy(int signum, siginfo_t *siginfo UNUSED, void *ucontext UNUSED) +{ + switch (signum) { + case SIGUSR1: + talloc_report_depth_cb(NULL, 0, 100, print_talloc_chunk, NULL); + break; + + case SIGUSR2: + talloc_report_depth_file(NULL, 0, 100, stderr); + break; + + default: + break; + } +} + +static int last_exit_status = -1; + +/** + * Check if this instance of PRoot can *technically* handle @tracee. + */ +static void check_architecture(Tracee *tracee) +{ + struct utsname utsname; + ElfHeader elf_header; + char path[PATH_MAX]; + int status; + + if (tracee->exe == NULL) + return; + + status = translate_path(tracee, path, AT_FDCWD, tracee->exe, false); + if (status < 0) + return; + + status = open_elf(path, &elf_header); + if (status < 0) + return; + close(status); + + if (!IS_CLASS64(elf_header) || sizeof(word_t) == sizeof(uint64_t)) + return; + + note(tracee, ERROR, USER, + "'%s' is a 64-bit program whereas this version of " + "%s handles 32-bit programs only", path, tracee->tool_name); + + status = uname(&utsname); + if (status < 0) + return; + + if (strcmp(utsname.machine, "x86_64") != 0) + return; + + note(tracee, INFO, USER, + "Get a 64-bit version that supports 32-bit binaries here: " + "http://static.proot.me/proot-x86_64"); +} + +/** + * Wait then handle any event from any tracee. This function returns + * the exit status of the last terminated program. + */ +int event_loop() +{ + struct sigaction signal_action; + long status; + int signum; + + /* Kill all tracees when exiting. */ + status = atexit(kill_all_tracees); + if (status != 0) + note(NULL, WARNING, INTERNAL, "atexit() failed"); + + /* All signals are blocked when the signal handler is called. + * SIGINFO is used to know which process has signaled us and + * RESTART is used to restart waitpid(2) seamlessly. */ + bzero(&signal_action, sizeof(signal_action)); + signal_action.sa_flags = SA_SIGINFO | SA_RESTART; + status = sigfillset(&signal_action.sa_mask); + if (status < 0) + note(NULL, WARNING, SYSTEM, "sigfillset()"); + + /* Handle all signals. */ + for (signum = 0; signum < SIGRTMAX; signum++) { + switch (signum) { + case SIGQUIT: + case SIGILL: + case SIGABRT: + case SIGFPE: + case SIGSEGV: + /* Kill all tracees on abnormal termination + * signals. This ensures no process is left + * untraced. */ + signal_action.sa_sigaction = kill_all_tracees2; + break; + + case SIGUSR1: + case SIGUSR2: + /* Print on stderr the complete talloc + * hierarchy, useful for debug purpose. */ + signal_action.sa_sigaction = print_talloc_hierarchy; + break; + + case SIGCHLD: + case SIGCONT: + case SIGSTOP: + case SIGTSTP: + case SIGTTIN: + case SIGTTOU: + /* The default action is OK for these signals, + * they are related to tty and job control. */ + continue; + + default: + /* Ignore all other signals, including + * terminating ones (^C for instance). */ + signal_action.sa_sigaction = (void *)SIG_IGN; + break; + } + + status = sigaction(signum, &signal_action, NULL); + if (status < 0 && errno != EINVAL) + note(NULL, WARNING, SYSTEM, "sigaction(%d)", signum); + } + + while (1) { + int tracee_status; + Tracee *tracee; + int signal; + pid_t pid; + + /* This is the only safe place to free tracees. */ + free_terminated_tracees(); + + /* Wait for the next tracee's stop. */ + pid = waitpid(-1, &tracee_status, __WALL); + if (pid < 0) { + if (errno != ECHILD) { + note(NULL, ERROR, SYSTEM, "waitpid()"); + return EXIT_FAILURE; + } + break; + } + + /* Get information about this tracee. */ + tracee = get_tracee(NULL, pid, true); + assert(tracee != NULL); + + tracee->running = false; + + status = notify_extensions(tracee, NEW_STATUS, tracee_status, 0); + if (status != 0) + continue; + + if (tracee->as_ptracee.ptracer != NULL) { + bool keep_stopped = handle_ptracee_event(tracee, tracee_status); + if (keep_stopped) + continue; + } + + signal = handle_tracee_event(tracee, tracee_status); + (void) restart_tracee(tracee, signal); + } + + return last_exit_status; +} + +/** + * Handle the current event (@tracee_status) of the given @tracee. + * This function returns the "computed" signal that should be used to + * restart the given @tracee. + */ +int handle_tracee_event(Tracee *tracee, int tracee_status) +{ + static bool seccomp_detected = false; + static bool seccomp_after_ptrace_enter_checked = false; + long status; + int signal; + bool sysexit_necessary; + + if (!seccomp_after_ptrace_enter_checked) { + seccomp_after_ptrace_enter = getenv("PROOT_ASSUME_NEW_SECCOMP") != NULL; + seccomp_after_ptrace_enter_checked = true; + } + + /* When seccomp is enabled, all events are restarted in + * non-stop mode, but this default choice could be overwritten + * later if necessary. The check against "sysexit_pending" + * ensures PTRACE_SYSCALL (used to hit the exit stage under + * seccomp) is not cleared due to an event that would happen + * before the exit stage, eg. PTRACE_EVENT_EXEC for the exit + * stage of execve(2). */ + sysexit_necessary = tracee->sysexit_pending + || tracee->chain.syscalls != NULL + || tracee->restore_original_regs_after_seccomp_event; + /* Don't overwrite restart_how if it is explicitly set + * elsewhere, i.e in the ptrace emulation when single + * stepping. */ + if (tracee->restart_how == 0) { + if (tracee->seccomp == ENABLED && !sysexit_necessary) + tracee->restart_how = PTRACE_CONT; + else + tracee->restart_how = PTRACE_SYSCALL; + } + + /* Not a signal-stop by default. */ + signal = 0; + + if (WIFEXITED(tracee_status)) { + last_exit_status = WEXITSTATUS(tracee_status); + VERBOSE(tracee, 1, + "vpid %" PRIu64 ": exited with status %d", + tracee->vpid, last_exit_status); + terminate_tracee(tracee); + } + else if (WIFSIGNALED(tracee_status)) { + check_architecture(tracee); + VERBOSE(tracee, (int) (tracee->vpid != 1), + "vpid %" PRIu64 ": terminated with signal %d", + tracee->vpid, WTERMSIG(tracee_status)); + terminate_tracee(tracee); + } + else if (WIFSTOPPED(tracee_status)) { + /* Don't use WSTOPSIG() to extract the signal + * since it clears the PTRACE_EVENT_* bits. */ + signal = (tracee_status & 0xfff00) >> 8; + + switch (signal) { + static bool deliver_sigtrap = false; + + case SIGTRAP: { + const unsigned long default_ptrace_options = ( + PTRACE_O_TRACESYSGOOD | + PTRACE_O_TRACEFORK | + PTRACE_O_TRACEVFORK | + PTRACE_O_TRACEVFORKDONE | + PTRACE_O_TRACEEXEC | + PTRACE_O_TRACECLONE | + PTRACE_O_TRACEEXIT); + + /* Distinguish some events from others and + * automatically trace each new process with + * the same options. + * + * Note that only the first bare SIGTRAP is + * related to the tracing loop, others SIGTRAP + * carry tracing information because of + * TRACE*FORK/CLONE/EXEC. */ + if (deliver_sigtrap) + break; /* Deliver this signal as-is. */ + + deliver_sigtrap = true; + + /* Try to enable seccomp mode 2... */ + status = ptrace(PTRACE_SETOPTIONS, tracee->pid, NULL, + default_ptrace_options | PTRACE_O_TRACESECCOMP); + if (status < 0) { + /* ... otherwise use default options only. */ + status = ptrace(PTRACE_SETOPTIONS, tracee->pid, NULL, + default_ptrace_options); + if (status < 0) { + note(tracee, ERROR, SYSTEM, "ptrace(PTRACE_SETOPTIONS)"); + exit(EXIT_FAILURE); + } + /* Without PTRACE_O_TRACESECCOMP, a seccomp filter + * installed by the tracee that returns + * SECCOMP_RET_TRACE would silently return -ENOSYS + * for filtered syscalls instead of generating a + * ptrace event. Record this so the prctl handler + * can refuse PR_SET_SECCOMP, SECCOMP_MODE_FILTER + * from the tracee to keep proot's syscall + * interception working. */ + seccomp_ptrace_event_supported = false; + } else { + /* PTRACE_SETOPTIONS succeeded, but some old + * Android kernels (e.g. 3.1.x nougat) silently + * accept unknown option bits without implementing + * them. Cross-check with the kernel version: + * PTRACE_EVENT_SECCOMP requires Linux >= 3.5. */ + seccomp_ptrace_event_supported = + kernel_supports_ptrace_event_seccomp(); + } + } + /* Fall through. */ + case SIGTRAP | 0x80: + signal = 0; + + /* This tracee got signaled then freed during the + sysenter stage but the kernel reports the sysexit + stage; just discard this spurious tracee/event. */ + if (tracee->exe == NULL) { + tracee->restart_how = PTRACE_CONT; + return 0; + } + + switch (tracee->seccomp) { + case ENABLED: + if (IS_IN_SYSENTER(tracee)) { + /* sysenter: ensure the sysexit + * stage will be hit under seccomp. */ + tracee->restart_how = PTRACE_SYSCALL; + tracee->sysexit_pending = true; + } + else { + /* sysexit: the next sysenter + * will be notified by seccomp. */ + tracee->restart_how = PTRACE_CONT; + tracee->sysexit_pending = false; + } + /* Fall through. */ + case DISABLED: + if (!tracee->seccomp_already_handled_enter) + { + bool was_sysenter = IS_IN_SYSENTER(tracee); + + translate_syscall(tracee); + + /* In case we've changed on enter sysnum to PR_void, + * seccomp check is happening after our change + * and seccomp policy raises SIGSYS on -1 syscall, + * set tracee flag to ignore next SIGSYS. */ + if (was_sysenter) { + tracee->skip_next_seccomp_signal = ( + seccomp_after_ptrace_enter && + get_sysnum(tracee, CURRENT) == PR_void); + } + + /* Redeliver signal suppressed during + * syscall chain once it's finished. */ + if (tracee->chain.suppressed_signal && tracee->chain.syscalls == NULL && !tracee->restore_original_regs_after_seccomp_event) { + signal = tracee->chain.suppressed_signal; + tracee->chain.suppressed_signal = 0; + VERBOSE(tracee, 6, "vpid %" PRIu64 ": redelivering suppressed signal %d", tracee->vpid, signal); + } + } + else { + VERBOSE(tracee, 6, "skipping SIGTRAP for already handled sysenter"); + assert(!IS_IN_SYSENTER(tracee)); + assert(!seccomp_after_ptrace_enter); + tracee->seccomp_already_handled_enter = false; + tracee->restart_how = PTRACE_SYSCALL; + } + + /* This syscall has disabled seccomp. */ + if (tracee->seccomp == DISABLING) { + tracee->restart_how = PTRACE_SYSCALL; + tracee->seccomp = DISABLED; + } + + break; + + case DISABLING: + /* Seccomp was disabled by the + * previous syscall, but its sysenter + * stage was already handled. */ + tracee->seccomp = DISABLED; + if (IS_IN_SYSENTER(tracee)) + tracee->status = 1; + break; + } + break; + + case SIGTRAP | PTRACE_EVENT_SECCOMP2 << 8: + case SIGTRAP | PTRACE_EVENT_SECCOMP << 8: { + unsigned long flags = 0; + + signal = 0; + + if (!seccomp_detected) { + tracee->seccomp = ENABLED; + seccomp_detected = true; + seccomp_after_ptrace_enter = !IS_IN_SYSENTER(tracee); + VERBOSE(tracee, 1, "ptrace acceleration (seccomp mode 2, %s syscall order) enabled", + seccomp_after_ptrace_enter ? "new" : "old"); + } + + tracee->skip_next_seccomp_signal = false; + + /* If kernel triggered seccomp event after we handled + * syscall enter, skip this event and continue as it didn't happen */ + if (seccomp_after_ptrace_enter && !IS_IN_SYSENTER(tracee)) + { + tracee->restart_how = tracee->last_restart_how; + VERBOSE(tracee, 6, "skipping PTRACE_EVENT_SECCOMP for already handled sysenter"); + + /* "!IS_IN_SYSENTER(tracee)" in condition above means we have pending sysexit, + * so requesting to not be notified about it makes no sense */ + assert(tracee->restart_how != PTRACE_CONT); + break; + } + + assert(IS_IN_SYSENTER(tracee)); + + /* Use the common ptrace flow if seccomp was + * explicitely disabled for this tracee. */ + if (tracee->seccomp != ENABLED) + break; + + status = ptrace(PTRACE_GETEVENTMSG, tracee->pid, NULL, &flags); + if (status < 0) + break; + + /* Use the common ptrace flow when + * sysexit has to be handled. */ + if ((flags & FILTER_SYSEXIT) != 0 || sysexit_necessary) { + if (seccomp_after_ptrace_enter) { + tracee->restart_how = PTRACE_SYSCALL; + translate_syscall(tracee); + } + tracee->restart_how = PTRACE_SYSCALL; + break; + } + + /* Otherwise, handle the sysenter + * stage right now. */ + tracee->restart_how = PTRACE_CONT; + translate_syscall(tracee); + + /* Sysenter handler may have requested sysexit + * interception by setting sysexit_pending. */ + if (tracee->sysexit_pending) + tracee->restart_how = PTRACE_SYSCALL; + + /* This syscall has disabled seccomp, so move + * the ptrace flow back to the common path to + * ensure its sysexit will be handled. */ + if (tracee->seccomp == DISABLING) + tracee->restart_how = PTRACE_SYSCALL; + + if (!seccomp_after_ptrace_enter && tracee->restart_how == PTRACE_SYSCALL) + tracee->seccomp_already_handled_enter = true; + break; + } + + case SIGTRAP | PTRACE_EVENT_VFORK << 8: + signal = 0; + (void) new_child(tracee, CLONE_VFORK); + break; + + case SIGTRAP | PTRACE_EVENT_FORK << 8: + case SIGTRAP | PTRACE_EVENT_CLONE << 8: + signal = 0; + (void) new_child(tracee, 0); + break; + + case SIGTRAP | PTRACE_EVENT_VFORK_DONE << 8: + case SIGTRAP | PTRACE_EVENT_EXEC << 8: + case SIGTRAP | PTRACE_EVENT_EXIT << 8: + signal = 0; + if (tracee->last_restart_how) { + tracee->restart_how = tracee->last_restart_how; + } + break; + + case SIGSTOP: + /* Stop this tracee until PRoot has received + * the EVENT_*FORK|CLONE notification. */ + if (tracee->exe == NULL) { + tracee->sigstop = SIGSTOP_PENDING; + signal = -1; + } + + /* For each tracee, the first SIGSTOP + * is only used to notify the tracer. */ + if (tracee->sigstop == SIGSTOP_IGNORED) { + tracee->sigstop = SIGSTOP_ALLOWED; + signal = 0; + } + break; + + case SIGSYS: { + siginfo_t siginfo = {}; + ptrace(PTRACE_GETSIGINFO, tracee->pid, NULL, &siginfo); + if (siginfo.si_code == SYS_SECCOMP) { + /* Signal cannot happen when we're inside syscall, + * tracee would have to exit from syscall first. + * Execute exit handler now if seccomp triggered sysexit skip. */ + if (!IS_IN_SYSENTER(tracee)) { + VERBOSE(tracee, 1, "Handling syscall exit from SIGSYS"); + translate_syscall(tracee); + } + + if (tracee->skip_next_seccomp_signal || (seccomp_after_ptrace_enter && siginfo.si_syscall == SYSCALL_AVOIDER)) { + VERBOSE(tracee, 4, "suppressed SIGSYS after void syscall"); + tracee->skip_next_seccomp_signal = false; + signal = 0; + } else { + signal = handle_seccomp_event(tracee); + } + } else { + VERBOSE(tracee, 1, "non-seccomp SIGSYS"); + } + break; + } + + default: + /* Deliver this signal as-is, + * unless we're chaining syscall. */ + if (tracee->chain.syscalls != NULL || tracee->restore_original_regs_after_seccomp_event) { + VERBOSE(tracee, 5, + "vpid %" PRIu64 ": suppressing signal during chain signal=%d, prev suppressed_signal=%d", + tracee->vpid, signal, tracee->chain.suppressed_signal); + tracee->chain.suppressed_signal = signal; + signal = 0; + } + break; + } + } + + /* Clear the pending event, if any. */ + tracee->as_ptracee.event4.proot.pending = false; + + return signal; +} + +/** + * Returns true if on current system SIGTRAP|0x80 + * for syscall enter is reported before SIGSYS + * when syscall is being blocked by seccomp + */ +bool seccomp_event_happens_after_enter_sigtrap() +{ + return !seccomp_after_ptrace_enter; +} + +/** + * Return true if PTRACE_O_TRACESECCOMP is supported by the kernel, + * i.e. the kernel will generate PTRACE_EVENT_SECCOMP stops instead of + * silently returning -ENOSYS when a seccomp filter returns + * SECCOMP_RET_TRACE. When this returns false, the prctl handler + * blocks tracee attempts to install SECCOMP_MODE_FILTER so that + * proot's PTRACE_SYSCALL interception path remains functional. + */ +bool seccomp_ptrace_event_is_supported() +{ + return seccomp_ptrace_event_supported; +} + +/** + * Restart the given @tracee with the specified @signal. This + * function returns false if the tracee was not restarted (error or + * put in the "waiting for ptracee" state), otherwise true. + */ +bool restart_tracee(Tracee *tracee, int signal) +{ + int status; + + /* Put in the "stopped"/"waiting for ptracee" state?. */ + if (tracee->as_ptracer.wait_pid != 0 || signal == -1) + return false; + + /* Restart the tracee and stop it at the next instruction, or + * at the next entry or exit of a system call. */ + assert(tracee->restart_how != 0); + status = ptrace(tracee->restart_how, tracee->pid, NULL, signal); + if (status < 0) + return false; /* The process likely died in a syscall. */ + + tracee->last_restart_how = tracee->restart_how; + tracee->restart_how = 0; + tracee->running = true; + + return true; +} diff --git a/core/proot/src/main/cpp/tracee/event.h b/core/proot/src/main/cpp/tracee/event.h new file mode 100644 index 000000000..722bd2a6d --- /dev/null +++ b/core/proot/src/main/cpp/tracee/event.h @@ -0,0 +1,37 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#ifndef TRACEE_EVENT_H +#define TRACEE_EVENT_H + +#include + +#include "tracee/tracee.h" + +extern int launch_process(Tracee *tracee, char *const argv[]); +extern int event_loop(); +extern int handle_tracee_event(Tracee *tracee, int tracee_status); +extern bool restart_tracee(Tracee *tracee, int signal); +extern bool seccomp_event_happens_after_enter_sigtrap(); +extern bool seccomp_ptrace_event_is_supported(); + +#endif /* TRACEE_EVENT_H */ diff --git a/core/proot/src/main/cpp/tracee/mem.c b/core/proot/src/main/cpp/tracee/mem.c new file mode 100644 index 000000000..81ce55f14 --- /dev/null +++ b/core/proot/src/main/cpp/tracee/mem.c @@ -0,0 +1,700 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#include /* ptrace(2), PTRACE_*, */ +#include /* pid_t, size_t, */ +#include /* NULL, */ +#include /* offsetof(), */ +#include /* struct user*, */ +#include /* errno, */ +#include /* assert(3), */ +#include /* waitpid(2), */ +#include /* memcpy(3), */ +#include /* uint*_t, */ +#include /* process_vm_*, struct iovec, */ +#include /* sysconf(3), */ +#include /* mmap(2), munmap(2), MAP_*, */ + +#include "tracee/mem.h" +#include "tracee/abi.h" +#include "syscall/heap.h" +#include "arch.h" /* word_t, NO_MISALIGNED_ACCESS */ +#include "build.h" /* HAVE_PROCESS_VM, */ +#include "cli/note.h" +#include "compat.h" + +#ifdef HAS_POKEDATA_WORKAROUND + +#include "tracee/reg.h" +#include "syscall/sysnum.h" + +extern const ssize_t offset_to_pokedata_workaround; +void launcher_pokedata_workaround(); + +// See loader/assembly.S +#if defined(__aarch64__) +__asm( + ".globl launcher_pokedata_workaround\n" + "launcher_pokedata_workaround:\n" + "str x1, [x2]\n" + // https://stackoverflow.com/a/16087057 + ".word 0xf7f0a000\n" +); +#endif /* defined(__aarch64__) */ +#endif /* HAS_POKEDATA_WORKAROUND */ + +/** + * Load the word at the given @address, potentially *not* aligned. + */ +static inline word_t load_word(const void *address) +{ +#ifdef NO_MISALIGNED_ACCESS + if (((word_t)address) % sizeof(word_t) == 0) + return *(word_t *)address; + else { + word_t value; + memcpy(&value, address, sizeof(word_t)); + return value; + } +#else + return *(word_t *)address; +#endif +} + +/** + * Store the word with the given @value to the given @address, + * potentially *not* aligned. + */ +static inline void store_word(void *address, word_t value) +{ +#ifdef NO_MISALIGNED_ACCESS + if (((word_t)address) % sizeof(word_t) == 0) + *((word_t *)address) = value; + else + memcpy(address, &value, sizeof(word_t)); +#else + *((word_t *)address) = value; +#endif +} + +static int ptrace_pokedata_or_via_stub(Tracee *tracee, word_t addr, word_t word) +{ + int status = -1; +#if HAS_POKEDATA_WORKAROUND + static bool pokedata_workaround_needed, pokedata_workaround_checked; + if (!pokedata_workaround_needed) + { +#endif + status = ptrace(PTRACE_POKEDATA, tracee->pid, addr, word); +#if HAS_POKEDATA_WORKAROUND + } + if (!pokedata_workaround_checked) { + pokedata_workaround_needed = (status != 0); + pokedata_workaround_checked = true; + if (pokedata_workaround_needed) { + VERBOSE(tracee, 1, "Detected broken PTRACE_POKEDATA - enabling workaround"); + } + } + if (pokedata_workaround_needed && tracee->is_aarch32) { + note(tracee, ERROR, INTERNAL, "POKEDATA workaround is not supported on AArch32"); + status = -1; + errno = EIO; + } else if (pokedata_workaround_needed) { + struct user_regs_struct orig_regs = tracee->_regs[CURRENT]; + bool restore_original_regs = tracee->restore_original_regs; + sigset_t orig_sigset; + sigset_t modified_sigset; + + // Block signals + ptrace(PTRACE_GETSIGMASK, tracee->pid, sizeof(sigset_t), &orig_sigset); + sigfillset(&modified_sigset); + sigdelset(&modified_sigset, SIGILL); + sigdelset(&modified_sigset, SIGTRAP); + sigdelset(&modified_sigset, SIGBUS); + sigdelset(&modified_sigset, SIGSEGV); + sigdelset(&modified_sigset, SIGSYS); + int sigmask_result = ptrace(PTRACE_SETSIGMASK, tracee->pid, sizeof(sigset_t), &modified_sigset); + + // Set registers so memory will be written + word_t pokedata_workaround_stub_addr = tracee->pokedata_workaround_stub_addr; + poke_reg(tracee, INSTR_POINTER, pokedata_workaround_stub_addr); + poke_reg(tracee, SYSARG_2, word); + poke_reg(tracee, SYSARG_3, addr); + set_sysnum(tracee, PR_void); + tracee->_regs_were_changed = true; + tracee->restore_original_regs = false; + push_specific_regs(tracee, true); + print_current_regs(tracee, 5, "pokedata workaround" ); + + // Continue tracee, retry if SIGSYS or SIGSTOP occurs + int wstatus = 0; + bool redeliver_sigstop = false; + do + { + ptrace(PTRACE_CONT, tracee->pid, 0, 0); + waitpid(tracee->pid, &wstatus, 0); + // For SIGSTOP, we kill tracee after handling POKEDATA + if (WIFSTOPPED(wstatus) && WSTOPSIG(wstatus) == SIGSTOP) + { + redeliver_sigstop = true; + } + // SIGSYS is silently skipped (In case of seccomp disallowing void syscall) + } while (WIFSTOPPED(wstatus) && (WSTOPSIG(wstatus) == SIGSYS || WSTOPSIG(wstatus) == SIGSTOP)); + + // Redeliver SIGSTOP if occured + if (redeliver_sigstop) + { + kill(tracee->pid, SIGSTOP); + } + + // Check status + if (tracee->verbose >= 3) + { + note(tracee, INFO, INTERNAL, "pokedata wstatus=%x stub=%lx addr=%lx word=%lx sigmask_result=%d", + wstatus, pokedata_workaround_stub_addr, addr, word, sigmask_result); + } + bool success = (WIFSTOPPED(wstatus) && WSTOPSIG(wstatus) == SIGILL); + + // Restore tracee state to one before intervention + ptrace(PTRACE_SETSIGMASK, tracee->pid, sizeof(sigset_t), &orig_sigset); + tracee->_regs[CURRENT] = orig_regs; + tracee->_regs_were_changed = true; + tracee->pokedata_workaround_cancelled_syscall = true; + tracee->restore_original_regs = restore_original_regs; + + if (success) + { + status = 0; + } + else + { + // Report failure + note(tracee, ERROR, INTERNAL, "POKEDATA workaround stub got signal %d", WSTOPSIG(wstatus)); + status = -1; + errno = EFAULT; + } + } +#endif + return status; +} + +void mem_prepare_after_execve(Tracee *tracee) +{ +#if HAS_POKEDATA_WORKAROUND + tracee->pokedata_workaround_stub_addr = peek_reg(tracee, CURRENT, INSTR_POINTER) + offset_to_pokedata_workaround; +#endif +} + +void mem_prepare_before_first_execve(Tracee *tracee) +{ +#if HAS_POKEDATA_WORKAROUND + tracee->pokedata_workaround_stub_addr = (word_t)&launcher_pokedata_workaround; +#endif +} + +/** + * Copy @size bytes from the buffer @src_tracer to the address + * @dest_tracee within the memory space of the @tracee process. It + * returns -errno if an error occured, otherwise 0. + */ +int write_data(Tracee *tracee, word_t dest_tracee, const void *src_tracer, word_t size) +{ + dest_tracee = untag_pointer(dest_tracee); + word_t *src = (word_t *)src_tracer; + word_t *dest = (word_t *)dest_tracee; + + long status; + word_t word, i, j; + word_t nb_trailing_bytes; + word_t nb_full_words; + + uint8_t *last_dest_word; + uint8_t *last_src_word; + +#if defined(HAVE_PROCESS_VM) + struct iovec local; + struct iovec remote; + + local.iov_base = src; + local.iov_len = size; + + remote.iov_base = dest; + remote.iov_len = size; + + status = process_vm_writev(tracee->pid, &local, 1, &remote, 1, 0); + if ((size_t) status == size) + return 0; + /* Fallback to ptrace if something went wrong. */ + +#endif /* HAVE_PROCESS_VM */ + + nb_trailing_bytes = size % sizeof(word_t); + nb_full_words = (size - nb_trailing_bytes) / sizeof(word_t); + + /* Clear errno so we won't detect previous syscall failure as ptrace one */ + errno = 0; + + /* Copy one word by one word, except for the last one. */ + for (i = 0; i < nb_full_words; i++) { + status = ptrace_pokedata_or_via_stub(tracee, (word_t)(dest + i), load_word(&src[i])); + if (status < 0) { + note(tracee, WARNING, SYSTEM, "ptrace(POKEDATA)"); + return -EFAULT; + } + } + + if (nb_trailing_bytes == 0) + return 0; + + /* Copy the bytes in the last word carefully since we have to + * overwrite only the relevant ones. */ + + /* Clear errno so we won't detect previous syscall failure as ptrace one */ + errno = 0; + + word = ptrace(PTRACE_PEEKDATA, tracee->pid, dest + i, NULL); + if (errno != 0) { + note(tracee, WARNING, SYSTEM, "ptrace(PEEKDATA)"); + return -EFAULT; + } + + last_dest_word = (uint8_t *)&word; + last_src_word = (uint8_t *)&src[i]; + + for (j = 0; j < nb_trailing_bytes; j++) + last_dest_word[j] = last_src_word[j]; + + status = ptrace_pokedata_or_via_stub(tracee, (word_t)(dest + i), word); + if (status < 0) { + note(tracee, WARNING, SYSTEM, "ptrace(POKEDATA)"); + return -EFAULT; + } + + return 0; +} + +/** + * Gather the @src_tracer_count buffers pointed to by @src_tracer to + * the address @dest_tracee within the memory space of the @tracee + * process. This function returns -errno if an error occured, + * otherwise 0. + */ +int writev_data(Tracee *tracee, word_t dest_tracee, const struct iovec *src_tracer, int src_tracer_count) +{ + dest_tracee = untag_pointer(dest_tracee); + size_t size; + int status; + int i; + +#if defined(HAVE_PROCESS_VM) + struct iovec remote; + + for (i = 0, size = 0; i < src_tracer_count; i++) + size += src_tracer[i].iov_len; + + remote.iov_base = (word_t *)dest_tracee; + remote.iov_len = size; + + status = process_vm_writev(tracee->pid, src_tracer, src_tracer_count, &remote, 1, 0); + if ((size_t) status == size) + return 0; + /* Fallback to iterative-write if something went wrong. */ + +#endif /* HAVE_PROCESS_VM */ + + for (i = 0, size = 0; i < src_tracer_count; i++) { + status = write_data(tracee, dest_tracee + size, + src_tracer[i].iov_base, src_tracer[i].iov_len); + if (status < 0) + return status; + + size += src_tracer[i].iov_len; + } + + return 0; +} + +/** + * Copy @size bytes to the buffer @dest_tracer from the address + * @src_tracee within the memory space of the @tracee process. It + * returns -errno if an error occured, otherwise 0. + */ +int read_data(const Tracee *tracee, void *dest_tracer, word_t src_tracee, word_t size) +{ + src_tracee = untag_pointer(src_tracee); + word_t *src = (word_t *)src_tracee; + word_t *dest = (word_t *)dest_tracer; + + word_t nb_trailing_bytes; + word_t nb_full_words; + word_t word, i, j; + + uint8_t *last_src_word; + uint8_t *last_dest_word; + +#if defined(HAVE_PROCESS_VM) + long status; + struct iovec local; + struct iovec remote; + + local.iov_base = dest; + local.iov_len = size; + + remote.iov_base = src; + remote.iov_len = size; + + status = process_vm_readv(tracee->pid, &local, 1, &remote, 1, 0); + if ((size_t) status == size) + return 0; + /* Fallback to ptrace if something went wrong. */ + +#endif /* HAVE_PROCESS_VM */ + + nb_trailing_bytes = size % sizeof(word_t); + nb_full_words = (size - nb_trailing_bytes) / sizeof(word_t); + + /* Clear errno so we won't detect previous syscall failure as ptrace one */ + errno = 0; + + /* Copy one word by one word, except for the last one. */ + for (i = 0; i < nb_full_words; i++) { + word = ptrace(PTRACE_PEEKDATA, tracee->pid, src + i, NULL); + if (errno != 0) { + note(tracee, WARNING, SYSTEM, "ptrace(PEEKDATA)"); + return -EFAULT; + } + store_word(&dest[i], word); + } + + if (nb_trailing_bytes == 0) + return 0; + + /* Copy the bytes from the last word carefully since we have + * to not overwrite the bytes lying beyond @dest_tracer. */ + + word = ptrace(PTRACE_PEEKDATA, tracee->pid, src + i, NULL); + if (errno != 0) { + note(tracee, WARNING, SYSTEM, "ptrace(PEEKDATA)"); + return -EFAULT; + } + + last_dest_word = (uint8_t *)&dest[i]; + last_src_word = (uint8_t *)&word; + + for (j = 0; j < nb_trailing_bytes; j++) + last_dest_word[j] = last_src_word[j]; + + return 0; +} + +/** + * Copy to @dest_tracer at most @max_size bytes from the string + * pointed to by @src_tracee within the memory space of the @tracee + * process. This function returns -errno on error, otherwise + * it returns the number in bytes of the string, including the + * end-of-string terminator. + */ +int read_string(const Tracee *tracee, char *dest_tracer, word_t src_tracee, word_t max_size) +{ + src_tracee = untag_pointer(src_tracee); + word_t *src = (word_t *)src_tracee; + word_t *dest = (word_t *)dest_tracer; + + word_t nb_trailing_bytes; + word_t nb_full_words; + word_t word, i, j; + + uint8_t *src_word; + uint8_t *dest_word; + +#if defined(HAVE_PROCESS_VM) + /* [process_vm] system calls do not check the memory regions + * in the remote process until just before doing the + * read/write. Consequently, a partial read/write [1] may + * result if one of the remote_iov elements points to an + * invalid memory region in the remote process. No further + * reads/writes will be attempted beyond that point. Keep + * this in mind when attempting to read data of unknown length + * (such as C strings that are null-terminated) from a remote + * process, by avoiding spanning memory pages (typically 4KiB) + * in a single remote iovec element. (Instead, split the + * remote read into two remote_iov elements and have them + * merge back into a single write local_iov entry. The first + * read entry goes up to the page boundary, while the second + * starts on the next page boundary.). + * + * [1] Partial transfers apply at the granularity of iovec + * elements. These system calls won't perform a partial + * transfer that splits a single iovec element. + * + * -- man 2 process_vm_readv + */ + long status; + size_t size; + size_t offset; + struct iovec local; + struct iovec remote; + + static size_t chunk_size = 0; + static uintptr_t chunk_mask; + + /* A chunk shall not cross a page boundary. */ + if (chunk_size == 0) { + chunk_size = sysconf(_SC_PAGE_SIZE); + chunk_size = (chunk_size > 0 && chunk_size < 1024 ? chunk_size : 1024); + chunk_mask = ~(chunk_size - 1); + } + + /* Read the string by chunk. */ + offset = 0; + do { + uintptr_t current_chunk = (src_tracee + offset) & chunk_mask; + uintptr_t next_chunk = current_chunk + chunk_size; + + /* Compute the number of bytes available up to the + * next chunk or up to max_size. */ + size = next_chunk - (src_tracee + offset); + size = (size < max_size - offset ? size : max_size - offset); + + local.iov_base = (uint8_t *)dest + offset; + local.iov_len = size; + + remote.iov_base = (uint8_t *)src + offset; + remote.iov_len = size; + + status = process_vm_readv(tracee->pid, &local, 1, &remote, 1, 0); + if ((size_t) status != size) + goto fallback; + + status = strnlen(local.iov_base, size); + if ((size_t) status < size) { + size = offset + status + 1; + assert(size <= max_size); + return size; + } + + offset += size; + } while (offset < max_size); + assert(offset == max_size); + + /* Fallback to ptrace if something went wrong. */ +fallback: +#endif /* HAVE_PROCESS_VM */ + + nb_trailing_bytes = max_size % sizeof(word_t); + nb_full_words = (max_size - nb_trailing_bytes) / sizeof(word_t); + + /* Clear errno so we won't detect previous syscall failure as ptrace one */ + errno = 0; + + /* Copy one word by one word, except for the last one. */ + for (i = 0; i < nb_full_words; i++) { + word = ptrace(PTRACE_PEEKDATA, tracee->pid, src + i, NULL); + if (errno != 0) + return -EFAULT; + + store_word(&dest[i], word); + + /* Stop once an end-of-string is detected. */ + src_word = (uint8_t *)&word; + for (j = 0; j < sizeof(word_t); j++) + if (src_word[j] == '\0') + return i * sizeof(word_t) + j + 1; + } + + /* Copy the bytes from the last word carefully since we have + * to not overwrite the bytes lying beyond @dest_tracer. */ + + word = ptrace(PTRACE_PEEKDATA, tracee->pid, src + i, NULL); + if (errno != 0) + return -EFAULT; + + dest_word = (uint8_t *)&dest[i]; + src_word = (uint8_t *)&word; + + for (j = 0; j < nb_trailing_bytes; j++) { + dest_word[j] = src_word[j]; + if (src_word[j] == '\0') + break; + } + + return i * sizeof(word_t) + j + 1; +} + +/** + * Return the value of the word at the given @address in the @tracee's + * memory space. The caller must test errno to check if an error + * occured. + */ +word_t peek_word(const Tracee *tracee, word_t address) +{ + address = untag_pointer(address); + word_t result = 0; + +#if defined(HAVE_PROCESS_VM) + int status; + struct iovec local; + struct iovec remote; + + local.iov_base = &result; + local.iov_len = sizeof_word(tracee); + + remote.iov_base = (void *)address; + remote.iov_len = sizeof_word(tracee); + + errno = 0; + status = process_vm_readv(tracee->pid, &local, 1, &remote, 1, 0); + if (status > 0) + return result; + /* Fallback to ptrace if something went wrong. */ +#endif + errno = 0; + result = (word_t) ptrace(PTRACE_PEEKDATA, tracee->pid, address, NULL); + + /* From ptrace(2) manual: "Unfortunately, under Linux, + * different variations of this fault will return EIO or + * EFAULT more or less arbitrarily." */ + if (errno == EIO) + errno = EFAULT; + + /* Use only the 32 LSB when running a 32-bit process on a + * 64-bit kernel. */ + if (is_32on64_mode(tracee)) + result &= 0xFFFFFFFF; + + return result; +} + +/** + * Set the word at the given @address in the @tracee's memory space to + * the given @value. The caller must test errno to check if an error + * occured. + */ +void poke_word(const Tracee *tracee, word_t address, word_t value) +{ + address = untag_pointer(address); + word_t tmp; + +#if defined(HAVE_PROCESS_VM) + int status; + struct iovec local; + struct iovec remote; + + /* Note: &value points to the 32 LSB on 64-bit little-endian + * architecture. */ + local.iov_base = &value; + local.iov_len = sizeof_word(tracee); + + remote.iov_base = (void *)address; + remote.iov_len = sizeof_word(tracee); + + errno = 0; + status = process_vm_writev(tracee->pid, &local, 1, &remote, 1, 0); + if (status > 0) + return; + /* Fallback to ptrace if something went wrong. */ +#endif + /* Don't overwrite the 32 MSB when running a 32-bit process on + * a 64-bit kernel. */ + if (is_32on64_mode(tracee)) { + errno = 0; + tmp = (word_t) ptrace(PTRACE_PEEKDATA, tracee->pid, address, NULL); + if (errno != 0) + return; + + value |= (tmp & 0xFFFFFFFF00000000ULL); + } + + errno = 0; + (void) ptrace(PTRACE_POKEDATA, tracee->pid, address, value); + + /* From ptrace(2) manual: "Unfortunately, under Linux, + * different variations of this fault will return EIO or + * EFAULT more or less arbitrarily." */ + if (errno == EIO) + errno = EFAULT; + + return; +} + +/** + * Allocate @size bytes in the @tracee's memory space. This function + * returns the address of the allocated memory in the @tracee's memory + * space, otherwise 0 if an error occured. + */ +word_t alloc_mem(Tracee *tracee, ssize_t size) +{ + word_t stack_pointer; + + /* This function should be called in sysenter only since the + * stack pointer is systematically restored at the end of + * sysexit (except for execve, but in this case the stack + * pointer should be handled with care since it is used by the + * process to retrieve argc, argv, envp, and auxv). */ + assert(IS_IN_SYSENTER(tracee)); + + /* Get the current value of the stack pointer from the tracee's + * USER area. */ + stack_pointer = peek_reg(tracee, CURRENT, STACK_POINTER); + + /* Some ABIs specify an amount of bytes after the stack + * pointer that shall not be used by anything but the compiler + * (for optimization purpose). */ + if (stack_pointer == peek_reg(tracee, ORIGINAL, STACK_POINTER)) + size += RED_ZONE_SIZE; + + /* Sanity check. */ + if ( (size > 0 && stack_pointer <= (word_t) size) + || (size < 0 && stack_pointer >= ULONG_MAX + size)) { + note(tracee, WARNING, INTERNAL, "integer under/overflow detected in %s", + __FUNCTION__); + return 0; + } + + /* Remember the stack grows downward. */ + stack_pointer -= size; + + /* Set the new value of the stack pointer in the tracee's USER + * area. */ + poke_reg(tracee, STACK_POINTER, stack_pointer); + return stack_pointer; +} + +/** + * Clear @size bytes at the given @address in the @tracee's memory + * space. This function returns -errno if an error occured, otherwise + * 0. + */ +int clear_mem(Tracee *tracee, word_t address, size_t size) +{ + address = untag_pointer(address); + int status; + void *zeros; + + zeros = mmap(NULL, size, PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (zeros == MAP_FAILED) + return -errno; + + status = write_data(tracee, address, zeros, size); + munmap(zeros, size); + return status; +} diff --git a/core/proot/src/main/cpp/tracee/mem.h b/core/proot/src/main/cpp/tracee/mem.h new file mode 100644 index 000000000..b982dd869 --- /dev/null +++ b/core/proot/src/main/cpp/tracee/mem.h @@ -0,0 +1,114 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#ifndef TRACEE_MEM_H +#define TRACEE_MEM_H + +#include /* PATH_MAX, */ +#include /* pid_t, size_t, */ +#include /* pid_t, size_t, */ +#include /* struct iovec, */ +#include /* ENAMETOOLONG, */ + +#include "arch.h" /* word_t, */ +#include "tracee/tracee.h" + +extern int write_data(Tracee *tracee, word_t dest_tracee, const void *src_tracer, word_t size); +extern int writev_data(Tracee *tracee, word_t dest_tracee, const struct iovec *src_tracer, int src_tracer_count); +extern int read_data(const Tracee *tracee, void *dest_tracer, word_t src_tracee, word_t size); +extern int read_string(const Tracee *tracee, char *dest_tracer, word_t src_tracee, word_t max_size); +extern word_t peek_word(const Tracee *tracee, word_t address); +extern void poke_word(const Tracee *tracee, word_t address, word_t value); +extern word_t alloc_mem(Tracee *tracee, ssize_t size); +extern int clear_mem(Tracee *tracee, word_t address, size_t size); +extern void mem_prepare_after_execve(Tracee *tracee); +extern void mem_prepare_before_first_execve(Tracee *tracee); + +/** + * Copy to @dest_tracer at most PATH_MAX bytes -- including the + * end-of-string terminator -- from the string pointed to by + * @src_tracee within the memory space of the @tracee process. This + * function returns -errno on error, otherwise it returns the number + * in bytes of the string, including the end-of-string terminator. + */ +static inline int read_path(const Tracee *tracee, char dest_tracer[PATH_MAX], word_t src_tracee) +{ + int status; + + status = read_string(tracee, dest_tracer, src_tracee, PATH_MAX); + if (status < 0) + return status; + if (status >= PATH_MAX) + return -ENAMETOOLONG; + + return status; +} + +/** + * Generate a function that returns the value of the @type at the + * given @address in the @tracee's memory space. The caller must test + * errno to check if an error occured. + */ +#define GENERATE_peek(type) \ +static inline type ## _t peek_ ## type(const Tracee *tracee, word_t address) \ +{ \ + type ## _t result; \ + errno = -read_data(tracee, &result, address, sizeof(type ## _t)); \ + return result; \ +} + +GENERATE_peek(uint8); +GENERATE_peek(uint16); +GENERATE_peek(uint32); +GENERATE_peek(uint64); + +GENERATE_peek(int8); +GENERATE_peek(int16); +GENERATE_peek(int32); +GENERATE_peek(int64); + +#undef GENERATE_peek + +/** + * Generate a function that set the @type at the given @address in the + * @tracee's memory space to the given @value. The caller must test + * errno to check if an error occured. + */ +#define GENERATE_poke(type) \ +static inline void poke_ ## type(Tracee *tracee, word_t address, type ## _t value) \ +{ \ + errno = -write_data(tracee, address, &value, sizeof(type ## _t)); \ +} + +GENERATE_poke(uint8); +GENERATE_poke(uint16); +GENERATE_poke(uint32); +GENERATE_poke(uint64); + +GENERATE_poke(int8); +GENERATE_poke(int16); +GENERATE_poke(int32); +GENERATE_poke(int64); + +#undef GENERATE_poke + +#endif /* TRACEE_MEM_H */ diff --git a/core/proot/src/main/cpp/tracee/reg.c b/core/proot/src/main/cpp/tracee/reg.c new file mode 100644 index 000000000..3859f8e56 --- /dev/null +++ b/core/proot/src/main/cpp/tracee/reg.c @@ -0,0 +1,399 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#include /* off_t */ +#include /* struct user*, */ +#include /* ptrace(2), PTRACE*, */ +#include /* assert(3), */ +#include /* errno(3), */ +#include /* offsetof(), */ +#include /* *int*_t, */ +#include /* PRI*, */ +#include /* ULONG_MAX, */ +#include /* memcpy(3), */ +#include /* struct iovec, */ + +#include "arch.h" + +#if defined(ARCH_ARM64) +#include /* NT_PRSTATUS */ +#endif + +#include "syscall/sysnum.h" +#include "tracee/reg.h" +#include "tracee/abi.h" +#include "cli/note.h" +#include "compat.h" + +/** + * Compute the offset of the register @reg_name in the USER area. + */ +#define USER_REGS_OFFSET(reg_name) \ + (offsetof(struct user, regs) \ + + offsetof(struct user_regs_struct, reg_name)) + +#define REG(tracee, version, index) \ + (*(word_t*) (((uint8_t *) &tracee->_regs[version]) + reg_offset[index])) + +/* Specify the ABI registers (syscall argument passing, stack pointer). + * See sysdeps/unix/sysv/linux/${ARCH}/syscall.S from the GNU C Library. */ +#if defined(ARCH_X86_64) + + static off_t reg_offset[] = { + [SYSARG_NUM] = USER_REGS_OFFSET(orig_rax), + [SYSARG_1] = USER_REGS_OFFSET(rdi), + [SYSARG_2] = USER_REGS_OFFSET(rsi), + [SYSARG_3] = USER_REGS_OFFSET(rdx), + [SYSARG_4] = USER_REGS_OFFSET(r10), + [SYSARG_5] = USER_REGS_OFFSET(r8), + [SYSARG_6] = USER_REGS_OFFSET(r9), + [SYSARG_RESULT] = USER_REGS_OFFSET(rax), + [STACK_POINTER] = USER_REGS_OFFSET(rsp), + [INSTR_POINTER] = USER_REGS_OFFSET(rip), + [RTLD_FINI] = USER_REGS_OFFSET(rdx), + [STATE_FLAGS] = USER_REGS_OFFSET(eflags), + [USERARG_1] = USER_REGS_OFFSET(rdi), + }; + + static off_t reg_offset_x86[] = { + [SYSARG_NUM] = USER_REGS_OFFSET(orig_rax), + [SYSARG_1] = USER_REGS_OFFSET(rbx), + [SYSARG_2] = USER_REGS_OFFSET(rcx), + [SYSARG_3] = USER_REGS_OFFSET(rdx), + [SYSARG_4] = USER_REGS_OFFSET(rsi), + [SYSARG_5] = USER_REGS_OFFSET(rdi), + [SYSARG_6] = USER_REGS_OFFSET(rbp), + [SYSARG_RESULT] = USER_REGS_OFFSET(rax), + [STACK_POINTER] = USER_REGS_OFFSET(rsp), + [INSTR_POINTER] = USER_REGS_OFFSET(rip), + [RTLD_FINI] = USER_REGS_OFFSET(rdx), + [STATE_FLAGS] = USER_REGS_OFFSET(eflags), + [USERARG_1] = USER_REGS_OFFSET(rax), + }; + + #undef REG + #define REG(tracee, version, index) \ + (*(word_t*) (tracee->_regs[version].cs == 0x23 \ + ? (((uint8_t *) &tracee->_regs[version]) + reg_offset_x86[index]) \ + : (((uint8_t *) &tracee->_regs[version]) + reg_offset[index]))) + +#elif defined(ARCH_ARM_EABI) + + static off_t reg_offset[] = { + [SYSARG_NUM] = USER_REGS_OFFSET(uregs[7]), + [SYSARG_1] = USER_REGS_OFFSET(uregs[0]), + [SYSARG_2] = USER_REGS_OFFSET(uregs[1]), + [SYSARG_3] = USER_REGS_OFFSET(uregs[2]), + [SYSARG_4] = USER_REGS_OFFSET(uregs[3]), + [SYSARG_5] = USER_REGS_OFFSET(uregs[4]), + [SYSARG_6] = USER_REGS_OFFSET(uregs[5]), + [SYSARG_RESULT] = USER_REGS_OFFSET(uregs[0]), + [STACK_POINTER] = USER_REGS_OFFSET(uregs[13]), + [INSTR_POINTER] = USER_REGS_OFFSET(uregs[15]), + [USERARG_1] = USER_REGS_OFFSET(uregs[0]), + }; + +#elif defined(ARCH_ARM64) + + #undef USER_REGS_OFFSET + #define USER_REGS_OFFSET(reg_name) offsetof(struct user_regs_struct, reg_name) + #define USER_REGS_OFFSET_32(reg_number) ((reg_number) * 4) + + static off_t reg_offset[] = { + [SYSARG_NUM] = USER_REGS_OFFSET(regs[8]), + [SYSARG_1] = USER_REGS_OFFSET(regs[0]), + [SYSARG_2] = USER_REGS_OFFSET(regs[1]), + [SYSARG_3] = USER_REGS_OFFSET(regs[2]), + [SYSARG_4] = USER_REGS_OFFSET(regs[3]), + [SYSARG_5] = USER_REGS_OFFSET(regs[4]), + [SYSARG_6] = USER_REGS_OFFSET(regs[5]), + [SYSARG_RESULT] = USER_REGS_OFFSET(regs[0]), + [STACK_POINTER] = USER_REGS_OFFSET(sp), + [INSTR_POINTER] = USER_REGS_OFFSET(pc), + [USERARG_1] = USER_REGS_OFFSET(regs[0]), + }; + + static off_t reg_offset_armeabi[] = { + [SYSARG_NUM] = USER_REGS_OFFSET_32(7), + [SYSARG_1] = USER_REGS_OFFSET_32(0), + [SYSARG_2] = USER_REGS_OFFSET_32(1), + [SYSARG_3] = USER_REGS_OFFSET_32(2), + [SYSARG_4] = USER_REGS_OFFSET_32(3), + [SYSARG_5] = USER_REGS_OFFSET_32(4), + [SYSARG_6] = USER_REGS_OFFSET_32(5), + [SYSARG_RESULT] = USER_REGS_OFFSET_32(0), + [STACK_POINTER] = USER_REGS_OFFSET_32(13), + [INSTR_POINTER] = USER_REGS_OFFSET_32(15), + [USERARG_1] = USER_REGS_OFFSET_32(0), + }; + + #undef REG + #define REG(tracee, version, index) \ + (*(word_t*) (tracee->is_aarch32 \ + ? (((uint8_t *) &tracee->_regs[version]) + reg_offset_armeabi[index]) \ + : (((uint8_t *) &tracee->_regs[version]) + reg_offset[index]))) + +#elif defined(ARCH_X86) + + static off_t reg_offset[] = { + [SYSARG_NUM] = USER_REGS_OFFSET(orig_eax), + [SYSARG_1] = USER_REGS_OFFSET(ebx), + [SYSARG_2] = USER_REGS_OFFSET(ecx), + [SYSARG_3] = USER_REGS_OFFSET(edx), + [SYSARG_4] = USER_REGS_OFFSET(esi), + [SYSARG_5] = USER_REGS_OFFSET(edi), + [SYSARG_6] = USER_REGS_OFFSET(ebp), + [SYSARG_RESULT] = USER_REGS_OFFSET(eax), + [STACK_POINTER] = USER_REGS_OFFSET(esp), + [INSTR_POINTER] = USER_REGS_OFFSET(eip), + [RTLD_FINI] = USER_REGS_OFFSET(edx), + [STATE_FLAGS] = USER_REGS_OFFSET(eflags), + [USERARG_1] = USER_REGS_OFFSET(eax), + }; + +#elif defined(ARCH_SH4) + + static off_t reg_offset[] = { + [SYSARG_NUM] = USER_REGS_OFFSET(regs[3]), + [SYSARG_1] = USER_REGS_OFFSET(regs[4]), + [SYSARG_2] = USER_REGS_OFFSET(regs[5]), + [SYSARG_3] = USER_REGS_OFFSET(regs[6]), + [SYSARG_4] = USER_REGS_OFFSET(regs[7]), + [SYSARG_5] = USER_REGS_OFFSET(regs[0]), + [SYSARG_6] = USER_REGS_OFFSET(regs[1]), + [SYSARG_RESULT] = USER_REGS_OFFSET(regs[0]), + [STACK_POINTER] = USER_REGS_OFFSET(regs[15]), + [INSTR_POINTER] = USER_REGS_OFFSET(pc), + [RTLD_FINI] = USER_REGS_OFFSET(r4), + }; + +#else + + #error "Unsupported architecture" + +#endif + +/** + * Return the *cached* value of the given @tracees' @reg. + */ +word_t peek_reg(const Tracee *tracee, RegVersion version, Reg reg) +{ + word_t result; + + assert(version < NB_REG_VERSION); + + result = REG(tracee, version, reg); + + /* Use only the 32 least significant bits (LSB) when running + * 32-bit processes on a 64-bit kernel. */ + if (is_32on64_mode(tracee)) + result &= 0xFFFFFFFF; + + return result; +} + +/** + * Set the *cached* value of the given @tracees' @reg. + */ +void poke_reg(Tracee *tracee, Reg reg, word_t value) +{ + if (peek_reg(tracee, CURRENT, reg) == value) + return; + +#ifdef ARCH_ARM64 + if (is_32on64_mode(tracee)) { + *(uint32_t *) ®(tracee, CURRENT, reg) = value; + } else +#endif + REG(tracee, CURRENT, reg) = value; + tracee->_regs_were_changed = true; +} + +/** + * Print the value of the current @tracee's registers according + * to the @verbose_level. Note: @message is mixed to the output. + */ +void print_current_regs(Tracee *tracee, int verbose_level, const char *message) +{ + if (tracee->verbose < verbose_level) + return; + + note(tracee, INFO, INTERNAL, + "vpid %" PRIu64 ": %s: %s(0x%lx, 0x%lx, 0x%lx, 0x%lx, 0x%lx, 0x%lx) = 0x%lx [0x%lx, %d]", + tracee->vpid, message, + stringify_sysnum(get_sysnum(tracee, CURRENT)), + peek_reg(tracee, CURRENT, SYSARG_1), peek_reg(tracee, CURRENT, SYSARG_2), + peek_reg(tracee, CURRENT, SYSARG_3), peek_reg(tracee, CURRENT, SYSARG_4), + peek_reg(tracee, CURRENT, SYSARG_5), peek_reg(tracee, CURRENT, SYSARG_6), + peek_reg(tracee, CURRENT, SYSARG_RESULT), + peek_reg(tracee, CURRENT, STACK_POINTER), + get_abi(tracee)); +} + +/** + * Save the @tracee's current register bank into the @version register + * bank. + */ +void save_current_regs(Tracee *tracee, RegVersion version) +{ + /* Optimization: don't restore original register values if + * they were never changed. */ + if (version == ORIGINAL) + tracee->_regs_were_changed = false; + + memcpy(&tracee->_regs[version], &tracee->_regs[CURRENT], sizeof(tracee->_regs[CURRENT])); +} + +/** + * Copy all @tracee's general purpose registers into a dedicated + * cache. This function returns -errno if an error occured, 0 + * otherwise. + */ +int fetch_regs(Tracee *tracee) +{ + int status; + +#if defined(ARCH_ARM64) + struct iovec regs; + + regs.iov_base = &tracee->_regs[CURRENT]; + regs.iov_len = sizeof(tracee->_regs[CURRENT]); + + status = ptrace(PTRACE_GETREGSET, tracee->pid, NT_PRSTATUS, ®s); +#else + status = ptrace(PTRACE_GETREGS, tracee->pid, NULL, &tracee->_regs[CURRENT]); +#endif + if (status < 0) + return status; + + return 0; +} + +int push_specific_regs(Tracee *tracee, bool including_sysnum) +{ + int status; + + if (tracee->_regs_were_changed + || (tracee->restore_original_regs && tracee->restore_original_regs_after_seccomp_event)) { + /* At the very end of a syscall, with regard to the + * entry, only the result register can be modified by + * PRoot. */ + if (tracee->restore_original_regs) { + RegVersion restore_from = ORIGINAL; + if (tracee->restore_original_regs_after_seccomp_event) { + restore_from = ORIGINAL_SECCOMP_REWRITE; + tracee->restore_original_regs_after_seccomp_event = false; + } + /* Restore the sysarg register only if it is + * not the same as the result register. Note: + * it's never the case on x86 architectures, + * so don't make this check, otherwise it + * would introduce useless complexity because + * of the multiple ABI support. */ +#if defined(ARCH_X86) || defined(ARCH_X86_64) +# define RESTORE(sysarg) (REG(tracee, CURRENT, sysarg) = REG(tracee, restore_from, sysarg)) +#else +# define RESTORE(sysarg) (void) (reg_offset[SYSARG_RESULT] != reg_offset[sysarg] && \ + (REG(tracee, CURRENT, sysarg) = REG(tracee, restore_from, sysarg))) +#endif + + RESTORE(SYSARG_NUM); + RESTORE(SYSARG_1); + RESTORE(SYSARG_2); + RESTORE(SYSARG_3); + RESTORE(SYSARG_4); + RESTORE(SYSARG_5); + RESTORE(SYSARG_6); + RESTORE(STACK_POINTER); + } + +#if defined(ARCH_ARM64) + struct iovec regs; + word_t current_sysnum = REG(tracee, CURRENT, SYSARG_NUM); + + /* Update syscall number if needed. On arm64, a new + * subcommand has been added to PTRACE_{S,G}ETREGSET + * to allow write/read of current sycall number. */ + if (including_sysnum && current_sysnum != REG(tracee, ORIGINAL, SYSARG_NUM)) { + regs.iov_base = ¤t_sysnum; + regs.iov_len = sizeof(current_sysnum); + status = ptrace(PTRACE_SETREGSET, tracee->pid, NT_ARM_SYSTEM_CALL, ®s); + if (status < 0) { + //note(tracee, WARNING, SYSTEM, "can't set the syscall number"); + return status; + } + } + + /* Update other registers. */ + regs.iov_base = &tracee->_regs[CURRENT]; + regs.iov_len = sizeof(tracee->_regs[CURRENT]); + + status = ptrace(PTRACE_SETREGSET, tracee->pid, NT_PRSTATUS, ®s); +#else +# if defined(ARCH_ARM_EABI) + /* On ARM, a special ptrace request is required to + * change effectively the syscall number during a + * ptrace-stop. */ + word_t current_sysnum = REG(tracee, CURRENT, SYSARG_NUM); + if (including_sysnum && current_sysnum != REG(tracee, ORIGINAL, SYSARG_NUM)) { + status = ptrace(PTRACE_SET_SYSCALL, tracee->pid, 0, current_sysnum); + if (status < 0) { + //note(tracee, WARNING, SYSTEM, "can't set the syscall number"); + return status; + } + } +# endif + + status = ptrace(PTRACE_SETREGS, tracee->pid, NULL, &tracee->_regs[CURRENT]); +#endif + if (status < 0) + return status; + } + + return 0; +} + +/** + * Copy the cached values of all @tracee's general purpose registers + * back to the process, if necessary. This function returns -errno if + * an error occured, 0 otherwise. + */ +int push_regs(Tracee *tracee) { + return push_specific_regs(tracee, true); +} + +word_t get_systrap_size(Tracee *tracee) { +#if defined(ARCH_ARM_EABI) + /* On ARM thumb mode systrap size is 2 */ + if (tracee->_regs[CURRENT].ARM_cpsr & PSR_T_BIT) { + return 2; + } +#elif defined(ARCH_ARM64) + /* Same for AArch32, but we don't have nice macros */ + if (tracee->is_aarch32 && (((unsigned char *) &tracee->_regs[CURRENT])[0x40] & 0x20) != 0) { + return 2; + } +#else + (void) tracee; +#endif + return SYSTRAP_SIZE; +} diff --git a/core/proot/src/main/cpp/tracee/reg.h b/core/proot/src/main/cpp/tracee/reg.h new file mode 100644 index 000000000..453638893 --- /dev/null +++ b/core/proot/src/main/cpp/tracee/reg.h @@ -0,0 +1,57 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#ifndef TRACEE_REG_H +#define TRACEE_REG_H + +#include "tracee/tracee.h" +#include "arch.h" + +typedef enum { + SYSARG_NUM = 0, + SYSARG_1, + SYSARG_2, + SYSARG_3, + SYSARG_4, + SYSARG_5, + SYSARG_6, + SYSARG_RESULT, + STACK_POINTER, + INSTR_POINTER, + RTLD_FINI, + STATE_FLAGS, + USERARG_1, +} Reg; + +extern int fetch_regs(Tracee *tracee); +extern int push_specific_regs(Tracee *tracee, bool including_sysnum); +extern int push_regs(Tracee *tracee); + +extern word_t peek_reg(const Tracee *tracee, RegVersion version, Reg reg); +extern void poke_reg(Tracee *tracee, Reg reg, word_t value); + +extern void print_current_regs(Tracee *tracee, int verbose_level, const char *message); +extern void save_current_regs(Tracee *tracee, RegVersion version); + +extern word_t get_systrap_size(Tracee *tracee); + +#endif /* TRACEE_REG_H */ diff --git a/core/proot/src/main/cpp/tracee/seccomp.c b/core/proot/src/main/cpp/tracee/seccomp.c new file mode 100644 index 000000000..32bfac322 --- /dev/null +++ b/core/proot/src/main/cpp/tracee/seccomp.c @@ -0,0 +1,621 @@ +#include /* E*, */ +#include /* SIGSYS, */ +#include /* getpgid, */ +#include /* utimbuf, */ +#include /* statfs64 */ +#include /* memset */ +#include /* SYS_SENDMMSG */ +#include /* assert(3), */ +#include /* time(2), */ + +#include "extension/extension.h" +#include "cli/note.h" +#include "syscall/chain.h" +#include "syscall/syscall.h" +#include "tracee/seccomp.h" +#include "tracee/mem.h" +#include "tracee/statx.h" +#include "path/path.h" + +static int handle_seccomp_event_common(Tracee *tracee); + +/** + * Restart syscall that caused seccomp event + * after changing it in tracee registers + * + * Syscall that will be restarted will be translated by proot + * so SIGSYS handler sees untranslated paths and should leave + * them untranslated. + */ +void restart_syscall_after_seccomp(Tracee* tracee) { + word_t instr_pointer; + + /* Enable restore regs at end of replaced call. + * This also defers delivering of signals until restarted syscall finishes. */ + tracee->restore_original_regs_after_seccomp_event = true; + tracee->restart_how = PTRACE_SYSCALL; + + /* Move the instruction pointer back to the original trap */ + instr_pointer = peek_reg(tracee, CURRENT, INSTR_POINTER); + poke_reg(tracee, INSTR_POINTER, instr_pointer - get_systrap_size(tracee)); + + /* X86 usually uses orig_rax when selecting syscall, + * but as this code is happening outside syscall handler + * we need to copy orig_eax back to eax. */ +#if defined(ARCH_X86_64) + tracee->_regs[CURRENT].rax = tracee->_regs[CURRENT].orig_rax; +#elif defined(ARCH_X86) + tracee->_regs[CURRENT].eax = tracee->_regs[CURRENT].orig_eax; +#endif + + /* Write registers. (Omiting special sysnum logic as we're not during syscall + * execution, but we're queueing new syscall to be called) */ + push_specific_regs(tracee, false); +} + +/** + * Set specified result (negative for errno) and do not restart syscall. + */ +void set_result_after_seccomp(Tracee *tracee, word_t result) { + VERBOSE(tracee, 3, "Setting result after SIGSYS to 0x%lx", result); + poke_reg(tracee, SYSARG_RESULT, result); + push_specific_regs(tracee, false); +} + +/** + * Handle SIGSYS signal that was caused by system seccomp policy. + * + * Return 0 to swallow signal or SIGSYS to deliver it to process. + */ +int handle_seccomp_event(Tracee* tracee) +{ + int ret; + + /* Reset status so next SIGTRAP | 0x80 is + * recognized as syscall entry. */ + tracee->status = 0; + + /* Registers are never restored at this stage as they weren't saved. */ + tracee->restore_original_regs = false; + + /* Fetch registers. */ + ret = fetch_regs(tracee); + if (ret != 0) { + VERBOSE(tracee, 1, "Couldn't fetch regs on seccomp SIGSYS"); + return SIGSYS; + } + + /* Save regs so they can be restored at end of replaced call. */ + save_current_regs(tracee, ORIGINAL_SECCOMP_REWRITE); + + /* X86 uses orig_rax when selecting syscall, + * however at this point we are after syscall has been rejected + * and orig_rax was reset to -1. */ +#if defined(ARCH_X86_64) + tracee->_regs[CURRENT].orig_rax = tracee->_regs[CURRENT].rax; +#elif defined(ARCH_X86) + tracee->_regs[CURRENT].orig_eax = tracee->_regs[CURRENT].eax; +#endif + + print_current_regs(tracee, 3, "seccomp SIGSYS"); + + return handle_seccomp_event_common(tracee); +} + +void fix_and_restart_enosys_syscall(Tracee* tracee) +{ + /* Reset tracee state so we're not handling syscall exit */ + tracee->status = 0; + tracee->restore_original_regs = false; + + /* Restore and save original registers */ + memcpy(&tracee->_regs[CURRENT], &tracee->_regs[ORIGINAL], sizeof(tracee->_regs[CURRENT])); + save_current_regs(tracee, ORIGINAL_SECCOMP_REWRITE); + + handle_seccomp_event_common(tracee); +} + +static int handle_seccomp_event_common(Tracee *tracee) +{ + int ret; + int status; + Sysnum sysnum = get_sysnum(tracee, CURRENT); + + sysnum = get_sysnum(tracee, CURRENT); + + status = notify_extensions(tracee, SIGSYS_OCC, 0, 0); + if (status < 0) { + VERBOSE(tracee, 4, "SIGSYS errored out when being handled by an extension"); + set_result_after_seccomp(tracee, status); + return 0; + } + if (status == 1) { + VERBOSE(tracee, 4, "SIGSYS fully handled by an extension"); + set_result_after_seccomp(tracee, 0); + return 0; + } + if (status == 2) { + VERBOSE(tracee, 4, "SIGSYS fully handled by an extension with result set"); + return 0; + } + + switch (sysnum) { + case PR_open: + set_sysnum(tracee, PR_openat); + poke_reg(tracee, SYSARG_4, peek_reg(tracee, CURRENT, SYSARG_3)); + poke_reg(tracee, SYSARG_3, peek_reg(tracee, CURRENT, SYSARG_2)); + poke_reg(tracee, SYSARG_2, peek_reg(tracee, CURRENT, SYSARG_1)); + poke_reg(tracee, SYSARG_1, AT_FDCWD); + restart_syscall_after_seccomp(tracee); + break; + + case PR_openat2: { + /* int openat2(int dirfd, const char *pathname, + * struct open_how *how, size_t size); + * + * Convert to openat() so the call survives an outer seccomp + * policy that rejects the newer syscall (this is what raised + * the SIGSYS that brought us here), and so PRoot translates + * the path when the syscall is restarted. The how.resolve + * flags (RESOLVE_BENEATH, ...) are dropped: they are not + * compatible with PRoot rewriting paths to absolute host + * paths, and PRoot already confines resolution to the rootfs. */ + struct proot_open_how how = {}; + word_t how_size = peek_reg(tracee, CURRENT, SYSARG_4); + if (how_size > sizeof(how)) + how_size = sizeof(how); + ret = read_data(tracee, &how, peek_reg(tracee, CURRENT, SYSARG_3), how_size); + if (ret < 0) { + set_result_after_seccomp(tracee, ret); + break; + } + set_sysnum(tracee, PR_openat); + poke_reg(tracee, SYSARG_3, how.flags); + poke_reg(tracee, SYSARG_4, how.mode); + restart_syscall_after_seccomp(tracee); + break; + } + + case PR_accept: + set_sysnum(tracee, PR_accept4); + poke_reg(tracee, SYSARG_4, 0); + restart_syscall_after_seccomp(tracee); + break; + + case PR_setgroups: + case PR_setgroups32: + set_result_after_seccomp(tracee, 0); + break; + + /* The Android parent process commonly installs a seccomp + * filter that traps mount/umount/pivot_root/unshare/setns + * with SIGSYS. Mirror what enter.c does for these: pretend + * they succeeded and apply the mount/pivot_root binding + * emulation so sandbox helpers like bubblewrap can proceed. */ + case PR_mount: + apply_emulated_mount(tracee); + set_result_after_seccomp(tracee, 0); + break; + + case PR_pivot_root: + apply_emulated_pivot_root(tracee); + set_result_after_seccomp(tracee, 0); + break; + + case PR_umount: + case PR_umount2: + apply_emulated_umount(tracee); + set_result_after_seccomp(tracee, 0); + break; + + case PR_unshare: + case PR_setns: + set_result_after_seccomp(tracee, 0); + break; + + case PR_getpgrp: + /* Query value with getpgid and set it as result. */ + set_result_after_seccomp(tracee, getpgid(tracee->pid)); + break; + + case PR_symlink: + set_sysnum(tracee, PR_symlinkat); + poke_reg(tracee, SYSARG_3, peek_reg(tracee, CURRENT, SYSARG_2)); + poke_reg(tracee, SYSARG_2, AT_FDCWD); + restart_syscall_after_seccomp(tracee); + break; + + case PR_link: + set_sysnum(tracee, PR_linkat); + poke_reg(tracee, SYSARG_4, peek_reg(tracee, CURRENT, SYSARG_2)); + poke_reg(tracee, SYSARG_2, peek_reg(tracee, CURRENT, SYSARG_1)); + poke_reg(tracee, SYSARG_1, AT_FDCWD); + poke_reg(tracee, SYSARG_3, AT_FDCWD); + poke_reg(tracee, SYSARG_5, 0); + restart_syscall_after_seccomp(tracee); + break; + + case PR_chmod: + set_sysnum(tracee, PR_fchmodat); + poke_reg(tracee, SYSARG_3, peek_reg(tracee, CURRENT, SYSARG_2)); + poke_reg(tracee, SYSARG_2, peek_reg(tracee, CURRENT, SYSARG_1)); + poke_reg(tracee, SYSARG_1, AT_FDCWD); + poke_reg(tracee, SYSARG_4, 0); + restart_syscall_after_seccomp(tracee); + break; + + case PR_chown: + case PR_lchown: + case PR_chown32: + case PR_lchown32: + set_sysnum(tracee, PR_fchownat); + poke_reg(tracee, SYSARG_4, peek_reg(tracee, CURRENT, SYSARG_3)); + poke_reg(tracee, SYSARG_3, peek_reg(tracee, CURRENT, SYSARG_2)); + poke_reg(tracee, SYSARG_2, peek_reg(tracee, CURRENT, SYSARG_1)); + poke_reg(tracee, SYSARG_1, AT_FDCWD); + if (sysnum == PR_lchown || sysnum == PR_lchown32) { + poke_reg(tracee, SYSARG_5, AT_SYMLINK_NOFOLLOW); + } else { + poke_reg(tracee, SYSARG_5, 0); + } + restart_syscall_after_seccomp(tracee); + break; + + case PR_unlink: + case PR_rmdir: + set_sysnum(tracee, PR_unlinkat); + poke_reg(tracee, SYSARG_2, peek_reg(tracee, CURRENT, SYSARG_1)); + poke_reg(tracee, SYSARG_1, AT_FDCWD); + poke_reg(tracee, SYSARG_3, sysnum==PR_rmdir ? AT_REMOVEDIR : 0); + restart_syscall_after_seccomp(tracee); + break; + + case PR_send: + set_sysnum(tracee, PR_sendto); + poke_reg(tracee, SYSARG_5, 0); + poke_reg(tracee, SYSARG_6, 0); + restart_syscall_after_seccomp(tracee); + break; + + case PR_recv: + set_sysnum(tracee, PR_recvfrom); + poke_reg(tracee, SYSARG_5, 0); + poke_reg(tracee, SYSARG_6, 0); + restart_syscall_after_seccomp(tracee); + break; + + case PR_waitpid: + set_sysnum(tracee, PR_wait4); + poke_reg(tracee, SYSARG_4, 0); + restart_syscall_after_seccomp(tracee); + break; + + case PR_statfs: + { + int size; + int status; + char path[PATH_MAX]; + char original[PATH_MAX]; + char devshm_path[PATH_MAX]; + struct statfs64 my_statfs64; + struct compat_statfs my_statfs; + size = read_string(tracee, original, peek_reg(tracee, CURRENT, SYSARG_1), PATH_MAX); + if (size < 0) { + set_result_after_seccomp(tracee, size); + break; + } + if (size >= PATH_MAX) { + set_result_after_seccomp(tracee, -ENAMETOOLONG); + break; + } + translate_path(tracee, path, AT_FDCWD, original, true); + errno = 0; + status = statfs64(path, &my_statfs64); + if (errno != 0) { + set_result_after_seccomp(tracee, -errno); + break; + } + + /* Fake /dev/shm being tmpfs, see statfs handler in syscall/exit.c */ + if (translate_path(tracee, devshm_path, AT_FDCWD, "/dev/shm", true) >= 0) { + Comparison comparison = compare_paths(devshm_path, path); + if (comparison == PATHS_ARE_EQUAL || comparison == PATH1_IS_PREFIX) { + my_statfs64.f_type = 0x01021994; + } + } + + if ((my_statfs64.f_blocks | my_statfs64.f_bfree | my_statfs64.f_bavail | + my_statfs64.f_bsize | my_statfs64.f_frsize | my_statfs64.f_files | + my_statfs64.f_ffree) & 0xffffffff00000000ULL) { + set_result_after_seccomp(tracee, -EOVERFLOW); + break; + } + my_statfs.f_type = my_statfs64.f_type; + my_statfs.f_bsize = my_statfs64.f_bsize; + my_statfs.f_blocks = my_statfs64.f_blocks; + my_statfs.f_bfree = my_statfs64.f_bfree; + my_statfs.f_bavail = my_statfs64.f_bavail; + my_statfs.f_files = my_statfs64.f_files; + my_statfs.f_ffree = my_statfs64.f_ffree; + my_statfs.f_fsid = my_statfs64.f_fsid; + my_statfs.f_namelen = my_statfs64.f_namelen; + my_statfs.f_frsize = my_statfs64.f_frsize; + my_statfs.f_flags = my_statfs64.f_flags; + memset(my_statfs.f_spare, 0, sizeof(my_statfs.f_spare)); + write_data(tracee, peek_reg(tracee, CURRENT, SYSARG_2), &my_statfs, sizeof(struct compat_statfs)); + + set_result_after_seccomp(tracee, 0); + break; + } + + case PR_utimes: + { + /* int utimes(const char *filename, const struct timeval times[2]); + * + * convert to: + * int utimensat(int dirfd, const char *pathname, const struct timespec times[2], int flags); */ + struct timeval times[2]; + struct timespec timens[2]; + + set_sysnum(tracee, PR_utimensat); + if (peek_reg(tracee, CURRENT, SYSARG_2) != 0) { + ret = read_data(tracee, times, peek_reg(tracee, CURRENT, SYSARG_2), sizeof(times)); + if (ret < 0) { + set_result_after_seccomp(tracee, ret); + break; + } + timens[0].tv_sec = (time_t)times[0].tv_sec; + timens[0].tv_nsec = (long)times[0].tv_usec * 1000; + timens[1].tv_sec = (time_t)times[1].tv_sec; + timens[1].tv_nsec = (long)times[1].tv_usec * 1000; + ret = set_sysarg_data(tracee, timens, sizeof(timens), SYSARG_2); + if (ret < 0) { + set_result_after_seccomp(tracee, ret); + break; + } + } + poke_reg(tracee, SYSARG_4, 0); + poke_reg(tracee, SYSARG_3, peek_reg(tracee, CURRENT, SYSARG_2)); + poke_reg(tracee, SYSARG_2, peek_reg(tracee, CURRENT, SYSARG_1)); + poke_reg(tracee, SYSARG_1, AT_FDCWD); + restart_syscall_after_seccomp(tracee); + break; + } + + case PR_utime: + { + /* int utime(const char *filename, const struct utimbuf *times); + * + * convert to: + * int utimensat(int dirfd, const char *pathname, const struct timespec times[2], int flags); */ + struct utimbuf times; + struct timespec timens[2]; + + set_sysnum(tracee, PR_utimensat); + if (peek_reg(tracee, CURRENT, SYSARG_2) != 0) { + ret = read_data(tracee, ×, peek_reg(tracee, CURRENT, SYSARG_2), sizeof(times)); + if (ret < 0) { + set_result_after_seccomp(tracee, ret); + break; + } + timens[0].tv_sec = (time_t)times.actime; + timens[0].tv_nsec = 0; + timens[1].tv_sec = (time_t)times.modtime; + timens[1].tv_nsec = 0; + ret = set_sysarg_data(tracee, timens, sizeof(timens), SYSARG_2); + if (ret < 0) { + set_result_after_seccomp(tracee, ret); + break; + } + } + poke_reg(tracee, SYSARG_4, 0); + poke_reg(tracee, SYSARG_3, peek_reg(tracee, CURRENT, SYSARG_2)); + poke_reg(tracee, SYSARG_2, peek_reg(tracee, CURRENT, SYSARG_1)); + poke_reg(tracee, SYSARG_1, AT_FDCWD); + restart_syscall_after_seccomp(tracee); + break; + } + +#if defined(ARCH_X86) || defined(ARCH_X86_64) + case PR_sendmmsg: + { + /* Convert direct sendmmsg syscall to socketcall. + * This affects only 32-bit x86, in other archs + * bionic doesn't use socketcall() for sendmmsg. */ + size_t arg_size = sizeof_word(tracee); + assert(arg_size <= sizeof(word_t)); + byte_t args[arg_size * 4]; + memset(args, 0, arg_size * 4); + *(word_t*)(args) = peek_reg(tracee, CURRENT, SYSARG_1); + *(word_t*)(args + arg_size) = peek_reg(tracee, CURRENT, SYSARG_2); + *(word_t*)(args + 2 * arg_size) = peek_reg(tracee, CURRENT, SYSARG_3); + *(word_t*)(args + 3 * arg_size) = peek_reg(tracee, CURRENT, SYSARG_4); + word_t tracee_args = alloc_mem(tracee, arg_size * 4); + write_data(tracee, tracee_args, args, arg_size * 4); + set_sysnum(tracee, PR_socketcall); + poke_reg(tracee, SYSARG_1, SYS_SENDMMSG); + poke_reg(tracee, SYSARG_2, tracee_args); + restart_syscall_after_seccomp(tracee); + break; + } +#endif + + case PR_stat: + case PR_lstat: + set_sysnum(tracee, PR_newfstatat); + poke_reg(tracee, SYSARG_4, sysnum == PR_lstat ? AT_SYMLINK_NOFOLLOW : 0); + poke_reg(tracee, SYSARG_3, peek_reg(tracee, CURRENT, SYSARG_2)); + poke_reg(tracee, SYSARG_2, peek_reg(tracee, CURRENT, SYSARG_1)); + poke_reg(tracee, SYSARG_1, AT_FDCWD); + restart_syscall_after_seccomp(tracee); + break; + + case PR_pipe: + set_sysnum(tracee, PR_pipe2); + poke_reg(tracee, SYSARG_2, 0); + restart_syscall_after_seccomp(tracee); + break; + + case PR_dup2: + set_sysnum(tracee, PR_dup3); + poke_reg(tracee, SYSARG_3, 0); + restart_syscall_after_seccomp(tracee); + break; + + case PR_access: + set_sysnum(tracee, PR_faccessat); + poke_reg(tracee, SYSARG_4, 0); + poke_reg(tracee, SYSARG_3, peek_reg(tracee, CURRENT, SYSARG_2)); + poke_reg(tracee, SYSARG_2, peek_reg(tracee, CURRENT, SYSARG_1)); + poke_reg(tracee, SYSARG_1, AT_FDCWD); + restart_syscall_after_seccomp(tracee); + break; + + case PR_mkdir: + set_sysnum(tracee, PR_mkdirat); + poke_reg(tracee, SYSARG_3, peek_reg(tracee, CURRENT, SYSARG_2)); + poke_reg(tracee, SYSARG_2, peek_reg(tracee, CURRENT, SYSARG_1)); + poke_reg(tracee, SYSARG_1, AT_FDCWD); + restart_syscall_after_seccomp(tracee); + break; + + case PR_rename: + set_sysnum(tracee, PR_renameat); + poke_reg(tracee, SYSARG_4, peek_reg(tracee, CURRENT, SYSARG_2)); + poke_reg(tracee, SYSARG_3, AT_FDCWD); + poke_reg(tracee, SYSARG_2, peek_reg(tracee, CURRENT, SYSARG_1)); + poke_reg(tracee, SYSARG_1, AT_FDCWD); + restart_syscall_after_seccomp(tracee); + break; + + case PR_select: + { + // TODO: This doesn't update timeout with time spent inside select(2) + // after returning from syscall + word_t timeval_arg = peek_reg(tracee, CURRENT, SYSARG_5); + word_t timespec_arg = 0; + if (timeval_arg != 0) { + struct timeval tv = {}; + if (read_data(tracee, &tv, timeval_arg, sizeof(tv))) { + set_result_after_seccomp(tracee, -EFAULT); + break; + } + if (tv.tv_usec >= 1000000 || tv.tv_usec < 0) { + set_result_after_seccomp(tracee, -EINVAL); + break; + } + struct timespec ts = { + .tv_sec = tv.tv_sec, + .tv_nsec = tv.tv_usec * 1000 + }; + timespec_arg = alloc_mem(tracee, sizeof(ts)); + if(write_data(tracee, timespec_arg, &ts, sizeof(ts))) { + set_result_after_seccomp(tracee, -EFAULT); + break; + } + } + set_sysnum(tracee, PR_pselect6); + poke_reg(tracee, SYSARG_5, timespec_arg); + poke_reg(tracee, SYSARG_6, 0); + restart_syscall_after_seccomp(tracee); + break; + } + + case PR_poll: + { + int ms_arg = (int) peek_reg(tracee, CURRENT, SYSARG_3); + word_t timespec_arg = 0; + if (ms_arg >= 0) { + struct timespec ts = { + .tv_sec = ms_arg / 1000, + .tv_nsec = (ms_arg % 1000) * 1000000 + }; + timespec_arg = alloc_mem(tracee, sizeof(ts)); + if(write_data(tracee, timespec_arg, &ts, sizeof(ts))) { + set_result_after_seccomp(tracee, -EFAULT); + break; + } + } + set_sysnum(tracee, PR_ppoll); + poke_reg(tracee, SYSARG_3, timespec_arg); + poke_reg(tracee, SYSARG_4, 0); + poke_reg(tracee, SYSARG_5, 0); + restart_syscall_after_seccomp(tracee); + break; + } + + case PR_epoll_wait: + { + set_sysnum(tracee, PR_epoll_pwait); + poke_reg(tracee, SYSARG_5, 0); + poke_reg(tracee, SYSARG_6, 0); + restart_syscall_after_seccomp(tracee); + break; + } + + case PR_time: + { + time_t t = time(NULL); + word_t addr = peek_reg(tracee, CURRENT, SYSARG_1); + errno = 0; + if (addr != 0) { + poke_word(tracee, addr, t); + } + set_result_after_seccomp(tracee, errno ? -EFAULT : t); + break; + } + + case PR_statx: + { + set_result_after_seccomp(tracee, handle_statx_syscall(tracee, true)); + break; + } + + case PR_ftruncate: + { + if (detranslate_sysnum(get_abi(tracee), PR_ftruncate64) == SYSCALL_AVOIDER) { + set_result_after_seccomp(tracee, -ENOSYS); + break; + } + set_sysnum(tracee, PR_ftruncate64); + poke_reg(tracee, SYSARG_3, peek_reg(tracee, CURRENT, SYSARG_2)); + poke_reg(tracee, SYSARG_2, 0); + poke_reg(tracee, SYSARG_4, 0); + restart_syscall_after_seccomp(tracee); + break; + } + + case PR_setresuid: + case PR_setresgid: + { + gid_t rxid, exid, sxid, rxid_, exid_, sxid_; + rxid = peek_reg(tracee, CURRENT, SYSARG_1); + exid = peek_reg(tracee, CURRENT, SYSARG_2); + sxid = peek_reg(tracee, CURRENT, SYSARG_3); + if (sysnum == PR_setresuid) + ret = getresuid(&rxid_, &exid_, &sxid_); + else if (sysnum == PR_setresgid) + ret = getresgid(&rxid_, &exid_, &sxid_); + if (ret) { // EFAULT = address outside address space + set_result_after_seccomp(tracee, -EPERM); + break; + } + ret = 0; + if (rxid != rxid_ && rxid != -1) + ret = -EPERM; + if (exid != exid_ && exid != -1) + ret = -EPERM; + if (sxid != sxid_ && sxid != -1) + ret = -EPERM; + set_result_after_seccomp(tracee, ret); + break; + } + + case PR_set_robust_list: + default: + /* Set errno to -ENOSYS */ + set_result_after_seccomp(tracee, -ENOSYS); + } + + return 0; +} diff --git a/core/proot/src/main/cpp/tracee/seccomp.h b/core/proot/src/main/cpp/tracee/seccomp.h new file mode 100644 index 000000000..61d9c1c3a --- /dev/null +++ b/core/proot/src/main/cpp/tracee/seccomp.h @@ -0,0 +1,22 @@ +#include "tracee/tracee.h" +#include "sys/vfs.h" + +struct compat_statfs { + int f_type; + int f_bsize; + int f_blocks; + int f_bfree; + int f_bavail; + int f_files; + int f_ffree; + fsid_t f_fsid; + int f_namelen; + int f_frsize; + int f_flags; + int f_spare[4]; +}; + +void restart_syscall_after_seccomp(Tracee* tracee); +void set_result_after_seccomp(Tracee *tracee, word_t result); +int handle_seccomp_event(Tracee* tracee); +void fix_and_restart_enosys_syscall(Tracee* tracee); diff --git a/core/proot/src/main/cpp/tracee/statx.c b/core/proot/src/main/cpp/tracee/statx.c new file mode 100644 index 000000000..ca972f892 --- /dev/null +++ b/core/proot/src/main/cpp/tracee/statx.c @@ -0,0 +1,140 @@ +#include /* E*, */ +#include /* major, minor, */ + +#include "tracee/statx.h" +#include "tracee/mem.h" + +int handle_statx_syscall(Tracee *tracee, bool from_sigsys) { + RegVersion regVersion = from_sigsys ? CURRENT : ORIGINAL; + struct statx_syscall_state state = {}; + char guest_path[PATH_MAX] = {}; + struct stat stat_buf = {}; + bool do_fstat = false; + + /* Read arguments and translate path */ + word_t flags = peek_reg(tracee, regVersion, SYSARG_3); + bool do_lstat = ((flags & AT_SYMLINK_NOFOLLOW) != 0); + word_t mask = peek_reg(tracee, regVersion, SYSARG_4); + int status = read_string(tracee, guest_path, peek_reg(tracee, regVersion, SYSARG_2), PATH_MAX); + if (status < 0) { + return status; + } + + word_t dirfd = peek_reg(tracee, regVersion, SYSARG_1); + if (status == 0) { + return -EFAULT; + } + if (status == 1) { + if ((flags & AT_EMPTY_PATH) == 0) { + return -ENOENT; + } + status = readlink_proc_pid_fd(tracee->pid, dirfd, state.host_path); + do_fstat = true; + } else { + if (status >= PATH_MAX) { + return -ENAMETOOLONG; + } + status = translate_path(tracee, state.host_path, dirfd, guest_path, !do_lstat); + } + if (status < 0) { + return status; + } + + if (from_sigsys || peek_reg(tracee, CURRENT, SYSARG_RESULT) != 0) { + /* Call [l]stat() on translated path */ + if (do_fstat) { + char link[32] = {}; /* 32 > sizeof("/proc//cwd") + sizeof(#ULONG_MAX) */ + snprintf(link, sizeof(link), "/proc/%d/fd/%d", tracee->pid, (int) dirfd); + status = stat(link, &stat_buf); + } else if (do_lstat) { + status = lstat(state.host_path, &stat_buf); + } else { + status = stat(state.host_path, &stat_buf); + } + if (status < 0) { + status = -errno; + if (status >= 0) status = -EPERM; + return status; + } + + /* Translate results from stat to statx */ + state.statx_buf.stx_mask = ( + mask & ( + STATX_TYPE | + STATX_MODE | + STATX_NLINK | + STATX_UID | + STATX_GID | + STATX_ATIME | + STATX_MTIME | + STATX_CTIME | + STATX_INO | + STATX_SIZE | + STATX_BLOCKS | + STATX_BTIME + ) + ); + state.statx_buf.stx_blksize = stat_buf.st_blksize; + if (mask & (STATX_TYPE | STATX_MODE)) { + state.statx_buf.stx_mode = stat_buf.st_mode; + } + if (mask & STATX_NLINK) { + state.statx_buf.stx_nlink = stat_buf.st_nlink; + } + if (mask & STATX_UID) { + state.statx_buf.stx_uid = stat_buf.st_uid; + } + if (mask & STATX_GID) { + state.statx_buf.stx_gid = stat_buf.st_gid; + } + if (mask & STATX_ATIME) { + state.statx_buf.stx_atime.tv_sec = stat_buf.st_atim.tv_sec; + state.statx_buf.stx_atime.tv_nsec = stat_buf.st_atim.tv_nsec; + } + if (mask & STATX_MTIME) { + state.statx_buf.stx_mtime.tv_sec = stat_buf.st_mtim.tv_sec; + state.statx_buf.stx_mtime.tv_nsec = stat_buf.st_mtim.tv_nsec; + } + if (mask & STATX_CTIME) { + state.statx_buf.stx_ctime.tv_sec = stat_buf.st_ctim.tv_sec; + state.statx_buf.stx_ctime.tv_nsec = stat_buf.st_ctim.tv_nsec; + } + if (mask & STATX_INO) { + state.statx_buf.stx_ino = stat_buf.st_ino; + } + if (mask & STATX_SIZE) { + state.statx_buf.stx_size = stat_buf.st_size; + } + if (mask & STATX_BLOCKS) { + state.statx_buf.stx_blocks = stat_buf.st_blocks; + } + if (mask & STATX_BTIME) { + // stat() doesn't expose this, take ctime + state.statx_buf.stx_btime.tv_sec = stat_buf.st_ctim.tv_sec; + state.statx_buf.stx_btime.tv_nsec = stat_buf.st_ctim.tv_nsec; + } + state.statx_buf.stx_rdev_major = major(stat_buf.st_rdev); + state.statx_buf.stx_rdev_minor = minor(stat_buf.st_rdev); + state.updated_stats = true; + } else { + status = read_data(tracee, &state.statx_buf, peek_reg(tracee, ORIGINAL, SYSARG_5), sizeof(struct statx)); + if (status < 0) { + return status; + } + } + + /* Notify extensions */ + status = notify_extensions(tracee, STATX_SYSCALL, (intptr_t) &state, 0); + if (status < 0) { + return status; + } + + /* Return results to tracee */ + if (state.updated_stats) { + status = write_data(tracee, peek_reg(tracee, CURRENT, SYSARG_5), &state.statx_buf, sizeof(state.statx_buf)); + if (status < 0) { + return status; + } + } + return 0; +} diff --git a/core/proot/src/main/cpp/tracee/statx.h b/core/proot/src/main/cpp/tracee/statx.h new file mode 100644 index 000000000..d06dfae31 --- /dev/null +++ b/core/proot/src/main/cpp/tracee/statx.h @@ -0,0 +1,33 @@ +#ifndef STATX_H +#define STATX_H + +#include "tracee/tracee.h" +#include "sys/vfs.h" +#include "path/path.h" +#include "extension/extension.h" + +/* + * This structure is passed to extensions + * for STATX_SYSCALL event + */ +struct statx_syscall_state { + /* Host path to statx()'d file */ + char host_path[PATH_MAX]; + + /* This is statx structure that will be returned + * Extensions can fill additional data in it + * + * After changing data there set updated_stats to true + */ + struct statx statx_buf; + + /* Flag indicating that statx_buf was changed + * and needs to be copied back to tracee + */ + bool updated_stats; +}; + +int handle_statx_syscall(Tracee *tracee, bool from_sigsys); + + +#endif // STATX_H diff --git a/core/proot/src/main/cpp/tracee/tracee.c b/core/proot/src/main/cpp/tracee/tracee.c new file mode 100644 index 000000000..22f57ec08 --- /dev/null +++ b/core/proot/src/main/cpp/tracee/tracee.c @@ -0,0 +1,683 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#include /* CLONE_*, */ +#include /* pid_t, size_t, */ +#include /* NULL, */ +#include /* assert(3), */ +#include /* bzero(3), */ +#include /* bool, true, false, */ +#include /* LIST_*, */ +#include /* talloc_*, */ +#include /* kill(2), SIGKILL, */ +#include /* ptrace(2), PTRACE_*, */ +#include /* E*, */ +#include /* PRI*, */ + +#include "tracee/tracee.h" +#include "tracee/reg.h" +#include "tracee/mem.h" +#include "path/binding.h" +#include "syscall/sysnum.h" +#include "tracee/event.h" +#include "ptrace/ptrace.h" +#include "ptrace/wait.h" +#include "extension/extension.h" +#include "cli/note.h" + +#include "compat.h" + +static Tracees tracees; + + +/** + * Remove @zombie from its parent's list of zombies. Note: this is a + * talloc destructor. + */ +static int remove_zombie(Tracee *zombie) +{ + LIST_REMOVE(zombie, link); + return 0; +} + +/** + * Perform some specific treatments against @pointer according to its + * type, before it gets unlinked from @tracee_->life_context. + */ +static void clean_life_span_object(const void *pointer, int depth UNUSED, + int max_depth UNUSED, int is_ref UNUSED, void *tracee_) +{ + Binding *binding; + Tracee *tracee; + + tracee = talloc_get_type_abort(tracee_, Tracee); + + /* So far, only bindings need a special treatment. */ + binding = talloc_get_type(pointer, Binding); + if (binding != NULL) + remove_binding_from_all_lists(tracee, binding); +} + +/** + * Remove @tracee from the list of tracees and update all of its + * children & ptracees, and its ptracer. Note: this is a talloc + * destructor. + */ +static int remove_tracee(Tracee *tracee) +{ + Tracee *relative; + Tracee *ptracer; + int event; + + LIST_REMOVE(tracee, link); + + /* Clean objects that are linked to this tracee's life + * span. */ + talloc_report_depth_cb(tracee->life_context, 0, 100, clean_life_span_object, tracee); + + /* This could be optimize by using a dedicated list of + * children and ptracees. */ + LIST_FOREACH(relative, &tracees, link) { + /* Its children are now orphan. */ + if (relative->parent == tracee) + relative->parent = NULL; + + /* Its tracees are now free. */ + if (relative->as_ptracee.ptracer == tracee) { + /* Release the pending event, if any. */ + relative->as_ptracee.ptracer = NULL; + + if (relative->as_ptracee.event4.proot.pending) { + event = handle_tracee_event(relative, + relative->as_ptracee.event4.proot.value); + (void) restart_tracee(relative, event); + } + else if (relative->as_ptracee.event4.ptracer.pending) { + event = relative->as_ptracee.event4.proot.value; + (void) restart_tracee(relative, event); + } + + bzero(&relative->as_ptracee, sizeof(relative->as_ptracee)); + } + } + + /* Nothing else to do if it's not a ptracee. */ + ptracer = tracee->as_ptracee.ptracer; + if (ptracer == NULL) + return 0; + + /* Zombify this ptracee until its ptracer is notified about + * its death. */ + event = tracee->as_ptracee.event4.ptracer.value; + if (tracee->as_ptracee.event4.ptracer.pending + && (WIFEXITED(event) || WIFSIGNALED(event))) { + Tracee *zombie; + + zombie = new_dummy_tracee(ptracer); + if (zombie != NULL) { + LIST_INSERT_HEAD(&PTRACER.zombies, zombie, link); + talloc_set_destructor(zombie, remove_zombie); + + zombie->parent = tracee->parent; + zombie->clone = tracee->clone; + zombie->pid = tracee->pid; + + detach_from_ptracer(tracee); + attach_to_ptracer(zombie, ptracer); + + zombie->as_ptracee.event4.ptracer.pending = true; + zombie->as_ptracee.event4.ptracer.value = event; + zombie->as_ptracee.is_zombie = true; + + return 0; + } + /* Fallback to the common path. */ + } + + detach_from_ptracer(tracee); + + /* Wake its ptracer if there's nothing else to wait for. */ + if (PTRACER.nb_ptracees == 0 && PTRACER.wait_pid != 0) { + /* Update the return value of ptracer's wait(2). */ + poke_reg(ptracer, SYSARG_RESULT, -ECHILD); + + /* Don't forget to write its register cache back. */ + (void) push_regs(ptracer); + + PTRACER.wait_pid = 0; + (void) restart_tracee(ptracer, 0); + } + + return 0; +} + +/** + * Allocate a new entry for a dummy tracee (no pid, no destructor, not + * in the list of tracees, ...). The new allocated memory is attached + * to the given @context. This function returns NULL if an error + * occurred (ENOMEM), otherwise it returns the newly allocated + * structure. + */ +Tracee *new_dummy_tracee(TALLOC_CTX *context) +{ + Tracee *tracee; + + tracee = talloc_zero(context, Tracee); + if (tracee == NULL) + return NULL; + + /* Allocate a memory collector. */ + tracee->ctx = talloc_new(tracee); + if (tracee->ctx == NULL) + goto no_mem; + + /* By default new tracees have an empty file-system + * name-space and heap. */ + tracee->fs = talloc_zero(tracee, FileSystemNameSpace); + tracee->heap = talloc_zero(tracee, Heap); + tracee->auxv_fd = -1; + if (tracee->fs == NULL || tracee->heap == NULL) + goto no_mem; + + return tracee; + +no_mem: + TALLOC_FREE(tracee); + return NULL; +} + +static uint64_t next_vpid = 1; + +/** + * Allocate a new entry for the tracee @pid, then set its destructor + * and add it to the list of tracees. This function returns NULL if + * an error occurred (ENOMEM), otherwise it returns the newly + * allocated structure. + */ +static Tracee *new_tracee(pid_t pid) +{ + Tracee *tracee; + + tracee = new_dummy_tracee(NULL); + if (tracee == NULL) + return NULL; + + talloc_set_destructor(tracee, remove_tracee); + + tracee->pid = pid; + tracee->vpid = next_vpid++; + + LIST_INSERT_HEAD(&tracees, tracee, link); + + tracee->life_context = talloc_new(tracee); + + return tracee; +} + +/** + * Return the first [stopped?] tracee with the given + * @pid (-1 for any) which has the given @ptracer, and which has a + * pending event for its ptracer if @only_with_pevent is true. See + * wait(2) manual for the meaning of @wait_options. This function + * returns NULL if there's no such ptracee. + */ +static Tracee *get_ptracee(const Tracee *ptracer, pid_t pid, bool only_stopped, + bool only_with_pevent, word_t wait_options) +{ + Tracee *ptracee; + + /* Return zombies first. */ + LIST_FOREACH(ptracee, &PTRACER.zombies, link) { + /* Not the ptracee you're looking for? */ + if (pid != ptracee->pid && pid != -1) + continue; + + /* Not the expected kind of cloned process? */ + if (!EXPECTED_WAIT_CLONE(wait_options, ptracee)) + continue; + + return ptracee; + } + + LIST_FOREACH(ptracee, &tracees, link) { + /* Discard tracees that don't have this ptracer. */ + if (PTRACEE.ptracer != ptracer) + continue; + + /* Not the ptracee you're looking for? */ + if (pid != ptracee->pid && pid != -1) + continue; + + /* Not the expected kind of cloned process? */ + if (!EXPECTED_WAIT_CLONE(wait_options, ptracee)) + continue; + + /* No need to do more checks if its stopped state + * doesn't matter. Be careful when using such + * maybe-running tracee. */ + if (!only_stopped) + return ptracee; + + /* Is this tracee in the stopped state? */ + if (ptracee->running) + continue; + + /* Has a pending event for its ptracer? */ + if (PTRACEE.event4.ptracer.pending || !only_with_pevent) + return ptracee; + + /* No need to go further if the specific tracee isn't + * in the expected state? */ + if (pid == ptracee->pid) + return NULL; + } + + return NULL; +} + +/** + * Wrapper for get_ptracee(), this ensures only a stopped tracee is + * returned (or NULL). + */ +Tracee *get_stopped_ptracee(const Tracee *ptracer, pid_t pid, + bool only_with_pevent, word_t wait_options) +{ + return get_ptracee(ptracer, pid, true, only_with_pevent, wait_options); +} + +/** + * Wrapper for get_ptracee(), this ensures no running tracee is + * returned. + */ +bool has_ptracees(const Tracee *ptracer, pid_t pid, word_t wait_options) +{ + return (get_ptracee(ptracer, pid, false, false, wait_options) != NULL); +} + +/** + * Return the entry related to the tracee @pid. If no entry were + * found, a new one is created if @create is true, otherwise NULL is + * returned. + */ +Tracee *get_tracee(const Tracee *current_tracee, pid_t pid, bool create) +{ + Tracee *tracee; + + /* Don't reset the memory collector if the searched tracee is + * the current one: there's likely pointers to the + * sub-allocated data in the caller. */ + if (current_tracee != NULL && current_tracee->pid == pid) + return (Tracee *)current_tracee; + + LIST_FOREACH(tracee, &tracees, link) { + if (tracee->pid == pid) { + /* Flush then allocate a new memory collector. */ + TALLOC_FREE(tracee->ctx); + tracee->ctx = talloc_new(tracee); + + return tracee; + } + } + + return (create ? new_tracee(pid) : NULL); +} + +/** + * Mark tracee as terminated and optionally take action. + */ +void terminate_tracee(Tracee *tracee) +{ + tracee->terminated = true; + + /* Case where the terminated tracee is marked + to kill all tracees on exit. + */ + if (tracee->killall_on_exit) { + VERBOSE(tracee, 1, "terminating all tracees on exit"); + kill_all_tracees(); + } +} + +/** + * Free all tracees marked as terminated. + */ +void free_terminated_tracees() +{ + Tracee *next; + + /* Items can't be deleted when using LIST_FOREACH. */ + next = tracees.lh_first; + while (next != NULL) { + Tracee *tracee = next; + next = tracee->link.le_next; + + if (tracee->terminated) + TALLOC_FREE(tracee); + } +} + +/** + * Make new @parent's child inherit from it. Depending on + * @clone_flags, some information are copied or shared. This function + * returns -errno if an error occured, otherwise 0. + */ +int new_child(Tracee *parent, word_t clone_flags) +{ + int ptrace_options; + unsigned long pid; + Tracee *child; + int status; + + /* If the tracee calls clone(2) with the CLONE_VFORK flag, + * PTRACE_EVENT_VFORK will be delivered instead [...]; + * otherwise if the tracee calls clone(2) with the exit signal + * set to SIGCHLD, PTRACE_EVENT_FORK will be delivered [...] + * + * -- ptrace(2) man-page + * + * That means we have to check if it's actually a clone(2) in + * order to get the right flags. + */ + status = fetch_regs(parent); + if (status >= 0 && get_sysnum(parent, CURRENT) == PR_clone) + clone_flags = peek_reg(parent, CURRENT, SYSARG_1); + else if (status >= 0 && get_sysnum(parent, CURRENT) == PR_clone3) + // Look at the first word of the clone_args structure, which + // contains the usual clone flags. + clone_flags = peek_word(parent, peek_reg(parent, CURRENT, SYSARG_1)); + + /* Get the pid of the parent's new child. */ + status = ptrace(PTRACE_GETEVENTMSG, parent->pid, NULL, &pid); + if (status < 0 || pid == 0) { + note(parent, WARNING, SYSTEM, "ptrace(GETEVENTMSG)"); + return status; + } + + child = get_tracee(parent, (pid_t) pid, true); + if (child == NULL) { + note(parent, WARNING, SYSTEM, "running out of memory"); + return -ENOMEM; + } + + /* Sanity checks. */ + assert(child != NULL + && child->exe == NULL + && child->fs->cwd == NULL + && child->fs->bindings.pending == NULL + && child->fs->bindings.guest == NULL + && child->fs->bindings.host == NULL + && child->qemu == NULL + && child->glue == NULL + && child->parent == NULL + && child->as_ptracee.ptracer == NULL); + + child->verbose = parent->verbose; + child->seccomp = parent->seccomp; + child->sysexit_pending = parent->sysexit_pending; + child->execfn_addr = parent->execfn_addr; + child->auxv_fd = parent->auxv_fd; + child->no_new_privs = parent->no_new_privs; + child->seen_execve = parent->seen_execve; +#ifdef HAS_POKEDATA_WORKAROUND + child->pokedata_workaround_stub_addr = parent->pokedata_workaround_stub_addr; +#endif +#ifdef ARCH_ARM64 + child->is_aarch32 = parent->is_aarch32; +#endif + + /* If CLONE_VM is set, the calling process and the child + * process run in the same memory space [...] any memory + * mapping or unmapping performed with mmap(2) or munmap(2) by + * the child or calling process also affects the other + * process. + * + * If CLONE_VM is not set, the child process runs in a + * separate copy of the memory space of the calling process at + * the time of clone(). Memory writes or file + * mappings/unmappings performed by one of the processes do + * not affect the other, as with fork(2). + * + * -- clone(2) man-page + */ + TALLOC_FREE(child->heap); + child->heap = ((clone_flags & CLONE_VM) != 0) + ? talloc_reference(child, parent->heap) + : talloc_memdup(child, parent->heap, sizeof(Heap)); + if (child->heap == NULL) + return -ENOMEM; + + child->load_info = talloc_reference(child, parent->load_info); + + /* If CLONE_PARENT is set, then the parent of the new child + * (as returned by getppid(2)) will be the same as that of the + * calling process. + * + * If CLONE_PARENT is not set, then (as with fork(2)) the + * child's parent is the calling process. + * + * -- clone(2) man-page + */ + if ((clone_flags & CLONE_PARENT) != 0) + child->parent = parent->parent; + else + child->parent = parent; + + /* Remember if this child belongs to the same thread group as + * its parent. This is currently useful for ptrace emulation + * only but it deserves to be extended to support execve(2) + * specificity (ie. when a thread calls execve(2), its pid + * gets replaced by the pid of its thread group leader). */ + child->clone = ((clone_flags & CLONE_THREAD) != 0); + + /* Depending on how the new process is created, it may be + * automatically traced by the parent's tracer. */ + ptrace_options = ( clone_flags == 0 ? PTRACE_O_TRACEFORK + : (clone_flags & 0xFF) == SIGCHLD ? PTRACE_O_TRACEFORK + : (clone_flags & CLONE_VFORK) != 0 ? PTRACE_O_TRACEVFORK + : PTRACE_O_TRACECLONE); + if (parent->as_ptracee.ptracer != NULL + && ( (ptrace_options & parent->as_ptracee.options) != 0 + || (clone_flags & CLONE_PTRACE) != 0)) { + attach_to_ptracer(child, parent->as_ptracee.ptracer); + + /* All these flags are inheritable, no matter why this + * child is being traced. */ + child->as_ptracee.options |= (parent->as_ptracee.options + & ( PTRACE_O_TRACECLONE + | PTRACE_O_TRACEEXEC + | PTRACE_O_TRACEEXIT + | PTRACE_O_TRACEFORK + | PTRACE_O_TRACESYSGOOD + | PTRACE_O_TRACEVFORK + | PTRACE_O_TRACEVFORKDONE)); + } + + /* If CLONE_FS is set, the parent and the child process share + * the same file system information. This includes the root + * of the file system, the current working directory, and the + * umask. Any call to chroot(2), chdir(2), or umask(2) + * performed by the parent process or the child process also + * affects the other process. + * + * If CLONE_FS is not set, the child process works on a copy + * of the file system information of the parent process at the + * time of the clone() call. Calls to chroot(2), chdir(2), + * umask(2) performed later by one of the processes do not + * affect the other process. + * + * -- clone(2) man-page + */ + TALLOC_FREE(child->fs); + if ((clone_flags & CLONE_FS) != 0) { + /* File-system name-space is shared. */ + child->fs = talloc_reference(child, parent->fs); + } + else { + /* File-system name-space is copied. */ + child->fs = talloc_zero(child, FileSystemNameSpace); + if (child->fs == NULL) + return -ENOMEM; + + child->fs->cwd = talloc_strdup(child->fs, parent->fs->cwd); + if (child->fs->cwd == NULL) + return -ENOMEM; + talloc_set_name_const(child->fs->cwd, "$cwd"); + + if (parent->clone_stripped_newns + && parent->fs->bindings.guest != NULL) { + /* Caller asked for CLONE_NEWNS (which we + * silently stripped). Give the child its own + * copy of the binding tree so emulated mount(2) + * calls don't propagate back to the parent. */ + Binding *iter; + + child->fs->bindings.guest = talloc_zero(child->fs, Bindings); + child->fs->bindings.host = talloc_zero(child->fs, Bindings); + if ( child->fs->bindings.guest == NULL + || child->fs->bindings.host == NULL) + return -ENOMEM; + CIRCLEQ_INIT(child->fs->bindings.guest); + CIRCLEQ_INIT(child->fs->bindings.host); + + for (iter = CIRCLEQ_FIRST(parent->fs->bindings.guest); + iter != (void *) parent->fs->bindings.guest; + iter = CIRCLEQ_NEXT(iter, link.guest)) + (void) insort_binding3(child, child->fs, + iter->host.path, + iter->guest.path); + } + else { + /* Bindings are shared across file-system name-spaces since a + * "mount --bind" made by a process affects all other processes + * under Linux. Actually they are copied when a sub + * reconfiguration occured (nested proot or chroot(2)). */ + child->fs->bindings.guest = talloc_reference(child->fs, parent->fs->bindings.guest); + child->fs->bindings.host = talloc_reference(child->fs, parent->fs->bindings.host); + } + } + + /* Always consume the stripped-NEWNS flag once a child has been + * processed, regardless of which branch above we took (the flag + * could persist otherwise — e.g. when CLONE_FS sent us straight + * to the shared-fs path, or when bindings.guest wasn't ready + * yet — and incorrectly isolate the bindings of an unrelated + * later clone in the same parent). */ + parent->clone_stripped_newns = false; + + /* The path to the executable is unshared only once the child + * process does a call to execve(2). */ + child->exe = talloc_reference(child, parent->exe); + + child->qemu = talloc_reference(child, parent->qemu); + child->glue = talloc_reference(child, parent->glue); + + child->host_ldso_paths = talloc_reference(child, parent->host_ldso_paths); + child->guest_ldso_paths = talloc_reference(child, parent->guest_ldso_paths); + + child->tool_name = parent->tool_name; + + inherit_extensions(child, parent, clone_flags); + + /* Restart the child tracee if it was already alive but + * stopped until that moment. */ + if (child->sigstop == SIGSTOP_PENDING) { + bool keep_stopped = false; + + child->sigstop = SIGSTOP_ALLOWED; + + /* Notify its ptracer if it is ready to be traced. */ + if (child->as_ptracee.ptracer != NULL) { + /* Sanity check. */ + assert(!child->as_ptracee.tracing_started); + +#ifndef __W_STOPCODE + #define __W_STOPCODE(sig) ((sig) << 8 | 0x7f) +#endif + keep_stopped = handle_ptracee_event(child, __W_STOPCODE(SIGSTOP)); + + /* Note that this event was already handled by + * PRoot since child->as_ptracee.ptracer was + * NULL up to now. */ + child->as_ptracee.event4.proot.pending = false; + child->as_ptracee.event4.proot.value = 0; + } + + if (!keep_stopped) + (void) restart_tracee(child, 0); + } + + VERBOSE(child, 1, "vpid %" PRIu64 ": pid %d", child->vpid, child->pid); + + return 0; +} + +/** + * Helper for swap_config(). + */ +static void reparent_config(Tracee *new_parent, Tracee *old_parent) +{ + new_parent->verbose = old_parent->verbose; + +#define REPARENT(field) do { \ + talloc_reparent(old_parent, new_parent, old_parent->field); \ + new_parent->field = old_parent->field; \ + } while(0); + + REPARENT(fs); + REPARENT(exe); + REPARENT(qemu); + REPARENT(glue); + REPARENT(extensions); + +#undef REPARENT +} + +/** + * Swap configuration (pointers and parentality) between @tracee1 and @tracee2. + */ +int swap_config(Tracee *tracee1, Tracee *tracee2) +{ + Tracee *tmp; + + tmp = talloc_zero(tracee1->ctx, Tracee); + if (tmp == NULL) + return -ENOMEM; + + reparent_config(tmp, tracee1); + reparent_config(tracee1, tracee2); + reparent_config(tracee2, tmp); + + return 0; +} + +/* Send the KILL signal to all tracees. */ +void kill_all_tracees() +{ + Tracee *tracee; + + LIST_FOREACH(tracee, &tracees, link) + kill(tracee->pid, SIGKILL); +} + +Tracees *get_tracees_list_head() { + return &tracees; +} diff --git a/core/proot/src/main/cpp/tracee/tracee.h b/core/proot/src/main/cpp/tracee/tracee.h new file mode 100644 index 000000000..7ac4ad120 --- /dev/null +++ b/core/proot/src/main/cpp/tracee/tracee.h @@ -0,0 +1,366 @@ +/* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- + * + * This file is part of PRoot. + * + * Copyright (C) 2015 STMicroelectronics + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#ifndef TRACEE_H +#define TRACEE_H + +#include /* pid_t, size_t, */ +#include /* struct user*, */ +#include /* bool, */ +#include /* LIST_*, */ +#include /* enum __ptrace_request */ +#include /* talloc_*, */ +#include /* *int*_t, */ + +#include "arch.h" /* word_t, user_regs_struct, HAS_POKEDATA_WORKAROUND */ +#include "compat.h" + +typedef enum { + CURRENT = 0, + ORIGINAL = 1, + MODIFIED = 2, + ORIGINAL_SECCOMP_REWRITE = 3, + NB_REG_VERSION +} RegVersion; + +struct bindings; +struct load_info; +struct extensions; +struct chained_syscalls; + +/* Information related to a file-system name-space. */ +typedef struct { + struct { + /* List of bindings as specified by the user but not canonicalized yet. */ + struct bindings *pending; + + /* List of bindings canonicalized and sorted in the "guest" order. */ + struct bindings *guest; + + /* List of bindings canonicalized and sorted in the "host" order. */ + struct bindings *host; + } bindings; + + /* Current working directory, à la /proc/self/pwd. */ + char *cwd; +} FileSystemNameSpace; + +/* Virtual heap, emulated with a regular memory mapping. */ +typedef struct { + word_t base; + size_t size; + bool disabled; +} Heap; + +/* Information related to a tracee process. */ +typedef struct tracee { + /********************************************************************** + * Private resources * + **********************************************************************/ + + /* Link for the list of all tracees. */ + LIST_ENTRY(tracee) link; + + /* Process identifier. */ + pid_t pid; + + /* Unique tracee identifier. */ + uint64_t vpid; + + /* Is it currently running or not? */ + bool running; + + /* Is this tracee ready to be freed? TODO: move to a list + * dedicated to terminated tracees instead. */ + bool terminated; + + /* Whether termination of this tracee implies an immediate kill + * of all tracees. */ + bool killall_on_exit; + + /* Parent of this tracee, NULL if none. */ + struct tracee *parent; + + /* Is it a "clone", i.e has the same parent as its creator. */ + bool clone; + + /* Set when the current clone(2)/clone3(2) had CLONE_NEW* flags + * stripped (see translate_syscall_enter); the new child should + * get its own copy of the bindings so emulated mount(2) calls + * stay scoped to the would-be namespace. Reset once consumed. */ + bool clone_stripped_newns; + + /* Emulation of AF_NETLINK / NETLINK_ROUTE sockets for + * sandbox helpers like bubblewrap that try to bring up the + * loopback interface inside their would-be net namespace. + * fake_netlink_fds holds the fds of sockets we silently + * redirected from AF_NETLINK to AF_UNIX/SOCK_DGRAM; see + * enter.c / exit.c for the intercepts. */ +#define MAX_FAKE_NETLINK_FDS 8 + int fake_netlink_fds[MAX_FAKE_NETLINK_FDS]; + int fake_netlink_fds_count; + bool pending_fake_netlink_socket; + /* Reply synthesised at send time for the most recent request on a + * fake netlink fd, awaiting the matching recvmsg / recvfrom. The + * buffer is word-aligned because we lay out struct nlmsghdr and the + * rtnetlink payloads directly into it. */ +#define MAX_FAKE_NETLINK_REPLY 8192 + uint8_t fake_netlink_reply[MAX_FAKE_NETLINK_REPLY] __attribute__((aligned(8))); + size_t fake_netlink_reply_len; + + /* Support for ptrace emulation (tracer side). */ + struct { + size_t nb_ptracees; + LIST_HEAD(zombies, tracee) zombies; + + pid_t wait_pid; + word_t wait_options; + + enum { + DOESNT_WAIT = 0, + WAITS_IN_KERNEL, + WAITS_IN_PROOT + } waits_in; + } as_ptracer; + + /* Support for ptrace emulation (tracee side). */ + struct { + struct tracee *ptracer; + + struct { + #define STRUCT_EVENT struct { int value; bool pending; } + + STRUCT_EVENT proot; + STRUCT_EVENT ptracer; + } event4; + + bool tracing_started; + bool ignore_loader_syscalls; + bool ignore_syscalls; + word_t options; + bool is_zombie; + } as_ptracee; + + /* Current status: + * 0: enter syscall + * 1: exit syscall no error + * -errno: exit syscall with error. */ + int status; + +#define IS_IN_SYSENTER(tracee) ((tracee)->status == 0) +#define IS_IN_SYSEXIT(tracee) (!IS_IN_SYSENTER(tracee)) +#define IS_IN_SYSEXIT2(tracee, sysnum) (IS_IN_SYSEXIT(tracee) \ + && get_sysnum((tracee), ORIGINAL) == sysnum) + + /* How this tracee is restarted. */ +#ifdef __GLIBC__ + enum __ptrace_request +#else + int +#endif + restart_how, last_restart_how; + + /* Value of the tracee's general purpose registers. */ + struct user_regs_struct _regs[NB_REG_VERSION]; + bool _regs_were_changed; + bool restore_original_regs; + bool restore_original_regs_after_seccomp_event; + + /* State for the special handling of SIGSTOP. */ + enum { + SIGSTOP_IGNORED = 0, /* Ignore SIGSTOP (once the parent is known). */ + SIGSTOP_ALLOWED, /* Allow SIGSTOP (once the parent is known). */ + SIGSTOP_PENDING, /* Block SIGSTOP until the parent is unknown. */ + } sigstop; + + /* True if next SIGSYS caused by seccomp should be silently dropped + * without affecting state of any registers. */ + bool skip_next_seccomp_signal; + + /* Context used to collect all the temporary dynamic memory + * allocations. */ + TALLOC_CTX *ctx; + + /* Context used to collect all dynamic memory allocations that + * should be released once this tracee is freed. */ + TALLOC_CTX *life_context; + + /* Note: I could rename "ctx" in "event_span" and + * "life_context" in "life_span". */ + + /* Specify the type of the final component during the + * initialization of a binding. This variable is first + * defined in bind_path() then used in build_glue(). */ + mode_t glue_type; + + /* During a sub-reconfiguration, the new setup is relatively + * to @tracee's file-system name-space. Also, @paths holds + * its $PATH environment variable in order to emulate the + * execvp(3) behavior. */ + struct { + struct tracee *tracee; + const char *paths; + } reconf; + + /* Unrequested syscalls inserted by PRoot after an actual + * syscall. */ + struct { + struct chained_syscalls *syscalls; + bool force_final_result; + word_t final_result; + enum { + SYSNUM_WORKAROUND_INACTIVE, + SYSNUM_WORKAROUND_PROCESS_FAULTY_CALL, + SYSNUM_WORKAROUND_PROCESS_REPLACED_CALL + } sysnum_workaround_state; + int suppressed_signal; + } chain; + + /* Load info generated during execve sysenter and used during + * execve sysexit. */ + struct load_info *load_info; + + /* Address of argv[0] string in the tracee's initial stack, captured + * at execve sysexit. Used to fix AT_EXECFN in prctl(PR_GET_AUXV) + * responses: the kernel's saved auxv has AT_EXECFN pointing to the + * loader temp file, but we want it to point to the actual program name. */ + word_t execfn_addr; + + /* fd the tracee used to open /proc/self/auxv, tracked so that read() + * calls on it can have AT_EXECFN patched (fallback for kernels < 6.4 + * that don't support prctl(PR_GET_AUXV)). -1 when not active. */ + int auxv_fd; + +#ifdef HAS_POKEDATA_WORKAROUND + word_t pokedata_workaround_stub_addr; + bool pokedata_workaround_cancelled_syscall; + bool pokedata_workaround_relaunched_syscall; +#endif + +#ifdef ARCH_ARM64 + bool is_aarch32; +#endif + + + /********************************************************************** + * Private but inherited resources * + **********************************************************************/ + + /* Verbose level. */ + int verbose; + + /* State of the seccomp acceleration for this tracee. */ + enum { DISABLED = 0, DISABLING, ENABLED } seccomp; + + /* Ensure the sysexit stage is always hit under seccomp. */ + bool sysexit_pending; + + /* If true, syscall entry was handled by seccomp and next SIGTRAP | 0x80 + * has to be ignored as it's same syscall entry */ + bool seccomp_already_handled_enter; + + /* Whether the tracee itself requested the "no new privileges" flag + * via prctl(PR_SET_NO_NEW_PRIVS). PRoot always sets the real kernel + * flag in the child before execve (it is a precondition for installing + * its seccomp filter), so prctl(PR_GET_NO_NEW_PRIVS) would otherwise + * report 1 even though the guest never asked for it. This field lets + * PRoot report the guest's own intent instead, which is required by + * tools like sudo-rs that refuse to run when the flag appears set. */ + bool no_new_privs; + + /* Set once the tracee has gone through its initial execve, i.e. once + * the guest program is actually running. Used to ignore the + * PR_SET_NO_NEW_PRIVS that PRoot performs itself in the launch child + * (before that execve) when tracking @no_new_privs. */ + bool seen_execve; + + /********************************************************************** + * Shared or private resources, depending on the CLONE_FS/VM flags. * + **********************************************************************/ + + /* Information related to a file-system name-space. */ + FileSystemNameSpace *fs; + + /* Virtual heap, emulated with a regular memory mapping. */ + Heap *heap; + + + /********************************************************************** + * Shared resources until the tracee makes a call to execve(). * + **********************************************************************/ + + /* Path to the executable, à la /proc/self/exe. */ + char *exe; + char *new_exe; + char *host_exe; + + + /********************************************************************** + * Shared or private resources, depending on the (re-)configuration * + **********************************************************************/ + + /* Runner command-line. */ + char **qemu; + bool skip_proot_loader; + + /* Path to glue between the guest rootfs and the host rootfs. */ + const char *glue; + + /* List of extensions enabled for this tracee. */ + struct extensions *extensions; + + + /********************************************************************** + * Shared but read-only resources * + **********************************************************************/ + + /* For the mixed-mode, the guest LD_LIBRARY_PATH is saved + * during the "guest -> host" transition, in order to be + * restored during the "host -> guest" transition (only if the + * host LD_LIBRARY_PATH hasn't changed). */ + const char *host_ldso_paths; + const char *guest_ldso_paths; + + /* For diagnostic purpose. */ + const char *tool_name; + +} Tracee; + +#define HOST_ROOTFS "/host-rootfs" + +#define TRACEE(a) talloc_get_type_abort(talloc_parent(talloc_parent(a)), Tracee) + +extern Tracee *get_tracee(const Tracee *tracee, pid_t pid, bool create); +extern Tracee *get_stopped_ptracee(const Tracee *ptracer, pid_t pid, + bool only_with_pevent, word_t wait_options); +extern bool has_ptracees(const Tracee *ptracer, pid_t pid, word_t wait_options); +extern int new_child(Tracee *parent, word_t clone_flags); +extern Tracee *new_dummy_tracee(TALLOC_CTX *context); +extern void terminate_tracee(Tracee *tracee); +extern void free_terminated_tracees(); +extern int swap_config(Tracee *tracee1, Tracee *tracee2); +extern void kill_all_tracees(); + +typedef LIST_HEAD(tracees, tracee) Tracees; +extern Tracees *get_tracees_list_head(); + +#endif /* TRACEE_H */ diff --git a/core/resources/build.gradle.kts b/core/resources/build.gradle.kts index bed719972..cc65cdf46 100644 --- a/core/resources/build.gradle.kts +++ b/core/resources/build.gradle.kts @@ -1,12 +1,11 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) alias(libs.plugins.ktfmt) } android { namespace = "com.rk.resources" - compileSdk = 36 + compileSdk = 37 lint.disable += "MissingTranslation" diff --git a/core/resources/src/main/res/drawable/nix.xml b/core/resources/src/main/res/drawable/nix.xml new file mode 100644 index 000000000..48e1e4cbf --- /dev/null +++ b/core/resources/src/main/res/drawable/nix.xml @@ -0,0 +1,10 @@ + + + diff --git a/core/resources/src/main/res/drawable/server.xml b/core/resources/src/main/res/drawable/server.xml new file mode 100644 index 000000000..dbd1a7c10 --- /dev/null +++ b/core/resources/src/main/res/drawable/server.xml @@ -0,0 +1,27 @@ + + + + + + diff --git a/core/resources/src/main/res/drawable/sort_by_alphabet.xml b/core/resources/src/main/res/drawable/sort_by_alphabet.xml new file mode 100644 index 000000000..4664f22f0 --- /dev/null +++ b/core/resources/src/main/res/drawable/sort_by_alphabet.xml @@ -0,0 +1,27 @@ + + + + + + diff --git a/core/resources/src/main/res/values-ar/strings.xml b/core/resources/src/main/res/values-ar/strings.xml index 69e1940f7..4ad5e47a6 100644 --- a/core/resources/src/main/res/values-ar/strings.xml +++ b/core/resources/src/main/res/values-ar/strings.xml @@ -145,7 +145,7 @@ لا توجد إضافة. التثبيت من وحدة تخزين تحميل… - جارٍ التحميل… + جارٍ التنزيل… خطأ في الشبكة. يُرجى التأكد من أن اتصالك بالإنترنت يعمل بشكل صحيح. إدارة الإضافات إضف جلسة @@ -294,4 +294,9 @@ تعذر اعادة تسمية الرمز. لا يمكنك اعادة تسمية هذا الرمز. عرض سجلات التطبيق + جاري التحديث… + الإكمال التلقائي ل TextMate + نوع الغلاف + اكتشاف الوصول إلى القرص أو الشبكة على الخيط الرئيسي + إدخال حرف التبويب diff --git a/core/resources/src/main/res/values-bn/strings.xml b/core/resources/src/main/res/values-bn/strings.xml index 6110ca15b..5574cdb2e 100644 --- a/core/resources/src/main/res/values-bn/strings.xml +++ b/core/resources/src/main/res/values-bn/strings.xml @@ -48,5 +48,21 @@ সব প্রতিস্থাপন করুন ডাউনলোড প্রয়োগ করুন - একটি টার্মিনাল খুলুন + টার্মিনালে খুলুন + আপনি কি নিশ্চিতভাবে %d ফাইলগুলি সম্পূর্ণরূপে মুছে ফেলতে চান? + এই ফাইলটি অন্য অ্যাপ দিয়ে খুলার অনুমতি নেই + আপনি কি নিশ্চিতভাবে %s প্রজেক্টটি বন্ধ করতে চান? + ফাইলের নাম + পথ + খুলুন + অনুমতি অগ্রাহ্য করা হলো + ঠিক আছে + শাখা + গোপন ফাইল সমূহ + অভ্যন্তরীণ স্টোরেজ + যোগ করুন + থিম সমূহ + সম্পাদক + লাইনের সংখ্যা দেখুন + লাইন নম্বর পিন করুন diff --git a/core/resources/src/main/res/values-cs/strings.xml b/core/resources/src/main/res/values-cs/strings.xml index 43d4486fd..9beaf2684 100644 --- a/core/resources/src/main/res/values-cs/strings.xml +++ b/core/resources/src/main/res/values-cs/strings.xml @@ -232,7 +232,7 @@ Nelze zapisovat do souboru (žádná oprávnění zapisování) Úložiště Přeložit - Terminál a spouštěče + Terminál, spouštěče a jazykové servery Jméno složky Neplatné znaky Podrobnosti @@ -575,7 +575,7 @@ Protokolový soubor, který jste chtěli nahlásit je příliš dlouhý. Prosím manuálně vložte zkopírovaný protokolový soubor do hlášení o chyb. Restartování… Zastavování.… - Doplňky: %s + Koncovky: %s Funkce Dokončování kódu Zapnout návrhy automatického dokončování @@ -632,4 +632,48 @@ Automaticky vložit shodnou uzávírecí závorku Potvrzení o ukončení Zobrazit dialog o potvrzení o ukončení před zavření aplikace + Symboly + Terminál využívá Termuxův formát na extra klávesy + Jít k referenci + Vyberte jednu z(e) %s referencí pro přesun k její umístění. + Žádné reference nenalezeny. + Nelze najít reference. + Seřadit řádky vzestupně + Seřadit řádky sestupně + Aktualizování… + Vybrat spouštěč + Odinstalovat doplňek? + Chcete opravdu odinstalovat doplňek \'%s\'? + Neplatný Termux formát pro extra klávesy v terminálu. Výchozí klávesy budou načteny. + Selhalo čištění po doplňku + Klávesové zkratky schránky + Zapnout klávesové zkratky pro zkopírování a vložení pro fyzické klávesnice + Spravovat formátory + Upřednostňovat a nakonfigurovat formátery + Formátory jsou použity podle pořadí. První formáter, který podporuje aktuální jazyk, bude využíván. Můžete dlouhým klepnutím a přetáhnutím změnit jejich pořadí. + Používat aktuální jazykový server + Prohlížet doplňky + Spustit kontroly pro identifikování problémů v terminálovém prostředí + Zdraví terminálu + Žádné protokoly dostupné + Vaše zařízení Samsung má známý problém, který způsobuje selhání instalace terminálu pomocí PRoot. + Terminál teď běží v degredovaném režimu kvůli známé chyby kompatibility způsobena problémem v prostředí PRoot na některých zařízení. Některé funkce mohou být omezené nebo se budou rozdílně chovat. + Vypnuto (Havarovalo) + Přejmenovat prvek + Selhalo přejmenování prvku. + Tento prvek nelze změnit. + Přejmenování přes několik souborů zatím není podporováno. + Zvýraznit výskyty + Zvýraznit výskyty prvku u místa kurzoru + Čtecí režim pro binární soubory + Výchoze otevřít binární soubory v čtecím režimu + Nevybráno + Žádný seccomp + Seccomp + Zkontrolovat PRoot + Zkontrolovat shell systému + Zkontrolovat úložiště a oprávění + Zkontrolovat Ubuntu + Zkontrolovat přístup k síti + Zkontrolovat abnormality diff --git a/core/resources/src/main/res/values-de/strings.xml b/core/resources/src/main/res/values-de/strings.xml index db6fbedae..4f0b44c28 100644 --- a/core/resources/src/main/res/values-de/strings.xml +++ b/core/resources/src/main/res/values-de/strings.xml @@ -646,4 +646,39 @@ Git-Status Ungültige Farbe Wähle eine Farbe + Einen Bestätigungsdialog anzeigen, bevor die App geschlossen wird + Klammern automatisch schließen + Passende schließende Klammer automatisch einfügen + Beenden bestätigen + Zeilen aufsteigend sortieren + Zeilen absteigend sortieren + Wird aktualisiert… + Runner auswählen + Bereinigung der Erweiterung fehlgeschlagen + Erweiterung deinstallieren? + Bist du sicher, dass du die Erweiterung \'%s\' deinstallieren möchtest? + Ungültiges Format für die Terminal-Zusatztasten. Standardtasten werden geladen. + Tastenkombinationen für die Zwischenablage + Tastenkombinationen zum Kopieren und Einfügen für Hardware-Tastaturen aktivieren + Formatierer verwalten + Formatierer priorisieren und konfigurieren + Formatierer werden nach Priorität angewendet. Der erste Formatierer, der die aktuelle Sprache unterstützt, wird verwendet. Du kannst die Reihenfolge durch langes Drücken und Ziehen ändern. + Aktuellen Sprachserver verwenden + Erweiterungen durchsuchen + Terminal-Status + Diagnosen durchführen, um Probleme mit der Terminalumgebung zu identifizieren + Keine Logs verfügbar + Ausstehend + PRoot überprüfen + System-Shell überprüfen + Speicher und Berechtigungen überprüfen + Ubuntu überprüfen + Netzwerkzugriff überprüfen + Auf Auffälligkeiten überprüfen + Dein Samsung-Gerät ist von einem bekannten Problem betroffen, das dazu führt, dass die PRoot-basierte Terminal-Installation fehlschlägt. + Vorkommen hervorheben + Vorkommen des Symbols an der Cursorposition hervorheben. + Nicht angegeben + Kein Seccomp + Seccomp diff --git a/core/resources/src/main/res/values-es/strings.xml b/core/resources/src/main/res/values-es/strings.xml index 9ba1ea5ce..4b75c4d3a 100644 --- a/core/resources/src/main/res/values-es/strings.xml +++ b/core/resources/src/main/res/values-es/strings.xml @@ -522,7 +522,7 @@ Indexar puede mejorar los tiempos de búsqueda para proyectos grandes Siempre indexar proyectos %1$s archivos indexados, el tamaño de la base de datos es %2$s - ¿Está seguro que desea eliminar permanentemente %d archivos? + ¿Confirma que desea eliminar permanentemente %d archivos? Archivos no guardados ¿Está seguro que desea descartes estos documentos no guardados? ¿Le gustaría actualizar el idioma \'%s\' del servidor? @@ -629,4 +629,45 @@ Color no válido Elije un color Consejos embebidos + Actualizar… + Auto‐cerrar corchete + Inserta automáticamente coincidencias de cierre de corchete + Confirmar diálogo de salida + Muestra un diálogo de confirmación antes de cerrar la aplicación + Ordena líneas ascencentes + Ordenar líneas descendentes + Elija corredor + Limpieza de extensión incorrecta + ¿Desinstalo extensión? + ¿Seguro que desea desinstalar la extensión \'%s\'? + Formato no válido para teclas de terminal extras. Se cargaran las teclas por defecto. + Vínculos del portapapel + Habilita copia y pega vínculos de teclas para teclados de hardware. + + Hace %d segundo + Hace %d segundos + Hace %d segundos + + + Hace %d minuto + Hace %d minutos + Hace %d minutos + + + Hace %d hora + Hace %d horas + Hace %d horas + + + Hace %d día + Hace %d días + Hace %d días + + Gestionar formatos + Priorizar y configurar formatos + Formateadores están aplicado por prioridad. Será utilizado el primer formateador que admite el idioma actual. Puedes pulsar más tiempo y arrastrar para cambiar su ordenación. + Sucesos destacados + Sucesos resaltados del símbolo en el cursor. + Un servidor de idioma personalizado + Extensiones del navegador diff --git a/core/resources/src/main/res/values-fa/strings.xml b/core/resources/src/main/res/values-fa/strings.xml index 79ff2f056..9d580ef89 100644 --- a/core/resources/src/main/res/values-fa/strings.xml +++ b/core/resources/src/main/res/values-fa/strings.xml @@ -77,4 +77,37 @@ حذف ترمینال حذف نصب نصب + چسباندن + نام فایل + مسیر + Xed-Editor نیاز به دسترسی به ویرایش فایل‌های موجود در فضای ذخیره‌سازی شما دارد. لطفا اجازه دسترسی را در تنظیمات سیستم بدهید. + شاخه + فایل های خصوصی + فایل های خصوصی Xed-Editor + حافظه داخلی + باز کردن حافظه داخلی + نصب شده + ذخیره خودکار فایل + مقدار نامعتبر + تنظیم اندازه متن + تنظیمات کلی برنامه + تنظیمات عمومی ویرایشگر + تنظیمات عمومی ترمینال + درباره Xed-Editor + برسی برای به‌روزرسانی ها + برسی دوره ای برای به‌روزرسانی ها + برای باز کردن فایل اینجا کلیک کنید + مدیریت فونت های ویرایشگر + سایر + فایل‌ها ذخیره نشده‌اند + هشدار + فاصله خطوط + به‌روزرسانی های جدید در دسترس است + به‌روزرسانی + نام نمی‌تواند خالی باشد + مقدار نمی‌تواند خالی باشد + در حال نصب… + در حال به‌روزرسانی… + در حال بارگذاری… + در حال دانلود… diff --git a/core/resources/src/main/res/values-fi/strings.xml b/core/resources/src/main/res/values-fi/strings.xml index 768e281bc..208805569 100644 --- a/core/resources/src/main/res/values-fi/strings.xml +++ b/core/resources/src/main/res/values-fi/strings.xml @@ -59,4 +59,6 @@ Kloonaa etävarasto Arkiston URL Haara + Näytä rivinumerot + Tekstin koko diff --git a/core/resources/src/main/res/values-fr/strings.xml b/core/resources/src/main/res/values-fr/strings.xml index 3eb9fca99..e1bd084dc 100644 --- a/core/resources/src/main/res/values-fr/strings.xml +++ b/core/resources/src/main/res/values-fr/strings.xml @@ -256,7 +256,7 @@ Type de wrapper Assistance Traduire - Terminal et lanceurs + Terminal, lanceurs et serveurs de langue Nom de répertoire Caractères invalides Propriétés @@ -654,4 +654,44 @@ Insère automatiquement la parenthèse fermante correspondante Confirmer la fenêtre de fermeture Affiche un message de confirmation avant de fermer le logiciel + Trier les lignes (ascendant) + Trier les lignes (descendant) + Mise à jour… + Choisir un lanceur + Échec de l\'extension de nettoyage + Désinstaller l\'extension ? + Êtes-vous sûr de vouloir désinstaller l\'extension \'%s\' ? + Format invalide pour les touches additionnelles du terminal. Les touches par défaut seront chargées. + Raccourcis clavier du presse-papier + Active les raccourcis clavier pour copier et coller pour les claviers physiques + Gérer les formateurs + Prioriser et configurer les formateurs + Les formateurs sont appliqués par ordre de priorité. Le premier formateur qui gère la langue actuelle sera utilisé. Vous pouvez faire un appui long et le déplacer pour changer cet ordre. + Surligner les occurrences + Surligne les occurrences du symbole au niveau du curseur + Utilise le serveur de langue actuel + Parcourir les extensions + État du terminal + Lance des diagnostics pour identifier les problèmes avec l\'environnement du terminal + Pas de journaux disponibles + En attente + Vérifier PRoot + Vérifier le stockage et les permissions + Vérifier Ubuntu + Vérifier les accès réseau + Vérifier les anomalies + Votre appareil Samsung est affecté par un problème connu qui cause l\'échec de l\'installation du terminal basé sur PRoot. + Vérifier le shell du système + Le terminal fonctionne actuellement dans un mode dégradé à cause d\'un problème de compatibilité connue, causé par un dysfonctionnement dans l\'environnement PRoot sur certains appareils. Certaines fonctionnalités peuvent être limitées ou se comporter différemment. + Désactivé (crashé) + Non précisé + Pas de seccomp + Seccomp + Nombre de téléchargements + Téléchargement des extensions, thèmes et lots d\'icônes + Aucun thème trouvé. + Aucun lot d\'icônes trouvé. + Rechercher des thèmes + Rechercher des lots d\'icônes + Les extensions peuvent exécuter du code quelconque et être potentiellement malveillantes. N\'installez des extensions que si vous faites confiance à leur développeur et si vous avez vérifié leur code source. diff --git a/core/resources/src/main/res/values-in/strings.xml b/core/resources/src/main/res/values-in/strings.xml index 570142f2c..b49704f20 100644 --- a/core/resources/src/main/res/values-in/strings.xml +++ b/core/resources/src/main/res/values-in/strings.xml @@ -256,7 +256,7 @@ Penyimpanan Dukungan Terjemahkan - Terminal dan runner + Terminal, runner dan server bahasa Nama folder Karakter tidak valid Properti @@ -645,4 +645,44 @@ Secara otomatis menyisipkan kurung penutup yang sesuai Dialog konfirmasi keluar Tampilkan dialog konfirmasi sebelum menutup aplikasi + Urutkan baris secara menaik + Urutkan baris secara menurun + Memperbarui… + Pilih runner + Pembersihan ekstensi gagal + Hapus ekstensi? + Apakah Anda yakin ingin menghapus ekstensi \'%s\'? + Format tombol tambahan terminal tidak valid. Tombol default akan dimuat. + Tombol pintas untuk papan klip + Aktifkan pintasan tombol salin dan tempel untuk keyboard fisik + Kelola pemformatan + Prioritaskan dan konfigurasikan pemformatan + Penerapan pemformatan dilakukan berdasarkan prioritas. Pemformatan pertama yang mendukung bahasa saat ini akan digunakan. Anda dapat menekan dan menahan, lalu menyeret untuk mengubah urutannya. + Sorot kemunculan + Sorot kemunculan simbol pada kursor + Gunakan server bahasa saat ini + Jelajahi ekstensi + Kondisi terminal + Lakukan pemeriksaan untuk mengidentifikasi masalah pada lingkungan terminal + Tidak ada log yang tersedia + Tertunda + Periksa PRoot + Periksa shell sistem + Periksa penyimpanan dan izin + Periksa Ubuntu + Periksa akses jaringan + Periksa kelainan + Perangkat Samsung Anda mengalami masalah yang telah diketahui, yang menyebabkan kegagalan penginstalan terminal berbasis PRoot. + Terminal saat ini beroperasi dalam mode terbatas akibat masalah kompatibilitas yang telah diketahui, yang disebabkan oleh gangguan pada lingkungan PRoot di perangkat tertentu. Beberapa fitur mungkin terbatas atau berfungsi secara berbeda. + Tidak ditentukan + Tanpa seccomp + Seccomp + Tidak berfungsi (Rusak) + Jumlah Unduhan + Unduh ekstensi, tema, dan paket ikon + Ekstensi dapat menjalankan kode sembarangan dan berpotensi berbahaya. Hanya pasang ekstensi jika Anda mempercayai pengembangnya dan telah memverifikasi kode sumber ekstensi tersebut. + Tema tidak ditemukan. + Paket ikon tidak ditemukan. + Cari tema + Cari paket ikon diff --git a/core/resources/src/main/res/values-it/strings.xml b/core/resources/src/main/res/values-it/strings.xml index 5c1d57eb7..1240e25e7 100644 --- a/core/resources/src/main/res/values-it/strings.xml +++ b/core/resources/src/main/res/values-it/strings.xml @@ -5,7 +5,7 @@ Chiudi Permesso negato Percorso - Xed-Editor ha bisogno dei permessi per modificare i file. Per favore concedi i permessi nella schermata seguente. + Xed-Editor ha bisogno di accedere all\'archiviazione per modificare i file. Concedi l\'accesso nella schermata seguente. Temi Apri una cartella Salva @@ -198,7 +198,7 @@ Memoria insufficiente per aprire questa scheda. Aprirla potrebbe causare il crash di Xed-Editor. Vuoi procedere comunque? Abilita modalità lettura per impostazione predefinita Installa - Vuoi installare il server linguistico per %s? + Vuoi installare il language server %s? Disinstallare Ubuntu in modo permanente? Questa azione non può essere annullata. Disinstalla Disinstalla terminale @@ -240,7 +240,7 @@ Modalità strict Individua dischi o accessi di rete nel thread principale In alcuni dispositivi, specialmente alcuni modelli dalla Cina, è possibile imbattersi in un avviso di sicurezza di sistema quando molti processi sono in esecuzione nel terminale (per esempio durante \'apt update\' o \'apt upgrade\'). Questo è un falso positivo e non indica realmente un virus. - Vuoi aggiornare il server linguistico \'%s\'? + Vuoi aggiornare il language server \'%s\'? Termina Xed-Editor se non risponde per più di 5 secondi Errori dettagliati Inclusi lo stackrace nelle finestre di dialogo di errore @@ -251,9 +251,9 @@ Community Discord Inserisci carattere di tabulazione Usa una vera tabulazione (\\t) invece degli spazi per l\'indentazione - Server linguistico - Gestisci server linguistici - Installa, connetti e configura server linguistici + Language server + Gestisci language server + Installa, connetti e configura language server Formatta al salvataggio Formatta il file automaticamente quando viene salvato Sviluppatore @@ -268,11 +268,11 @@ Errore API Sconosciuto Impossibile scrivere nel file (nessuna autorizzazione di scrittura) - I server linguistici sono processi separati che forniscono funzionalità intelligenti, come il completamento del codice, l\'evidenziazione degli errori e la documentazione in linea. + I language server sono processi separati che forniscono funzionalità intelligenti, come il completamento del codice, l\'evidenziazione degli errori e la documentazione in linea. Archiviazione Assistenza Traduci - Terminale ed esecutori + Terminale, esecutori e language server Nome cartella Carattere non valido Proprietà @@ -390,7 +390,7 @@ Ctrl Alt Maiusc - Xed-Editor supporta le scorciatoie da tastiera hardware per aiutarti a lavorare più velocemente ed efficientemente. È già disponibile un\'ampia serie di scorciatoie predefinite e puoi configurarle di seguito a tuo piacimento. + Xed-Editor supporta le scorciatoie da tastiera fisica per aiutarti a lavorare più velocemente ed efficientemente. È già disponibile un\'ampia serie di scorciatoie predefinite e puoi configurarle di seguito a tuo piacimento. Reimposta tutto Cerca comandi o scorciatoie da tastiera Trasforma in minuscolo @@ -461,7 +461,7 @@ Esiste già una cartella con il nome \'%s\' Impossibile creare il file Impossibile creare la cartella - Crea un backup di tutte le attuali impostazioni + Crea un backup di tutte le impostazioni attuali Ripristina le impostazioni da un backup Esportazione riuscita Esportazione non riuscita @@ -585,8 +585,8 @@ Gestisci Nessuna istanza Tempo di attività: %s - Impossibile connettersi al server linguistico - Riavvia il server linguistico per applicare le modifiche + Impossibile connettersi al language server + Riavvia il language server per applicare le modifiche Escludi i file nel drawer Cambia i file da escludere dall\'albero dei file Escludi i file nella ricerca @@ -652,4 +652,37 @@ Inserisci automaticamente la parentesi di chiusura corrispondente Dialogo di conferma uscita Mostra una finestra di dialogo di conferma prima di chiudere l\'app + Ordina righe crescente + Ordina righe decrescente + Aggiornamento… + Scegli esecutore + Pulizia dell\'estensione non riuscita + Disinstallare l\'estensione? + Sei sicuro di voler disinstallare l\'estensione \'%s\'? + Formato non valido per i tasti extra nel terminale. Verranno caricati i tasti predefiniti. + Scorciatoie da tastiera per gli appunti + Abilita le scorciatoie da tastiera per copia e incolla per le tastiere fisiche + Gestisci formattatori + Ordina per priorità e configura i formattatori + I formattatori vengono applicati in ordine di priorità. Verrà utilizzato il primo formattatore che supporta la lingua corrente. Puoi tenere premuto e trascinare per cambiare il loro ordine. + Evidenzia occorrenze + Evidenzia le occorrenze del simbolo in corrispondenza del cursore + Utilizza il language server corrente + Sfoglia estensioni + Salute del terminale + Esegui la diagnostica per identificare problemi con l\'ambiente del terminale + Nessun registro disponibile + In attesa + Verifica PRoot + Verifica shell di sistema + Verifica spazio di archiviazione e autorizzazioni + Verifica Ubuntu + Verifica accesso alle rete + Verifica anomalie + Il tuo dispositivo Samsung è interessato da un problema noto che causa il fallimento dell\'installazione del terminale basato su PRoot. + Il terminale è attualmente in esecuzione in una modalità degradata, a causa di un problema di compatibilità noto dovuto da un malfunzionamento nell\'ambiente PRoot su alcuni dispositivi. Alcune funzionalità potrebbero essere limitate o funzionare in maniera diversa. + Disabilitato (In crash) + Non specificato + Seccomp + No seccomp diff --git a/core/resources/src/main/res/values-iw/strings.xml b/core/resources/src/main/res/values-iw/strings.xml index 4a53a3c0e..7b014374c 100644 --- a/core/resources/src/main/res/values-iw/strings.xml +++ b/core/resources/src/main/res/values-iw/strings.xml @@ -4,7 +4,7 @@ אחסון חיצוני תוכן לא נתמך שמירה - פתח תיקיה + פתיחת תיקיה קובץ חדש אנא המתן… העתק @@ -92,7 +92,7 @@ בטל החל הגדר גודל טקסט - ‫נדרש הפעלה מחדש + ‫נדרשת הפעלה מחדש הגדרות בדוק אם יש עדכונים עיצוב diff --git a/core/resources/src/main/res/values-ko/strings.xml b/core/resources/src/main/res/values-ko/strings.xml index c850f4fd9..5a1c5e343 100644 --- a/core/resources/src/main/res/values-ko/strings.xml +++ b/core/resources/src/main/res/values-ko/strings.xml @@ -38,7 +38,7 @@ 주의 정말 \'%s\'를 영구적으로 삭제하실 건가요? 정말로 \'%d\'를 영구적으로 삭제하실 건가요? - 이 파일을 처리할 앱을 찾지 못했어요 + 이 파일을 처리할 앱이 없습니다 터미널 외부 파일 추가하기 프로젝트로 열기 @@ -46,7 +46,7 @@ 다운로드 적용 터미널에서 열기 - 이 파일을 다른 앱으로 여는 것이 허용되지 않았어요 + 이 파일을 다른 앱에서 여는 것이 허용되지 않았습니다 정말 \'%s\' 프로젝트를 닫으실 건가요? 파일 이름 경로 @@ -61,7 +61,7 @@ 브렌치 전용 파일 Xed-Editor의 전용 파일 - 저장소를 복제하고 있어요… + 저장소 복제 중… 오류 시스템 파일 선택기로 디렉터리 열기 내부 저장공간 @@ -74,24 +74,24 @@ 테마 편집기 부드러운 탭 - 탭 사이의 전환을 부드럽게 해줘요 - 에디터에서 자동 줄바꿈을 켜줘요 + 탭 사이의 전환을 부드럽게 합니다 + 에디터에서 자동 줄바꿈을 활성화합니다 파일을 열 때 사이드 메뉴 잠금을 유지합니다 커서 애니메이션 부드러운 커서 애니메이션을 활성화합니다 커서 스타일 - 커서 스타일을 바꿔요 + 커서 스타일을 바꿉니다 줄 번호 표시하기 줄 번호 고정 개별 키 키보드 위에 개별 키를 표시합니다 자동 저장 - 파일을 자동으로 저장하요 + 파일을 자동으로 저장합니다 자동 저장 지연 시간 자동 저장 사이의 지연 시간(ms)을 조절합니다 유효하지 않는 값 - 값이 너무 작아요 - 값이 너무 커요 + 값이 너무 작습니다 + 값이 너무 큽니다 글자 크기 글자 크기를 선택합니다 탭 크기 @@ -102,7 +102,7 @@ 일반 에디터 설정 일반 터미널 설정 알 수 없는 에러 - 설치하지 못했어요 + 설치 실패 라이트 다크 시스템 설정에 따름 @@ -129,7 +129,7 @@ 기타 내용 폰트 - 폰트가 성공적으로 추가되었어요 + 폰트가 성공적으로 추가되었습니다 폰트 삭제 새로운 폰트 추가 저장공간에서 TTF 폰트 선택 @@ -139,10 +139,10 @@ 정말 저장하지 않은 문서들을 버리시겠어요? 버리기 경고 - 기본 인코딩을 바꾸는 것은 강력히 추천드리지 않아요, 변경할 경우 파일 변형이 일어날 수 있습니다. 이 변경으로 인한 영향을 완전히 이해하지 않았거나 적절한 백업 파일을 만들어 두지 않았을 경우, UTF-8로 사용하는 것을 강력히 권장드립니다. + 기본 인코딩을 바꾸는 것은 강력히 추천드리지 않으며, 변경할 경우 파일 변형이 일어날 수 있습니다. 이 변경으로 인한 영향을 완전히 이해하지 않았거나 적절한 백업 파일을 만들어 두지 않았을 경우, UTF-8로 사용하는 것을 강력히 권장드립니다. 줄 간격 에디터에서 각 줄의 높이 배율 - 새로운 업데이트를 이용할 수 있어요 + 새로운 업데이트 이용 가능 업데이트 무시 세션 복구 @@ -153,16 +153,16 @@ 세션 추가 확장 기능 확장 기능 관리 - 탭이 열렸어요 - 이름은 비워둘 수 없어요 - 값은 비워둘 수 없어요 + 탭 열림 + 이름은 비워둘 수 없습니다 + 값은 비워둘 수 없습니다 설치 중… 저장공간에서 설치 - 확장 기능이 발견되지 않았어요. + 확장 기능이 발견되지 않았습니다. 불러오는 중… - 확장 기능은 Xed-Editor의 동작 방식이나 기능을 변경하는 데에 사용될 수 있어요. 확장 기능을 만드는 방법을 배우고 싶다면, 여기를 클릭하세요. + 확장 기능은 Xed-Editor의 동작 방식이나 기능을 변경하는 데에 사용될 수 있습니다. 확장 기능을 만드는 방법을 배우고 싶다면, 여기를 클릭하세요. 다운로드 중… - 네트워크 에러. 인터넷 연결이 제대로 되고 있는지 확인해주세요. + 네트워크 오류. 인터넷 연결이 제대로 되고 있는지 확인해주세요. 가상 키보드 비활성화 하드웨어 키보드를 이용할 수 있는 경우, 가상 키보드를 비활성화합니다 구문 강조 @@ -175,7 +175,7 @@ 복구 성공 실패 - 오류가 발생했어요 + 오류가 발생했습니다 홈 디렉터리 노출 시스템 파일 선택기를 통해서 외부 앱에서 홈 디렉터리에 접근할 수 있게 합니다 홈 디렉터리를 노출하면 Git 토큰, 비밀번호, 사용자 이름 등, 터미널 홈 디렉터리에 저장된 다른 파일이 악성 앱에 노출될 수 있습니다. 계속하시겠습니까? @@ -202,7 +202,7 @@ 수정 모드 폴더가 열려있지 않음 잘라내기 - 타임아웃으로 인해 다운로드가 취소되었습니다. 인터넷 연결이 안정한지 반드시 확인해주세요 + 타임아웃으로 인해 다운로드가 취소되었습니다. 인터넷 연결이 안정한지 반드시 확인해 주세요 테마 설정 테마 추가 이 기능은 이 스마트폰에서 지원하지 않습니다 @@ -252,4 +252,73 @@ 파일 확장자 (예시: *.py) 디스코드 커뮤니티 탭 문자 삽입 + + 업데이트 중… + 언어 서버 + 언어 서버 관리 + 언어 서버를 설치, 연결, 구성하기 + 저장 시 서식 지정 + 들여 쓰기에 스페이스 대신 Tab 키를 사용하기 + 파일이 저장되었을 때 자동으로 서식을 지정합니다 + 개발자 + 빌드 정보 + 커뮤니티 + 안전 모드 + 터미널을 안전 모드로 시작합니다 + 화이트스페이스 표시 + 공백, 탭, 그리고 줄바꿈을 보이는 기호처럼 표시합니다 + 정말 이 문서를 새로고침 하시겠어요? 저장되지 않은 변경점들은 모두 사라집니다. + 도움말 + GitHub 스타 + API 오류 + 알 수 없음 + 파일을 작성할 수 없습니다(쓰기 권한 없음) + 언어 서버는 코드 완성, 오류 표시 그리고 인라인 문서 같은 인텔리전스 기능을 제공하는 별도의 프로세스 입니다. + 저장공간 + 지원 + 개발 지원 + 번역 + 터미널과 러너 + 폴더 이름 + 유효하지 않는 문자 + 속성 + 폴더 이름 바꾸기 + 사례 무시 + 정규식 + 전체 단어 + 정의로 이동 + 참조로 이동 + 이름 바꾸기 + 문서 서식 지정 + 선택 영역 형식 지정 + ‐정의 위치로 이동하려면 %s개의 정의 중 하나를 선택하세요. + 참조 위치로 이동하려면 %s개의 정의 중 하나를 선택하세요. + 정의를 찾을 수 없음. + 참조를 찾을 수 없음. + 정의를 찾을 수 없습니다. + 참조를 찾을 수 없습니다. + 기호를 바꿀 수 없습니다. + 이 기호를 변경할 수 없습니다. + 여러 파일에 걸친 이름 바꾸기는 아직 지원되지 않습니다. + 문서의 서식을 지정할 수 없습니다. + 선택한 부분의 서식을 지정할 수 없습니다. + 계속 + 고정 헤더 활성화 + 스크롤하는 동안 현재 코드 블록 헤더를 계속 표시합니다 + 명령어 입력 + 빠른 줄 삭제 활성화 + 줄이 비어있을 경우, 해당 줄을 자동으로 삭제합니다 + 언어 선택 + (최근 사용) + 명령 팔레트 + 파일 이름을 변경할 수 없습니다 + 파일을 삭제할 수 없습니다 + 파일을 붙여넣을 수 없습니다 + 디버그 모드는 안전 제한을 비활성화하며, 잘못 사용할 경우, 데이터 손실 또는 다른 문제로 이어질 수 있습니다. 자신이 무엇을 하고 있는지 알 경우에만 활성화하세요. + 외부 LSP + 외부 + 데스크탑 모드 (실험용) + 태블릿이나 데스트탑 크기의 화면에서 앱을 더욱 쉽게 사용할 수 있도록 만들어 줍니다 + 내장됨 + 확장 가능, 테마, 그리고 아이콘 팩을 다운로드 합니다 diff --git a/core/resources/src/main/res/values-pl/strings.xml b/core/resources/src/main/res/values-pl/strings.xml index 3abbeb1d6..a956d5ef0 100644 --- a/core/resources/src/main/res/values-pl/strings.xml +++ b/core/resources/src/main/res/values-pl/strings.xml @@ -255,7 +255,7 @@ Rozmiar Wsparcie Przetłumacz - Terminal oraz programy uruchamiające + Terminal, procesy i serwery językowe Nazwa katalogu Nieprawidłowe znaki Właściwości @@ -373,7 +373,7 @@ Alt Shift Resetuj wszystkie - Szukaj komend lub skrótów klawiszowych + Szukaj komend lub przypisań klawiszy Zamień na małe litery Zamień na duże litery Przypisanie klawisza konfliktuje z innym skrótem klawiszowym. @@ -385,7 +385,7 @@ Konfiguruj przypisania klawiszy Nie ustawiono Naciśnij kombinacje klawiszy (musi zawierać Ctrl lub Alt) - Aplikacja wspiera skróty klawiszowe klawiatury fizycznej, aby pracować szybciej i efektywniej. Rozbudowany zestaw domyślnych skrótów jest dostępny od ręki oraz możesz go dostosować do upodobań poniżej. + Aplikacja wspiera skróty klawiszowe klawiatury fizycznej, aby pracować szybciej i efektywniej. Rozbudowany zestaw domyślnych przypisań klawiszy jest dostępny od ręki oraz możesz go dostosować do upodobań poniżej. Tło dodatkowych klawiszy Pokaż tło dodatkowych klawiszy Czynności @@ -658,4 +658,43 @@ Automatycznie wstawiaj pasujacy nawias zamykajacy Potwierdzenie wyjścia Pokaż dialog potwierdzający wyjście z aplikacji + Sortuj linijki rosnąco + Sortuj linijki malejąco + Aktualizacja… + Wybierz program uruchamiający + Niepowodzenie czyszczenia rozszerzenia + Odinstalować rozszerzenie? + Czy napewno chcesz odinstalować rozszerzenie \'%s\'? + Nieprawidłowy format dodatkowych klawiszy terminalu. Domyślne klawisze zostaną zastosowane. + Przypisania klawiszy schowka + Włącz skróty klawiszowe do kopiowania i wklejania dla klawiatur sprzętowych + Zarządzaj formatownikami + Ustaw priorytet oraz konfiguruj formatowniki + Formatowniki są stosowane według priorytetu. Pierwszy formatownik wspierający obecny język zostanie użyty. Zmień kolejność przytrzymując i przeciągając. + Podświetlanie wystąpienia + Podświetl wystąpienia symbolu pod kursorem + Użyj obecnego serwera językowego + Przeglądaj rozszerzenia + Stan techniczny terminala + Przeprowadź diagnostykę w celu wykrycia problemów ze środowiskiem terminala + Brak dostępnych logów + Oczekujące + Sprawdź PRoot + Sprawdź powłokę systemu + Sprawdź pamięć i uprawnienia + Sprawdź Ubuntu + Sprawdź dostęp do sieci + Sprawdź, czy nie ma nieprawidłowości + W Twoim urządzeniu Samsung występuje znany błąd, który powoduje niepowodzenie instalacji terminala opartego na PRoot. + Terminal działa obecnie w trybie ograniczonym z powodu znanego problemu kompatybilności spowodowanego nieprawidłowym działaniem środowiska PRoot na niektórych urządzeniach. Niektóre funkcje mogą być ograniczone lub działać inaczej niż zwykle. + Nieaktywne (Awaria) + Nieokreślony + Brak Seccomp + Seccomp + Liczba pobrań + Pobierz rozszerzenia, motywy oraz paczki ikon + Nie znaleziono motywów. + Nie znaleziono paczek ikon. + Wyszukaj motywy + Wyszukaj paczki ikon diff --git a/core/resources/src/main/res/values-ru/strings.xml b/core/resources/src/main/res/values-ru/strings.xml index 075455ee9..9099055a3 100644 --- a/core/resources/src/main/res/values-ru/strings.xml +++ b/core/resources/src/main/res/values-ru/strings.xml @@ -256,7 +256,7 @@ Хранилище Поддержка Перевод - Терминал и обработчики + Терминал, раннеры и языковые сервера Имя папки Недопустимые символы Свойства @@ -653,4 +653,48 @@ Статус Git Недопустимый цвет Выберите цвет + Обновление… + Не указано + Авто-закрытие скобок + Автоматически всталять закрывающую скобку + Подтвердить закрытие диалога + Показывать подтверждающий диалог перед закрытием приложения + Сортировка строк по возврастанию + Сортировка строк по убыванию + Выбрать раннер + Чистка расширения не удалась + Удалить расширение? + Вы уверены, что хотите удалить расширение \'%s\'? + Не корректный формат клавиш для терминала. Загружены клавиши по умолчанию. + Менеджер форматирования + Расставьте приоритеты и настройте форматтеры + Форматтеры быьи применены как приоритетные. Будет использован первый форматтер, который поддерживает текущий язык. Для изменения порядка зажмите и перетаскивайте. + События подсветки + Выделите вхождения символа в месте расположения курсора. + Использовать текущий языковой сервер + Найти расширения + Сочитания клавиш буфера обмена + Включить функции копирования и вставки сочетаний клавиш для аппаратных клавиатур + Работоспособность терминала + Запустите диагностику для нахождения проблем в среде терминала + Логи недоступны + В ожидании + Проверьте PRoot + Проверка системного shell + Проверка хранилища и разрешений + Проверка Ubuntu + Проверка доступности сети + Проверьте наличие отклонений от нормы + На вашем устройстве Samsung обнаружена известная проблема, из-за которой установка терминала с правами PRoot завершается с ошибкой. + В настоящее время терминал работает в режиме с пониженной производительностью из-за известной проблемы совместимости, вызванной сбоем в среде PRoot на некоторых устройствах. Некоторые функции могут быть ограничены или работать иначе. + Отключено (Вылет приложения) + Seccomp не доступен + Seccomp + Скачать расширения, темы и наборы иконок + Расширения могут выполнять произвольный код и потенциально быть вредоносными. Устанавливайте расширения только в том случае, если вы доверяете разработчику расширения и проверили его исходный код. + Темы не найдены. + Наборы иконок не найдены. + Поиск тем + Поиск наборов иконок + Количество скачиваний diff --git a/core/resources/src/main/res/values-tr/strings.xml b/core/resources/src/main/res/values-tr/strings.xml index 514a76204..c3b7b2572 100644 --- a/core/resources/src/main/res/values-tr/strings.xml +++ b/core/resources/src/main/res/values-tr/strings.xml @@ -254,7 +254,7 @@ Depolama Destek Tercüme et - Terminal ve çalıştırıcılar + Terminal, çalıştırıcılar ve dil sunucuları Klasör adı Geçersiz karakterler Nitelikler @@ -650,4 +650,44 @@ Eşleşen kapanış parantezini otomatik olarak ekle Çıkış onaylama diyaloğu Uygulamayı kapatmadan önce bir onay iletişim kutusu göster + Satırları artan sırada sırala + Satırları azalan sırada sırala + Güncelleniyor… + Çalıştırıcıyı seç + Uzantı temizleme başarısız oldu + Uzantı kaldırılsın mı? + \'%s\' uzantısını kaldırmak istediğinizden emin misiniz? + Terminal ek tuşları için geçersiz biçim. Varsayılan tuşlar yüklenecektir. + Pano kısayol tuşları + Donanım klavyeleri için kopyala ve yapıştır tuş kısayollarını etkinleştir + Biçimlendiricileri yönet + Önceliklendir ve biçimlendiricileri yapılandır + Biçimlendiriciler önceliğe göre uygulanır. Mevcut dili destekleyen ilk biçimlendirici kullanılacaktır. Sıralarını değiştirmek için uzun basıp sürükleyebilirsiniz. + Olayları vurgula + İmleçteki sembolün geçtiği yerleri vurgula + Mevcut dil sunucusunu kullan + Uzantılara Göz At + Terminal durumu + Terminal ortamındaki sorunları belirlemek için tanılama çalıştırın + Kayıt yok + Beklemede + PRoot\'u kontrol et + Sistem kabuğunu kontrol et + Depolama ve izinleri kontrol et + Ubuntu\'yu kontrol et + Ağ erişimini kontrol et + Anormallikleri kontrol et + Samsung cihazınız, PRoot tabanlı terminal kurulumunun başarısız olmasına neden olan bilinen bir sorundan etkilenmektedir. + Terminal, bazı cihazlardaki PRoot ortamında meydana gelen bir arızadan kaynaklanan bilinen bir uyumluluk sorunu nedeniyle şu anda kısıtlı modda çalışmaktadır. Bazı özellikler sınırlı olabilir veya farklı çalışabilir. + Devre dışı (Çöktü) + Belirtilmemiş + Seccomp yok + Güvenli Hesaplama Modu + Eklentileri, temaları ve simge paketlerini indirin + Eklentiler rastgele kod çalıştırabilir ve potansiyel olarak kötü amaçlı olabilir. Eklentileri yalnızca eklentinin geliştiricisine güveniyorsanız ve eklentinin kaynak kodunu doğruladıysanız yükleyin. + Tema bulunamadı. + Simge paketi bulunamadı. + Temaları ara + Simge paketlerini ara + İndirme Sayısı diff --git a/core/resources/src/main/res/values-vi/strings.xml b/core/resources/src/main/res/values-vi/strings.xml index e7cf1b7e7..3251a0bbc 100644 --- a/core/resources/src/main/res/values-vi/strings.xml +++ b/core/resources/src/main/res/values-vi/strings.xml @@ -452,4 +452,16 @@ Khởi động terminal ở chế độ bảo trì Language server là các tiến trình riêng biệt cung cấp các tính năng thông minh, như tự động hoàn thành code, tô sáng lỗi và tài liệu nội dòng. Bộ nhớ + Đang cập nhật… + Bỏ qua trường hợp + Regex + Toàn bộ từ + Lựa chọn định dạng + Bật cuộn dính + Giữ cho tiêu đề đoạn code hiện tại luôn hiển thị khi cuộn trang + Cho phép xóa dòng nhanh + (Mới sử dụng gần đây) + Bảng lệnh + Chế độ gỡ lỗi vô hiệu hóa các hạn chế an toàn, điều này có thể dẫn đến mất dữ liệu hoặc các sự cố khác nếu sử dụng sai cách. Chỉ bật chế độ này nếu bạn biết mình đang làm gì. + Chế độ máy tính để bàn (Thử nghiệm) diff --git a/core/resources/src/main/res/values-zh-rTW/strings.xml b/core/resources/src/main/res/values-zh-rTW/strings.xml index fcec2be51..8fe881905 100644 --- a/core/resources/src/main/res/values-zh-rTW/strings.xml +++ b/core/resources/src/main/res/values-zh-rTW/strings.xml @@ -641,4 +641,25 @@ Git 狀態 無效的顏色 選擇顏色 + 自動閉合括號 + 自動插入對應的閉合括號 + 結束確認對話框 + 關閉應用程式前顯示確認對話框 + 更新中… + 遞增排序行內容 + 遞減排序行內容 + 選擇執行器 + 擴充功能清理失敗 + 要解除安裝擴充功能嗎? + 確定要解除安裝擴充功能「%s」嗎? + 終端機額外按鍵格式無效。將載入預設按鍵。 + 管理格式化工具 + 設定格式化工具優先順序與配置 + 格式化工具會依照優先順序套用。系統將使用第一個支援目前語言的格式化工具。您可以長按並拖曳來調整順序。 + 醒目標示出現項目 + 醒目標示游標所在符號的所有出現位置。 + 使用目前語言伺服器 + 瀏覽擴充功能 + 剪貼簿快捷鍵 + 啟用實體鍵盤的複製與貼上快捷鍵。 diff --git a/core/resources/src/main/res/values-zh/strings.xml b/core/resources/src/main/res/values-zh/strings.xml index 9177e983a..f6e65a6ea 100644 --- a/core/resources/src/main/res/values-zh/strings.xml +++ b/core/resources/src/main/res/values-zh/strings.xml @@ -256,7 +256,7 @@ 包装 支持 翻译 - 终端和运行器 + 终端、运行器和语言服务器 文件夹名称 非法字符 属性 @@ -642,4 +642,41 @@ Git 状态 选择颜色 无效的颜色 + 自动闭合括号 + 自动插入匹配的括号 + 确认退出对话框 + 在关闭 App 前显示确认对话框 + 按升序排列行 + 按降序排列行 + 更新中… + 选择运行器 + 扩展清理失败 + 是否卸载扩展? + 你确定要卸载扩展 \'%s\'? + 终端的符号栏格式不正确。将加载默认符号。 + 管理 formatter + 调整 formatter 的优先级和参数 + 根据 formatter 的优先级,第一个支持当前编程语言的 formatter 将被调用。你可以通过长按并拖动的方式来调整顺序。 + 高亮匹配项 + 高亮和光标所在内容相匹配的所有项目 + 使用当前语言服务器 + 浏览扩展 + 剪贴板快捷键 + 启用实体键盘的复制和粘贴快捷键 + 终端诊断 + 诊断终端环境是否异常 + 无日志 + 检查即将开始 + 检查 PRoot + 检查系统 shell + 检查存储和权限 + 检查 Ubuntu® + 检查网络 + 检查异常 + 你的三星®设备存在一个会导致基于 PRoot 的终端发生故障的已知问题。 + 由于一个已知的兼容性问题,PRoot 环境的终端正以降级模式运行。部分特性暂时受限或行为异常。 + 未指定 + 无 seccomp + 启用 seccomp + 已禁用(已崩溃) diff --git a/core/resources/src/main/res/values/strings.xml b/core/resources/src/main/res/values/strings.xml index 836920ec9..dacadf49d 100644 --- a/core/resources/src/main/res/values/strings.xml +++ b/core/resources/src/main/res/values/strings.xml @@ -153,12 +153,17 @@ Add session Extensions Manage extensions + Download extensions, themes, and icon packs + Extensions can execute arbitrary code and potentially be malicious. Only install extensions if you trust the developer of the extension and has verified the source code of the extension. Tab opened Name cannot be empty Value cannot be empty Installing… + Updating… Install from storage No extensions found. + No themes found. + No icon packs found. Loading… Extensions can be used to modify the behavior and features of Xed-Editor. To learn how to build an extension, click here. Downloading… @@ -227,6 +232,9 @@ Use project as working directory Use project as working directory in terminal Fix \"function not implemented\" error on some devices + Unspecified + No seccomp + Seccomp Running code is not supported in this location. Consider moving your file to the terminal\'s home directory to run it. Running code is not recommended in this location. Consider moving your file to the terminal\'s home directory. Rename file @@ -277,7 +285,7 @@ Support Support development Translate - Terminal and runners + Terminal, runners and language servers Folder name Invalid characters Properties @@ -623,6 +631,8 @@ Local Store Search extensions + Search themes + Search icon packs Date added Verified Filter options @@ -652,4 +662,34 @@ Automatically insert matching closing bracket Confirm exit dialog Show a confirmation dialog before closing the app + Sort lines ascending + Sort lines descending + Choose runner + Extension cleanup failed + Uninstall extension? + Are you sure you want to uninstall the extension \'%s\'? + Invalid format for terminal extra keys. Default keys will be loaded. + Manage formatters + Prioritize and configure formatters + Formatters are applied by priority. The first formatter that supports the current language will be used. You can long-press and drag to change their order. + Highlight occurrences + Highlight occurrences of the symbol at the cursor + Use current language server + Browse extensions + Clipboard keybindings + Enable copy and paste keybindings for hardware keyboards + Terminal health + Run diagnostics to identify issues with the terminal environment + No logs available + Pending + Check PRoot + Check system shell + Check storage and permissions + Check Ubuntu + Check network access + Check abnormalities + Your Samsung device is affected by a known issue that causes the PRoot-based terminal installation to fail. + The terminal is currently running in a degraded mode due to a known compatibility issue caused by a malfunction in the PRoot environment on certain devices. Some features may be limited or behave differently. + Disabled (Crashed) + Download Count diff --git a/core/termux-shared/build.gradle b/core/termux-shared/build.gradle index dc975129a..66e18f814 100644 --- a/core/termux-shared/build.gradle +++ b/core/termux-shared/build.gradle @@ -3,7 +3,7 @@ apply plugin: 'maven-publish' android { compileSdkVersion 36 - ndkVersion = "28.0.13004108" + ndkVersion = "29.0.13846066" namespace = "com.termux.shared" dependencies { diff --git a/gradle.properties b/gradle.properties index 004b8639f..02ab7a24e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,5 +9,6 @@ org.gradle.reproducibleArchives=true # Android android.useAndroidX=true android.nonTransitiveRClass=true - -android.builder.sdkDownload=true \ No newline at end of file +android.builder.sdkDownload=true +android.defaults.buildfeatures.resvalues=true +android.disallowKotlinSourceSets=false \ No newline at end of file diff --git a/gradle/gradle-daemon-jvm.properties b/gradle/gradle-daemon-jvm.properties new file mode 100644 index 000000000..baa28d154 --- /dev/null +++ b/gradle/gradle-daemon-jvm.properties @@ -0,0 +1,13 @@ +#This file is generated by updateDaemonJvm +toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect +toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect +toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect +toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect +toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/7083b89563e7ce20943037b8cd2b8cc2/redirect +toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/060bbb778a1f55ea705fdebd2ccfeab9/redirect +toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect +toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect +toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/d09679dc60fe5aa05ef7d03efdefac20/redirect +toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/ed4e3bf2f5e7c5d9aabc4cbd8acd555e/redirect +toolchainVendor=JETBRAINS +toolchainVersion=21 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 30be2c742..6975f67c7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,11 +1,10 @@ [versions] -# kotlin, agp and r8 must be compatible with each other aftr update -agp = "8.13.1" -kotlin = "2.3.0" -ksp = "2.3.0" -kotlinReflect = "2.3.0" -r8 = "8.13.19" +# kotlin and agp must be compatible with each other after update +agp = "9.2.1" +kotlin = "2.3.20" +ksp = "2.3.2" +kotlinReflect = "2.3.20" annotation = "1.9.1" anrwatchdog = "1.4.0" @@ -25,7 +24,6 @@ coreKtx = "1.17.0" activity = "1.12.0" okhttp = "5.3.2" photoview = "2.3.0" -quickjsAndroid = "0.9.2" composeRuntime = "1.10.0" junitVersion = "1.3.0" espressoCore = "3.7.0" @@ -38,8 +36,8 @@ jgit = "6.2.0.202206071550-r" tsBinding = "4.3.2" lsp4j = "1.0.0" uiautomator = "2.3.0" -benchmark = "1.4.1" -baselineprofile = "1.4.1" +benchmark = "1.5.0-alpha06" +baselineprofile = "1.5.0-alpha06" profileinstaller = "1.4.1" runner = "1.7.0" composeDnd = "0.4.0" @@ -66,6 +64,7 @@ monarchJson = "1.0.2" regexOnig = "1.0.3" regexRe2j = "1.0.2" gson = "2.13.2" +semver = "3.1.0" leakcanary = "2.14" motionCompose = "2.0.1" @@ -77,7 +76,6 @@ android-baselineprofile = { id = "androidx.baselineprofile", version.ref = "base android-benchmark = { id = "androidx.benchmark", version.ref = "benchmark" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } -kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } @@ -146,7 +144,6 @@ androidx-test-junit = { module = "androidx.test.ext:junit", version.ref = "junit androidx-test-espresso = { module = "androidx.test.espresso:espresso-core", version.ref = "espressoCore" } androidx-runner = { module = "androidx.test:runner", version.ref = "runner" } androidx-uiautomator = { module = "androidx.test.uiautomator:uiautomator", version.ref = "uiautomator" } -r8 = { module = "com.android.tools:r8", version.ref = "r8" } tests-robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" } tests-google-truth = { module = "com.google.truth:truth", version.ref = "truth" } leakcanary = { module = "com.squareup.leakcanary:leakcanary-android", version.ref = "leakcanary" } @@ -159,7 +156,6 @@ androidx-profileinstaller = { module = "androidx.profileinstaller:profileinstall # Misc material = { module = "com.google.android.material:material", version.ref = "material" } photoview = { module = "com.github.chrisbanes:PhotoView", version.ref = "photoview" } -quickjs-android = { module = "app.cash.quickjs:quickjs-android", version.ref = "quickjsAndroid" } jgit = { module = "org.eclipse.jgit:org.eclipse.jgit", version.ref = "jgit" } lsp4j = { module = "org.eclipse.lsp4j:org.eclipse.lsp4j", version.ref = "lsp4j" } jcodings = { module = "org.jruby.jcodings:jcodings", version.ref = "jcodings" } @@ -173,6 +169,7 @@ anrwatchdog = { module = "com.github.anrwatchdog:anrwatchdog", version.ref = "an pine-core = { module = "top.canyie.pine:core", version.ref = "coreVersion" } utilcode = { module = "com.blankj:utilcodex", version.ref = "utilcode" } gson = { module = "com.google.code.gson:gson", version.ref = "gson" } +semver = { module = "io.github.z4kn4fein:semver", version.ref = "semver" } # Tree-sitter / editor tree-sitter = { module = "com.itsaky.androidide.treesitter:android-tree-sitter", version.ref = "tsBinding" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 8e61ef125..4bdb2a9eb 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=2ab2958f2a1e51120c326cad6f385153bb11ee93b3c216c5fccebfdfbb7ec6cb -distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip +distributionSha256Sum=bafc141b619ad6350fd975fc903156dd5c151998cc8b058e8c1044ab5f7b031f +distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/plugin-sdk/build.gradle.kts b/plugin-sdk/build.gradle.kts index afc09691f..9136bd78e 100644 --- a/plugin-sdk/build.gradle.kts +++ b/plugin-sdk/build.gradle.kts @@ -14,6 +14,7 @@ tasks.named("shadowJar") { destinationDirectory.set(file("./output")) } + repositories { mavenCentral() google() diff --git a/plugin-sdk/gradle/libs.versions.toml b/plugin-sdk/gradle/libs.versions.toml index e7d4b5634..f17c96db0 100644 --- a/plugin-sdk/gradle/libs.versions.toml +++ b/plugin-sdk/gradle/libs.versions.toml @@ -1,5 +1,11 @@ [versions] -agp = "8.13.1" + +# kotlin and agp must be compatible with each other after update +agp = "9.2.1" +kotlin = "2.3.20" +ksp = "2.3.2" +kotlinReflect = "2.3.20" + annotation = "1.9.1" anrwatchdog = "1.4.0" appcompat = "1.7.1" @@ -9,13 +15,11 @@ coreVersion = "0.3.0" desugar_jdk_libs = "2.1.5" documentfile = "1.1.0" glide = "5.0.5" -kotlinReflect = "2.3.20" material = "1.13.0" constraintlayout = "2.2.1" materialIconsCore = "1.7.8" nanohttpd = "2.3.1" navigation = "2.9.6" -kotlin = "2.3.20" coreKtx = "1.17.0" activity = "1.12.0" okhttp = "5.3.2" @@ -29,6 +33,7 @@ composeBom = "2025.11.01" coil = "2.7.0" ktfmt = "0.25.0" jgit = "6.2.0.202206071550-r" + tsBinding = "4.3.2" lsp4j = "1.0.0" uiautomator = "2.3.0" @@ -41,7 +46,7 @@ androidsvgAar = "1.4" ec4jCore = "1.2.0" kotlinxSerializationJson = "1.10.0" room = "2.8.4" -ksp = "2.3.6" + publish = "0.36.0" collection = "1.5.0" jcodings = "1.0.64" @@ -60,6 +65,7 @@ monarchJson = "1.0.2" regexOnig = "1.0.3" regexRe2j = "1.0.2" gson = "2.13.2" +semver = "3.1.0" leakcanary = "2.14" motionCompose = "2.0.1" @@ -152,7 +158,6 @@ androidx-profileinstaller = { module = "androidx.profileinstaller:profileinstall # Misc material = { module = "com.google.android.material:material", version.ref = "material" } photoview = { module = "com.github.chrisbanes:PhotoView", version.ref = "photoview" } -quickjs-android = { module = "app.cash.quickjs:quickjs-android", version.ref = "quickjsAndroid" } jgit = { module = "org.eclipse.jgit:org.eclipse.jgit", version.ref = "jgit" } lsp4j = { module = "org.eclipse.lsp4j:org.eclipse.lsp4j", version.ref = "lsp4j" } jcodings = { module = "org.jruby.jcodings:jcodings", version.ref = "jcodings" } @@ -166,6 +171,7 @@ anrwatchdog = { module = "com.github.anrwatchdog:anrwatchdog", version.ref = "an pine-core = { module = "top.canyie.pine:core", version.ref = "coreVersion" } utilcode = { module = "com.blankj:utilcodex", version.ref = "utilcode" } gson = { module = "com.google.code.gson:gson", version.ref = "gson" } +semver = { module = "io.github.z4kn4fein:semver", version.ref = "semver" } # Tree-sitter / editor tree-sitter = { module = "com.itsaky.androidide.treesitter:android-tree-sitter", version.ref = "tsBinding" } diff --git a/plugin-sdk/gradle/wrapper/gradle-wrapper.jar b/plugin-sdk/gradle/wrapper/gradle-wrapper.jar index d997cfc60..b1b8ef56b 100644 Binary files a/plugin-sdk/gradle/wrapper/gradle-wrapper.jar and b/plugin-sdk/gradle/wrapper/gradle-wrapper.jar differ diff --git a/plugin-sdk/gradle/wrapper/gradle-wrapper.properties b/plugin-sdk/gradle/wrapper/gradle-wrapper.properties index 8e61ef125..bd82f36ba 100644 --- a/plugin-sdk/gradle/wrapper/gradle-wrapper.properties +++ b/plugin-sdk/gradle/wrapper/gradle-wrapper.properties @@ -1,8 +1,10 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=2ab2958f2a1e51120c326cad6f385153bb11ee93b3c216c5fccebfdfbb7ec6cb -distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip +distributionSha256Sum=bafc141b619ad6350fd975fc903156dd5c151998cc8b058e8c1044ab5f7b031f +distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip networkTimeout=10000 +retries=0 +retryBackOffMs=500 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/plugin-sdk/gradlew b/plugin-sdk/gradlew index 739907dfd..b9bb139f7 100755 --- a/plugin-sdk/gradlew +++ b/plugin-sdk/gradlew @@ -57,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. diff --git a/plugin-sdk/gradlew.bat b/plugin-sdk/gradlew.bat index e509b2dd8..aa5f10b06 100644 --- a/plugin-sdk/gradlew.bat +++ b/plugin-sdk/gradlew.bat @@ -23,8 +23,8 @@ @rem @rem ########################################################################## -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @@ -51,7 +51,7 @@ echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 -goto fail +"%COMSPEC%" /c exit 1 :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% @@ -65,7 +65,7 @@ echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 -goto fail +"%COMSPEC%" /c exit 1 :execute @rem Setup the command line @@ -73,21 +73,10 @@ goto fail @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/plugin-sdk/settings.gradle.kts b/plugin-sdk/settings.gradle.kts index 6bff96ae3..53aaf1666 100644 --- a/plugin-sdk/settings.gradle.kts +++ b/plugin-sdk/settings.gradle.kts @@ -1,6 +1,3 @@ -plugins { - id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" -} - +plugins { id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" } rootProject.name = "Xed-Editor-Sdk" diff --git a/settings.gradle.kts b/settings.gradle.kts index 1029e19a2..f1f5518ad 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -26,15 +26,7 @@ dependencyResolutionManagement { } } -include( - ":app", - ":core:main", - ":core:components", - ":core:resources", - ":core:terminal-view", - ":core:terminal-emulator", - ":core:extension", -) +include(":app", ":core:main", ":core:components", ":core:resources", ":terminal-view", ":terminal-emulator") val soraX = file("soraX") @@ -61,3 +53,5 @@ project(":editor-lsp").projectDir = file("soraX/editor-lsp") project(":language-textmate").projectDir = file("soraX/language-textmate") include(":baselineprofile", ":benchmark", ":benchmark2") +include(":core:proot") +include(":core:link2symlink") diff --git a/soraX b/soraX index 4b761b316..2b05794ac 160000 --- a/soraX +++ b/soraX @@ -1 +1 @@ -Subproject commit 4b761b316326ff412b83ebaf7afe623da27d33d2 +Subproject commit 2b05794ac9db15ef3ddf15ecc0f29c1ff17afe9d diff --git a/core/terminal-emulator/build.gradle b/terminal-emulator/build.gradle similarity index 94% rename from core/terminal-emulator/build.gradle rename to terminal-emulator/build.gradle index 567d56c98..aeb51826f 100644 --- a/core/terminal-emulator/build.gradle +++ b/terminal-emulator/build.gradle @@ -3,7 +3,7 @@ apply plugin: 'maven-publish' android { compileSdkVersion 36 - ndkVersion = "28.0.13004108" + ndkVersion = "29.0.13846066" namespace = "com.termux.terminal" defaultConfig { @@ -22,7 +22,7 @@ android { buildTypes { release { minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } diff --git a/core/terminal-emulator/proguard-rules.pro b/terminal-emulator/proguard-rules.pro similarity index 100% rename from core/terminal-emulator/proguard-rules.pro rename to terminal-emulator/proguard-rules.pro diff --git a/core/terminal-emulator/src/main/AndroidManifest.xml b/terminal-emulator/src/main/AndroidManifest.xml similarity index 100% rename from core/terminal-emulator/src/main/AndroidManifest.xml rename to terminal-emulator/src/main/AndroidManifest.xml diff --git a/core/terminal-emulator/src/main/java/com/termux/terminal/ByteQueue.java b/terminal-emulator/src/main/java/com/termux/terminal/ByteQueue.java similarity index 100% rename from core/terminal-emulator/src/main/java/com/termux/terminal/ByteQueue.java rename to terminal-emulator/src/main/java/com/termux/terminal/ByteQueue.java diff --git a/core/terminal-emulator/src/main/java/com/termux/terminal/JNI.java b/terminal-emulator/src/main/java/com/termux/terminal/JNI.java similarity index 100% rename from core/terminal-emulator/src/main/java/com/termux/terminal/JNI.java rename to terminal-emulator/src/main/java/com/termux/terminal/JNI.java diff --git a/core/terminal-emulator/src/main/java/com/termux/terminal/KeyHandler.java b/terminal-emulator/src/main/java/com/termux/terminal/KeyHandler.java similarity index 100% rename from core/terminal-emulator/src/main/java/com/termux/terminal/KeyHandler.java rename to terminal-emulator/src/main/java/com/termux/terminal/KeyHandler.java diff --git a/core/terminal-emulator/src/main/java/com/termux/terminal/Logger.java b/terminal-emulator/src/main/java/com/termux/terminal/Logger.java similarity index 100% rename from core/terminal-emulator/src/main/java/com/termux/terminal/Logger.java rename to terminal-emulator/src/main/java/com/termux/terminal/Logger.java diff --git a/core/terminal-emulator/src/main/java/com/termux/terminal/TerminalBuffer.java b/terminal-emulator/src/main/java/com/termux/terminal/TerminalBuffer.java similarity index 100% rename from core/terminal-emulator/src/main/java/com/termux/terminal/TerminalBuffer.java rename to terminal-emulator/src/main/java/com/termux/terminal/TerminalBuffer.java diff --git a/core/terminal-emulator/src/main/java/com/termux/terminal/TerminalColorScheme.java b/terminal-emulator/src/main/java/com/termux/terminal/TerminalColorScheme.java similarity index 100% rename from core/terminal-emulator/src/main/java/com/termux/terminal/TerminalColorScheme.java rename to terminal-emulator/src/main/java/com/termux/terminal/TerminalColorScheme.java diff --git a/core/terminal-emulator/src/main/java/com/termux/terminal/TerminalColors.java b/terminal-emulator/src/main/java/com/termux/terminal/TerminalColors.java similarity index 100% rename from core/terminal-emulator/src/main/java/com/termux/terminal/TerminalColors.java rename to terminal-emulator/src/main/java/com/termux/terminal/TerminalColors.java diff --git a/core/terminal-emulator/src/main/java/com/termux/terminal/TerminalEmulator.java b/terminal-emulator/src/main/java/com/termux/terminal/TerminalEmulator.java similarity index 100% rename from core/terminal-emulator/src/main/java/com/termux/terminal/TerminalEmulator.java rename to terminal-emulator/src/main/java/com/termux/terminal/TerminalEmulator.java diff --git a/core/terminal-emulator/src/main/java/com/termux/terminal/TerminalOutput.java b/terminal-emulator/src/main/java/com/termux/terminal/TerminalOutput.java similarity index 100% rename from core/terminal-emulator/src/main/java/com/termux/terminal/TerminalOutput.java rename to terminal-emulator/src/main/java/com/termux/terminal/TerminalOutput.java diff --git a/core/terminal-emulator/src/main/java/com/termux/terminal/TerminalRow.java b/terminal-emulator/src/main/java/com/termux/terminal/TerminalRow.java similarity index 100% rename from core/terminal-emulator/src/main/java/com/termux/terminal/TerminalRow.java rename to terminal-emulator/src/main/java/com/termux/terminal/TerminalRow.java diff --git a/core/terminal-emulator/src/main/java/com/termux/terminal/TerminalSession.java b/terminal-emulator/src/main/java/com/termux/terminal/TerminalSession.java similarity index 100% rename from core/terminal-emulator/src/main/java/com/termux/terminal/TerminalSession.java rename to terminal-emulator/src/main/java/com/termux/terminal/TerminalSession.java diff --git a/core/terminal-emulator/src/main/java/com/termux/terminal/TerminalSessionClient.java b/terminal-emulator/src/main/java/com/termux/terminal/TerminalSessionClient.java similarity index 100% rename from core/terminal-emulator/src/main/java/com/termux/terminal/TerminalSessionClient.java rename to terminal-emulator/src/main/java/com/termux/terminal/TerminalSessionClient.java diff --git a/core/terminal-emulator/src/main/java/com/termux/terminal/TextStyle.java b/terminal-emulator/src/main/java/com/termux/terminal/TextStyle.java similarity index 100% rename from core/terminal-emulator/src/main/java/com/termux/terminal/TextStyle.java rename to terminal-emulator/src/main/java/com/termux/terminal/TextStyle.java diff --git a/core/terminal-emulator/src/main/java/com/termux/terminal/WcWidth.java b/terminal-emulator/src/main/java/com/termux/terminal/WcWidth.java similarity index 100% rename from core/terminal-emulator/src/main/java/com/termux/terminal/WcWidth.java rename to terminal-emulator/src/main/java/com/termux/terminal/WcWidth.java diff --git a/core/terminal-emulator/src/main/jni/Android.mk b/terminal-emulator/src/main/jni/Android.mk similarity index 100% rename from core/terminal-emulator/src/main/jni/Android.mk rename to terminal-emulator/src/main/jni/Android.mk diff --git a/core/terminal-emulator/src/main/jni/termux.c b/terminal-emulator/src/main/jni/termux.c similarity index 100% rename from core/terminal-emulator/src/main/jni/termux.c rename to terminal-emulator/src/main/jni/termux.c diff --git a/core/terminal-emulator/src/test/java/com/termux/terminal/ApcTest.java b/terminal-emulator/src/test/java/com/termux/terminal/ApcTest.java similarity index 100% rename from core/terminal-emulator/src/test/java/com/termux/terminal/ApcTest.java rename to terminal-emulator/src/test/java/com/termux/terminal/ApcTest.java diff --git a/core/terminal-emulator/src/test/java/com/termux/terminal/ByteQueueTest.java b/terminal-emulator/src/test/java/com/termux/terminal/ByteQueueTest.java similarity index 100% rename from core/terminal-emulator/src/test/java/com/termux/terminal/ByteQueueTest.java rename to terminal-emulator/src/test/java/com/termux/terminal/ByteQueueTest.java diff --git a/core/terminal-emulator/src/test/java/com/termux/terminal/ControlSequenceIntroducerTest.java b/terminal-emulator/src/test/java/com/termux/terminal/ControlSequenceIntroducerTest.java similarity index 100% rename from core/terminal-emulator/src/test/java/com/termux/terminal/ControlSequenceIntroducerTest.java rename to terminal-emulator/src/test/java/com/termux/terminal/ControlSequenceIntroducerTest.java diff --git a/core/terminal-emulator/src/test/java/com/termux/terminal/CursorAndScreenTest.java b/terminal-emulator/src/test/java/com/termux/terminal/CursorAndScreenTest.java similarity index 100% rename from core/terminal-emulator/src/test/java/com/termux/terminal/CursorAndScreenTest.java rename to terminal-emulator/src/test/java/com/termux/terminal/CursorAndScreenTest.java diff --git a/core/terminal-emulator/src/test/java/com/termux/terminal/DecSetTest.java b/terminal-emulator/src/test/java/com/termux/terminal/DecSetTest.java similarity index 100% rename from core/terminal-emulator/src/test/java/com/termux/terminal/DecSetTest.java rename to terminal-emulator/src/test/java/com/termux/terminal/DecSetTest.java diff --git a/core/terminal-emulator/src/test/java/com/termux/terminal/DeviceControlStringTest.java b/terminal-emulator/src/test/java/com/termux/terminal/DeviceControlStringTest.java similarity index 100% rename from core/terminal-emulator/src/test/java/com/termux/terminal/DeviceControlStringTest.java rename to terminal-emulator/src/test/java/com/termux/terminal/DeviceControlStringTest.java diff --git a/core/terminal-emulator/src/test/java/com/termux/terminal/HistoryTest.java b/terminal-emulator/src/test/java/com/termux/terminal/HistoryTest.java similarity index 100% rename from core/terminal-emulator/src/test/java/com/termux/terminal/HistoryTest.java rename to terminal-emulator/src/test/java/com/termux/terminal/HistoryTest.java diff --git a/core/terminal-emulator/src/test/java/com/termux/terminal/KeyHandlerTest.java b/terminal-emulator/src/test/java/com/termux/terminal/KeyHandlerTest.java similarity index 100% rename from core/terminal-emulator/src/test/java/com/termux/terminal/KeyHandlerTest.java rename to terminal-emulator/src/test/java/com/termux/terminal/KeyHandlerTest.java diff --git a/core/terminal-emulator/src/test/java/com/termux/terminal/OperatingSystemControlTest.java b/terminal-emulator/src/test/java/com/termux/terminal/OperatingSystemControlTest.java similarity index 100% rename from core/terminal-emulator/src/test/java/com/termux/terminal/OperatingSystemControlTest.java rename to terminal-emulator/src/test/java/com/termux/terminal/OperatingSystemControlTest.java diff --git a/core/terminal-emulator/src/test/java/com/termux/terminal/RectangularAreasTest.java b/terminal-emulator/src/test/java/com/termux/terminal/RectangularAreasTest.java similarity index 100% rename from core/terminal-emulator/src/test/java/com/termux/terminal/RectangularAreasTest.java rename to terminal-emulator/src/test/java/com/termux/terminal/RectangularAreasTest.java diff --git a/core/terminal-emulator/src/test/java/com/termux/terminal/ResizeTest.java b/terminal-emulator/src/test/java/com/termux/terminal/ResizeTest.java similarity index 100% rename from core/terminal-emulator/src/test/java/com/termux/terminal/ResizeTest.java rename to terminal-emulator/src/test/java/com/termux/terminal/ResizeTest.java diff --git a/core/terminal-emulator/src/test/java/com/termux/terminal/ScreenBufferTest.java b/terminal-emulator/src/test/java/com/termux/terminal/ScreenBufferTest.java similarity index 100% rename from core/terminal-emulator/src/test/java/com/termux/terminal/ScreenBufferTest.java rename to terminal-emulator/src/test/java/com/termux/terminal/ScreenBufferTest.java diff --git a/core/terminal-emulator/src/test/java/com/termux/terminal/ScrollRegionTest.java b/terminal-emulator/src/test/java/com/termux/terminal/ScrollRegionTest.java similarity index 100% rename from core/terminal-emulator/src/test/java/com/termux/terminal/ScrollRegionTest.java rename to terminal-emulator/src/test/java/com/termux/terminal/ScrollRegionTest.java diff --git a/core/terminal-emulator/src/test/java/com/termux/terminal/TerminalRowTest.java b/terminal-emulator/src/test/java/com/termux/terminal/TerminalRowTest.java similarity index 100% rename from core/terminal-emulator/src/test/java/com/termux/terminal/TerminalRowTest.java rename to terminal-emulator/src/test/java/com/termux/terminal/TerminalRowTest.java diff --git a/core/terminal-emulator/src/test/java/com/termux/terminal/TerminalTest.java b/terminal-emulator/src/test/java/com/termux/terminal/TerminalTest.java similarity index 100% rename from core/terminal-emulator/src/test/java/com/termux/terminal/TerminalTest.java rename to terminal-emulator/src/test/java/com/termux/terminal/TerminalTest.java diff --git a/core/terminal-emulator/src/test/java/com/termux/terminal/TerminalTestCase.java b/terminal-emulator/src/test/java/com/termux/terminal/TerminalTestCase.java similarity index 100% rename from core/terminal-emulator/src/test/java/com/termux/terminal/TerminalTestCase.java rename to terminal-emulator/src/test/java/com/termux/terminal/TerminalTestCase.java diff --git a/core/terminal-emulator/src/test/java/com/termux/terminal/TextStyleTest.java b/terminal-emulator/src/test/java/com/termux/terminal/TextStyleTest.java similarity index 100% rename from core/terminal-emulator/src/test/java/com/termux/terminal/TextStyleTest.java rename to terminal-emulator/src/test/java/com/termux/terminal/TextStyleTest.java diff --git a/core/terminal-emulator/src/test/java/com/termux/terminal/UnicodeInputTest.java b/terminal-emulator/src/test/java/com/termux/terminal/UnicodeInputTest.java similarity index 100% rename from core/terminal-emulator/src/test/java/com/termux/terminal/UnicodeInputTest.java rename to terminal-emulator/src/test/java/com/termux/terminal/UnicodeInputTest.java diff --git a/core/terminal-emulator/src/test/java/com/termux/terminal/WcWidthTest.java b/terminal-emulator/src/test/java/com/termux/terminal/WcWidthTest.java similarity index 100% rename from core/terminal-emulator/src/test/java/com/termux/terminal/WcWidthTest.java rename to terminal-emulator/src/test/java/com/termux/terminal/WcWidthTest.java diff --git a/core/terminal-view/build.gradle b/terminal-view/build.gradle similarity index 90% rename from core/terminal-view/build.gradle rename to terminal-view/build.gradle index 2ef19d6d5..56d3aa9d3 100644 --- a/core/terminal-view/build.gradle +++ b/terminal-view/build.gradle @@ -7,7 +7,7 @@ android { dependencies { implementation libs.androidx.annotation - api project(":core:terminal-emulator") + api project(":terminal-emulator") } defaultConfig { @@ -18,7 +18,7 @@ android { buildTypes { release { minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } diff --git a/core/terminal-view/proguard-rules.pro b/terminal-view/proguard-rules.pro similarity index 100% rename from core/terminal-view/proguard-rules.pro rename to terminal-view/proguard-rules.pro diff --git a/core/terminal-view/src/main/AndroidManifest.xml b/terminal-view/src/main/AndroidManifest.xml similarity index 100% rename from core/terminal-view/src/main/AndroidManifest.xml rename to terminal-view/src/main/AndroidManifest.xml diff --git a/core/terminal-view/src/main/java/com/termux/view/GestureAndScaleRecognizer.java b/terminal-view/src/main/java/com/termux/view/GestureAndScaleRecognizer.java similarity index 100% rename from core/terminal-view/src/main/java/com/termux/view/GestureAndScaleRecognizer.java rename to terminal-view/src/main/java/com/termux/view/GestureAndScaleRecognizer.java diff --git a/core/terminal-view/src/main/java/com/termux/view/TerminalRenderer.java b/terminal-view/src/main/java/com/termux/view/TerminalRenderer.java similarity index 100% rename from core/terminal-view/src/main/java/com/termux/view/TerminalRenderer.java rename to terminal-view/src/main/java/com/termux/view/TerminalRenderer.java diff --git a/core/terminal-view/src/main/java/com/termux/view/TerminalView.java b/terminal-view/src/main/java/com/termux/view/TerminalView.java similarity index 98% rename from core/terminal-view/src/main/java/com/termux/view/TerminalView.java rename to terminal-view/src/main/java/com/termux/view/TerminalView.java index 435c10251..bef3b0691 100644 --- a/core/terminal-view/src/main/java/com/termux/view/TerminalView.java +++ b/terminal-view/src/main/java/com/termux/view/TerminalView.java @@ -770,7 +770,29 @@ public boolean onKeyDown(int keyCode, KeyEvent event) { if (TERMINAL_VIEW_KEY_LOGGING_ENABLED) mClient.logInfo(LOG_TAG, "onKeyDown(keyCode=" + keyCode + ", isSystem()=" + event.isSystem() + ", event=" + event + ")"); if (mEmulator == null) return true; - if (isSelectingText()) { + + // Xed-Editor addition + if (mClient.shouldSupportClipboardKeybindings()) { + final boolean controlDown = event.isCtrlPressed(); + + // Copy + if (controlDown && keyCode == KeyEvent.KEYCODE_C && isSelectingText()) { + String selectedText = getSelectedText(); + if (selectedText != null) { + mTermSession.onCopyTextToClipboard(selectedText); + stopTextSelectionMode(); + } + return true; + } + + // Paste + if (controlDown && keyCode == KeyEvent.KEYCODE_V) { + mTermSession.onPasteTextFromClipboard(); + return true; + } + } + + if (isSelectingText() && !mClient.shouldSupportClipboardKeybindings()) { stopTextSelectionMode(); } diff --git a/core/terminal-view/src/main/java/com/termux/view/TerminalViewClient.java b/terminal-view/src/main/java/com/termux/view/TerminalViewClient.java similarity index 97% rename from core/terminal-view/src/main/java/com/termux/view/TerminalViewClient.java rename to terminal-view/src/main/java/com/termux/view/TerminalViewClient.java index d6b49b892..3e61d3744 100644 --- a/core/terminal-view/src/main/java/com/termux/view/TerminalViewClient.java +++ b/terminal-view/src/main/java/com/termux/view/TerminalViewClient.java @@ -34,6 +34,8 @@ public interface TerminalViewClient { boolean shouldUseCtrlSpaceWorkaround(); + boolean shouldSupportClipboardKeybindings(); + boolean isTerminalViewSelected(); diff --git a/core/terminal-view/src/main/java/com/termux/view/support/PopupWindowCompatGingerbread.java b/terminal-view/src/main/java/com/termux/view/support/PopupWindowCompatGingerbread.java similarity index 100% rename from core/terminal-view/src/main/java/com/termux/view/support/PopupWindowCompatGingerbread.java rename to terminal-view/src/main/java/com/termux/view/support/PopupWindowCompatGingerbread.java diff --git a/core/terminal-view/src/main/java/com/termux/view/textselection/CursorController.java b/terminal-view/src/main/java/com/termux/view/textselection/CursorController.java similarity index 100% rename from core/terminal-view/src/main/java/com/termux/view/textselection/CursorController.java rename to terminal-view/src/main/java/com/termux/view/textselection/CursorController.java diff --git a/core/terminal-view/src/main/java/com/termux/view/textselection/TextSelectionCursorController.java b/terminal-view/src/main/java/com/termux/view/textselection/TextSelectionCursorController.java similarity index 100% rename from core/terminal-view/src/main/java/com/termux/view/textselection/TextSelectionCursorController.java rename to terminal-view/src/main/java/com/termux/view/textselection/TextSelectionCursorController.java diff --git a/core/terminal-view/src/main/java/com/termux/view/textselection/TextSelectionHandleView.java b/terminal-view/src/main/java/com/termux/view/textselection/TextSelectionHandleView.java similarity index 100% rename from core/terminal-view/src/main/java/com/termux/view/textselection/TextSelectionHandleView.java rename to terminal-view/src/main/java/com/termux/view/textselection/TextSelectionHandleView.java diff --git a/core/terminal-view/src/main/res/drawable/text_select_handle_left_material.xml b/terminal-view/src/main/res/drawable/text_select_handle_left_material.xml similarity index 100% rename from core/terminal-view/src/main/res/drawable/text_select_handle_left_material.xml rename to terminal-view/src/main/res/drawable/text_select_handle_left_material.xml diff --git a/core/terminal-view/src/main/res/drawable/text_select_handle_right_material.xml b/terminal-view/src/main/res/drawable/text_select_handle_right_material.xml similarity index 100% rename from core/terminal-view/src/main/res/drawable/text_select_handle_right_material.xml rename to terminal-view/src/main/res/drawable/text_select_handle_right_material.xml diff --git a/core/terminal-view/src/main/res/values/strings.xml b/terminal-view/src/main/res/values/strings.xml similarity index 100% rename from core/terminal-view/src/main/res/values/strings.xml rename to terminal-view/src/main/res/values/strings.xml