├── .github ├── dependabot.yaml └── workflows │ ├── audit.yaml │ ├── build.yaml │ ├── clippy.yaml │ ├── docs.yaml │ ├── release.yml │ └── tests.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── boxxy.yaml ├── data ├── hardcoded-applications.json └── partial-support-applications.json ├── fixtures └── helloworld-appimage-x86_64.AppImage ├── fork-test.sh ├── install.sh ├── peckish.yaml ├── release.sh └── src ├── config └── mod.rs ├── enclosure ├── fs.rs ├── linux.rs ├── mod.rs ├── register.rs ├── rule.rs ├── syscall │ ├── mod.rs │ ├── riscv64.rs │ └── x86_64.rs └── tracer.rs ├── main.rs └── scanner └── mod.rs /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "12:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.github/workflows/audit.yaml: -------------------------------------------------------------------------------- 1 | name: "Run cargo audit" 2 | on: 3 | push: 4 | branches: 5 | - "mistress" 6 | paths: 7 | - "**.rs" 8 | - "**.toml" 9 | pull_request: 10 | branches: 11 | - "mistress" 12 | paths: 13 | - "**.rs" 14 | - "**.toml" 15 | 16 | jobs: 17 | run-cargo-audit: 18 | strategy: 19 | matrix: 20 | version: ["stable", "1.67"] 21 | runs-on: "ubuntu-latest" 22 | steps: 23 | - uses: "actions/checkout@v2" 24 | - name: "Install latest stable Rust" 25 | uses: "actions-rs/toolchain@v1" 26 | with: 27 | toolchain: "${{ matrix.version }}" 28 | override: true 29 | - name: "Install cargo-audit" 30 | run: "cargo install cargo-audit" 31 | - uses: "Swatinem/rust-cache@v1" 32 | with: 33 | key: "cargo-audit" 34 | - name: "Run cargo-audit" 35 | run: "cargo audit -q" 36 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: "Ensure packages build" 2 | on: 3 | push: 4 | branches: 5 | - "mistress" 6 | pull_request: 7 | branches: 8 | - "mistress" 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: "actions/checkout@v2" 15 | - name: "Install latest stable Rust" 16 | uses: "actions-rs/toolchain@v1" 17 | with: 18 | toolchain: "stable" 19 | override: true 20 | - uses: "Swatinem/rust-cache@v1" 21 | with: 22 | key: "build-pkg" 23 | - uses: "queer/actions/peckish_install@mistress" 24 | with: 25 | token: "${{ secrets.GITHUB_TOKEN }}" 26 | - name: "Build debug binary" 27 | run: "cargo build" 28 | - uses: "queer/actions/peckish_run@mistress" 29 | -------------------------------------------------------------------------------- /.github/workflows/clippy.yaml: -------------------------------------------------------------------------------- 1 | name: "Run clippy lints" 2 | on: 3 | push: 4 | branches: 5 | - "mistress" 6 | paths: 7 | - "**.rs" 8 | pull_request: 9 | branches: 10 | - "mistress" 11 | paths: 12 | - "**.rs" 13 | 14 | jobs: 15 | run-clippy: 16 | strategy: 17 | matrix: 18 | version: ["stable", "1.67"] 19 | runs-on: "ubuntu-latest" 20 | steps: 21 | - uses: "actions/checkout@v2" 22 | - name: "Install latest stable Rust" 23 | uses: "actions-rs/toolchain@v1" 24 | with: 25 | toolchain: "${{ matrix.version }}" 26 | override: true 27 | components: "clippy" 28 | - uses: "Swatinem/rust-cache@v1" 29 | with: 30 | key: "clippy" 31 | - name: "Run clippy" 32 | run: "cargo clippy --all-targets --all-features -- -D warnings" 33 | -------------------------------------------------------------------------------- /.github/workflows/docs.yaml: -------------------------------------------------------------------------------- 1 | name: "Build docs" 2 | on: 3 | push: 4 | branches: 5 | - "mistress" 6 | paths: 7 | - "**.rs" 8 | pull_request: 9 | branches: 10 | - "mistress" 11 | paths: 12 | - "**.rs" 13 | 14 | jobs: 15 | run-clippy: 16 | strategy: 17 | matrix: 18 | version: ["stable", "1.67", "nightly"] 19 | if: "github.actor != 'dependabot'" 20 | runs-on: "ubuntu-latest" 21 | steps: 22 | - uses: "actions/checkout@v2" 23 | - name: "Install latest stable Rust" 24 | uses: "actions-rs/toolchain@v1" 25 | with: 26 | toolchain: "${{ matrix.version }}" 27 | override: true 28 | - uses: "Swatinem/rust-cache@v1" 29 | with: 30 | key: "doc" 31 | - name: "Run cargo doc" 32 | run: "cargo doc --workspace --all-features --examples --no-deps --locked" 33 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # CI that: 2 | # 3 | # * checks for a Git Tag that looks like a release ("v1.2.0") 4 | # * creates a Github Release™️ 5 | # * builds binaries/packages with cargo-dist 6 | # * uploads those packages to the Github Release™️ 7 | # 8 | # Note that the Github Release™️ will be created before the packages, 9 | # so there will be a few minutes where the release has no packages 10 | # and then they will slowly trickle in, possibly failing. To make 11 | # this more pleasant we mark the release as a "draft" until all 12 | # artifacts have been successfully uploaded. This allows you to 13 | # choose what to do with partial successes and avoids spamming 14 | # anyone with notifications before the release is actually ready. 15 | name: Release 16 | 17 | permissions: 18 | contents: write 19 | 20 | # This task will run whenever you push a git tag that looks like 21 | # a version number. We just look for `v` followed by at least one number 22 | # and then whatever. so `v1`, `v1.0.0`, and `v1.0.0-prerelease` all work. 23 | # 24 | # If there's a prerelease-style suffix to the version then the Github Release™️ 25 | # will be marked as a prerelease (handled by taiki-e/create-gh-release-action). 26 | # 27 | # Note that when generating links to uploaded artifacts, cargo-dist will currently 28 | # assume that your git tag is always v{VERSION} where VERSION is the version in 29 | # the published package's Cargo.toml (this is the default behaviour of cargo-release). 30 | # In the future this may be made more robust/configurable. 31 | on: 32 | push: 33 | tags: 34 | - v[0-9]+.* 35 | 36 | env: 37 | ALL_CARGO_DIST_TARGET_ARGS: --target=x86_64-unknown-linux-gnu --target=x86_64-unknown-linux-musl 38 | ALL_CARGO_DIST_INSTALLER_ARGS: 39 | 40 | jobs: 41 | # Create the Github Release™️ so the packages have something to be uploaded to 42 | create-release: 43 | runs-on: ubuntu-latest 44 | outputs: 45 | tag: ${{ steps.create-gh-release.outputs.computed-prefix }}${{ steps.create-gh-release.outputs.version }} 46 | steps: 47 | - uses: actions/checkout@v3 48 | - id: create-gh-release 49 | uses: taiki-e/create-gh-release-action@v1 50 | with: 51 | draft: true 52 | # (required) GitHub token for creating GitHub Releases. 53 | token: ${{ secrets.GITHUB_TOKEN }} 54 | 55 | # Build and packages all the things 56 | upload-artifacts: 57 | needs: create-release 58 | strategy: 59 | matrix: 60 | # For these target platforms 61 | include: 62 | - target: x86_64-unknown-linux-gnu 63 | os: ubuntu-20.04 64 | install-dist: curl --proto '=https' --tlsv1.2 -L -sSf https://github.com/axodotdev/cargo-dist/releases/download/v0.0.2/installer.sh | sh 65 | runs-on: ${{ matrix.os }} 66 | env: 67 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 68 | steps: 69 | - uses: actions/checkout@v3 70 | - name: Install Rust 71 | run: rustup update stable && rustup default stable 72 | - name: Install cargo-dist 73 | run: ${{ matrix.install-dist }} 74 | - name: Run cargo-dist 75 | # This logic is a bit janky because it's trying to be a polyglot between 76 | # powershell and bash since this will run on windows, macos, and linux! 77 | # The two platforms don't agree on how to talk about env vars but they 78 | # do agree on 'cat' and '$()' so we use that to marshal values between commands. 79 | run: | 80 | # Actually do builds and make zips and whatnot 81 | cargo dist --target=${{ matrix.target }} --output-format=json > dist-manifest.json 82 | echo "dist ran successfully" 83 | cat dist-manifest.json 84 | # Parse out what we just built and upload it to the Github Release™️ 85 | cat dist-manifest.json | jq --raw-output ".releases[].artifacts[].path" > uploads.txt 86 | echo "uploading..." 87 | cat uploads.txt 88 | gh release upload ${{ needs.create-release.outputs.tag }} $(cat uploads.txt) 89 | echo "uploaded!" 90 | 91 | # Compute and upload the manifest for everything 92 | upload-manifest: 93 | needs: create-release 94 | runs-on: ubuntu-latest 95 | env: 96 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 97 | steps: 98 | - uses: actions/checkout@v3 99 | - name: Install Rust 100 | run: rustup update stable && rustup default stable 101 | - name: Install cargo-dist 102 | run: curl --proto '=https' --tlsv1.2 -L -sSf https://github.com/axodotdev/cargo-dist/releases/download/v0.0.2/installer.sh | sh 103 | - name: Run cargo-dist manifest 104 | run: | 105 | # Generate a manifest describing everything 106 | cargo dist manifest --no-local-paths --output-format=json $ALL_CARGO_DIST_TARGET_ARGS $ALL_CARGO_DIST_INSTALLER_ARGS > dist-manifest.json 107 | echo "dist manifest ran successfully" 108 | cat dist-manifest.json 109 | # Upload the manifest to the Github Release™️ 110 | gh release upload ${{ needs.create-release.outputs.tag }} dist-manifest.json 111 | echo "uploaded manifest!" 112 | # Edit the Github Release™️ title/body to match what cargo-dist thinks it should be 113 | CHANGELOG_TITLE=$(cat dist-manifest.json | jq --raw-output ".releases[].changelog_title") 114 | cat dist-manifest.json | jq --raw-output ".releases[].changelog_body" > new_dist_changelog.md 115 | gh release edit ${{ needs.create-release.outputs.tag }} --title="$CHANGELOG_TITLE" --notes-file=new_dist_changelog.md 116 | echo "updated release notes!" 117 | 118 | # Mark the Github Release™️ as a non-draft now that everything has succeeded! 119 | publish-release: 120 | needs: [create-release, upload-artifacts, upload-manifest] 121 | runs-on: ubuntu-latest 122 | env: 123 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 124 | steps: 125 | - uses: actions/checkout@v3 126 | - name: mark release as non-draft 127 | run: | 128 | gh release edit ${{ needs.create-release.outputs.tag }} --draft=false 129 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: "Run all tests" 2 | on: 3 | push: 4 | branches: 5 | - "mistress" 6 | paths: 7 | - "**.rs" 8 | pull_request: 9 | branches: 10 | - "mistress" 11 | paths: 12 | - "**.rs" 13 | 14 | jobs: 15 | run-tests: 16 | strategy: 17 | matrix: 18 | version: ["stable", "nightly", "1.67"] 19 | runs-on: "ubuntu-latest" 20 | steps: 21 | - uses: "actions/checkout@v2" 22 | - name: "Install latest stable Rust" 23 | uses: "actions-rs/toolchain@v1" 24 | with: 25 | toolchain: "${{ matrix.version }}" 26 | override: true 27 | - uses: "Swatinem/rust-cache@v1" 28 | with: 29 | key: "tests" 30 | - name: "Run tests" 31 | run: "cargo test" 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /boxxy-report.txt 3 | /boxxy-dev.yaml 4 | /.env 5 | /release 6 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | repos: 3 | - repo: "https://github.com/pre-commit/pre-commit-hooks" 4 | rev: "v4.3.0" 5 | hooks: 6 | - id: "check-merge-conflict" 7 | - id: "check-toml" 8 | - id: "check-yaml" 9 | - id: "end-of-file-fixer" 10 | - id: "mixed-line-ending" 11 | - id: "trailing-whitespace" 12 | - repo: "local" 13 | hooks: 14 | - id: "format" 15 | name: "rust: cargo fmt" 16 | entry: "cargo fmt --all --check" 17 | language: "system" 18 | pass_filenames: false 19 | files: ".rs*$" 20 | - id: "clippy" 21 | name: "rust: cargo clippy" 22 | entry: "cargo clippy --all-targets --all-features -- -D warnings" 23 | language: "system" 24 | pass_filenames: false 25 | files: ".rs*$" 26 | - id: "test" 27 | name: "rust: cargo test" 28 | entry: "cargo test" 29 | language: "system" 30 | pass_filenames: false 31 | files: ".rs*$" 32 | - id: "doc" 33 | name: "rust: cargo doc" 34 | entry: "cargo doc --workspace --all-features --examples --no-deps --locked --frozen" 35 | language: "system" 36 | pass_filenames: false 37 | files: ".rs*$" 38 | - id: "audit" 39 | name: "rust: cargo audit" 40 | entry: "cargo audit -q" 41 | language: "system" 42 | pass_filenames: false 43 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "boxxy" 3 | version = "0.8.5" 4 | edition = "2021" 5 | repository = "https://github.com/queer/boxxy" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | bat = { version = "0.23.0", default-features = false, features = [ 11 | "atty", 12 | "regex-onig", 13 | ] } 14 | byteorder = "1.5.0" 15 | cfg-if = "1.0.0" 16 | clap = { version = "4.5.11", features = ["derive"] } 17 | color-eyre = { version = "0.6.3", features = ["issue-url"] } 18 | config = "0.14.0" 19 | ctrlc = "3.4.4" 20 | daemonize = "0.5.0" 21 | dirs = "5.0.1" 22 | dotenv = "0.15.0" 23 | dotenv-parser = "0.1.3" 24 | eyre = "0.6.12" 25 | grep = "0.3.1" 26 | haikunator = "0.1.2" 27 | lazy_static = "1.5.0" 28 | libc = "0.2.155" 29 | log = "0.4.22" 30 | nix = { version = "0.29.0", features = [ 31 | "process", 32 | "user", 33 | "mount", 34 | "sched", 35 | "ptrace", 36 | "signal", 37 | "fs", 38 | ] } 39 | owo-colors = { version = "4.0.0", features = [ 40 | "supports-color", 41 | "supports-colors", 42 | ] } 43 | pretty_env_logger = "0.5.0" 44 | regex = "1.10.5" 45 | rlimit = "0.10.1" 46 | serde = { version = "1.0.203", features = ["derive"] } 47 | serde_json = "1.0.120" 48 | serde_yaml = "0.9.34" 49 | shellexpand = "3.1.0" 50 | strum = { version = "0.26.3", features = ["derive"] } 51 | syscall-numbers = "3.1.1" 52 | which = "6.0.1" 53 | 54 | # generated by 'cargo dist init' 55 | [profile.dist] 56 | inherits = "release" 57 | debug = true 58 | split-debuginfo = "packed" 59 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023-present amy null 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # boxxy 2 | 3 | boxxy (case-sensitive) is a tool for boxing up misbehaving Linux applications 4 | and forcing them to put their files and directories in the right place, 5 | **without symlinks!** 6 | 7 | boxxy is a part of the [amyware discord server](https://discord.gg/7WgSTwh). 8 | 9 | If you like what I make, consider supporting me on Patreon: 10 | 11 | [](https://patreon.com/amyware) 12 | 13 | Linux-only! boxxy uses Linux namespaces for its functionality. 14 | 15 | For example, consider tmux. It wants to put its config in `~/.tmux.conf`. With 16 | boxxy, you can put its config in `~/.config/tmux/tmux.conf` instead: 17 | 18 | ```yaml 19 | # ~/.config/boxxy/boxxy.yaml 20 | rules: 21 | - name: "redirect tmux config from ~/.tmux.conf to ~/.config/tmux/tmux.conf" 22 | target: "~/.tmux.conf" 23 | rewrite: "~/.config/tmux/tmux.conf" 24 | mode: "file" 25 | ``` 26 | 27 | [![asciicast](https://asciinema.org/a/558679.svg)](https://asciinema.org/a/558679) 28 | 29 | ## maintenance status 30 | 31 | I am on a break from maintaining open-source projects due to health reasons. 32 | PRs will still be accepted and issues will still be looked at, but there are no 33 | promises about when this will happen. 34 | 35 | ## motivation 36 | 37 | I recently had to use the AWS CLI. It wants to save data in `~/.aws`, but I 38 | don't want it to just clutter up my `$HOME` however it wants. boxxy lets me 39 | force it to puts its data somewhere nice and proper. 40 | 41 | ## features 42 | 43 | - box any program and force it to put its files/directories where you want it to 44 | - context-dependent boxing, ie different rules apply in different directories 45 | depending on your configuration 46 | - minimal overhead 47 | - opt-in immutable fs outside of rule rewrites, ie only the files/directories 48 | you specify in rules are writable 49 | - `0.5.0`: boxxy can scan your homedir to automatically suggest rules for 50 | you! ![image of boxxy scan](https://amyware.nyc3.digitaloceanspaces.com/2023/03/25/G6hrd3iQjEy65.png) 51 | - `0.6.0`: boxxy can use project-local `boxxy.yaml` files, and can load 52 | `.env` files for you! ![image of 0.6.0 features](https://amyware.nyc3.digitaloceanspaces.com/2023/03/28/Jawp5It1xrnWN.png) 53 | - `0.6.1`: boxxy rules can inject env vars: ![image of 0.6.1 features](https://amyware.nyc3.digitaloceanspaces.com/2023/03/29/ukcWuiYdtI8yq.png) 54 | - `0.7.2`: boxxy can fork the boxxed process into the background with the 55 | `--daemon` flag. 56 | - `0.8.0`: boxxy can pass rules at the command line with `--rule`, and disable 57 | loading config files with `--no-config`. 58 | - `0.8.2`: Explain how to run AppImages properly: ![image of 0.8.2 features](https://amyware.nyc3.digitaloceanspaces.com/2023/10/31/yMiHJaURUud6E.png) 59 | 60 | ### potential drawbacks 61 | 62 | - new project, 0.x.y, comes with all those warnings 63 | - **cannot** use sudo inside the container (see [#6](https://github.com/queer/boxxy/issues/6)) 64 | - primarily tested for my use-cases 65 | 66 | ## example usage 67 | 68 | ```sh 69 | git:(mistress) | ▶ cat ~/.config/boxxy/boxxy.yaml 70 | rules: 71 | - name: "Store AWS CLI config in ~/.config/aws" 72 | target: "~/.aws" 73 | rewrite: "~/.config/aws" 74 | 75 | git:(mistress) | ▶ boxxy aws configure 76 | INFO boxxy > loaded 1 rules 77 | INFO boxxy::enclosure > applying rule 'Store AWS CLI config in ~/.config/aws' 78 | INFO boxxy::enclosure > redirect: ~/.aws -> ~/.config/aws 79 | INFO boxxy::enclosure > boxed "aws" ♥ 80 | AWS Access Key ID [****************d]: a 81 | AWS Secret Access Key [****************c]: b 82 | Default region name [b]: c 83 | Default output format [a]: d 84 | git:(mistress) | ▶ ls ~/.aws 85 | git:(mistress) | ▶ ls ~/.config/aws 86 | config credentials 87 | git:(mistress) | ▶ cat ~/.config/aws/config 88 | [default] 89 | region = c 90 | output = d 91 | git:(mistress) | ▶ 92 | ``` 93 | 94 | ### suggested usage 95 | 96 | - `alias aws="boxxy aws"` (repeat for other tools) 97 | - use contexts to keep project configs separate on disk 98 | - dotfiles! 99 | - stop using symlinks!!! 100 | - no more dev config files when writing code 101 | 102 | ## requirements 103 | 104 | boxxy requires `newuidmap` to function, which is not included by default in all 105 | distributions. To install: 106 | 107 | Alpine: 108 | ```sh 109 | $ apk add shadow-uidmap 110 | ``` 111 | 112 | Debian / Ubuntu: 113 | ```sh 114 | $ apt install uidmap 115 | ``` 116 | 117 | RHEL / Fedora: 118 | ```sh 119 | $ yum install shadow-utils 120 | ``` 121 | 122 | ## configuration 123 | 124 | The boxxy configuration file lives in `~/.config/boxxy/boxxy.yaml`. If none 125 | exists, an empty one will be created for you. 126 | 127 | ```yaml 128 | rules: 129 | # The name of the rule. User-friendly name for your reference 130 | - name: "redirect aws-cli from ~/.aws to ~/.config/aws" 131 | # The target of the rule, ie the file/directory that will be shadowed by the 132 | # rewrite. 133 | target: "~/.aws" 134 | # The rewrite of the rule, ie the file/directory that will be used instead of 135 | # the target. 136 | rewrite: "~/.config/aws" 137 | - name: "use different k8s configs when in ~/Projects/my-cool-startup" 138 | target: "~/.kube/config" 139 | rewrite: "~/Projects/my-cool-startup/.kube/config" 140 | # The context for the rule. Any paths listed in the context are paths where 141 | # this rule will apply. If no context is specified, the rule applies 142 | # globally. 143 | context: 144 | - "~/Projects/my-cool-startup" 145 | # The mode of this rule, either `directory` or `file`. `directory` is the 146 | # default. Must be specified for the correct behaviour when the target is a 147 | # file. Required because the target file/directory may not exist yet. 148 | mode: "file" 149 | # The list of commands that this rule applies to. If no commands are 150 | # specified, the rule applies to all programs run with boxxy. 151 | only: 152 | - "kubectl" 153 | ``` 154 | 155 | ### syntax 156 | 157 | ```yaml 158 | rules: 159 | - name: "any valid string" # required 160 | target: "path" # required 161 | rewrite: "path" # required 162 | context: # optional 163 | - "path" 164 | - "path" 165 | mode: "directory | file" # optional 166 | only: # optional 167 | - "binary name" 168 | - "binary name" 169 | env: # optional 170 | KEY: "value" 171 | ``` 172 | 173 | ## developing 174 | 175 | 1. set up pre-commit: `pre-commit install` 176 | 2. make sure it builds: `cargo build` 177 | 3. do the thing! 178 | 4. test with the command of your choice, ex. `cargo run -- ls -lah ~/.config` 179 | 180 | ### how does it work? 181 | 182 | - create temporary directory in /tmp 183 | - set up new user/mount namespace 184 | - bind-mount `/` to tmp directory 185 | - bind-mount rule mounts rw so that target programs can use them 186 | - remount `/` ro 187 | - run! 188 | 189 | ## credits 190 | 191 | - `fixtures/helloworld-appimage-x86_84.AppImage`: https://github.com/ClonedRepos/hello-world-appimage 192 | -------------------------------------------------------------------------------- /boxxy.yaml: -------------------------------------------------------------------------------- 1 | rules: 2 | - name: "Inject env var" 3 | target: "/tmp" 4 | rewrite: "/tmp" 5 | env: 6 | KEY: "TEST VALUE" 7 | - name: "forking binaries need to be wrapped properly" 8 | target: "./forks" 9 | rewrite: "/tmp/forks" 10 | mode: "directory" 11 | -------------------------------------------------------------------------------- /data/hardcoded-applications.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "adb & Android Studio", 4 | "paths": [ 5 | "~/.android/" 6 | ], 7 | "fixes": [ 8 | "~/.android:~/.local/share/android" 9 | ] 10 | }, 11 | { 12 | "name": "aegisub", 13 | "paths": [ 14 | "~/.aegisub/" 15 | ], 16 | "fixes": [ 17 | "~/.aegisub:~/.local/share/aegisub" 18 | ] 19 | }, 20 | { 21 | "name": "alpine", 22 | "paths": [ 23 | "~/.pinerc", 24 | "~/.addressbook", 25 | "~/.pine-debug[1-4]", 26 | "~/.newsrc", 27 | "~/.mailcap", 28 | "~/.mime.types", 29 | "~/.pine-interrupted-mail" 30 | ], 31 | "fixes": [ 32 | "~/.pinerc:~/.config/alpine/pinerc", 33 | "~/.addressbook:~/.config/alpine/addressbook", 34 | "~/.newsrc:~/.config/alpine/newsrc", 35 | "~/.mailcap:~/.config/alpine/mailcap", 36 | "~/.mime.types:~/.config/alpine/mime.types", 37 | "~/.pine-interrupted-mail:~/.config/alpine/pine-interrupted-mail" 38 | ] 39 | }, 40 | { 41 | "name": "aMule", 42 | "paths": [ 43 | "~/.aMule" 44 | ], 45 | "fixes": [ 46 | "~/.aMule:~/.local/share/aMule" 47 | ] 48 | }, 49 | { 50 | "name": "anthy", 51 | "paths": [ 52 | "~/.anthy" 53 | ], 54 | "fixes": [ 55 | "~/.anthy:~/.local/share/anthy" 56 | ] 57 | }, 58 | { 59 | "name": "Apache Directory Studio", 60 | "paths": [ 61 | "~/.ApacheDirectoryStudio" 62 | ], 63 | "fixes": [ 64 | "~/.ApacheDirectoryStudio:~/.local/share/ApacheDirectoryStudio" 65 | ] 66 | }, 67 | { 68 | "name": "ARandR", 69 | "paths": [ 70 | "~/.screenlayout" 71 | ], 72 | "fixes": [ 73 | "~/.screenlayout:~/.config/ARandR" 74 | ] 75 | }, 76 | { 77 | "name": "Arduino", 78 | "paths": [ 79 | "~/.arduino15", 80 | "~/.jssc" 81 | ], 82 | "fixes": [ 83 | "~/.arduino15:~/.local/share/arduino15", 84 | "~/.jssc:~/.local/share/jssc" 85 | ] 86 | }, 87 | { 88 | "name": "arduino-cli", 89 | "paths": [ 90 | "~/.arduino15/" 91 | ], 92 | "fixes": [ 93 | "~/.arduino15:~/.local/share/arduino15" 94 | ] 95 | }, 96 | { 97 | "name": "Avidemux", 98 | "paths": [ 99 | "~/.avidemux6" 100 | ], 101 | "fixes": [ 102 | "~/.avidemux6:~/.local/share/avidemux6" 103 | ] 104 | }, 105 | { 106 | "name": "Berkshelf", 107 | "paths": [ 108 | "~/.berkshelf/" 109 | ], 110 | "fixes": [ 111 | "~/.berkshelf:~/.local/share/berkshelf" 112 | ] 113 | }, 114 | { 115 | "name": "chatty", 116 | "paths": [ 117 | "~/.chatty/" 118 | ], 119 | "fixes": [ 120 | "~/.chatty:~/.local/share/chatty" 121 | ] 122 | }, 123 | { 124 | "name": "cmake", 125 | "paths": [ 126 | "~/.cmake/" 127 | ], 128 | "fixes": [ 129 | "~/.cmake:~/.local/share/cmake" 130 | ] 131 | }, 132 | { 133 | "name": "Cinnamon", 134 | "paths": [ 135 | "~/.cinnamon/" 136 | ], 137 | "fixes": [ 138 | "~/.cinnamon:~/.local/share/cinnamon" 139 | ] 140 | }, 141 | { 142 | "name": "conan", 143 | "paths": [ 144 | "~/.conan/" 145 | ], 146 | "fixes": [ 147 | "~/.conan:~/.local/share/conan" 148 | ] 149 | }, 150 | { 151 | "name": "cryptomator", 152 | "paths": [ 153 | "~/.Cryptomator" 154 | ], 155 | "fixes": [ 156 | "~/.Cryptomator:~/.local/share/Cryptomator" 157 | ] 158 | }, 159 | { 160 | "name": "ctags (universial-ctags)", 161 | "paths": [ 162 | "~/.ctagsrc", 163 | "~/.ctags.d" 164 | ], 165 | "fixes": [ 166 | "~/.ctagsrc:~/.config/ctags/ctagsrc", 167 | "~/.ctags.d:~/.config/ctags" 168 | ] 169 | }, 170 | { 171 | "name": "CUPS", 172 | "paths": [ 173 | "~/.cups/" 174 | ], 175 | "fixes": [ 176 | "~/.cups:~/.local/share/cups" 177 | ] 178 | }, 179 | { 180 | "name": "cVim", 181 | "paths": [ 182 | "~/.cvimrc" 183 | ], 184 | "fixes": [ 185 | "~/.cvimrc:~/.config/cVim/cvimrc" 186 | ] 187 | }, 188 | { 189 | "name": "darcs", 190 | "paths": [ 191 | "~/.darcs/" 192 | ], 193 | "fixes": [ 194 | "~/.darcs:~/.local/share/darcs" 195 | ] 196 | }, 197 | { 198 | "name": "dart", 199 | "paths": [ 200 | "~/.dart", 201 | "~/.dartServer" 202 | ], 203 | "fixes": [ 204 | "~/.dart:~/.local/share/dart", 205 | "~/.dartServer:~/.local/share/dartServer" 206 | ] 207 | }, 208 | { 209 | "name": "devede", 210 | "paths": [ 211 | "~/.devedeng" 212 | ], 213 | "fixes": [ 214 | "~/.devedeng:~/.local/share/devedeng" 215 | ] 216 | }, 217 | { 218 | "name": "Dia", 219 | "paths": [ 220 | "~/.dia/" 221 | ], 222 | "fixes": [ 223 | "~/.dia:~/.local/share/dia" 224 | ] 225 | }, 226 | { 227 | "name": "dotnet-sdk", 228 | "paths": [ 229 | "~/.dotnet/" 230 | ], 231 | "fixes": [ 232 | "~/.dotnet:~/.local/share/dotnet" 233 | ] 234 | }, 235 | { 236 | "name": "dropbox", 237 | "paths": [ 238 | "~/.dropbox/" 239 | ], 240 | "fixes": [ 241 | "~/.dropbox:~/.local/share/dropbox" 242 | ] 243 | }, 244 | { 245 | "name": "Eclipse", 246 | "paths": [ 247 | "~/.eclipse/" 248 | ], 249 | "fixes": [ 250 | "~/.eclipse:~/.local/share/eclipse" 251 | ] 252 | }, 253 | { 254 | "name": "Fetchmail", 255 | "paths": [ 256 | "~/.fetchmailrc" 257 | ], 258 | "fixes": [ 259 | "~/.fetchmailrc:~/.config/fetchmailrc" 260 | ] 261 | }, 262 | { 263 | "name": "Flatpak", 264 | "paths": [ 265 | "~/.var/" 266 | ], 267 | "fixes": [ 268 | "~/.var:~/.local/share/var" 269 | ] 270 | }, 271 | { 272 | "name": "freesweep", 273 | "paths": [ 274 | "~/.sweeprc" 275 | ], 276 | "fixes": [ 277 | "~/.sweeprc:~/.config/freesweep/sweeprc" 278 | ] 279 | }, 280 | { 281 | "name": "gftp", 282 | "paths": [ 283 | "~/.gftp/" 284 | ], 285 | "fixes": [ 286 | "~/.gftp:~/.local/share/gftp" 287 | ] 288 | }, 289 | { 290 | "name": "gitkraken", 291 | "paths": [ 292 | "~/.gitkraken/" 293 | ], 294 | "fixes": [ 295 | "~/.gitkraken:~/.local/share/gitkraken" 296 | ] 297 | }, 298 | { 299 | "name": "GoldenDict", 300 | "paths": [ 301 | "~/.goldendict/" 302 | ], 303 | "fixes": [ 304 | "~/.goldendict:~/.local/share/goldendict" 305 | ] 306 | }, 307 | { 308 | "name": "gphoto2", 309 | "paths": [ 310 | "~/.gphoto" 311 | ], 312 | "fixes": [ 313 | "~/.gphoto:~/.local/share/gphoto" 314 | ] 315 | }, 316 | { 317 | "name": "gramps", 318 | "paths": [ 319 | "~/.gramps/" 320 | ], 321 | "fixes": [ 322 | "~/.gramps:~/.local/share/gramps" 323 | ] 324 | }, 325 | { 326 | "name": "groovy", 327 | "paths": [ 328 | "~/.groovy/" 329 | ], 330 | "fixes": [ 331 | "~/.groovy:~/.local/share/groovy" 332 | ] 333 | }, 334 | { 335 | "name": "grsync", 336 | "paths": [ 337 | "~/.grsync/" 338 | ], 339 | "fixes": [ 340 | "~/.grsync:~/.local/share/grsync" 341 | ] 342 | }, 343 | { 344 | "name": "google-cloud-cli", 345 | "paths": [ 346 | "~/.gsutil/" 347 | ], 348 | "fixes": [ 349 | "~/.gsutil:~/.local/share/gsutil" 350 | ] 351 | }, 352 | { 353 | "name": "gtk-recordMyDesktop", 354 | "paths": [ 355 | "~/.gtk-recordmydesktop" 356 | ], 357 | "fixes": [ 358 | "~/.gtk-recordmydesktop:~/.config/gtk-recordMyDesktop/gtk-recordmydesktop" 359 | ] 360 | }, 361 | { 362 | "name": "hplip", 363 | "paths": [ 364 | "~/.hplip/" 365 | ], 366 | "fixes": [ 367 | "~/.hplip:~/.local/share/hplip" 368 | ] 369 | }, 370 | { 371 | "name": "hydrogen", 372 | "paths": [ 373 | "~/.hydrogen/" 374 | ], 375 | "fixes": [ 376 | "~/.hydrogen:~/.local/share/hydrogen" 377 | ] 378 | }, 379 | { 380 | "name": "idris", 381 | "paths": [ 382 | "~/.idris" 383 | ], 384 | "fixes": [ 385 | "~/.idris:~/.local/share/idris" 386 | ] 387 | }, 388 | { 389 | "name": "itch-setup-bin", 390 | "paths": [ 391 | "~/.itch" 392 | ], 393 | "fixes": [ 394 | "~/.itch:~/.local/share/itch" 395 | ] 396 | }, 397 | { 398 | "name": "Jmol", 399 | "paths": [ 400 | "~/.jmol/" 401 | ], 402 | "fixes": [ 403 | "~/.jmol:~/.local/share/jmol" 404 | ] 405 | }, 406 | { 407 | "name": "lbdb", 408 | "paths": [ 409 | "~/.lbdbrc", 410 | "~/.lbdb/" 411 | ], 412 | "fixes": [ 413 | "~/.lbdbrc:~/.config/lbdb/lbdbrc", 414 | "~/.lbdb:~/.local/share/lbdb" 415 | ] 416 | }, 417 | { 418 | "name": "Java OpenJDK", 419 | "paths": [ 420 | "~/.java/fonts" 421 | ], 422 | "fixes": [ 423 | "~/.java/fonts:~/.local/share/java/fonts" 424 | ] 425 | }, 426 | { 427 | "name": "Java OpenJFX", 428 | "paths": [ 429 | "~/.java/webview" 430 | ], 431 | "fixes": [ 432 | "~/.java/webview:~/.local/share/java/webview" 433 | ] 434 | }, 435 | { 436 | "name": "jgmenu", 437 | "paths": [ 438 | "~/.jgmenu-lockfile" 439 | ], 440 | "fixes": [ 441 | "~/.jgmenu-lockfile:~/.config/jgmenu/jgmenu-lockfile" 442 | ] 443 | }, 444 | { 445 | "name": "julia", 446 | "paths": [ 447 | "~/.juliarc.jl", 448 | "~/.julia_history", 449 | "~/.julia" 450 | ], 451 | "fixes": [ 452 | "~/.juliarc.jl:~/.config/julia/juliarc.jl", 453 | "~/.julia_history:~/.local/share/julia/julia_history", 454 | "~/.julia:~/.local/share/julia" 455 | ] 456 | }, 457 | { 458 | "name": "kite", 459 | "paths": [ 460 | "~/.kite/" 461 | ], 462 | "fixes": [ 463 | "~/.kite:~/.local/share/kite" 464 | ] 465 | }, 466 | { 467 | "name": "kotlin", 468 | "paths": [ 469 | "~/.kotlinc_history" 470 | ], 471 | "fixes": [ 472 | "~/.kotlinc_history:~/.local/share/kotlinc/kotlinc_history" 473 | ] 474 | }, 475 | { 476 | "name": "Kubernetes", 477 | "paths": [ 478 | "~/.kube/" 479 | ], 480 | "fixes": [ 481 | "~/.kube:~/.local/share/kube" 482 | ] 483 | }, 484 | { 485 | "name": "lldb", 486 | "paths": [ 487 | "~/.lldb", 488 | "~/.lldbinit" 489 | ], 490 | "fixes": [ 491 | "~/.lldb:~/.local/share/lldb", 492 | "~/.lldbinit:~/.config/lldb/lldbinit" 493 | ] 494 | }, 495 | { 496 | "name": "LMMS", 497 | "paths": [ 498 | "~/.lmmsrc.xml" 499 | ], 500 | "fixes": [ 501 | "~/.lmmsrc.xml:~/.config/lmms/lmmsrc.xml" 502 | ] 503 | }, 504 | { 505 | "name": "mathomatic", 506 | "paths": [ 507 | "~/.mathomaticrc", 508 | "~/.matho_history" 509 | ], 510 | "fixes": [ 511 | "~/.mathomaticrc:~/.config/mathomatic/mathomaticrc", 512 | "~/.matho_history:~/.local/share/mathomatic/matho_history" 513 | ] 514 | }, 515 | { 516 | "name": "Minecraft", 517 | "paths": [ 518 | "~/.minecraft/" 519 | ], 520 | "fixes": [ 521 | "~/.minecraft:~/.local/share/minecraft" 522 | ] 523 | }, 524 | { 525 | "name": "Minetest", 526 | "paths": [ 527 | "~/.minetest/" 528 | ], 529 | "fixes": [ 530 | "~/.minetest:~/.local/share/minetest" 531 | ] 532 | }, 533 | { 534 | "name": "minicom", 535 | "paths": [ 536 | "~/.minirc.dfl" 537 | ], 538 | "fixes": [ 539 | "~/.minirc.dfl:~/.config/minicom/minirc.dfl" 540 | ] 541 | }, 542 | { 543 | "name": "Mono", 544 | "paths": [ 545 | "~/.mono/" 546 | ], 547 | "fixes": [ 548 | "~/.mono:~/.local/share/mono" 549 | ] 550 | }, 551 | { 552 | "name": "mongodb", 553 | "paths": [ 554 | "~/.mongorc.js", 555 | "~/.dbshell" 556 | ], 557 | "fixes": [ 558 | "~/.mongorc.js:~/.config/mongodb/mongorc.js", 559 | "~/.dbshell:~/.config/mongodb/dbshell" 560 | ] 561 | }, 562 | { 563 | "name": "", 564 | "paths": [ 565 | "~/.netrc" 566 | ], 567 | "fixes": [ 568 | "~/.netrc:~/.config/netrc" 569 | ] 570 | }, 571 | { 572 | "name": "nmcli", 573 | "paths": [ 574 | "~/.nmcli-history" 575 | ], 576 | "fixes": [ 577 | "~/.nmcli-history:~/.local/share/nmcli/nmcli-history" 578 | ] 579 | }, 580 | { 581 | "name": "Networkmanager-openvpn", 582 | "paths": [ 583 | "~/.cert/nm-openvpn" 584 | ], 585 | "fixes": [ 586 | "~/.cert/nm-openvpn:~/.local/share/cert/nm-openvpn" 587 | ] 588 | }, 589 | { 590 | "name": "ocaml-utop", 591 | "paths": [ 592 | "~/.utop-history" 593 | ], 594 | "fixes": [ 595 | "~/.utop-history:~/.local/share/utop/utop-history" 596 | ] 597 | }, 598 | { 599 | "name": "parsec-bin", 600 | "paths": [ 601 | "~/.parsec" 602 | ], 603 | "fixes": [ 604 | "~/.parsec:~/.local/share/parsec" 605 | ] 606 | }, 607 | { 608 | "name": "pcsxr", 609 | "paths": [ 610 | "~/.pcsxr" 611 | ], 612 | "fixes": [ 613 | "~/.pcsxr:~/.local/share/pcsxr" 614 | ] 615 | }, 616 | { 617 | "name": "perf", 618 | "paths": [ 619 | "~/.debug" 620 | ], 621 | "fixes": [ 622 | "~/.debug:~/.local/share/debug" 623 | ] 624 | }, 625 | { 626 | "name": "perl", 627 | "paths": [ 628 | "~/.cpan", 629 | "~/perl5" 630 | ], 631 | "fixes": [ 632 | "~/.cpan:~/.local/share/cpan", 633 | "~/perl5:~/.local/share/perl5" 634 | ] 635 | }, 636 | { 637 | "name": "phoronix-test-suite", 638 | "paths": [ 639 | "~/.phoronix-test-suite" 640 | ], 641 | "fixes": [ 642 | "~/.phoronix-test-suite:~/.local/share/phoronix-test-suite" 643 | ] 644 | }, 645 | { 646 | "name": "portfolio-performance-bin", 647 | "paths": [ 648 | "~/.PortfolioPerformance/" 649 | ], 650 | "fixes": [ 651 | "~/.PortfolioPerformance:~/.local/share/PortfolioPerformance" 652 | ] 653 | }, 654 | { 655 | "name": "psensor", 656 | "paths": [ 657 | "~/.psensor" 658 | ], 659 | "fixes": [ 660 | "~/.psensor:~/.local/share/psensor" 661 | ] 662 | }, 663 | { 664 | "name": "python", 665 | "paths": [ 666 | "~/.python_history" 667 | ], 668 | "fixes": [ 669 | "~/.python_history:~/.local/share/python/python_history" 670 | ] 671 | }, 672 | { 673 | "name": "python-tensorflow", 674 | "paths": [ 675 | "~/.keras" 676 | ], 677 | "fixes": [ 678 | "~/.keras:~/.local/share/keras" 679 | ] 680 | }, 681 | { 682 | "name": "qmmp", 683 | "paths": [ 684 | "~/.qmmp" 685 | ], 686 | "fixes": [ 687 | "~/.qmmp:~/.local/share/qmmp" 688 | ] 689 | }, 690 | { 691 | "name": "Qt Designer", 692 | "paths": [ 693 | "~/.designer" 694 | ], 695 | "fixes": [ 696 | "~/.designer:~/.local/share/designer" 697 | ] 698 | }, 699 | { 700 | "name": "RedNotebook", 701 | "paths": [ 702 | "~/.rednotebook" 703 | ], 704 | "fixes": [ 705 | "~/.rednotebook:~/.local/share/rednotebook" 706 | ] 707 | }, 708 | { 709 | "name": "Remarkable", 710 | "paths": [ 711 | "~/.remarkable" 712 | ], 713 | "fixes": [ 714 | "~/.remarkable:~/.local/share/remarkable" 715 | ] 716 | }, 717 | { 718 | "name": "renderdoc", 719 | "paths": [ 720 | "~/.renderdoc" 721 | ], 722 | "fixes": [ 723 | "~/.renderdoc:~/.local/share/renderdoc" 724 | ] 725 | }, 726 | { 727 | "name": "Ren'Py", 728 | "paths": [ 729 | "~/.renpy" 730 | ], 731 | "fixes": [ 732 | "~/.renpy:~/.local/share/renpy" 733 | ] 734 | }, 735 | { 736 | "name": "repo", 737 | "paths": [ 738 | "~/.repoconfig" 739 | ], 740 | "fixes": [ 741 | "~/.repoconfig:~/.config/repo/repoconfig" 742 | ] 743 | }, 744 | { 745 | "name": "rpm", 746 | "paths": [ 747 | "~/.rpmrc", 748 | "~/.rpmmacros" 749 | ], 750 | "fixes": [ 751 | "~/.rpmrc:~/.config/rpm/rpmrc", 752 | "~/.rpmmacros:~/.config/rpm/rpmmacros" 753 | ] 754 | }, 755 | { 756 | "name": "SANE", 757 | "paths": [ 758 | "~/.sane/" 759 | ], 760 | "fixes": [ 761 | "~/.sane:~/.local/share/sane" 762 | ] 763 | }, 764 | { 765 | "name": "sbcl", 766 | "paths": [ 767 | "~/.sbclrc" 768 | ], 769 | "fixes": [ 770 | "~/.sbclrc:~/.config/sbcl/sbclrc" 771 | ] 772 | }, 773 | { 774 | "name": "Solfege", 775 | "paths": [ 776 | "~/.solfege", 777 | "~/.solfegerc", 778 | "~/lessonfiles" 779 | ], 780 | "fixes": [ 781 | "~/.solfege:~/.local/share/solfege", 782 | "~/.solfegerc:~/.config/solfege/solfegerc", 783 | "~/lessonfiles:~/.local/share/solfege/lessonfiles" 784 | ] 785 | }, 786 | { 787 | "name": "SpamAssassin", 788 | "paths": [ 789 | "~/.spamassassin" 790 | ], 791 | "fixes": [ 792 | "~/.spamassassin:~/.local/share/spamassassin" 793 | ] 794 | }, 795 | { 796 | "name": "SQLite", 797 | "paths": [ 798 | "~/.sqlite_history", 799 | "~/.sqliterc" 800 | ], 801 | "fixes": [ 802 | "~/.sqlite_history:~/.local/share/sqlite/sqlite_history", 803 | "~/.sqliterc:~/.config/sqlite/sqliterc" 804 | ] 805 | }, 806 | { 807 | "name": "python-streamlit", 808 | "paths": [ 809 | "~/.streamlit" 810 | ], 811 | "fixes": [ 812 | "~/.streamlit:~/.local/share/streamlit" 813 | ] 814 | }, 815 | { 816 | "name": "TeamSpeak", 817 | "paths": [ 818 | "~/.ts3client" 819 | ], 820 | "fixes": [ 821 | "~/.ts3client:~/.local/share/ts3client" 822 | ] 823 | }, 824 | { 825 | "name": "terraform", 826 | "paths": [ 827 | "~/.terraform.d/" 828 | ], 829 | "fixes": [ 830 | "~/.terraform.d:~/.local/share/terraform" 831 | ] 832 | }, 833 | { 834 | "name": "texinfo", 835 | "paths": [ 836 | "~/.infokey" 837 | ], 838 | "fixes": [ 839 | "~/.infokey:~/.config/info/infokey" 840 | ] 841 | }, 842 | { 843 | "name": "Thunderbird", 844 | "paths": [ 845 | "~/.thunderbird/" 846 | ], 847 | "fixes": [ 848 | "~/.thunderbird:~/.local/share/thunderbird" 849 | ] 850 | }, 851 | { 852 | "name": "TigerVNC", 853 | "paths": [ 854 | "~/.vnc" 855 | ], 856 | "fixes": [ 857 | "~/.vnc:~/.local/share/vnc" 858 | ] 859 | }, 860 | { 861 | "name": "tllocalmgr", 862 | "paths": [ 863 | "~/.texlive" 864 | ], 865 | "fixes": [ 866 | "~/.texlive:~/.local/share/texlive" 867 | ] 868 | }, 869 | { 870 | "name": "urlview", 871 | "paths": [ 872 | "~/.urlview" 873 | ], 874 | "fixes": [ 875 | "~/.urlview:~/.config/urlview/urlview" 876 | ] 877 | }, 878 | { 879 | "name": "vale", 880 | "paths": [ 881 | "~/.vale.ini" 882 | ], 883 | "fixes": [ 884 | "~/.vale.ini:~/.config/vale/vale.ini" 885 | ] 886 | }, 887 | { 888 | "name": "vim", 889 | "paths": [ 890 | "~/.vim", 891 | "~/.vimrc", 892 | "~/.viminfo" 893 | ], 894 | "fixes": [ 895 | "~/.vim:~/.local/share/vim", 896 | "~/.vimrc:~/.config/vim/vimrc", 897 | "~/.viminfo:~/.local/share/vim/viminfo" 898 | ] 899 | }, 900 | { 901 | "name": "vimperator", 902 | "paths": [ 903 | "~/.vimperatorrc" 904 | ], 905 | "fixes": [ 906 | "~/.vimperatorrc:~/.config/vimperator/vimperatorrc" 907 | ] 908 | }, 909 | { 910 | "name": "visidata", 911 | "paths": [ 912 | "~/.visidata" 913 | ], 914 | "fixes": [ 915 | "~/.visidata:~/.config/visidata/visidata" 916 | ] 917 | }, 918 | { 919 | "name": "wpa_cli", 920 | "paths": [ 921 | "~/.wpa_cli_history" 922 | ], 923 | "fixes": [ 924 | "~/.wpa_cli_history:~/.local/share/wpa_cli/wpa_cli_history" 925 | ] 926 | }, 927 | { 928 | "name": "wego", 929 | "paths": [ 930 | "~/.wegorc" 931 | ], 932 | "fixes": [ 933 | "~/.wegorc:~/.config/wego/wegorc" 934 | ] 935 | }, 936 | { 937 | "name": "x2goclient", 938 | "paths": [ 939 | "~/.x2goclient" 940 | ], 941 | "fixes": [ 942 | "~/.x2goclient:~/.local/share/x2goclient" 943 | ] 944 | }, 945 | { 946 | "name": "xpdf", 947 | "paths": [ 948 | "~/.xpdfrc" 949 | ], 950 | "fixes": [ 951 | "~/.xpdfrc:~/.config/xpdf/xpdfrc" 952 | ] 953 | }, 954 | { 955 | "name": "xrdp", 956 | "paths": [ 957 | "~/thinclient_drives" 958 | ], 959 | "fixes": [ 960 | "~/thinclient_drives:~/.local/share/xrdp/thinclient_drives" 961 | ] 962 | }, 963 | { 964 | "name": "XVim2", 965 | "paths": [ 966 | "~/.xvimrc" 967 | ], 968 | "fixes": [ 969 | "~/.xvimrc:~/.config/xvim/xvimrc" 970 | ] 971 | }, 972 | { 973 | "name": "YARD", 974 | "paths": [ 975 | "~/.yard" 976 | ], 977 | "fixes": [ 978 | "~/.yard:~/.config/yard/yard" 979 | ] 980 | }, 981 | { 982 | "name": "zenmap nmap", 983 | "paths": [ 984 | "~/.zenmap" 985 | ], 986 | "fixes": [ 987 | "~/.zenmap:~/.local/share/zenmap" 988 | ] 989 | }, 990 | { 991 | "name": "zotero-bin", 992 | "paths": [ 993 | "~/.zotero", 994 | "~/Zotero" 995 | ], 996 | "fixes": [ 997 | "~/.zotero:~/.local/share/zotero", 998 | "~/Zotero:~/.local/share/zotero" 999 | ] 1000 | } 1001 | ] 1002 | -------------------------------------------------------------------------------- /data/partial-support-applications.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "abook", 4 | "paths": [ 5 | "~/.abook" 6 | ], 7 | "fixes": [ 8 | "~/.abook:~/.config/abook" 9 | ] 10 | }, 11 | { 12 | "name": "ack", 13 | "paths": [ 14 | "~/.ackrc" 15 | ], 16 | "fixes": [ 17 | "~/.ackrc:~/.config/ack/ackrc" 18 | ] 19 | }, 20 | { 21 | "name": "Ansible", 22 | "paths": [ 23 | "~/.ansible" 24 | ], 25 | "fixes": [ 26 | "~/.ansible:~/.config/ansible" 27 | ] 28 | }, 29 | { 30 | "name": "asdf-vm", 31 | "paths": [ 32 | "~/.asdfrc", 33 | "~/.asdf/" 34 | ], 35 | "fixes": [ 36 | "~/.asdfrc:~/.config/asdf/asdfrc", 37 | "~/.asdf/:~/.local/share/asdf" 38 | ] 39 | }, 40 | { 41 | "name": "aspell", 42 | "paths": [ 43 | "~/.aspell.conf" 44 | ], 45 | "fixes": [ 46 | "~/.aspell.conf:~/.config/aspell/aspell.conf" 47 | ] 48 | }, 49 | { 50 | "name": "Atom", 51 | "paths": [ 52 | "~/.atom" 53 | ], 54 | "fixes": [ 55 | "~/.atom:~/.local/share/atom" 56 | ] 57 | }, 58 | { 59 | "name": "aws-cli", 60 | "paths": [ 61 | "~/.aws" 62 | ], 63 | "fixes": [ 64 | "~/.aws:~/.config/aws" 65 | ] 66 | }, 67 | { 68 | "name": "bashdb", 69 | "paths": [ 70 | "~/.bashdbinit", 71 | "~/.bashdb_hist" 72 | ], 73 | "fixes": [ 74 | "~/.bashdbinit:~/.config/bashdb/bashdbinit", 75 | "~/.bashdb_hist:~/.local/share/bashdb/bashdb_hist" 76 | ] 77 | }, 78 | { 79 | "name": "bazaar", 80 | "paths": [ 81 | "~/.bazaar", 82 | "~/.bzr.log" 83 | ], 84 | "fixes": [ 85 | "~/.bzr.log:~/.local/share/bazaar/bzr.log", 86 | "~/.bazaar:~/.config/bazaar" 87 | ] 88 | }, 89 | { 90 | "name": "bogofilter", 91 | "paths": [ 92 | "~/.bogofilter" 93 | ], 94 | "fixes": [ 95 | "~/.bogofilter:~/.local/share/bogofilter" 96 | ] 97 | }, 98 | { 99 | "name": "btpd-git", 100 | "paths": [ 101 | "~/.btpd/" 102 | ], 103 | "fixes": [ 104 | "~/.btpd/:~/.config/btpd" 105 | ] 106 | }, 107 | { 108 | "name": "calc", 109 | "paths": [ 110 | "~/.calc_history" 111 | ], 112 | "fixes": [ 113 | "~/.calc_history:~/.cache/calc/calc_history" 114 | ] 115 | }, 116 | { 117 | "name": "Rust - Cargo", 118 | "paths": [ 119 | "~/.cargo" 120 | ], 121 | "fixes": [ 122 | "~/.cargo:~/.local/share/cargo" 123 | ] 124 | }, 125 | { 126 | "name": "cataclysm-dda", 127 | "paths": [ 128 | "~/.cataclysm-dda" 129 | ], 130 | "fixes": [ 131 | "~/.cataclysm-dda:~/.local/share/cataclysm-dda" 132 | ] 133 | }, 134 | { 135 | "name": "cd-bookmark", 136 | "paths": [ 137 | "~/.cdbookmark" 138 | ], 139 | "fixes": [ 140 | "~/.cdbookmark:~/.config/cd-bookmark" 141 | ] 142 | }, 143 | { 144 | "name": "cgdb", 145 | "paths": [ 146 | "~/.cgdb" 147 | ], 148 | "fixes": [ 149 | "~/.cgdb:~/.config/cgdb" 150 | ] 151 | }, 152 | { 153 | "name": "chez-scheme", 154 | "paths": [ 155 | "~/.chezscheme_history" 156 | ], 157 | "fixes": [ 158 | "~/.chezscheme_history:~/.local/share/chez-scheme/chezscheme_history" 159 | ] 160 | }, 161 | { 162 | "name": "cinelerra", 163 | "paths": [ 164 | "~/.bcast5" 165 | ], 166 | "fixes": [ 167 | "~/.bcast5:~/.local/share/cinelerra/bcast5" 168 | ] 169 | }, 170 | { 171 | "name": "conky", 172 | "paths": [ 173 | "~/.conkyrc" 174 | ], 175 | "fixes": [ 176 | "~/.conkyrc:~/.config/conky/conkyrc" 177 | ] 178 | }, 179 | { 180 | "name": "claws-mail", 181 | "paths": [ 182 | "~/.claws-mail" 183 | ], 184 | "fixes": [ 185 | "~/.claws-mail:~/.local/share/claws-mail" 186 | ] 187 | }, 188 | { 189 | "name": "coreutils", 190 | "paths": [ 191 | "~/.dircolors" 192 | ], 193 | "fixes": [ 194 | "~/.dircolors:~/.config/coreutils/dircolors" 195 | ] 196 | }, 197 | { 198 | "name": "crawl", 199 | "paths": [ 200 | "~/.crawl" 201 | ], 202 | "fixes": [ 203 | "~/.crawl:~/.local/share/crawl" 204 | ] 205 | }, 206 | { 207 | "name": "clusterssh", 208 | "paths": [ 209 | "~/.clusterssh/" 210 | ], 211 | "fixes": [ 212 | "~/.clusterssh/:~/.config/clusterssh" 213 | ] 214 | }, 215 | { 216 | "name": "CUDA", 217 | "paths": [ 218 | "~/.nv" 219 | ], 220 | "fixes": [ 221 | "~/.nv:~/.cache/cuda" 222 | ] 223 | }, 224 | { 225 | "name": "dict", 226 | "paths": [ 227 | "~/.dictrc" 228 | ], 229 | "fixes": [ 230 | "~/.dictrc:~/.config/dict/dictrc" 231 | ] 232 | }, 233 | { 234 | "name": "Docker", 235 | "paths": [ 236 | "~/.docker" 237 | ], 238 | "fixes": [ 239 | "~/.docker:~/.config/docker" 240 | ] 241 | }, 242 | { 243 | "name": "docker-machine", 244 | "paths": [ 245 | "~/.docker/machine" 246 | ], 247 | "fixes": [ 248 | "~/.docker/machine:~/.local/share/docker/machine" 249 | ] 250 | }, 251 | { 252 | "name": "DOSBox", 253 | "paths": [ 254 | "~/.dosbox" 255 | ], 256 | "fixes": [ 257 | "~/.dosbox:~/.config/dosbox" 258 | ] 259 | }, 260 | { 261 | "name": "dub", 262 | "paths": [ 263 | "~/.dub" 264 | ], 265 | "fixes": [ 266 | "~/.dub:~/.local/share/dub" 267 | ] 268 | }, 269 | { 270 | "name": "Electrum Bitcoin Wallet", 271 | "paths": [ 272 | "~/.electrum" 273 | ], 274 | "fixes": [ 275 | "~/.electrum:~/.local/share/electrum" 276 | ] 277 | }, 278 | { 279 | "name": "ELinks", 280 | "paths": [ 281 | "~/.elinks" 282 | ], 283 | "fixes": [ 284 | "~/.elinks:~/.config/elinks" 285 | ] 286 | }, 287 | { 288 | "name": "elixir", 289 | "paths": [ 290 | "~/.mix" 291 | ], 292 | "fixes": [ 293 | "~/.mix:~/.local/share/elixir" 294 | ] 295 | }, 296 | { 297 | "name": "Elm", 298 | "paths": [ 299 | "~/.elm" 300 | ], 301 | "fixes": [ 302 | "~/.elm:~/.config/elm" 303 | ] 304 | }, 305 | { 306 | "name": "fceux", 307 | "paths": [ 308 | "~/.fceux/" 309 | ], 310 | "fixes": [ 311 | "~/.fceux/:~/.config/fceux" 312 | ] 313 | }, 314 | { 315 | "name": "FFmpeg", 316 | "paths": [ 317 | "~/.ffmpeg" 318 | ], 319 | "fixes": [ 320 | "~/.ffmpeg:~/.config/ffmpeg" 321 | ] 322 | }, 323 | { 324 | "name": "flutter", 325 | "paths": [ 326 | "~/.flutter", 327 | "~/.flutter_settings", 328 | "~/.flutter_tool_state", 329 | "~/.pub-cache" 330 | ], 331 | "fixes": [ 332 | "~/.flutter:~/.local/share/flutter", 333 | "~/.flutter_settings:~/.config/flutter/flutter_settings", 334 | "~/.flutter_tool_state:~/.config/flutter/flutter_tool_state", 335 | "~/.pub-cache:~/.cache/flutter" 336 | ] 337 | }, 338 | { 339 | "name": "emscripten", 340 | "paths": [ 341 | "~/.emscripten", 342 | "~/.emscripten_sanity", 343 | "~/.emscripten_ports", 344 | "~/.emscripten_cache__last_clear" 345 | ], 346 | "fixes": [ 347 | "~/.emscripten:~/.config/emscripten/emscripten", 348 | "~/.emscripten_sanity:~/.config/emscripten/emscripten_sanity", 349 | "~/.emscripten_ports:~/.config/emscripten/emscripten_ports", 350 | "~/.emscripten_cache__last_clear:~/.cache/emscripten/emscripten_cache__last_clear" 351 | ] 352 | }, 353 | { 354 | "name": "get_iplayer", 355 | "paths": [ 356 | "~/.get_iplayer" 357 | ], 358 | "fixes": [ 359 | "~/.get_iplayer:~/.local/share/get_iplayer" 360 | ] 361 | }, 362 | { 363 | "name": "getmail", 364 | "paths": [ 365 | "~/.getmail" 366 | ], 367 | "fixes": [ 368 | "~/.getmail:~/.config/getmail" 369 | ] 370 | }, 371 | { 372 | "name": "ghc", 373 | "paths": [ 374 | "~/.ghci" 375 | ], 376 | "fixes": [ 377 | "~/.ghci:~/.config/ghc/ghci" 378 | ] 379 | }, 380 | { 381 | "name": "ghcup-hs-bin", 382 | "paths": [ 383 | "~/.ghcup" 384 | ], 385 | "fixes": [ 386 | "~/.ghcup:~/.local/share/ghcup" 387 | ] 388 | }, 389 | { 390 | "name": "gliv", 391 | "paths": [ 392 | "~/.glivrc" 393 | ], 394 | "fixes": [ 395 | "~/.glivrc:~/.config/gliv/glivrc" 396 | ] 397 | }, 398 | { 399 | "name": "gnuradio", 400 | "paths": [ 401 | "~/.gnuradio" 402 | ], 403 | "fixes": [ 404 | "~/.gnuradio:~/.config/gnuradio" 405 | ] 406 | }, 407 | { 408 | "name": "GnuPG", 409 | "paths": [ 410 | "~/.gnupg" 411 | ], 412 | "fixes": [ 413 | "~/.gnupg:~/.local/share/gnupg" 414 | ] 415 | }, 416 | { 417 | "name": "Go", 418 | "paths": [ 419 | "~/go" 420 | ], 421 | "fixes": [ 422 | "~/go:~/.local/share/go" 423 | ] 424 | }, 425 | { 426 | "name": "Google Earth", 427 | "paths": [ 428 | "~/.googleearth" 429 | ], 430 | "fixes": [ 431 | "~/.googleearth:~/.local/share/googleearth" 432 | ] 433 | }, 434 | { 435 | "name": "gopass", 436 | "paths": [ 437 | "~/.password-store" 438 | ], 439 | "fixes": [ 440 | "~/.password-store:~/.local/share/password-store" 441 | ] 442 | }, 443 | { 444 | "name": "gpodder", 445 | "paths": [ 446 | "~/gPodder" 447 | ], 448 | "fixes": [ 449 | "~/gPodder:~/.local/share/gpodder" 450 | ] 451 | }, 452 | { 453 | "name": "GQ LDAP client", 454 | "paths": [ 455 | "~/.gq", 456 | "~/.gq-state" 457 | ], 458 | "fixes": [ 459 | "~/.gq:~/.config/gq", 460 | "~/.gq-state:~/.local/share/gq" 461 | ] 462 | }, 463 | { 464 | "name": "Gradle", 465 | "paths": [ 466 | "~/.gradle" 467 | ], 468 | "fixes": [ 469 | "~/.gradle:~/.local/share/gradle" 470 | ] 471 | }, 472 | { 473 | "name": "GTK 1", 474 | "paths": [ 475 | "~/.gtkrc" 476 | ], 477 | "fixes": [ 478 | "~/.gtkrc:~/.config/gtk-1.0/gtkrc" 479 | ] 480 | }, 481 | { 482 | "name": "GTK 2", 483 | "paths": [ 484 | "~/.gtkrc-2.0" 485 | ], 486 | "fixes": [ 487 | "~/.gtkrc-2.0:~/.config/gtk-2.0/gtkrc" 488 | ] 489 | }, 490 | { 491 | "name": "hledger", 492 | "paths": [ 493 | "~/.hledger.journal" 494 | ], 495 | "fixes": [ 496 | "~/.hledger.journal:~/.local/share/hledger/hledger.journal" 497 | ] 498 | }, 499 | { 500 | "name": "imapfilter", 501 | "paths": [ 502 | "~/.imapfilter" 503 | ], 504 | "fixes": [ 505 | "~/.imapfilter:~/.config/imapfilter" 506 | ] 507 | }, 508 | { 509 | "name": "IPFS", 510 | "paths": [ 511 | "~/.ipfs" 512 | ], 513 | "fixes": [ 514 | "~/.ipfs:~/.local/share/ipfs" 515 | ] 516 | }, 517 | { 518 | "name": "irb", 519 | "paths": [ 520 | "~/.irbrc" 521 | ], 522 | "fixes": [ 523 | "~/.irbrc:~/.config/irb/irbrc" 524 | ] 525 | }, 526 | { 527 | "name": "irssi", 528 | "paths": [ 529 | "~/.irssi" 530 | ], 531 | "fixes": [ 532 | "~/.irssi:~/.config/irssi" 533 | ] 534 | }, 535 | { 536 | "name": "isync", 537 | "paths": [ 538 | "~/.mbsyncrc" 539 | ], 540 | "fixes": [ 541 | "~/.mbsyncrc:~/.config/isync/mbsyncrc" 542 | ] 543 | }, 544 | { 545 | "name": "Java - OpenJDK", 546 | "paths": [ 547 | "~/.java/.userPrefs" 548 | ], 549 | "fixes": [ 550 | "~/.java/.userPrefs:~/.local/share/java/.userPrefs" 551 | ] 552 | }, 553 | { 554 | "name": "jupyter", 555 | "paths": [ 556 | "~/.jupyter" 557 | ], 558 | "fixes": [ 559 | "~/.jupyter:~/.local/share/jupyter" 560 | ] 561 | }, 562 | { 563 | "name": "k9s", 564 | "paths": [ 565 | "~/.k9s" 566 | ], 567 | "fixes": [ 568 | "~/.k9s:~/.config/k9s" 569 | ] 570 | }, 571 | { 572 | "name": "KDE", 573 | "paths": [ 574 | "~/.kde", 575 | "~/.kde4" 576 | ], 577 | "fixes": [ 578 | "~/.kde:~/.config/kde", 579 | "~/.kde4:~/.config/kde4" 580 | ] 581 | }, 582 | { 583 | "name": "keychain", 584 | "paths": [ 585 | "~/.keychain" 586 | ], 587 | "fixes": [ 588 | "~/.keychain:~/.local/share/keychain" 589 | ] 590 | }, 591 | { 592 | "name": "kodi", 593 | "paths": [ 594 | "~/.kodi" 595 | ], 596 | "fixes": [ 597 | "~/.kodi:~/.local/share/kodi" 598 | ] 599 | }, 600 | { 601 | "name": "kscript", 602 | "paths": [ 603 | "~/.kscript" 604 | ], 605 | "fixes": [ 606 | "~/.kscript:~/.cache/kscript" 607 | ] 608 | }, 609 | { 610 | "name": "ledger", 611 | "paths": [ 612 | "~/.ledgerrc", 613 | "~/.pricedb" 614 | ], 615 | "fixes": [ 616 | "~/.ledgerrc:~/.config/ledger/ledgerrc", 617 | "~/.pricedb:~/.local/share/ledger/pricedb" 618 | ] 619 | }, 620 | { 621 | "name": "Leiningen", 622 | "paths": [ 623 | "~/.lein", 624 | "~/.m2" 625 | ], 626 | "fixes": [ 627 | "~/.lein:~/.local/share/lein", 628 | "~/.m2:~/.local/share/m2" 629 | ] 630 | }, 631 | { 632 | "name": "libdvdcss", 633 | "paths": [ 634 | "~/.dvdcss" 635 | ], 636 | "fixes": [ 637 | "~/.dvdcss:~/.local/share/dvdcss" 638 | ] 639 | }, 640 | { 641 | "name": "ltrace", 642 | "paths": [ 643 | "~/.ltrace.conf" 644 | ], 645 | "fixes": [ 646 | "~/.ltrace.conf:~/.config/ltrace/ltrace.conf" 647 | ] 648 | }, 649 | { 650 | "name": "m17n-db", 651 | "paths": [ 652 | "~/.m17n.d" 653 | ], 654 | "fixes": [ 655 | "~/.m17n.d:~/.local/share/m17n" 656 | ] 657 | }, 658 | { 659 | "name": "maptool-bin", 660 | "paths": [ 661 | "~/.maptool-rptools" 662 | ], 663 | "fixes": [ 664 | "~/.maptool-rptools:~/.local/share/maptool-rptools" 665 | ] 666 | }, 667 | { 668 | "name": "maven", 669 | "paths": [ 670 | "~/.m2" 671 | ], 672 | "fixes": [ 673 | "~/.m2:~/.local/share/m2" 674 | ] 675 | }, 676 | { 677 | "name": "Mathematica", 678 | "paths": [ 679 | "~/.Mathematica" 680 | ], 681 | "fixes": [ 682 | "~/.Mathematica:~/.config/Mathematica" 683 | ] 684 | }, 685 | { 686 | "name": "maxima", 687 | "paths": [ 688 | "~/.maxima" 689 | ], 690 | "fixes": [ 691 | "~/.maxima:~/.config/maxima" 692 | ] 693 | }, 694 | { 695 | "name": "mednafen", 696 | "paths": [ 697 | "~/.mednafen" 698 | ], 699 | "fixes": [ 700 | "~/.mednafen:~/.config/mednafen" 701 | ] 702 | }, 703 | { 704 | "name": "minikube", 705 | "paths": [ 706 | "~/.minikube" 707 | ], 708 | "fixes": [ 709 | "~/.minikube:~/.local/share/minikube" 710 | ] 711 | }, 712 | { 713 | "name": "mitmproxy", 714 | "paths": [ 715 | "~/.mitmproxy" 716 | ], 717 | "fixes": [ 718 | "~/.mitmproxy:~/.config/mitmproxy" 719 | ] 720 | }, 721 | { 722 | "name": "MOC", 723 | "paths": [ 724 | "~/.moc" 725 | ], 726 | "fixes": [ 727 | "~/.moc:~/.config/moc" 728 | ] 729 | }, 730 | { 731 | "name": "monero", 732 | "paths": [ 733 | "~/.bitmonero" 734 | ], 735 | "fixes": [ 736 | "~/.bitmonero:~/.local/share/monero" 737 | ] 738 | }, 739 | { 740 | "name": "most", 741 | "paths": [ 742 | "~/.mostrc" 743 | ], 744 | "fixes": [ 745 | "~/.mostrc:~/.config/most/mostrc" 746 | ] 747 | }, 748 | { 749 | "name": "MPlayer", 750 | "paths": [ 751 | "~/.mplayer" 752 | ], 753 | "fixes": [ 754 | "~/.mplayer:~/.config/mplayer" 755 | ] 756 | }, 757 | { 758 | "name": "mypy", 759 | "paths": [ 760 | "~/.config/mypy/config", 761 | "~/.mypy.ini", 762 | "~/.mypy_cache" 763 | ], 764 | "fixes": [ 765 | "~/.config/mypy/config:~/.config/mypy/config", 766 | "~/.mypy.ini:~/.config/mypy/config", 767 | "~/.mypy_cache:~/.cache/mypy" 768 | ] 769 | }, 770 | { 771 | "name": "MySQL", 772 | "paths": [ 773 | "~/.mysql_history", 774 | "~/.my.cnf", 775 | "~/.mylogin.cnf" 776 | ], 777 | "fixes": [ 778 | "~/.mysql_history:~/.local/share/mysql_history", 779 | "~/.my.cnf:~/.config/my.cnf", 780 | "~/.mylogin.cnf:~/.config/mylogin.cnf" 781 | ] 782 | }, 783 | { 784 | "name": "mysql-workbench", 785 | "paths": [ 786 | "~/.mysql/workbench" 787 | ], 788 | "fixes": [ 789 | "~/.mysql/workbench:~/.local/share/mysql/workbench" 790 | ] 791 | }, 792 | { 793 | "name": "ncurses", 794 | "paths": [ 795 | "~/.terminfo" 796 | ], 797 | "fixes": [ 798 | "~/.terminfo:~/.local/share/terminfo" 799 | ] 800 | }, 801 | { 802 | "name": "n", 803 | "paths": [ 804 | "/usr/local/n" 805 | ], 806 | "fixes": [ 807 | "/usr/local/n:~/.local/share/n" 808 | ] 809 | }, 810 | { 811 | "name": "ncmpc", 812 | "paths": [ 813 | "~/.ncmpc" 814 | ], 815 | "fixes": [ 816 | "~/.ncmpc:~/.config/ncmpc" 817 | ] 818 | }, 819 | { 820 | "name": "Netbeans", 821 | "paths": [ 822 | "~/.netbeans" 823 | ], 824 | "fixes": [ 825 | "~/.netbeans:~/.local/share/netbeans" 826 | ] 827 | }, 828 | { 829 | "name": "Node.js", 830 | "paths": [ 831 | "~/.node_repl_history" 832 | ], 833 | "fixes": [ 834 | "~/.node_repl_history:~/.local/share/node_repl_history" 835 | ] 836 | }, 837 | { 838 | "name": "npm", 839 | "paths": [ 840 | "~/.npm", 841 | "~/.npmrc" 842 | ], 843 | "fixes": [ 844 | "~/.npm:~/.local/share/npm", 845 | "~/.npmrc:~/.config/npm/npmrc" 846 | ] 847 | }, 848 | { 849 | "name": "opam", 850 | "paths": [ 851 | "~/.opam" 852 | ], 853 | "fixes": [ 854 | "~/.opam:~/.local/share/opam" 855 | ] 856 | }, 857 | { 858 | "name": "pnpm", 859 | "paths": [ 860 | "~/.pnpm-store" 861 | ], 862 | "fixes": [ 863 | "~/.pnpm-store:~/.local/share/pnpm-store" 864 | ] 865 | }, 866 | { 867 | "name": "PuTTY", 868 | "paths": [ 869 | "~/.putty/" 870 | ], 871 | "fixes": [ 872 | "~/.putty/:~/.config/putty/" 873 | ] 874 | }, 875 | { 876 | "name": "nuget", 877 | "paths": [ 878 | "~/.nuget/packages" 879 | ], 880 | "fixes": [ 881 | "~/.nuget/packages:~/.local/share/nuget/packages" 882 | ] 883 | }, 884 | { 885 | "name": "NVIDIA", 886 | "paths": [ 887 | "~/.nv" 888 | ], 889 | "fixes": [ 890 | "~/.nv:~/.cache/nv" 891 | ] 892 | }, 893 | { 894 | "name": "nvidia-settings", 895 | "paths": [ 896 | "~/.nvidia-settings-rc" 897 | ], 898 | "fixes": [ 899 | "~/.nvidia-settings-rc:~/.config/nvidia-settings-rc" 900 | ] 901 | }, 902 | { 903 | "name": "nvm", 904 | "paths": [ 905 | "~/.nvm" 906 | ], 907 | "fixes": [ 908 | "~/.nvm:~/.local/share/nvm" 909 | ] 910 | }, 911 | { 912 | "name": "Octave", 913 | "paths": [ 914 | "~/octave", 915 | "~/.octave_packages", 916 | "~/.octave_hist" 917 | ], 918 | "fixes": [ 919 | "~/octave:~/.local/share/octave", 920 | "~/.octave_packages:~/.local/share/octave", 921 | "~/.octave_hist:~/.local/share/octave" 922 | ] 923 | }, 924 | { 925 | "name": "openscad", 926 | "paths": [ 927 | "~/.OpenSCAD" 928 | ], 929 | "fixes": [ 930 | "~/.OpenSCAD:~/.local/share/OpenSCAD" 931 | ] 932 | }, 933 | { 934 | "name": "parallel", 935 | "paths": [ 936 | "~/.parallel" 937 | ], 938 | "fixes": [ 939 | "~/.parallel:~/.config/parallel" 940 | ] 941 | }, 942 | { 943 | "name": "pass", 944 | "paths": [ 945 | "~/.password-store" 946 | ], 947 | "fixes": [ 948 | "~/.password-store:~/.local/share/password-store" 949 | ] 950 | }, 951 | { 952 | "name": "Pidgin", 953 | "paths": [ 954 | "~/.purple" 955 | ], 956 | "fixes": [ 957 | "~/.purple:~/.local/share/pidgin" 958 | ] 959 | }, 960 | { 961 | "name": "PostgreSQL", 962 | "paths": [ 963 | "~/.psqlrc", 964 | "~/.psql_history", 965 | "~/.pgpass", 966 | "~/.pg_service.conf" 967 | ], 968 | "fixes": [ 969 | "~/.psqlrc:~/.config/psqlrc", 970 | "~/.psql_history:~/.local/share/psql_history", 971 | "~/.pgpass:~/.config/pgpass", 972 | "~/.pg_service.conf:~/.config/pg_service.conf" 973 | ] 974 | }, 975 | { 976 | "name": "pyenv", 977 | "paths": [ 978 | "~/.pyenv" 979 | ], 980 | "fixes": [ 981 | "~/.pyenv:~/.local/share/pyenv" 982 | ] 983 | }, 984 | { 985 | "name": "python-azure-cli", 986 | "paths": [ 987 | "~/.azure" 988 | ], 989 | "fixes": [ 990 | "~/.azure:~/.config/azure" 991 | ] 992 | }, 993 | { 994 | "name": "python-grip", 995 | "paths": [ 996 | "~/.grip" 997 | ], 998 | "fixes": [ 999 | "~/.grip:~/.config/grip" 1000 | ] 1001 | }, 1002 | { 1003 | "name": "python-setuptools", 1004 | "paths": [ 1005 | "~/.python-eggs" 1006 | ], 1007 | "fixes": [ 1008 | "~/.python-eggs:~/.cache/python-eggs" 1009 | ] 1010 | }, 1011 | { 1012 | "name": "racket", 1013 | "paths": [ 1014 | "~/.racketrc", 1015 | "~/.racket" 1016 | ], 1017 | "fixes": [ 1018 | "~/.racketrc:~/.config/racketrc", 1019 | "~/.racket:~/.local/share/racket" 1020 | ] 1021 | }, 1022 | { 1023 | "name": "rbenv", 1024 | "paths": [ 1025 | "~/.rbenv" 1026 | ], 1027 | "fixes": [ 1028 | "~/.rbenv:~/.local/share/rbenv" 1029 | ] 1030 | }, 1031 | { 1032 | "name": "nodenv", 1033 | "paths": [ 1034 | "~/.nodenv" 1035 | ], 1036 | "fixes": [ 1037 | "~/.nodenv:~/.local/share/nodenv" 1038 | ] 1039 | }, 1040 | { 1041 | "name": "readline", 1042 | "paths": [ 1043 | "~/.inputrc" 1044 | ], 1045 | "fixes": [ 1046 | "~/.inputrc:~/.config/inputrc" 1047 | ] 1048 | }, 1049 | { 1050 | "name": "recoll", 1051 | "paths": [ 1052 | "~/.recoll" 1053 | ], 1054 | "fixes": [ 1055 | "~/.recoll:~/.config/recoll" 1056 | ] 1057 | }, 1058 | { 1059 | "name": "redis", 1060 | "paths": [ 1061 | "~/.rediscli_history", 1062 | "~/.redisclirc" 1063 | ], 1064 | "fixes": [ 1065 | "~/.rediscli_history:~/.local/share/rediscli_history", 1066 | "~/.redisclirc:~/.config/redisclirc" 1067 | ] 1068 | }, 1069 | { 1070 | "name": "ruby-solargraph", 1071 | "paths": [ 1072 | "~/.solargraph/cache/" 1073 | ], 1074 | "fixes": [ 1075 | "~/.solargraph/cache/:~/.cache/solargraph" 1076 | ] 1077 | }, 1078 | { 1079 | "name": "Rust#Rustup", 1080 | "paths": [ 1081 | "~/.rustup" 1082 | ], 1083 | "fixes": [ 1084 | "~/.rustup:~/.local/share/rustup" 1085 | ] 1086 | }, 1087 | { 1088 | "name": "sbt", 1089 | "paths": [ 1090 | "~/.sbt", 1091 | "~/.ivy2" 1092 | ], 1093 | "fixes": [ 1094 | "~/.sbt:~/.config/sbt", 1095 | "~/.ivy2:~/.cache/ivy2" 1096 | ] 1097 | }, 1098 | { 1099 | "name": "SageMath", 1100 | "paths": [ 1101 | "~/.sage" 1102 | ], 1103 | "fixes": [ 1104 | "~/.sage:~/.local/share/sage" 1105 | ] 1106 | }, 1107 | { 1108 | "name": "GNU Screen", 1109 | "paths": [ 1110 | "~/.screenrc" 1111 | ], 1112 | "fixes": [ 1113 | "~/.screenrc:~/.config/screenrc" 1114 | ] 1115 | }, 1116 | { 1117 | "name": "simplescreenrecorder", 1118 | "paths": [ 1119 | "~/.ssr/" 1120 | ], 1121 | "fixes": [ 1122 | "~/.ssr/:~/.local/share/ssr" 1123 | ] 1124 | }, 1125 | { 1126 | "name": "spacemacs", 1127 | "paths": [ 1128 | "~/.spacemacs", 1129 | "~/.spacemacs.d" 1130 | ], 1131 | "fixes": [ 1132 | "~/.spacemacs:~/.config/spacemacs/init.el", 1133 | "~/.spacemacs.d:~/.config/spacemacs.d" 1134 | ] 1135 | }, 1136 | { 1137 | "name": "Haskell - Stack", 1138 | "paths": [ 1139 | "~/.stack" 1140 | ], 1141 | "fixes": [ 1142 | "~/.stack:~/.local/share/stack" 1143 | ] 1144 | }, 1145 | { 1146 | "name": "subversion", 1147 | "paths": [ 1148 | "~/.subversion" 1149 | ], 1150 | "fixes": [ 1151 | "~/.subversion:~/.config/subversion" 1152 | ] 1153 | }, 1154 | { 1155 | "name": "Local TeX Live TeXmf tree, TeXmf caches and config", 1156 | "paths": [ 1157 | "~/texmf", 1158 | "~/.texlive/texmf-var", 1159 | "~/.texlive/texmf-config" 1160 | ], 1161 | "fixes": [ 1162 | "~/.texlive/texmf-var:~/.cache/texlive/texmf-var", 1163 | "~/.texlive/texmf-config:~/.config/texlive/texmf-config", 1164 | "~/texmf:~/.local/share/texmf" 1165 | ] 1166 | }, 1167 | { 1168 | "name": "TeXmacs", 1169 | "paths": [ 1170 | "~/.TeXmacs" 1171 | ], 1172 | "fixes": [ 1173 | "~/.TeXmacs:~/.local/state/texmacs" 1174 | ] 1175 | }, 1176 | { 1177 | "name": "tiptop", 1178 | "paths": [ 1179 | "~/.tiptoprc" 1180 | ], 1181 | "fixes": [ 1182 | "~/.tiptoprc:~/.config/tiptop/tiptoprc" 1183 | ] 1184 | }, 1185 | { 1186 | "name": "ruby-travis", 1187 | "paths": [ 1188 | "~/.travis/" 1189 | ], 1190 | "fixes": [ 1191 | "~/.travis:~/.config/travis" 1192 | ] 1193 | }, 1194 | { 1195 | "name": "uncrustify", 1196 | "paths": [ 1197 | "~/.uncrustify.cfg" 1198 | ], 1199 | "fixes": [ 1200 | "~/.uncrustify.cfg:~/.config/uncrustify/uncrustify.cfg" 1201 | ] 1202 | }, 1203 | { 1204 | "name": "Unison", 1205 | "paths": [ 1206 | "~/.unison" 1207 | ], 1208 | "fixes": [ 1209 | "~/.unison:~/.local/share/unison" 1210 | ] 1211 | }, 1212 | { 1213 | "name": "units", 1214 | "paths": [ 1215 | "~/.units_history" 1216 | ], 1217 | "fixes": [ 1218 | "~/.units_history:~/.cache/units_history" 1219 | ] 1220 | }, 1221 | { 1222 | "name": "urxvtd", 1223 | "paths": [ 1224 | "~/.urxvt/urxvtd-hostname" 1225 | ], 1226 | "fixes": [ 1227 | "~/.urxvt/urxvtd-hostname:~/.local/state/urxvt/urxvtd-hostname" 1228 | ] 1229 | }, 1230 | { 1231 | "name": "Vagrant", 1232 | "paths": [ 1233 | "~/.vagrant.d", 1234 | "~/.vagrant.d/aliases" 1235 | ], 1236 | "fixes": [ 1237 | "~/.vagrant.d/aliases:~/.config/vagrant.d/aliases", 1238 | "~/.vagrant.d:~/.local/share/vagrant.d" 1239 | ] 1240 | }, 1241 | { 1242 | "name": "virtualenv", 1243 | "paths": [ 1244 | "~/.virtualenvs" 1245 | ], 1246 | "fixes": [ 1247 | "~/.virtualenvs:~/.local/share/virtualenvs" 1248 | ] 1249 | }, 1250 | { 1251 | "name": "Visual Studio Code", 1252 | "paths": [ 1253 | "~/.vscode-oss/" 1254 | ], 1255 | "fixes": [ 1256 | "~/.vscode-oss:~/.local/share/vscode-oss" 1257 | ] 1258 | }, 1259 | { 1260 | "name": "VSCodium", 1261 | "paths": [ 1262 | "~/.vscode-oss/" 1263 | ], 1264 | "fixes": [ 1265 | "~/.vscode-oss:~/.local/share/vscodium" 1266 | ] 1267 | }, 1268 | { 1269 | "name": "w3m", 1270 | "paths": [ 1271 | "~/.w3m" 1272 | ], 1273 | "fixes": [ 1274 | "~/.w3m:~/.config/w3m" 1275 | ] 1276 | }, 1277 | { 1278 | "name": "wget", 1279 | "paths": [ 1280 | "~/.wgetrc", 1281 | "~/.wget-hsts" 1282 | ], 1283 | "fixes": [ 1284 | "~/.wgetrc:~/.config/wget/wgetrc", 1285 | "~/.wget-hsts:~/.local/share/wget-hsts" 1286 | ] 1287 | }, 1288 | { 1289 | "name": "wine", 1290 | "paths": [ 1291 | "~/.wine" 1292 | ], 1293 | "fixes": [ 1294 | "~/.wine:~/.local/share/wineprefixes/default" 1295 | ] 1296 | }, 1297 | { 1298 | "name": "xbindkeys", 1299 | "paths": [ 1300 | "~/.xbindkeysrc" 1301 | ], 1302 | "fixes": [ 1303 | "~/.xbindkeysrc:~/.config/xbindkeys/config" 1304 | ] 1305 | }, 1306 | { 1307 | "name": "z", 1308 | "paths": [ 1309 | "~/.z" 1310 | ], 1311 | "fixes": [ 1312 | "~/.z:~/.local/share/z/z" 1313 | ] 1314 | }, 1315 | { 1316 | "name": "yarn", 1317 | "paths": [ 1318 | "~/.yarnrc", 1319 | "~/.yarn/", 1320 | "~/.yarncache/", 1321 | "~/.yarn-config/" 1322 | ], 1323 | "fixes": [ 1324 | "~/.yarnrc:~/.config/yarn/yarnrc", 1325 | "~/.yarn/:~/.config/yarn/global", 1326 | "~/.yarncache/:~/.cache/yarn", 1327 | "~/.yarn-config/:~/.config/yarn/local" 1328 | ] 1329 | } 1330 | ] 1331 | -------------------------------------------------------------------------------- /fixtures/helloworld-appimage-x86_64.AppImage: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/queer/boxxy/4941c8843e6c605d955d00c59ccf7c568f224171/fixtures/helloworld-appimage-x86_64.AppImage -------------------------------------------------------------------------------- /fork-test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eoux pipefail 4 | cd ./forks 5 | python -m venv forker 6 | source forker/bin/activate 7 | pip3 install uwsgi 8 | cat >> forker.py < Result { 21 | let self_exe = std::fs::read_link("/proc/self/exe")?; 22 | Ok(self_exe 23 | .into_os_string() 24 | .to_string_lossy() 25 | .contains("target/debug")) 26 | } 27 | 28 | pub fn default_config_path() -> Result { 29 | let config_dir = dirs::config_dir().unwrap(); 30 | Ok(crate::enclosure::fs::append_all( 31 | &config_dir, 32 | vec!["boxxy", Self::default_config_file_name()?], 33 | )) 34 | } 35 | 36 | pub fn default_config_file_name() -> Result<&'static str> { 37 | if Self::debug_mode()? { 38 | Ok("boxxy-dev.yaml") 39 | } else { 40 | Ok("boxxy.yaml") 41 | } 42 | } 43 | 44 | pub fn rule_paths() -> Result> { 45 | let config_file_name = Self::default_config_file_name()?; 46 | 47 | let default_config_file = { 48 | let config_dir = dirs::config_dir().unwrap(); 49 | let config_path = 50 | crate::enclosure::fs::append_all(&config_dir, vec!["boxxy", config_file_name]); 51 | 52 | std::fs::create_dir_all(config_path.parent().unwrap())?; 53 | 54 | config_path 55 | }; 56 | 57 | let mut config_paths = vec![]; 58 | if default_config_file.exists() { 59 | config_paths.push(default_config_file); 60 | } 61 | 62 | // Search up the tree for a `config_file_name` file 63 | let mut current_dir = std::env::current_dir()?; 64 | debug!( 65 | "searching for boxxy config starting at {}", 66 | current_dir.display() 67 | ); 68 | loop { 69 | let config_path = 70 | crate::enclosure::fs::append_all(¤t_dir, vec![config_file_name]); 71 | debug!("checking for: {}", config_path.display()); 72 | if config_path.exists() { 73 | debug!("found boxxy config file at {}", config_path.display()); 74 | config_paths.push(config_path); 75 | } 76 | 77 | if let Some(parent) = current_dir.parent() { 78 | if parent == current_dir { 79 | debug!("ran out of parents to search!"); 80 | break; 81 | } 82 | current_dir = parent.to_path_buf(); 83 | } else { 84 | debug!("ran out of parents to search!"); 85 | break; 86 | } 87 | } 88 | 89 | Ok(config_paths) 90 | } 91 | 92 | pub fn load_rules_from_path(path: &Path) -> Result { 93 | let config = config::Config::builder() 94 | .add_source(config::File::new( 95 | &path.to_string_lossy(), 96 | config::FileFormat::Yaml, 97 | )) 98 | .build()?; 99 | 100 | let rules = config.try_deserialize::()?; 101 | 102 | Ok(rules) 103 | } 104 | 105 | pub fn load_rules_from_cli_flag(rules: &[String]) -> Result { 106 | let rules = rules 107 | .iter() 108 | .map(|s| { 109 | let parts: Vec<&str> = s.split(':').collect(); 110 | match parts.as_slice() { 111 | [src, dest] => Rule { 112 | name: format!("cli-loaded rule: {src} -> {dest}"), 113 | target: src.to_string(), 114 | rewrite: dest.to_string(), 115 | mode: crate::enclosure::rule::RuleMode::File, 116 | context: vec![], 117 | only: vec![], 118 | env: HashMap::new(), 119 | }, 120 | 121 | [src, dest, mode] => Rule { 122 | name: format!("cli-loaded rule: {src} -> {dest} ({mode})"), 123 | target: src.to_string(), 124 | rewrite: dest.to_string(), 125 | mode: mode.parse().unwrap(), 126 | context: vec![], 127 | only: vec![], 128 | env: HashMap::new(), 129 | }, 130 | 131 | _ => panic!("invalid format for cli rule: {s}"), 132 | } 133 | }) 134 | .collect(); 135 | Ok(BoxxyRules { rules }) 136 | } 137 | 138 | pub fn merge(configs: Vec) -> BoxxyRules { 139 | let mut merged = BoxxyRules { rules: vec![] }; 140 | for config in configs { 141 | merged.rules.extend(config.rules); 142 | } 143 | 144 | merged 145 | } 146 | 147 | pub fn load_config(args: crate::Args) -> Result { 148 | // Load rules 149 | let rules = { 150 | let mut rules = vec![]; 151 | if !args.no_config { 152 | debug!("loading rules (not asked not to!)"); 153 | for config in BoxxyConfig::rule_paths()? { 154 | info!("loading rules from {}", config.display()); 155 | rules.push(BoxxyConfig::load_rules_from_path(&config)?); 156 | } 157 | } 158 | rules.push(BoxxyConfig::load_rules_from_cli_flag(&args.arg_rules)?); 159 | BoxxyConfig::merge(rules) 160 | }; 161 | info!("loaded {} total rule(s)", rules.rules.len()); 162 | 163 | let (cmd, cmd_args) = (&args.command_with_args[0], &args.command_with_args[1..]); 164 | 165 | if which::which(cmd).is_err() { 166 | // If `which` can't find it, check if the path exists. 167 | if !Path::new(cmd).exists() { 168 | error!("command not found in $PATH or by path: {}", cmd); 169 | debug!("searched $PATH: {}", std::env::var("PATH")?); 170 | std::process::exit(1); 171 | } 172 | } 173 | 174 | let mut command = Command::new(cmd); 175 | 176 | // Pass through current env 177 | command.envs(std::env::vars()); 178 | 179 | // Pass args 180 | if !cmd_args.is_empty() { 181 | command.args(cmd_args); 182 | } 183 | 184 | Ok(Self { 185 | rules, 186 | immutable_root: args.immutable_root, 187 | trace: args.trace, 188 | dotenv: args.dotenv, 189 | daemon: args.daemon, 190 | command, 191 | }) 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/enclosure/fs.rs: -------------------------------------------------------------------------------- 1 | use std::fs::{self, OpenOptions}; 2 | use std::path::{Path, PathBuf}; 3 | 4 | use color_eyre::Result; 5 | use eyre::eyre; 6 | use log::*; 7 | use nix::mount::{mount, MsFlags}; 8 | 9 | pub struct FsDriver; 10 | 11 | #[allow(unused)] 12 | impl FsDriver { 13 | #[allow(clippy::new_without_default)] 14 | pub fn new() -> Self { 15 | Self {} 16 | } 17 | 18 | pub fn all_containers_root(&self) -> PathBuf { 19 | PathBuf::from("/tmp/boxxy-containers") 20 | } 21 | 22 | pub fn container_root(&self, name: &str) -> PathBuf { 23 | append_all(&self.all_containers_root(), vec![name]) 24 | } 25 | 26 | pub fn setup_root(&self, name: &str) -> Result<()> { 27 | debug!("setting up root for {}", name); 28 | fs::create_dir_all(self.container_root(name))?; 29 | Ok(()) 30 | } 31 | 32 | pub fn cleanup_root(&self, name: &str) -> Result<()> { 33 | debug!("cleaning up root for {}", name); 34 | fs::remove_dir_all(self.container_root(name))?; 35 | Ok(()) 36 | } 37 | 38 | pub fn bind_mount_ro(&self, src: &Path, target: &Path) -> Result<()> { 39 | debug!("bind mount {src:?} onto {target:?} as ro"); 40 | // ro bindmount is a complicated procedure: https://unix.stackexchange.com/a/128388 41 | // tldr: You first do a normal bindmount, then remount bind+ro 42 | self.bind_mount(src, target, MsFlags::MS_BIND)?; 43 | self.remount_ro(target)?; 44 | Ok(()) 45 | } 46 | 47 | pub fn remount_ro(&self, target: &Path) -> Result<()> { 48 | debug!("remount {target:?} as ro"); 49 | mount::( 50 | None, 51 | target, 52 | Some(""), 53 | MsFlags::MS_REMOUNT | MsFlags::MS_BIND | MsFlags::MS_RDONLY, 54 | Some(""), 55 | )?; 56 | Ok(()) 57 | } 58 | 59 | pub fn bind_mount_rw(&self, src: &Path, target: &Path) -> Result<()> { 60 | debug!("bind mount {src:?} onto {target:?} as rw"); 61 | self.bind_mount(src, target, MsFlags::MS_BIND) 62 | } 63 | 64 | fn bind_mount(&self, src: &Path, target: &Path, flags: MsFlags) -> Result<()> { 65 | debug!("bind mount {src:?} onto {target:?}"); 66 | 67 | // Ensure that `src` and `target` are the same type of file, erroring if they aren't 68 | if src.is_dir() && !target.is_dir() { 69 | return Err(eyre!( 70 | "Cannot bind mount a directory onto a file: {src:?} -> {target:?}" 71 | )); 72 | } 73 | if src.is_file() && !target.is_file() { 74 | return Err(eyre!( 75 | "Cannot bind mount a file onto a directory: {src:?} -> {target:?}" 76 | )); 77 | } 78 | 79 | mount( 80 | Some(src), 81 | target, 82 | Some(""), 83 | MsFlags::MS_REC | flags, 84 | Some(""), 85 | )?; 86 | Ok(()) 87 | } 88 | 89 | pub fn touch(&self, path: &Path) -> Result<()> { 90 | debug!("touching {path:?}"); 91 | match OpenOptions::new() 92 | .create(true) 93 | .truncate(false) 94 | .write(true) 95 | .open(path) 96 | { 97 | Ok(_) => Ok(()), 98 | Err(e) => Err(e.into()), 99 | } 100 | } 101 | 102 | pub fn touch_dir(&self, path: &Path) -> Result<()> { 103 | debug!("touching dir {path:?}"); 104 | match fs::create_dir_all(path) { 105 | Ok(_) => Ok(()), 106 | Err(e) => Err(e.into()), 107 | } 108 | } 109 | 110 | pub fn fully_expand_path(&self, path: &String) -> Result { 111 | let expanded = shellexpand::tilde(&path).to_string(); 112 | match Path::new(&expanded).canonicalize() { 113 | Ok(path) => match self.maybe_resolve_symlink(&path) { 114 | Ok(path) => match path.canonicalize() { 115 | Ok(canonical_path) => Ok(canonical_path), 116 | Err(_) => Ok(path), 117 | }, 118 | err @ Err(_) => err, 119 | }, 120 | Err(_) => { 121 | // If the path doesn't exist, we'll create it 122 | Ok(PathBuf::from(&expanded)) 123 | } 124 | } 125 | } 126 | 127 | #[allow(clippy::only_used_in_recursion)] 128 | pub fn maybe_resolve_symlink(&self, path: &Path) -> Result { 129 | Self::do_resolve_symlink(path, 0) 130 | } 131 | 132 | fn do_resolve_symlink(path: &Path, depth: u32) -> Result { 133 | if depth > 10 { 134 | return Err(eyre!("Too many symlinks when resolving path: {:?}", path)); 135 | } 136 | 137 | let path = if path.is_symlink() { 138 | path.read_link()?.canonicalize()? 139 | } else { 140 | path.to_path_buf() 141 | }; 142 | 143 | if path.is_symlink() { 144 | return Self::do_resolve_symlink(&path, depth + 1); 145 | } 146 | 147 | Ok(path) 148 | } 149 | } 150 | 151 | pub fn append_all>(buf: &Path, parts: Vec

) -> PathBuf { 152 | let mut buf = buf.to_path_buf(); 153 | for part in parts { 154 | let path = part.as_ref(); 155 | let path = if path.starts_with("/") { 156 | path.strip_prefix("/").unwrap() 157 | } else { 158 | path 159 | }; 160 | 161 | buf.push(path); 162 | } 163 | buf 164 | } 165 | 166 | #[cfg(test)] 167 | mod tests { 168 | use super::*; 169 | 170 | use color_eyre::Result; 171 | 172 | #[test] 173 | fn test_append_all() { 174 | let buf = PathBuf::from("/tmp"); 175 | let parts = vec!["foo", "bar", "baz"]; 176 | let expected = PathBuf::from("/tmp/foo/bar/baz"); 177 | assert_eq!(append_all(&buf, parts), expected); 178 | } 179 | 180 | #[test] 181 | fn test_fs_driver_creates_and_destroys_roots() -> Result<()> { 182 | let driver = FsDriver::new(); 183 | let name = "test-create-destroy-root"; 184 | let root = driver.container_root(name); 185 | driver.setup_root(name)?; 186 | assert!(root.exists()); 187 | driver.cleanup_root(name)?; 188 | assert!(!root.exists()); 189 | 190 | Ok(()) 191 | } 192 | 193 | #[test] 194 | fn test_fs_driver_touches_files() -> Result<()> { 195 | let driver = FsDriver::new(); 196 | let name = "test-touch-file"; 197 | let root = driver.container_root(name); 198 | driver.setup_root(name)?; 199 | let file = append_all(&root, vec!["foo"]); 200 | driver.touch(&file)?; 201 | assert!(file.exists()); 202 | driver.cleanup_root(name)?; 203 | assert!(!root.exists()); 204 | 205 | Ok(()) 206 | } 207 | 208 | #[test] 209 | fn test_fs_driver_touches_dirs() -> Result<()> { 210 | let driver = FsDriver::new(); 211 | let name = "test-touch-dir"; 212 | let root = driver.container_root(name); 213 | driver.setup_root(name)?; 214 | let dir = append_all(&root, vec!["foo"]); 215 | driver.touch_dir(&dir)?; 216 | assert!(dir.exists()); 217 | driver.cleanup_root(name)?; 218 | assert!(!root.exists()); 219 | 220 | Ok(()) 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/enclosure/linux.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::process::Command; 3 | 4 | use color_eyre::Result; 5 | use log::*; 6 | use nix::unistd::{Gid, Uid}; 7 | use regex::Regex; 8 | 9 | pub fn map_uids>(pid: I, uids: &mut HashMap) -> Result<()> { 10 | let pid = pid.into(); 11 | let mut args = vec![pid.to_string()]; 12 | for (old_uid, new_uid) in uids.iter() { 13 | args.push(old_uid.to_string()); 14 | args.push(new_uid.to_string()); 15 | args.push("1".to_string()); 16 | } 17 | 18 | let newuidmap = Command::new("newuidmap").args(args).output(); 19 | 20 | if newuidmap.is_err() { 21 | return newuidmap.map(|_| ()).map_err(|e| e.into()); 22 | } 23 | 24 | let newuidmap = newuidmap?; 25 | let stderr = String::from_utf8(newuidmap.stderr)?; 26 | if let Some(bad_uid) = check_mapping_regex(r"newuidmap: uid range \[(\d+)-.*", &stderr)? { 27 | // Remove bad uid, continue to call newuidmap until it works 28 | uids.remove(&Uid::from_raw(bad_uid)); 29 | return map_uids(pid, uids); 30 | } 31 | 32 | debug!("mapped uids {:#?}", uids); 33 | 34 | Ok(()) 35 | } 36 | 37 | pub fn map_gids>(pid: I, gids: &mut HashMap) -> Result<()> { 38 | let pid = pid.into(); 39 | let mut args = vec![pid.to_string()]; 40 | for (old_gid, new_gid) in gids.iter() { 41 | args.push(old_gid.to_string()); 42 | args.push(new_gid.to_string()); 43 | args.push("1".to_string()); 44 | } 45 | 46 | let newgidmap = Command::new("newgidmap").args(args).output(); 47 | 48 | if newgidmap.is_err() { 49 | return newgidmap.map(|_| ()).map_err(|e| e.into()); 50 | } 51 | 52 | let newgidmap = newgidmap?; 53 | let stderr = String::from_utf8(newgidmap.stderr)?; 54 | if let Some(bad_gid) = check_mapping_regex(r"newgidmap: gid range \[(\d+)-.*", &stderr)? { 55 | // Remove bad gid, continue to call newgidmap until it works 56 | gids.remove(&Gid::from_raw(bad_gid)); 57 | return map_gids(pid, gids); 58 | } 59 | 60 | debug!("mapped gids {:#?}", gids); 61 | 62 | Ok(()) 63 | } 64 | 65 | fn check_mapping_regex(regex: &str, stderr: &str) -> Result> { 66 | let regex = Regex::new(regex)?; 67 | let bad_id = regex.captures(stderr); 68 | if let Some(bad_id) = bad_id { 69 | // Remove bad id, continue to call newuidmap until it works 70 | let bad_id = bad_id.get(1).unwrap().as_str().parse::().unwrap(); 71 | Ok(Some(bad_id)) 72 | } else { 73 | Ok(None) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/enclosure/mod.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap, HashSet}; 2 | use std::ffi::CString; 3 | use std::fs::{read_to_string, File}; 4 | use std::io::Write; 5 | use std::path::{Path, PathBuf}; 6 | use std::process::exit; 7 | use std::sync::mpsc::channel; 8 | use std::thread; 9 | use std::time::Duration; 10 | 11 | use color_eyre::Result; 12 | use daemonize::Daemonize; 13 | use dotenv_parser::parse_dotenv; 14 | use haikunator::Haikunator; 15 | use log::*; 16 | use nix::errno::Errno; 17 | use nix::mount::{umount2, MntFlags}; 18 | use nix::sched::{clone, CloneFlags}; 19 | use nix::sys::wait::{waitpid, WaitPidFlag, WaitStatus}; 20 | use nix::sys::{ptrace, signal}; 21 | use nix::unistd::{chdir, chroot, getgrouplist, getpid, pivot_root, Gid, Pid, User}; 22 | use owo_colors::colors::xterm::PinkSalmon; 23 | use owo_colors::OwoColorize; 24 | use rlimit::Resource; 25 | 26 | use crate::config::BoxxyConfig; 27 | use crate::enclosure::tracer::Tracer; 28 | 29 | use self::fs::{append_all, FsDriver}; 30 | use self::rule::{Rule, RuleMode}; 31 | 32 | pub mod fs; 33 | mod linux; 34 | mod register; 35 | pub mod rule; 36 | mod syscall; 37 | mod tracer; 38 | 39 | pub struct Enclosure { 40 | config: BoxxyConfig, 41 | fs: FsDriver, 42 | name: String, 43 | child_exit_status: i32, 44 | created_files: Vec, 45 | created_directories: Vec, 46 | } 47 | 48 | impl Enclosure { 49 | pub fn new(config: BoxxyConfig) -> Self { 50 | Self { 51 | config, 52 | fs: FsDriver::new(), 53 | name: Haikunator::default().haikunate(), 54 | child_exit_status: -1, 55 | created_files: vec![], 56 | created_directories: vec![], 57 | } 58 | } 59 | 60 | pub fn run(&mut self) -> Result<()> { 61 | // Prepare the filesystem 62 | let applicable_rules = &self 63 | .config 64 | .rules 65 | .get_all_applicable_rules(self.config.command.get_program(), &self.fs)?; 66 | self.set_up_temporary_files(applicable_rules)?; 67 | 68 | // Set up the container: callback, stack, etc. 69 | let callback = || match self.run_in_container(applicable_rules) { 70 | Ok(exit_code) => exit_code, 71 | Err(err) => { 72 | error!("{}", err); 73 | -1isize 74 | } 75 | }; 76 | 77 | let stack_size = match Resource::STACK.get() { 78 | Ok((soft, _hard)) => soft as usize, 79 | Err(_) => { 80 | // 8MB 81 | 8 * 1024 * 1024 82 | } 83 | }; 84 | 85 | let mut stack_vec = vec![0u8; stack_size]; 86 | let stack: &mut [u8] = stack_vec.as_mut_slice(); 87 | 88 | // Clone off the container process 89 | // SAFETY: we ask the OS for the right stack size, and failover to a 90 | // safe, probably-oversized stack in case. 91 | let pid = unsafe { 92 | clone( 93 | Box::new(callback), 94 | stack, 95 | CloneFlags::CLONE_NEWNS | CloneFlags::CLONE_NEWUSER, 96 | Some(nix::sys::signal::Signal::SIGCHLD as i32), 97 | )? 98 | }; 99 | if pid.as_raw() == -1 { 100 | return Err(std::io::Error::last_os_error().into()); 101 | } 102 | 103 | // Await PTRACE_TRACEME from child 104 | waitpid(pid, Some(WaitPidFlag::WSTOPPED))?; 105 | debug!("child stopped!"); 106 | 107 | // Map current UID + GID into the container so that things continue to 108 | // work as expected. 109 | 110 | // Get current UID + GID 111 | let uid = nix::unistd::geteuid(); 112 | let gid = nix::unistd::getegid(); 113 | 114 | // Call newuidmap + newgidmap 115 | 116 | // TODO: This is hacky. I don't like this. 117 | // It's... difficult... to map uids/gids properly. There is a proper 118 | // mechanism for doing so, but it's a part of the `shadow` package, and 119 | // I don't want to generate C bindings right now. Instead, this just 120 | // tries to map them over and over, removing broken uids/gids until it 121 | // happens to work. 122 | // This isn't optimal, but it works. 123 | if let Some(user) = User::from_uid(uid)? { 124 | let mut uid_map = HashMap::new(); 125 | uid_map.insert(user.uid, user.uid); 126 | 127 | linux::map_uids(pid, &mut uid_map)?; 128 | 129 | let mut gid_map = HashMap::new(); 130 | gid_map.insert(user.gid, user.gid); 131 | gid_map.insert(Gid::from_raw(0), Gid::from_raw(0)); 132 | getgrouplist(&CString::new(user.name)?, gid)? 133 | .iter() 134 | .for_each(|gid| { 135 | gid_map.insert(*gid, *gid); 136 | }); 137 | 138 | linux::map_gids(pid, &mut gid_map)?; 139 | 140 | debug!("finished setting up uid/gid mapping"); 141 | } else { 142 | unreachable!("it should be impossible to have a user that doesn't have your uid"); 143 | } 144 | 145 | // Set up ^C handling 146 | let name_clone = self.name.clone(); 147 | let pid_clone = pid.as_raw(); 148 | #[allow(unused_must_use)] 149 | ctrlc::set_handler(move || { 150 | nix::sys::signal::kill( 151 | nix::unistd::Pid::from_raw(pid_clone), 152 | nix::sys::signal::SIGTERM, 153 | ); 154 | FsDriver::new().cleanup_root(&name_clone); 155 | exit(1); 156 | })?; 157 | 158 | // Restart stopped child if not tracing 159 | if self.config.trace { 160 | self.run_with_tracing(pid)?; 161 | } else { 162 | match ptrace::detach(pid, None) { 163 | Ok(_) => { 164 | self.run_without_tracing(pid)?; 165 | } 166 | Err(Errno::ESRCH) => { 167 | error!("child exited early (ESRCH)! try running boxxy with `-l debug` or `-l trace` if it isn't obvious why"); 168 | return Ok(()); 169 | } 170 | err => return Ok(err?), 171 | } 172 | } 173 | 174 | Ok(()) 175 | } 176 | 177 | #[allow(unreachable_code)] 178 | fn run_with_tracing(&mut self, pid: Pid) -> Result<()> { 179 | Tracer::flag(pid)?; 180 | let (tx, rx) = channel(); 181 | 182 | debug!("restarting child and starting tracer!"); 183 | ptrace::syscall(pid, None)?; 184 | Tracer::new(pid).run(tx)?; 185 | debug!("tracing finished!"); 186 | 187 | match waitpid(pid, None)? { 188 | WaitStatus::Exited(_pid, status) => { 189 | self.child_exit_status = status; 190 | } 191 | _ => unreachable!("child should have exited!"), 192 | } 193 | 194 | let mut buffer = String::new(); 195 | let mut seen_paths = HashSet::new(); 196 | let mut counter = 0; 197 | { 198 | use std::fmt::Write; 199 | while let Ok(syscall) = rx.recv() { 200 | if let Some(path) = syscall.path { 201 | let container_root = self.fs.container_root(&self.name); 202 | 203 | if path.starts_with(&container_root) && !seen_paths.contains(&path) { 204 | writeln!(buffer, "/{}", path.strip_prefix(&container_root)?.display())?; 205 | seen_paths.insert(path); 206 | counter += 1; 207 | } 208 | } 209 | } 210 | writeln!(buffer, "# total: {counter}")?; 211 | } 212 | 213 | let mut file = File::create("./boxxy-report.txt")?; 214 | file.write_all(buffer.as_bytes())?; 215 | info!("wrote trace report to boxxy-report.txt"); 216 | 217 | exit(self.child_exit_status); 218 | } 219 | 220 | fn run_without_tracing(&mut self, pid: Pid) -> Result<()> { 221 | // Wait for exit 222 | let mut exit_status: i32 = -1; 223 | loop { 224 | match waitpid(pid, None) { 225 | Ok(WaitStatus::Exited(_pid, status)) => { 226 | exit_status = status; 227 | break; 228 | } 229 | Err(nix::errno::Errno::ECHILD) => { 230 | // We might need to wait to let stdout/err buffer 231 | thread::sleep(Duration::from_millis(100)); 232 | break; 233 | } 234 | _ => thread::sleep(Duration::from_millis(100)), 235 | } 236 | } 237 | self.child_exit_status = exit_status; 238 | 239 | // Clean up! 240 | self.fs.cleanup_root(&self.name)?; 241 | self.clean_up_container()?; 242 | 243 | // All done! Return the child's exit status 244 | debug!("exiting with status {}", self.child_exit_status); 245 | exit(self.child_exit_status); 246 | } 247 | 248 | fn set_up_temporary_files(&mut self, applicable_rules: &[Rule]) -> Result> { 249 | for rule in applicable_rules { 250 | debug!("processing path creation for rule '{}'", rule.name); 251 | 252 | let expanded_target = self.fs.fully_expand_path(&rule.target)?; 253 | let target_path = self.fs.maybe_resolve_symlink(&expanded_target)?; 254 | 255 | let rewrite_path = self.fs.fully_expand_path(&rule.rewrite)?; 256 | 257 | debug!("temp files: ensuring path: {target_path:?}"); 258 | debug!("temp files: rewriting to: {rewrite_path:?}"); 259 | 260 | match rule.mode { 261 | RuleMode::File => { 262 | self.ensure_file(&rewrite_path)?; 263 | if self.ensure_file(&target_path)? { 264 | self.created_files.push(target_path.clone()); 265 | } 266 | } 267 | RuleMode::Directory => { 268 | self.ensure_directory(&rewrite_path)?; 269 | if self.ensure_directory(&target_path)? { 270 | self.created_directories.push(target_path.clone()); 271 | } 272 | } 273 | } 274 | 275 | debug!("temp files: rewrote base path {rewrite_path:?} => {target_path:?}"); 276 | } 277 | 278 | Ok(vec![]) 279 | } 280 | 281 | fn set_up_container(&mut self, applicable_rules: &[Rule]) -> Result<()> { 282 | // Load .env vars 283 | if self.config.dotenv { 284 | debug!("dotenv enabled!"); 285 | if let Ok(dotenv_file) = dotenv::dotenv() { 286 | debug!("found dotenv path: {dotenv_file:?}"); 287 | info!("loading env vars from {}", dotenv_file.display()); 288 | // TODO: bleh error handling 289 | let dotenv = parse_dotenv(&read_to_string(dotenv_file)?).unwrap(); 290 | for (key, value) in dotenv.iter() { 291 | self.config.command.env(key, value); 292 | debug!("loaded env var: {}=********", key); 293 | } 294 | info!("loaded {} env vars", dotenv.len()); 295 | } 296 | } 297 | 298 | // Load env vars from applicable rules 299 | for rule in applicable_rules { 300 | for (key, value) in rule.env.iter() { 301 | self.config.command.env(key, value); 302 | debug!("loaded env var: {}=********", key); 303 | } 304 | if !rule.env.is_empty() { 305 | debug!( 306 | "loaded {} env vars from rule '{}'", 307 | rule.env.len(), 308 | rule.name 309 | ); 310 | } 311 | } 312 | 313 | // Mount root RW 314 | debug!("setup root"); 315 | self.fs.setup_root(&self.name)?; 316 | let container_root = self.fs.container_root(&self.name); 317 | debug!("bind mount root rw"); 318 | self.fs.bind_mount_rw(Path::new("/"), &container_root)?; 319 | 320 | // Apply all rules via bind mounts 321 | debug!("applying {} rules", applicable_rules.len()); 322 | for rule in applicable_rules { 323 | debug!("applying rule '{}'", rule.name); 324 | 325 | let expanded_target = self.fs.fully_expand_path(&rule.target)?; 326 | // Rewrite target path into the container 327 | let target_path = 328 | match append_all(&container_root, vec![&expanded_target]).canonicalize() { 329 | Ok(path) => path, 330 | Err(_) => { 331 | // If the path doesn't exist, we'll create it 332 | append_all(&container_root, vec![&expanded_target]) 333 | } 334 | }; 335 | let target_path = self.fs.maybe_resolve_symlink(&target_path)?; 336 | 337 | let rewrite_path = self.fs.fully_expand_path(&rule.rewrite)?; 338 | 339 | debug!("rule apply: source exists: {}", rewrite_path.exists()); 340 | debug!("rule apply: target exists: {}", target_path.exists()); 341 | 342 | // If the target file doesn't exist, we have to create it in order to bind mount over it. 343 | match rule.mode { 344 | RuleMode::File => { 345 | if !target_path.exists() { 346 | debug!("creating file: {target_path:?}"); 347 | self.ensure_file(&target_path)?; 348 | self.created_files.push(target_path.clone()); 349 | } 350 | self.fs.bind_mount_rw(&rewrite_path, &target_path)?; 351 | } 352 | RuleMode::Directory => { 353 | if !target_path.exists() { 354 | debug!("creating directory: {target_path:?}"); 355 | self.ensure_directory(&target_path)?; 356 | self.created_files.push(target_path.clone()); 357 | } 358 | self.fs.bind_mount_rw(&rewrite_path, &target_path)?; 359 | } 360 | } 361 | 362 | debug!("rule apply: rewrote base path {rewrite_path:?} => {target_path:?}"); 363 | } 364 | 365 | Ok(()) 366 | } 367 | 368 | fn clean_up_container(&mut self) -> Result<()> { 369 | debug!( 370 | "{}", 371 | format!( 372 | "cleaning up {} path(s) ♥", 373 | self.created_directories.len() + self.created_files.len() 374 | ) 375 | .if_supports_color(owo_colors::Stream::Stdout, |text| text.fg::()) 376 | ); 377 | for file in &self.created_files { 378 | debug!("removing temporary file {}", file.display()); 379 | std::fs::remove_file(file)?; 380 | } 381 | for dir in (&self.created_directories).iter().rev() { 382 | debug!("removing temporary directory {}", dir.display()); 383 | std::fs::remove_dir(dir)?; 384 | } 385 | 386 | Ok(()) 387 | } 388 | 389 | fn run_in_container(&mut self, applicable_rules: &[Rule]) -> Result { 390 | // TODO: There HAS to be a better way than this... 391 | let mut grep = grep::searcher::SearcherBuilder::new().build(); 392 | 393 | let path_to_input_binary = { 394 | let program = self.config.command.get_program(); 395 | match which::which(program) { 396 | Ok(path) => path, 397 | Err(_) => { 398 | // Check if it's a path we can resolve 399 | let path = PathBuf::from(program); 400 | if path.exists() { 401 | path 402 | } else { 403 | return Err(eyre::eyre!("could not resolve binary: {program:?}")); 404 | } 405 | } 406 | } 407 | }; 408 | 409 | // Search input binary for `--appimage-help` `--appimage-mount` and 410 | // `--appimage-extract`. 411 | // If it has all of these, it's PROBABLY an AppImage, and we should 412 | // warn the end-user that they need to extract it first. 413 | // TODO: Could we do this automatically? 414 | let mut found_appimage_help = false; 415 | let mut found_appimage_mount = false; 416 | let mut found_appimage_extract = false; 417 | let matcher = grep::regex::RegexMatcher::new( 418 | r"(--appimage-help|--appimage-mount|--appimage-extract)", 419 | )?; 420 | grep.search_path( 421 | matcher, 422 | path_to_input_binary, 423 | // TODO: Write a sink that doesn't care about line numbers and won't raise 424 | grep::searcher::sinks::UTF8(|_, line| { 425 | if line.contains("--appimage-help") { 426 | found_appimage_help = true; 427 | } else if line.contains("--appimage-mount") { 428 | found_appimage_mount = true; 429 | } else if line.contains("--appimage-extract") { 430 | found_appimage_extract = true; 431 | } 432 | Ok(true) 433 | }), 434 | )?; 435 | 436 | // If the user is autoextracting the AppImage, we don't want to tell 437 | // them to extract it first. 438 | let mut self_extracting = false; 439 | for arg in self.config.command.get_args() { 440 | if arg == "--appimage-extract-and-run" { 441 | info!( 442 | "self-extracting AppImages may take a while to extract! please be patient (:" 443 | ); 444 | self_extracting = true; 445 | debug!("self-extracting appimage detected!"); 446 | break; 447 | } 448 | } 449 | 450 | if found_appimage_extract && found_appimage_help && found_appimage_mount && !self_extracting 451 | { 452 | return Err(eyre::eyre!( 453 | "{program:?} is an AppImage! Please extract it first with --appimage-extract. You can also use --appimage-extract-and-run. For more information, see https://github.com/AppImage/AppImageKit/wiki/FUSE#fallback", 454 | program = self.config.command.get_program() 455 | )); 456 | } 457 | 458 | self.set_up_container(applicable_rules)?; 459 | 460 | let pwd = std::env::current_dir()?; 461 | 462 | if self.config.trace { 463 | chroot(&self.fs.container_root(&self.name))?; 464 | chdir(&pwd)?; 465 | } else { 466 | chdir(&self.fs.container_root(&self.name))?; 467 | pivot_root(".", ".")?; 468 | umount2(".", MntFlags::MNT_DETACH)?; 469 | chdir(&pwd)?; 470 | } 471 | 472 | // Remount rootfs as ro 473 | if self.config.immutable_root { 474 | debug!("remounting rootfs as ro!"); 475 | self.fs.remount_ro(Path::new("/"))?; 476 | } 477 | 478 | debug!( 479 | "chrooted to {}", 480 | self.fs.container_root(&self.name).display() 481 | ); 482 | 483 | // Initiate ptrace with the parent process 484 | ptrace::traceme()?; 485 | signal::kill(getpid(), signal::SIGSTOP)?; 486 | 487 | // We have to set the child subreaper so that we can track 488 | // grand-*children effectively. See https://github.com/queer/boxxy/issues/62 489 | debug!("setting CHILD_SUBREAPER to {}", getpid()); 490 | unsafe { libc::prctl(libc::PR_SET_CHILD_SUBREAPER, getpid()) }; 491 | 492 | // Do the thing! 493 | debug!("running command: {:?}", self.config.command.get_program()); 494 | info!( 495 | "{}", 496 | format!("boxed {:?} ♥", self.config.command.get_program()) 497 | .if_supports_color(owo_colors::Stream::Stdout, |text| text.fg::()) 498 | ); 499 | 500 | debug!("and spawn!"); 501 | let child = self.config.command.spawn()?; // .wait()?; 502 | 503 | debug!("checking daemonisation needs"); 504 | if self.config.daemon { 505 | let now = std::time::SystemTime::now() 506 | .duration_since(std::time::UNIX_EPOCH) 507 | .unwrap() 508 | .as_secs(); 509 | let stdout = File::create(format!("/tmp/boxxy-{now}.stdout"))?; 510 | let stderr = File::create(format!("/tmp/boxxy-{now}.stderr"))?; 511 | 512 | let out = Daemonize::new().stdout(stdout).stderr(stderr).execute(); 513 | if out.is_parent() { 514 | info!("daemonized!"); 515 | info!("read logs from /tmp/boxxy-{now}.{{stdout,stderr}}."); 516 | return Ok(0); 517 | } 518 | } 519 | 520 | debug!("waiting for child exit..."); 521 | let child_exit_status = unsafe { 522 | let mut exit_status = -1; 523 | loop { 524 | let mut wstatus = -1; 525 | let wpid = libc::wait(&mut wstatus); 526 | if wpid == -1 && Errno::last() != Errno::ECHILD { 527 | warn!("!!! NOT ECHLD"); 528 | break; 529 | } 530 | if wpid == child.id() as i32 { 531 | debug!("primary child exited with status {wstatus}!"); 532 | exit_status = wstatus; 533 | } 534 | if exit_status >= 0 && wpid == -1 { 535 | debug!("execution finished!"); 536 | break; 537 | } 538 | } 539 | exit_status 540 | }; 541 | 542 | debug!("command exited with status: {:?}", child); 543 | 544 | Ok(child_exit_status.try_into()?) 545 | } 546 | 547 | fn ensure_file(&self, path: &Path) -> Result { 548 | if !path.exists() { 549 | if let Some(parent) = path.parent() { 550 | if !parent.exists() { 551 | self.fs.touch_dir(parent)?; 552 | } 553 | } 554 | self.fs.touch(path)?; 555 | Ok(true) 556 | } else { 557 | Ok(false) 558 | } 559 | } 560 | 561 | fn ensure_directory(&self, path: &Path) -> Result { 562 | if !path.exists() { 563 | self.fs.touch_dir(path)?; 564 | Ok(true) 565 | } else { 566 | Ok(false) 567 | } 568 | } 569 | } 570 | -------------------------------------------------------------------------------- /src/enclosure/register.rs: -------------------------------------------------------------------------------- 1 | macro_rules! string_registers { 2 | ($($t:tt)*) => { 3 | #[allow(unused)] 4 | #[derive(Debug, Hash, PartialEq, Eq, Clone, Copy, PartialOrd, Ord)] 5 | pub enum StringRegister { 6 | $($t)* 7 | } 8 | }; 9 | } 10 | 11 | #[cfg(target_arch = "x86_64")] 12 | macro_rules! syscall_number_from_user_regs { 13 | ($regs: ident) => { 14 | $regs.orig_rax 15 | }; 16 | } 17 | 18 | #[cfg(target_arch = "riscv64")] 19 | macro_rules! syscall_number_from_user_regs { 20 | ($regs: ident) => { 21 | $regs.a7 22 | }; 23 | } 24 | 25 | #[cfg(target_arch = "x86_64")] 26 | string_registers! { 27 | Rdi, 28 | Rsi, 29 | Rdx, 30 | Rcx, 31 | R8, 32 | R9, 33 | } 34 | 35 | #[cfg(target_arch = "riscv64")] 36 | string_registers! { 37 | A0, 38 | A1, 39 | A2, 40 | A3, 41 | A4, 42 | A5 43 | } 44 | 45 | #[cfg(target_arch = "x86_64")] 46 | macro_rules! get_register_from_regs { 47 | ($string_register: expr, $registers: ident) => { 48 | match $string_register { 49 | StringRegister::Rdi => $registers.rdi, 50 | StringRegister::Rsi => $registers.rsi, 51 | StringRegister::Rdx => $registers.rdx, 52 | StringRegister::Rcx => $registers.rcx, 53 | StringRegister::R8 => $registers.r8, 54 | StringRegister::R9 => $registers.r9, 55 | } 56 | }; 57 | } 58 | 59 | #[cfg(target_arch = "riscv64")] 60 | macro_rules! get_register_from_regs { 61 | ($string_register: expr, $registers: ident) => { 62 | match $string_register { 63 | StringRegister::A0 => $registers.a0, 64 | StringRegister::A1 => $registers.a1, 65 | StringRegister::A2 => $registers.a2, 66 | StringRegister::A3 => $registers.a3, 67 | StringRegister::A4 => $registers.a4, 68 | StringRegister::A5 => $registers.a5, 69 | } 70 | }; 71 | } 72 | 73 | pub(crate) use get_register_from_regs; 74 | pub(crate) use syscall_number_from_user_regs; 75 | -------------------------------------------------------------------------------- /src/enclosure/rule.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::ffi::OsStr; 3 | use std::path::{Path, PathBuf}; 4 | use std::str::FromStr; 5 | 6 | use color_eyre::Result; 7 | use log::*; 8 | use serde::{Deserialize, Serialize}; 9 | 10 | use super::fs::FsDriver; 11 | 12 | /// Container for deserialisation 13 | #[derive(Debug, Clone, Deserialize, Serialize)] 14 | pub struct BoxxyRules { 15 | pub rules: Vec, 16 | } 17 | 18 | impl BoxxyRules { 19 | pub fn get_all_applicable_rules(&self, binary: &OsStr, fs: &FsDriver) -> Result> { 20 | let mut applicable_rules = vec![]; 21 | 22 | for rule in &self.rules { 23 | debug!("{}: checking if rule applies to binary", rule.name); 24 | if rule.currently_in_context(fs)? && rule.applies_to_binary(binary, fs)? { 25 | debug!("{}: rule applies to binary via only + context!", rule.name); 26 | applicable_rules.push(rule.clone()); 27 | } else if rule.applies_to_binary(binary, fs)? { 28 | debug!( 29 | "{}: rule applies to binary via only but NOT context!", 30 | rule.name 31 | ); 32 | applicable_rules.push(rule.clone()); 33 | } 34 | } 35 | 36 | Ok(applicable_rules) 37 | } 38 | } 39 | 40 | #[derive(Debug, Clone, Deserialize, Serialize)] 41 | pub struct Rule { 42 | /// The name of this rule 43 | pub name: String, 44 | /// The target directory/file of this rule, ie the path that will be 45 | /// shadowed. 46 | pub target: String, 47 | /// The path to shadow the target with. 48 | pub rewrite: String, 49 | /// The mode of the rule, ie whether the target is a file or a directory. 50 | #[serde(default = "default_rule_mode")] 51 | pub mode: RuleMode, 52 | /// The context of the rule, ie the full path to the directories where this rule applies. 53 | #[serde(default = "empty_vec")] 54 | pub context: Vec, 55 | /// The binaries that this rule applies to. If this is not specified, or if 56 | /// this is an empty list, then the rule applies to all binaries. 57 | #[serde(default = "empty_vec")] 58 | pub only: Vec, 59 | /// Environment variables that this rule applies if it matches. Any env 60 | /// vars listed here will be injected into the environment of the command 61 | /// that is being boxxed. 62 | #[serde(default = "empty_hashmap")] 63 | pub env: HashMap, 64 | } 65 | 66 | impl Rule { 67 | pub fn currently_in_context(&self, fs: &FsDriver) -> Result { 68 | if self.context.is_empty() { 69 | return Ok(true); 70 | } 71 | 72 | for context in &self.context { 73 | debug!("{}: resolving context: {}", self.name, context); 74 | let expanded_context = shellexpand::tilde(&context).to_string(); 75 | let expanded_context = Path::new(&expanded_context).canonicalize()?; 76 | let resolved_context = fs.maybe_resolve_symlink(&expanded_context)?; 77 | 78 | let pwd = std::env::current_dir()?; 79 | 80 | debug!( 81 | "{}: {} <> {}", 82 | self.name, 83 | pwd.display(), 84 | resolved_context.display() 85 | ); 86 | 87 | if pwd.starts_with(&resolved_context) { 88 | return Ok(true); 89 | } 90 | } 91 | 92 | Ok(false) 93 | } 94 | 95 | pub fn applies_to_binary(&self, program: &OsStr, fs: &FsDriver) -> Result { 96 | if self.only.is_empty() { 97 | return Ok(true); 98 | } 99 | 100 | for rule_binary in &self.only { 101 | if self.test_program(program, &PathBuf::from(rule_binary), fs)? { 102 | debug!("{}: rule applies to binary!", self.name); 103 | return Ok(true); 104 | } 105 | } 106 | 107 | Ok(false) 108 | } 109 | 110 | fn test_program(&self, program: &OsStr, rule_binary: &Path, fs: &FsDriver) -> Result { 111 | debug!( 112 | "{}: testing program: program={program:?}, rule_binary={rule_binary:?}", 113 | self.name 114 | ); 115 | 116 | // Compare program by file name, ex. ls == ls 117 | if let Some(file_name) = rule_binary.file_name() { 118 | debug!("{}: comparing file names: program={program:?}, rule binary file_name={file_name:?}", self.name); 119 | if program == file_name { 120 | return Ok(true); 121 | } 122 | } 123 | 124 | // Compare by given paths, ex. ls == /usr/bin/ls 125 | if let Some(path) = rule_binary.to_str() { 126 | debug!("{}: comparing binaries by given paths: program={program:?}, rule_binary={rule_binary:?}", self.name); 127 | if program == path { 128 | return Ok(true); 129 | } 130 | } 131 | 132 | // Fully expand rule path and program path, and compare. ex. /usr/bin/ls == /bin/ls 133 | let expanded_user_program = fs.fully_expand_path(&program.to_string_lossy().to_string())?; 134 | if let Ok(expanded_rule_binary) = rule_binary.canonicalize() { 135 | debug!("{}: comparing binaries by full expansion: expanded_user_program={expanded_user_program:?}, expanded_rule_binary={expanded_rule_binary:?}", self.name); 136 | if expanded_rule_binary == expanded_user_program { 137 | return Ok(true); 138 | } 139 | 140 | // Resolve rule path and program path as symlinks, and compare. ex. /bin/ls == /bin/ls 141 | let resolved_rule_binary = fs.maybe_resolve_symlink(&expanded_rule_binary)?; 142 | let resolved_user_program = fs.maybe_resolve_symlink(&expanded_user_program)?; 143 | debug!("{}: comparing binaries as resolved symlinks: resolved_user_program={resolved_user_program:?}, resolved_rule_binary={resolved_rule_binary:?}", self.name); 144 | if resolved_rule_binary == resolved_user_program { 145 | return Ok(true); 146 | } 147 | } else { 148 | // If we can't canonicalize the rule binary, try to resolve the 149 | // user program symlinks. 150 | let resolved_user_program = fs.maybe_resolve_symlink(&expanded_user_program)?; 151 | debug!("{}: comparing rule binary to user program as resolved symlinks: resolved_user_program={resolved_user_program:?}, rule_binary={rule_binary:?}", self.name); 152 | if let Some(file_name) = resolved_user_program.file_name() { 153 | if file_name == rule_binary { 154 | debug!("{}: rule binary {rule_binary:?} matches user program file name for {resolved_user_program:?}", self.name); 155 | return Ok(true); 156 | } 157 | } else if rule_binary == resolved_user_program { 158 | debug!("{}: rule binary {rule_binary:?} matches user program {resolved_user_program:?}", self.name); 159 | return Ok(true); 160 | } 161 | } 162 | 163 | // Resolve both program and rule_binary with `which` and compare. ex. /usr/bin/ls == /usr/bin/ls 164 | let which_rule_binary = match which::which(rule_binary) { 165 | Ok(which_rule_binary) => Some(which_rule_binary), 166 | Err(_) => None, 167 | }; 168 | let which_user_program = match which::which(program) { 169 | Ok(which_user_program) => Some(which_user_program), 170 | Err(_) => None, 171 | }; 172 | debug!("{}: comparing binaries with which(1): which_user_program={which_user_program:?}, which_rule_binary={which_rule_binary:?}", self.name); 173 | if which_rule_binary == which_user_program 174 | && (which_rule_binary.is_some() || which_user_program.is_some()) 175 | { 176 | return Ok(true); 177 | } 178 | 179 | debug!("{}: rule didn't match anything, does not apply!", self.name); 180 | Ok(false) 181 | } 182 | } 183 | 184 | fn default_rule_mode() -> RuleMode { 185 | RuleMode::Directory 186 | } 187 | 188 | fn empty_vec() -> Vec { 189 | Vec::new() 190 | } 191 | 192 | fn empty_hashmap() -> HashMap { 193 | HashMap::new() 194 | } 195 | 196 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)] 197 | #[serde(rename_all = "lowercase")] 198 | pub enum RuleMode { 199 | File, 200 | Directory, 201 | } 202 | 203 | impl FromStr for RuleMode { 204 | type Err = String; 205 | 206 | fn from_str(s: &str) -> Result { 207 | match s { 208 | "file" => Ok(RuleMode::File), 209 | "directory" | "dir" => Ok(RuleMode::Directory), 210 | _ => Err(format!("invalid rule mode: {}", s)), 211 | } 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /src/enclosure/syscall/mod.rs: -------------------------------------------------------------------------------- 1 | use cfg_if::cfg_if; 2 | use color_eyre::Result; 3 | use nix::unistd::Pid; 4 | use std::{fs, path::PathBuf}; 5 | 6 | use super::{ 7 | register::{get_register_from_regs, syscall_number_from_user_regs, StringRegister}, 8 | tracer::{ChildProcess, PtraceRegisters, Tracer}, 9 | }; 10 | 11 | #[allow(unused)] 12 | fn get_fd_path(pid: Pid, fd: i32) -> Result> { 13 | let fd_path = format!("/proc/{pid}/fd/{fd}"); 14 | match fs::read_link(fd_path) { 15 | Ok(path) => { 16 | if path.starts_with("pipe:[") { 17 | Ok(None) 18 | } else { 19 | Ok(Some(path)) 20 | } 21 | } 22 | Err(_) => Ok(None), 23 | } 24 | } 25 | 26 | #[allow(unused)] 27 | #[derive(Debug, Clone)] 28 | pub struct Syscall { 29 | pub name: String, 30 | pub number: u64, 31 | pub path: Option, 32 | } 33 | 34 | pub fn handle_syscall(tracer: &Tracer, pid: Pid) -> Result> { 35 | let child = match tracer.get_child(pid) { 36 | Some(child) => child, 37 | None => unreachable!( 38 | "should never get a child from the tracer that the tracer doesn't know about" 39 | ), 40 | }; 41 | let registers = child.get_registers()?; 42 | let syscall_no = syscall_number_from_user_regs!(registers); 43 | if let Some(syscall_name) = syscall_numbers::native::sys_call_name(syscall_no.try_into()?) { 44 | let path = get_path_from_syscall(child, syscall_no, &mut registers.clone())?; 45 | let syscall = Syscall { 46 | name: syscall_name.to_string(), 47 | number: syscall_no, 48 | path, 49 | }; 50 | 51 | Ok(Some(syscall)) 52 | } else { 53 | Ok(None) 54 | } 55 | } 56 | 57 | fn get_path_from_syscall( 58 | child: &ChildProcess, 59 | syscall_no: u64, 60 | registers: &mut PtraceRegisters, 61 | ) -> Result> { 62 | if let Some(register) = SYSCALL_REGISTERS.get(&(syscall_no as i64)) { 63 | let path_ptr = get_register_from_regs!(register, registers); 64 | let path = match child.read_string(register, path_ptr as *mut _) { 65 | Ok(path) => PathBuf::from(path), 66 | Err(_) => match get_fd_path(child.pid(), path_ptr as i32) { 67 | Ok(Some(path)) => path, 68 | Ok(None) => return Ok(None), 69 | Err(_) => return Ok(None), 70 | }, 71 | }; 72 | 73 | Ok(Some(path)) 74 | } else { 75 | Ok(None) 76 | } 77 | } 78 | 79 | cfg_if! { 80 | if #[cfg(target_arch = "x86_64")] { 81 | mod x86_64; 82 | pub use x86_64::*; 83 | } else if #[cfg(target_arch = "riscv64")] { 84 | mod riscv64; 85 | pub use riscv64::*; 86 | } else { 87 | compile_error!("The current architecture is unsupported!"); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/enclosure/syscall/riscv64.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crate::enclosure::register::StringRegister; 4 | 5 | lazy_static::lazy_static! { 6 | pub static ref SYSCALL_REGISTERS: HashMap = { 7 | let mut m = HashMap::new(); 8 | // read/write 9 | m.insert(libc::SYS_read, StringRegister::A0); 10 | m.insert(libc::SYS_write, StringRegister::A0); 11 | 12 | // openat 13 | m.insert(libc::SYS_openat, StringRegister::A1); 14 | 15 | // close 16 | m.insert(libc::SYS_close, StringRegister::A0); 17 | 18 | // unlinkat 19 | m.insert(libc::SYS_unlinkat, StringRegister::A1); 20 | 21 | // fstat 22 | m.insert(libc::SYS_fstat, StringRegister::A0); 23 | // statx 24 | m.insert(libc::SYS_statx, StringRegister::A0); 25 | // newfstatat 26 | m.insert(libc::SYS_newfstatat, StringRegister::A0); 27 | 28 | // lseek 29 | m.insert(libc::SYS_lseek, StringRegister::A0); 30 | 31 | // pread64/pwrite64/preadv/pwritev 32 | m.insert(libc::SYS_pread64, StringRegister::A0); 33 | m.insert(libc::SYS_pwrite64, StringRegister::A0); 34 | m.insert(libc::SYS_preadv, StringRegister::A0); 35 | m.insert(libc::SYS_pwritev, StringRegister::A0); 36 | 37 | // faccessat/faccessat2 38 | m.insert(libc::SYS_faccessat, StringRegister::A1); 39 | m.insert(libc::SYS_faccessat2, StringRegister::A1); 40 | 41 | // dup/dup3 42 | m.insert(libc::SYS_dup, StringRegister::A0); 43 | m.insert(libc::SYS_dup3, StringRegister::A0); 44 | 45 | // sendfile 46 | m.insert(libc::SYS_sendfile, StringRegister::A0); 47 | 48 | // fcntl 49 | m.insert(libc::SYS_fcntl, StringRegister::A0); 50 | 51 | // fsync/fdatasync 52 | m.insert(libc::SYS_fsync, StringRegister::A0); 53 | m.insert(libc::SYS_fdatasync, StringRegister::A0); 54 | 55 | // truncate/ftruncate 56 | m.insert(libc::SYS_truncate, StringRegister::A0); 57 | m.insert(libc::SYS_ftruncate, StringRegister::A0); 58 | 59 | // getdents64 60 | m.insert(libc::SYS_getdents64, StringRegister::A0); 61 | 62 | // chdir/fchdir 63 | m.insert(libc::SYS_chdir, StringRegister::A0); 64 | m.insert(libc::SYS_fchdir, StringRegister::A0); 65 | 66 | // renameat2 67 | // TODO: add renameat2 to x86_64 68 | m.insert(libc::SYS_renameat2, StringRegister::A1); 69 | 70 | // mkdirat 71 | m.insert(libc::SYS_mkdirat, StringRegister::A1); 72 | 73 | // linkat/symlinkat/unlinkat 74 | m.insert(libc::SYS_linkat, StringRegister::A1); 75 | m.insert(libc::SYS_symlinkat, StringRegister::A1); 76 | m.insert(libc::SYS_unlinkat, StringRegister::A0); 77 | 78 | // fchmod/fchown 79 | m.insert(libc::SYS_fchmod, StringRegister::A0); 80 | m.insert(libc::SYS_fchown, StringRegister::A0); 81 | 82 | // fchownat/fchmodat 83 | m.insert(libc::SYS_fchownat, StringRegister::A1); 84 | m.insert(libc::SYS_fchmodat, StringRegister::A1); 85 | 86 | // mknodat 87 | m.insert(libc::SYS_mknodat, StringRegister::A1); 88 | 89 | // pivot_root 90 | m.insert(libc::SYS_pivot_root, StringRegister::A0); 91 | 92 | // chroot 93 | m.insert(libc::SYS_chroot, StringRegister::A0); 94 | 95 | // mount/umount2 96 | m.insert(libc::SYS_mount, StringRegister::A0); 97 | m.insert(libc::SYS_umount2, StringRegister::A0); 98 | 99 | // swapon/swapoff 100 | m.insert(libc::SYS_swapon, StringRegister::A0); 101 | m.insert(libc::SYS_swapoff, StringRegister::A0); 102 | 103 | // readahead 104 | m.insert(libc::SYS_readahead, StringRegister::A0); 105 | 106 | // setxattr/lsetxattr/fsetxattr/getxattr/lgetxattr/fgetxattr/listxattr/llistxattr/flistxattr/removexattr/lremovexattr/fremovexattr 107 | m.insert(libc::SYS_setxattr, StringRegister::A0); 108 | m.insert(libc::SYS_lsetxattr, StringRegister::A0); 109 | m.insert(libc::SYS_fsetxattr, StringRegister::A0); 110 | m.insert(libc::SYS_getxattr, StringRegister::A0); 111 | m.insert(libc::SYS_lgetxattr, StringRegister::A0); 112 | m.insert(libc::SYS_fgetxattr, StringRegister::A0); 113 | m.insert(libc::SYS_listxattr, StringRegister::A0); 114 | m.insert(libc::SYS_llistxattr, StringRegister::A0); 115 | m.insert(libc::SYS_flistxattr, StringRegister::A0); 116 | m.insert(libc::SYS_removexattr, StringRegister::A0); 117 | m.insert(libc::SYS_lremovexattr, StringRegister::A0); 118 | m.insert(libc::SYS_fremovexattr, StringRegister::A0); 119 | 120 | // fadvise64 121 | m.insert(libc::SYS_fadvise64, StringRegister::A0); 122 | 123 | // utimensat 124 | m.insert(libc::SYS_utimensat, StringRegister::A0); 125 | 126 | // splice/tee 127 | m.insert(libc::SYS_splice, StringRegister::A0); 128 | m.insert(libc::SYS_tee, StringRegister::A0); 129 | 130 | // sync_file_range 131 | m.insert(libc::SYS_sync_file_range, StringRegister::A0); 132 | 133 | // vmsplice 134 | m.insert(libc::SYS_vmsplice, StringRegister::A0); 135 | 136 | // fallocate 137 | m.insert(libc::SYS_fallocate, StringRegister::A0); 138 | 139 | // inotify_init1/fanotify_init/fanonotify_mark 140 | m.insert(libc::SYS_inotify_init1, StringRegister::A0); 141 | m.insert(libc::SYS_fanotify_init, StringRegister::A0); 142 | m.insert(libc::SYS_fanotify_mark, StringRegister::A0); 143 | 144 | // name_to_handle_at/open_by_handle_at 145 | m.insert(libc::SYS_name_to_handle_at, StringRegister::A0); 146 | m.insert(libc::SYS_open_by_handle_at, StringRegister::A0); 147 | 148 | // syncfs 149 | m.insert(libc::SYS_syncfs, StringRegister::A0); 150 | 151 | m 152 | }; 153 | } 154 | -------------------------------------------------------------------------------- /src/enclosure/syscall/x86_64.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crate::enclosure::register::StringRegister; 4 | 5 | lazy_static::lazy_static! { 6 | pub static ref SYSCALL_REGISTERS: HashMap = { 7 | let mut m = HashMap::new(); 8 | // read/write 9 | m.insert(libc::SYS_read, StringRegister::Rdi); 10 | m.insert(libc::SYS_write, StringRegister::Rdi); 11 | 12 | // open/openat/creat 13 | m.insert(libc::SYS_openat, StringRegister::Rsi); 14 | m.insert(libc::SYS_open, StringRegister::Rdi); 15 | m.insert(libc::SYS_creat, StringRegister::Rdi); 16 | 17 | // close 18 | m.insert(libc::SYS_close, StringRegister::Rdi); 19 | 20 | // unlink/unlinkat 21 | m.insert(libc::SYS_unlinkat, StringRegister::Rsi); 22 | m.insert(libc::SYS_unlink, StringRegister::Rdi); 23 | 24 | // stat/fstat/lstat 25 | m.insert(libc::SYS_stat, StringRegister::Rdi); 26 | m.insert(libc::SYS_fstat, StringRegister::Rdi); 27 | m.insert(libc::SYS_lstat, StringRegister::Rdi); 28 | // statx 29 | m.insert(libc::SYS_statx, StringRegister::Rdi); 30 | // newfstatat 31 | m.insert(libc::SYS_newfstatat, StringRegister::Rdi); 32 | 33 | // lseek 34 | m.insert(libc::SYS_lseek, StringRegister::Rdi); 35 | 36 | // pread64/pwrite64/preadv/pwritev 37 | m.insert(libc::SYS_pread64, StringRegister::Rdi); 38 | m.insert(libc::SYS_pwrite64, StringRegister::Rdi); 39 | m.insert(libc::SYS_preadv, StringRegister::Rdi); 40 | m.insert(libc::SYS_pwritev, StringRegister::Rdi); 41 | 42 | // access/faccessat/faccessat2 43 | m.insert(libc::SYS_access, StringRegister::Rdi); 44 | m.insert(libc::SYS_faccessat, StringRegister::Rsi); 45 | m.insert(libc::SYS_faccessat2, StringRegister::Rsi); 46 | 47 | // dup/dup2/dup3 48 | m.insert(libc::SYS_dup, StringRegister::Rdi); 49 | m.insert(libc::SYS_dup2, StringRegister::Rdi); 50 | m.insert(libc::SYS_dup3, StringRegister::Rdi); 51 | 52 | // sendfile 53 | m.insert(libc::SYS_sendfile, StringRegister::Rdi); 54 | 55 | // fcntl 56 | m.insert(libc::SYS_fcntl, StringRegister::Rdi); 57 | 58 | // fsync/fdatasync 59 | m.insert(libc::SYS_fsync, StringRegister::Rdi); 60 | m.insert(libc::SYS_fdatasync, StringRegister::Rdi); 61 | 62 | // truncate/ftruncate 63 | m.insert(libc::SYS_truncate, StringRegister::Rdi); 64 | m.insert(libc::SYS_ftruncate, StringRegister::Rdi); 65 | 66 | // getdents/getdents64 67 | m.insert(libc::SYS_getdents, StringRegister::Rdi); 68 | m.insert(libc::SYS_getdents64, StringRegister::Rdi); 69 | 70 | // chdir/fchdir 71 | m.insert(libc::SYS_chdir, StringRegister::Rdi); 72 | m.insert(libc::SYS_fchdir, StringRegister::Rdi); 73 | 74 | // rename/renameat 75 | m.insert(libc::SYS_rename, StringRegister::Rdi); 76 | m.insert(libc::SYS_renameat, StringRegister::Rsi); 77 | 78 | // mkdir/rmdir/mkdirat 79 | m.insert(libc::SYS_mkdir, StringRegister::Rdi); 80 | m.insert(libc::SYS_rmdir, StringRegister::Rdi); 81 | m.insert(libc::SYS_mkdirat, StringRegister::Rsi); 82 | 83 | // link/unlink/symlink/readlink/linkat/symlinkat/unlinkat 84 | m.insert(libc::SYS_link, StringRegister::Rsi); 85 | m.insert(libc::SYS_unlink, StringRegister::Rdi); 86 | m.insert(libc::SYS_symlink, StringRegister::Rdi); 87 | m.insert(libc::SYS_readlink, StringRegister::Rdi); 88 | m.insert(libc::SYS_linkat, StringRegister::Rsi); 89 | m.insert(libc::SYS_symlinkat, StringRegister::Rsi); 90 | m.insert(libc::SYS_unlinkat, StringRegister::Rdi); 91 | 92 | // chmod/fchmod/chown/fchown/lchown 93 | m.insert(libc::SYS_chmod, StringRegister::Rdi); 94 | m.insert(libc::SYS_fchmod, StringRegister::Rdi); 95 | m.insert(libc::SYS_chown, StringRegister::Rdi); 96 | m.insert(libc::SYS_fchown, StringRegister::Rdi); 97 | m.insert(libc::SYS_lchown, StringRegister::Rdi); 98 | // fchownat/fchmodat 99 | m.insert(libc::SYS_fchownat, StringRegister::Rsi); 100 | m.insert(libc::SYS_fchmodat, StringRegister::Rsi); 101 | 102 | // mknod/mknodat 103 | m.insert(libc::SYS_mknod, StringRegister::Rdi); 104 | m.insert(libc::SYS_mknodat, StringRegister::Rsi); 105 | 106 | // pivot_root 107 | m.insert(libc::SYS_pivot_root, StringRegister::Rdi); 108 | 109 | // chroot 110 | m.insert(libc::SYS_chroot, StringRegister::Rdi); 111 | 112 | // mount/umount2 113 | m.insert(libc::SYS_mount, StringRegister::Rdi); 114 | m.insert(libc::SYS_umount2, StringRegister::Rdi); 115 | 116 | // swapon/swapoff 117 | m.insert(libc::SYS_swapon, StringRegister::Rdi); 118 | m.insert(libc::SYS_swapoff, StringRegister::Rdi); 119 | 120 | // readahead 121 | m.insert(libc::SYS_readahead, StringRegister::Rdi); 122 | 123 | // setxattr/lsetxattr/fsetxattr/getxattr/lgetxattr/fgetxattr/listxattr/llistxattr/flistxattr/removexattr/lremovexattr/fremovexattr 124 | m.insert(libc::SYS_setxattr, StringRegister::Rdi); 125 | m.insert(libc::SYS_lsetxattr, StringRegister::Rdi); 126 | m.insert(libc::SYS_fsetxattr, StringRegister::Rdi); 127 | m.insert(libc::SYS_getxattr, StringRegister::Rdi); 128 | m.insert(libc::SYS_lgetxattr, StringRegister::Rdi); 129 | m.insert(libc::SYS_fgetxattr, StringRegister::Rdi); 130 | m.insert(libc::SYS_listxattr, StringRegister::Rdi); 131 | m.insert(libc::SYS_llistxattr, StringRegister::Rdi); 132 | m.insert(libc::SYS_flistxattr, StringRegister::Rdi); 133 | m.insert(libc::SYS_removexattr, StringRegister::Rdi); 134 | m.insert(libc::SYS_lremovexattr, StringRegister::Rdi); 135 | m.insert(libc::SYS_fremovexattr, StringRegister::Rdi); 136 | 137 | // fadvise64 138 | m.insert(libc::SYS_fadvise64, StringRegister::Rdi); 139 | 140 | // futimesat/utimensat 141 | m.insert(libc::SYS_futimesat, StringRegister::Rdi); 142 | m.insert(libc::SYS_utimensat, StringRegister::Rdi); 143 | 144 | // splice/tee 145 | m.insert(libc::SYS_splice, StringRegister::Rdi); 146 | m.insert(libc::SYS_tee, StringRegister::Rdi); 147 | 148 | // sync_file_range 149 | m.insert(libc::SYS_sync_file_range, StringRegister::Rdi); 150 | 151 | // vmsplice 152 | m.insert(libc::SYS_vmsplice, StringRegister::Rdi); 153 | 154 | // fallocate 155 | m.insert(libc::SYS_fallocate, StringRegister::Rdi); 156 | 157 | // inotify_init1/fanotify_init/fanonotify_mark 158 | m.insert(libc::SYS_inotify_init1, StringRegister::Rdi); 159 | m.insert(libc::SYS_fanotify_init, StringRegister::Rdi); 160 | m.insert(libc::SYS_fanotify_mark, StringRegister::Rdi); 161 | 162 | // name_to_handle_at/open_by_handle_at 163 | m.insert(libc::SYS_name_to_handle_at, StringRegister::Rdi); 164 | m.insert(libc::SYS_open_by_handle_at, StringRegister::Rdi); 165 | 166 | // syncfs 167 | m.insert(libc::SYS_syncfs, StringRegister::Rdi); 168 | 169 | m 170 | }; 171 | } 172 | -------------------------------------------------------------------------------- /src/enclosure/tracer.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::collections::HashMap; 3 | use std::sync::mpsc::Sender; 4 | 5 | use byteorder::{LittleEndian, WriteBytesExt}; 6 | use cfg_if::cfg_if; 7 | use color_eyre::Result; 8 | use log::*; 9 | use nix::sys::ptrace; 10 | use nix::sys::signal::Signal; 11 | use nix::sys::wait::{waitpid, WaitStatus}; 12 | use nix::unistd::Pid; 13 | 14 | use super::register::{syscall_number_from_user_regs, StringRegister}; 15 | use super::syscall::Syscall; 16 | 17 | pub struct Tracer { 18 | children: HashMap, 19 | } 20 | 21 | impl Tracer { 22 | pub fn new(pid: Pid) -> Self { 23 | debug!("starting new tracer for root pid {pid}"); 24 | let mut children = HashMap::new(); 25 | let mut root_child = ChildProcess::new(pid, None); 26 | root_child.state = ChildProcessState::Running; 27 | children.insert(pid, root_child); 28 | Self { children } 29 | } 30 | 31 | pub fn flag(pid: Pid) -> Result<()> { 32 | debug!("applying ptrace flags to {pid}..."); 33 | ptrace::setoptions( 34 | pid, 35 | ptrace::Options::PTRACE_O_EXITKILL 36 | | ptrace::Options::PTRACE_O_TRACESYSGOOD 37 | | ptrace::Options::PTRACE_O_TRACEFORK 38 | | ptrace::Options::PTRACE_O_TRACEEXEC 39 | | ptrace::Options::PTRACE_O_TRACECLONE 40 | | ptrace::Options::PTRACE_O_TRACEEXIT 41 | | ptrace::Options::PTRACE_O_TRACEVFORK, 42 | )?; 43 | 44 | Ok(()) 45 | } 46 | 47 | pub fn run(&mut self, tx: Sender) -> Result<()> { 48 | debug!("starting to run!"); 49 | while !self.children.is_empty() { 50 | let mut pids = self.children.keys().cloned().collect::>(); 51 | pids.sort(); 52 | for pid in pids { 53 | self.wait_on_child(pid, &tx)?; 54 | } 55 | } 56 | 57 | Ok(()) 58 | } 59 | 60 | fn wait_on_child(&mut self, pid: Pid, tx: &Sender) -> Result<()> { 61 | let status = waitpid(pid, Some(nix::sys::wait::WaitPidFlag::WNOHANG))?; 62 | match status { 63 | WaitStatus::Exited(pid, status) => { 64 | debug!("process {pid} exited with status {status}"); 65 | self.remove_child(pid)?; 66 | } 67 | WaitStatus::PtraceEvent(pid, signal, event) => { 68 | let child = self.children.get_mut(&pid).unwrap(); 69 | child.last_signal = Some(signal); 70 | match event { 71 | libc::PTRACE_EVENT_CLONE 72 | | libc::PTRACE_EVENT_FORK 73 | | libc::PTRACE_EVENT_VFORK => { 74 | let child_pid = ptrace::getevent(pid)?; 75 | let child_pid = Pid::from_raw(child_pid as i32); 76 | self.children 77 | .insert(child_pid, ChildProcess::new(child_pid, Some(pid))); 78 | debug!("process {pid} spawned {child_pid}"); 79 | ptrace::syscall(pid, signal)?; 80 | } 81 | libc::PTRACE_EVENT_EXEC => { 82 | debug!("process {pid} exec'd"); 83 | ptrace::syscall(pid, signal)?; 84 | } 85 | libc::PTRACE_EVENT_EXIT => { 86 | debug!("process {pid} exited"); 87 | if let Some(child) = self.children.get(&pid) { 88 | if child.parent.is_none() { 89 | ptrace::detach(pid, None)?; 90 | self.handle_root_exit()?; 91 | return Ok(()); 92 | } 93 | } 94 | self.remove_child(pid)?; 95 | } 96 | _ => {} 97 | } 98 | } 99 | WaitStatus::PtraceSyscall(pid) => { 100 | let child = self.children.get_mut(&pid).unwrap(); 101 | child.last_signal = None; 102 | match &child.state { 103 | ChildProcessState::Running => { 104 | trace!("process {pid} entered syscall"); 105 | child.state = ChildProcessState::EnteringSyscall; 106 | self.handle_syscall_enter(pid, tx)?; 107 | ptrace::syscall(pid, None)?; 108 | } 109 | ChildProcessState::EnteringSyscall => { 110 | trace!("process {pid} exited syscall"); 111 | child.state = ChildProcessState::ExitingSyscall; 112 | self.handle_syscall_exit(pid)?; 113 | ptrace::syscall(pid, None)?; 114 | } 115 | ChildProcessState::ExitingSyscall => { 116 | trace!("process {pid} returned to running"); 117 | child.state = ChildProcessState::Running; 118 | ptrace::syscall(pid, None)?; 119 | } 120 | _ => {} 121 | } 122 | } 123 | WaitStatus::Signaled(pid, signal, _core_dumped) => { 124 | debug!("process {pid} signalled with {signal}"); 125 | let child = self.children.get_mut(&pid).unwrap(); 126 | child.last_signal = Some(signal); 127 | match signal { 128 | Signal::SIGTRAP => match child.state { 129 | ChildProcessState::Created => { 130 | debug!("transition created => running"); 131 | child.state = ChildProcessState::Running; 132 | Self::flag(child.pid)?; 133 | ptrace::syscall(pid, None)?; 134 | } 135 | ChildProcessState::Running => { 136 | debug!("ptrace event"); 137 | child.state = ChildProcessState::PtraceEvent; 138 | ptrace::syscall(pid, None)?; 139 | } 140 | _ => {} 141 | }, 142 | Signal::SIGTERM | Signal::SIGKILL => { 143 | debug!("process {pid} signalled with {signal}"); 144 | self.remove_child(pid)?; 145 | } 146 | _ => { 147 | debug!("process {pid} signalled with {signal}"); 148 | ptrace::syscall(pid, child.last_signal)?; 149 | } 150 | } 151 | } 152 | WaitStatus::Stopped(pid, signal) => { 153 | let child = self.children.get_mut(&pid).unwrap(); 154 | debug!( 155 | "{} {pid} stopped with {signal}", 156 | if child.parent.is_none() { 157 | "root" 158 | } else { 159 | "child" 160 | } 161 | ); 162 | child.last_signal = None; 163 | match signal { 164 | Signal::SIGTRAP | Signal::SIGSTOP => match child.state { 165 | ChildProcessState::Created => { 166 | debug!("transition created => running"); 167 | child.state = ChildProcessState::Running; 168 | ptrace::syscall(pid, None)?; 169 | } 170 | ChildProcessState::Running => { 171 | debug!("ptrace event"); 172 | ptrace::syscall(pid, child.last_signal)?; 173 | } 174 | _ => {} 175 | }, 176 | _ => { 177 | self.remove_child(pid)?; 178 | debug!("process {pid} stopped with {signal}"); 179 | } 180 | } 181 | } 182 | _ => {} 183 | } 184 | 185 | if let Some(child) = self.children.get_mut(&pid) { 186 | child.clear_register_cache(); 187 | } 188 | 189 | Ok(()) 190 | } 191 | 192 | fn remove_child(&mut self, pid: Pid) -> Result<()> { 193 | debug!("! removing child {pid}"); 194 | let child = self.children.remove(&pid); 195 | ptrace::detach(pid, None)?; 196 | 197 | if let Some(child) = child { 198 | if child.parent.is_none() { 199 | self.handle_root_exit()?; 200 | } 201 | } 202 | 203 | Ok(()) 204 | } 205 | 206 | fn handle_root_exit(&mut self) -> Result<()> { 207 | debug!("!!! parent died, stopping all children!"); 208 | 209 | let children = self.children.clone(); 210 | let children = children.values(); 211 | debug!("cleaning up {} children!", children.len()); 212 | for child in children { 213 | ptrace::detach(child.pid, Signal::SIGTERM)?; 214 | self.children.remove(&child.pid); 215 | debug!("removed child {}", child.pid); 216 | } 217 | 218 | Ok(()) 219 | } 220 | 221 | fn handle_syscall_enter(&mut self, pid: Pid, tx: &Sender) -> Result<()> { 222 | if let Some(syscall) = super::syscall::handle_syscall(self, pid)? { 223 | tx.send(syscall)?; 224 | } 225 | Ok(()) 226 | } 227 | 228 | fn handle_syscall_exit(&self, pid: Pid) -> Result<()> { 229 | let child = self.children.get(&pid).unwrap(); 230 | let regs = child.get_registers()?; 231 | trace!( 232 | "child {pid} exited syscall {:?}", 233 | syscall_numbers::native::sys_call_name(syscall_number_from_user_regs!(regs) as i64) 234 | ); 235 | Ok(()) 236 | } 237 | 238 | pub fn get_child(&self, pid: Pid) -> Option<&ChildProcess> { 239 | self.children.get(&pid) 240 | } 241 | } 242 | 243 | pub type PtraceRegisters = libc::user_regs_struct; 244 | 245 | #[derive(Debug, Clone)] 246 | pub struct ChildProcess { 247 | #[allow(unused)] 248 | pid: Pid, 249 | state: ChildProcessState, 250 | last_signal: Option, 251 | parent: Option, 252 | register_cache: RefCell>, 253 | } 254 | 255 | impl ChildProcess { 256 | fn new(pid: Pid, parent: Option) -> Self { 257 | Self { 258 | pid, 259 | state: ChildProcessState::Created, 260 | last_signal: None, 261 | parent, 262 | register_cache: RefCell::new(HashMap::new()), 263 | } 264 | } 265 | 266 | pub fn pid(&self) -> Pid { 267 | self.pid 268 | } 269 | 270 | pub fn get_registers(&self) -> Result { 271 | cfg_if! { 272 | if #[cfg(target_arch = "x86_64")] { 273 | ptrace::getregs(self.pid).map_err(|e| e.into()) 274 | } else { 275 | let mut regs = std::mem::MaybeUninit::::uninit(); 276 | let iovec = libc::iovec { 277 | iov_base: regs.as_mut_ptr() as *mut libc::c_void, 278 | iov_len: std::mem::size_of::(), 279 | }; 280 | if -1 == unsafe { 281 | // ptrace returns -1 on error, and sets errno 282 | libc::ptrace(libc::PTRACE_GETREGSET, libc::pid_t::from(self.pid), libc::NT_PRSTATUS, &iovec as *const _ as *const libc::c_void) 283 | } { 284 | Err(nix::errno::Errno::last().into()) 285 | } else { 286 | Ok(unsafe { regs.assume_init() }) 287 | } 288 | } 289 | } 290 | } 291 | 292 | pub fn clear_register_cache(&self) { 293 | self.register_cache.borrow_mut().clear(); 294 | } 295 | 296 | pub fn read_string(&self, register: &StringRegister, addr: *mut u64) -> Result { 297 | if let Some(cached_str) = self.register_cache.borrow().get(register) { 298 | return Ok(cached_str.clone()); 299 | } 300 | 301 | let mut buf = vec![]; 302 | let mut addr = addr; 303 | loop { 304 | let c = ptrace::read(self.pid, addr as *mut _)?; 305 | if c == 0 { 306 | break; 307 | } 308 | buf.write_u64::(c as u64)?; 309 | if buf.len() >= libc::PATH_MAX as usize { 310 | let zero = buf.iter().position(|c| *c == 0); 311 | if let Some(idx) = zero { 312 | buf.truncate(idx); 313 | } 314 | break; 315 | } 316 | 317 | let zero = buf.iter().position(|c| *c == 0); 318 | if let Some(idx) = zero { 319 | buf.truncate(idx); 320 | break; 321 | } 322 | 323 | // Safety: We're just iterating a C-style string, and exit 324 | // condition is checked. Unfortunately, we can't know the length of 325 | // the string ahead of time. 326 | addr = unsafe { addr.add(1) }; 327 | } 328 | 329 | match String::from_utf8(buf.clone()) { 330 | Ok(s) => { 331 | let mut register_cache = self.register_cache.borrow_mut(); 332 | register_cache.insert(*register, s.clone()); 333 | Ok(s) 334 | } 335 | err @ Err(_) => err.map_err(|e| e.into()), 336 | } 337 | } 338 | } 339 | 340 | #[derive(Debug, Clone)] 341 | pub enum ChildProcessState { 342 | Created, 343 | Running, 344 | EnteringSyscall, 345 | ExitingSyscall, 346 | PtraceEvent, 347 | } 348 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::io::IsTerminal; 3 | use std::path::PathBuf; 4 | 5 | use clap::{ArgAction, Parser, Subcommand}; 6 | use color_eyre::Result; 7 | use log::*; 8 | use scanner::App; 9 | 10 | use crate::config::BoxxyConfig; 11 | use crate::enclosure::rule::{BoxxyRules, Rule, RuleMode}; 12 | use crate::scanner::Scanner; 13 | 14 | pub mod config; 15 | pub mod enclosure; 16 | pub mod scanner; 17 | 18 | const VERSION: &str = env!("CARGO_PKG_VERSION"); 19 | 20 | #[derive(Parser)] 21 | #[command( 22 | name = "boxxy", 23 | display_name = "boxxy", 24 | about = "Put bad programs in a box with only their files.", 25 | long_about = "boxxy forces bad programs to put their files somewhere else via Linux user namespaces.", 26 | version = VERSION, 27 | subcommand_negates_reqs = true, 28 | )] 29 | pub struct Args { 30 | #[arg( 31 | short = 'i', 32 | long = "immutable", 33 | default_value = "false", 34 | help = "Make the root filesystem immutable." 35 | )] 36 | pub immutable_root: bool, 37 | 38 | #[arg( 39 | trailing_var_arg = true, 40 | name = "COMMAND TO RUN", 41 | required = true, 42 | help = "The command to run, ex. `ls -lah` or `aws configure`." 43 | )] 44 | pub command_with_args: Vec, 45 | 46 | #[arg(short = 'l', long = "log-level", default_value = "info")] 47 | pub log_level: String, 48 | 49 | #[arg( 50 | long = "force-colour", 51 | default_value = "false", 52 | help = "Force colour output even when stdout is not a tty." 53 | )] 54 | pub force_colour: bool, 55 | 56 | #[arg( 57 | short = 't', 58 | long = "trace", 59 | default_value = "false", 60 | help = "Enable tracing of I/O-related syscalls and generate a report of files/directories the program touched." 61 | )] 62 | pub trace: bool, 63 | 64 | #[arg( 65 | short = 'd', 66 | long = "dotenv", 67 | default_value = "false", 68 | help = "Load environment variables from the .env file in the current directory and apply them to the boxxed program." 69 | )] 70 | pub dotenv: bool, 71 | 72 | #[arg( 73 | long = "daemon", 74 | default_value = "false", 75 | help = "Fork to the background and run as a daemon." 76 | )] 77 | pub daemon: bool, 78 | 79 | #[arg( 80 | long = "no-config", 81 | default_value = "false", 82 | help = "Disable loading config files entirely.", 83 | action = ArgAction::SetTrue 84 | )] 85 | pub no_config: bool, 86 | 87 | #[arg( 88 | short = 'r', 89 | long = "rule", 90 | help = "Pass rules via CLI. -r/--rule `/remount/this:/to/this:`", 91 | action = ArgAction::Append 92 | )] 93 | pub arg_rules: Vec, 94 | 95 | #[command(subcommand)] 96 | pub command: Option, 97 | } 98 | 99 | #[derive(Subcommand)] 100 | pub enum BoxxySubcommand { 101 | #[command( 102 | name = "config", 103 | about = "View the config file.", 104 | subcommand_negates_reqs = true, 105 | aliases = &["cfg", "conf", "c"] 106 | )] 107 | Config, 108 | #[command( 109 | name = "scan", 110 | about = "Scan your homedir for applications that may benefit from boxxy.", 111 | subcommand_negates_reqs = true, 112 | aliases = &["s"] 113 | )] 114 | Scan, 115 | } 116 | 117 | fn main() -> Result<()> { 118 | // Fetch command to run 119 | let cfg = Args::parse(); 120 | setup_logging(&cfg)?; 121 | 122 | if let Some(cmd) = cfg.command { 123 | match cmd { 124 | BoxxySubcommand::Config => { 125 | for config_path in BoxxyConfig::rule_paths()? { 126 | let mut printer = bat::PrettyPrinter::new(); 127 | printer.input_file(config_path).print()?; 128 | } 129 | return Ok(()); 130 | } 131 | BoxxySubcommand::Scan => { 132 | let apps = Scanner::new().scan()?; 133 | return scan_homedir(apps); 134 | } 135 | } 136 | } 137 | 138 | // Do the thing! 139 | enclosure::Enclosure::new(BoxxyConfig::load_config(cfg)?).run()?; 140 | 141 | Ok(()) 142 | } 143 | 144 | fn setup_logging(cfg: &Args) -> Result<()> { 145 | if BoxxyConfig::debug_mode()? { 146 | // If no debug set up, basic debugging in dev 147 | if std::env::var("RUST_DEBUG").is_err() { 148 | std::env::set_var("RUST_DEBUG", "1"); 149 | } 150 | if std::env::var("RUST_LOG").is_err() { 151 | std::env::set_var("RUST_LOG", "debug"); 152 | } 153 | } else if std::env::var("RUST_LOG").is_err() { 154 | std::env::set_var("RUST_LOG", &cfg.log_level); 155 | } 156 | 157 | if !std::io::stdout().is_terminal() && !cfg.force_colour { 158 | // Disable user-friendliness if we're not outputting to a terminal. 159 | std::env::set_var("NO_COLOR", "1"); 160 | std::env::set_var("RUST_LOG", "warn"); 161 | std::env::remove_var("RUST_DEBUG"); 162 | } 163 | 164 | // Set up basics 165 | color_eyre::config::HookBuilder::new() 166 | .issue_url(concat!(env!("CARGO_PKG_REPOSITORY"), "/issues/new")) 167 | .add_issue_metadata("version", env!("CARGO_PKG_VERSION")) 168 | .install()?; 169 | 170 | pretty_env_logger::init(); 171 | 172 | Ok(()) 173 | } 174 | 175 | fn scan_homedir(apps: Vec) -> Result<()> { 176 | if !apps.is_empty() { 177 | info!( 178 | "found {} applications that might be boxxable! generating config...", 179 | apps.len() 180 | ); 181 | let mut rules = vec![]; 182 | for app in apps { 183 | for fix in app.fixes { 184 | let (old, new) = fix.split_once(':').unwrap(); 185 | let path = PathBuf::from(old); 186 | let mode = if path.is_dir() { 187 | RuleMode::Directory 188 | } else { 189 | RuleMode::File 190 | }; 191 | rules.push(Rule { 192 | name: app.name.clone(), 193 | target: old.into(), 194 | rewrite: new.into(), 195 | mode, 196 | context: vec![], 197 | only: vec![], 198 | // TODO: populate for apps where possible 199 | env: HashMap::new(), 200 | }); 201 | } 202 | } 203 | let config = BoxxyRules { 204 | rules: rules.clone(), 205 | }; 206 | let config = &serde_yaml::to_string(&config)?; 207 | let mut printer = bat::PrettyPrinter::new(); 208 | println!(); 209 | printer 210 | .input_from_bytes(config.as_bytes()) 211 | .language("yaml") 212 | .print() 213 | .expect("failed to print config"); 214 | println!(); 215 | warn!("!!! BE CAREFUL WITH THIS CONFIG !!!"); 216 | warn!("SAFETY IS NOT GUARANTEED!!!"); 217 | warn!("this config was automatically generated and may not be correct."); 218 | warn!("please review the config before using it!"); 219 | warn!("report bad rules!! https://github.com/queer/boxxy/issues/new"); 220 | info!("rules generated: {}", rules.len()); 221 | info!( 222 | "you can put relevant rules in your config file located at: {}", 223 | BoxxyConfig::default_config_path()?.display() 224 | ); 225 | } 226 | 227 | Ok(()) 228 | } 229 | -------------------------------------------------------------------------------- /src/scanner/mod.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use color_eyre::Result; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | #[derive(Serialize, Deserialize, Debug, Clone)] 7 | pub struct App { 8 | pub name: String, 9 | pub paths: Vec, 10 | pub fixes: Vec, 11 | } 12 | 13 | pub struct Scanner { 14 | pub apps: Vec, 15 | } 16 | 17 | const HARDCODED_APPS_JSON: &str = include_str!("../../data/hardcoded-applications.json"); 18 | const PARTIAL_APPS_JSON: &str = include_str!("../../data/partial-support-applications.json"); 19 | 20 | impl Scanner { 21 | #[allow(clippy::new_without_default)] 22 | pub fn new() -> Self { 23 | let mut hardcoded = serde_json::from_str::>(HARDCODED_APPS_JSON).unwrap(); 24 | let mut partial = serde_json::from_str::>(PARTIAL_APPS_JSON).unwrap(); 25 | let mut apps = vec![]; 26 | apps.append(&mut hardcoded); 27 | apps.append(&mut partial); 28 | 29 | Self { apps } 30 | } 31 | 32 | pub fn scan(&mut self) -> Result> { 33 | let mut out = vec![]; 34 | 35 | for app in &self.apps { 36 | for path in &app.paths { 37 | let path = shellexpand::full(&path)?.to_string(); 38 | if PathBuf::from(path).exists() { 39 | out.push(app.clone()); 40 | } 41 | } 42 | } 43 | 44 | Ok(out) 45 | } 46 | } 47 | --------------------------------------------------------------------------------