Repository: ish-app/ish Branch: master Commit: cc391ff2e56d Files: 413 Total size: 2.2 MB Directory structure: gitextract_xua3mraa/ ├── .editorconfig ├── .github/ │ ├── FUNDING.yml │ └── workflows/ │ ├── autolabel.yml │ ├── ci.yml │ ├── deploy-site.yml │ ├── update-alpine-repo.yml │ └── upload-build.yml ├── .gitignore ├── .gitmodules ├── Gemfile ├── ISSUE_TEMPLATE.md ├── LICENSE.IOS ├── LICENSE.md ├── README.md ├── README_JP.md ├── README_KO.md ├── README_ZH.md ├── SECURITY.md ├── app/ │ ├── AboutAppearanceViewController.h │ ├── AboutAppearanceViewController.m │ ├── AboutExternalKeyboardViewController.h │ ├── AboutExternalKeyboardViewController.m │ ├── AboutNavigationController.h │ ├── AboutNavigationController.m │ ├── AboutViewController.h │ ├── AboutViewController.m │ ├── AccessibilityFixes.m │ ├── AltIconViewController.h │ ├── AltIconViewController.m │ ├── App.xcconfig │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── AppGroup.h │ ├── AppGroup.m │ ├── AppLib.xcconfig │ ├── ArrowBarButton.h │ ├── ArrowBarButton.m │ ├── Assets.xcassets/ │ │ ├── AppIcon.appiconset/ │ │ │ └── Contents.json │ │ ├── Checkbox.imageset/ │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── Hide Keyboard.imageset/ │ │ │ └── Contents.json │ │ ├── Paste.imageset/ │ │ │ └── Contents.json │ │ ├── Saddam Hussein.imageset/ │ │ │ └── Contents.json │ │ └── X.imageset/ │ │ └── Contents.json │ ├── BarButton.h │ ├── BarButton.m │ ├── Base.lproj/ │ │ ├── About.storyboard │ │ ├── LaunchScreen.storyboard │ │ └── Terminal.storyboard │ ├── CLI.xcconfig │ ├── CurrentRoot.h │ ├── CurrentRoot.m │ ├── DelayedUITask.h │ ├── DelayedUITask.m │ ├── ExceptionExfiltrator.h │ ├── ExceptionExfiltrator.m │ ├── FileProvider/ │ │ ├── FileProviderEnumerator.h │ │ ├── FileProviderEnumerator.m │ │ ├── FileProviderExtension.h │ │ ├── FileProviderExtension.m │ │ ├── FileProviderItem.h │ │ ├── FileProviderItem.m │ │ ├── Info.plist │ │ ├── NSError+ISHErrno.h │ │ ├── NSError+ISHErrno.m │ │ └── iSHFileProvider.entitlements │ ├── FontPickerViewController.h │ ├── FontPickerViewController.m │ ├── IOSCalls.m │ ├── Icons/ │ │ └── Icons.plist │ ├── Info.plist │ ├── Linux.xcconfig │ ├── LinuxInterop.c │ ├── LinuxInterop.h │ ├── LinuxPTY.c │ ├── LinuxRoot.c │ ├── LinuxTTY.c │ ├── LocationDevice.h │ ├── LocationDevice.m │ ├── NSObject+SaneKVO.h │ ├── NSObject+SaneKVO.m │ ├── NotLinux.xcconfig │ ├── PassthroughView.h │ ├── PassthroughView.m │ ├── PasteboardDevice.h │ ├── PasteboardDevice.m │ ├── PasteboardDeviceLinux.c │ ├── ProgressReportViewController.h │ ├── ProgressReportViewController.m │ ├── Project.xcconfig │ ├── ProjectDebug.xcconfig │ ├── ProjectDebugLinux.xcconfig │ ├── ProjectRelease.xcconfig │ ├── ProjectReleaseLinux.xcconfig │ ├── Roots.h │ ├── Roots.m │ ├── Roots.storyboard │ ├── RootsTableViewController.h │ ├── RootsTableViewController.m │ ├── SceneDelegate.h │ ├── SceneDelegate.m │ ├── ScrollbarView.h │ ├── ScrollbarView.m │ ├── Settings.bundle/ │ │ └── Root.plist │ ├── StaticLib.xcconfig │ ├── StaticLibLinux.xcconfig │ ├── StaticLibLinuxUser.xcconfig │ ├── Terminal.h │ ├── Terminal.m │ ├── TerminalView.h │ ├── TerminalView.m │ ├── TerminalViewController.h │ ├── TerminalViewController.m │ ├── Theme.h │ ├── Theme.m │ ├── ThemeViewController.h │ ├── ThemeViewController.m │ ├── ThemesViewController.h │ ├── ThemesViewController.m │ ├── UIApplication+OpenURL.h │ ├── UIApplication+OpenURL.m │ ├── UITests/ │ │ ├── Info.plist │ │ ├── Screenshots.m │ │ ├── Screenshots.xctestplan │ │ └── UITests.m │ ├── UIViewController+Extras.h │ ├── UIViewController+Extras.m │ ├── UpgradeRootViewController.h │ ├── UpgradeRootViewController.m │ ├── UserPreferences.h │ ├── UserPreferences.m │ ├── ViewController.h │ ├── XcodeDebug.xcconfig │ ├── XcodeDefault.xcconfig │ ├── XcodeRelease.xcconfig │ ├── gen_apk_repositories.py │ ├── hook.c │ ├── hook.h │ ├── iOS.xcconfig │ ├── iOSFS.h │ ├── iOSFS.m │ ├── iSH.entitlements │ ├── iSH.xcconfig │ ├── main.m │ ├── terminal/ │ │ ├── term.css │ │ ├── term.html │ │ └── term.js │ ├── xcode-meson.sh │ └── xcode-ninja.sh ├── asbestos/ │ ├── asbestos.c │ ├── asbestos.h │ ├── frame.h │ ├── gadgets-aarch64/ │ │ ├── bits.S │ │ ├── control.S │ │ ├── entry.S │ │ ├── gadgets.h │ │ ├── math.S │ │ ├── math.h │ │ ├── memory.S │ │ ├── misc.S │ │ └── string.S │ ├── gadgets-generic.h │ ├── gadgets-x86_64/ │ │ ├── bits.S │ │ ├── control.S │ │ ├── entry.S │ │ ├── gadgets.h │ │ ├── math.S │ │ ├── memory.S │ │ ├── misc.S │ │ └── string.S │ ├── gen.c │ ├── gen.h │ ├── helpers.c │ └── offsets.c ├── debug.h ├── deps/ │ ├── aports/ │ │ ├── sync-archive.sh │ │ ├── v3.14/ │ │ │ ├── community/ │ │ │ │ └── x86/ │ │ │ │ └── index.txt │ │ │ └── main/ │ │ │ └── x86/ │ │ │ └── index.txt │ │ ├── v3.17/ │ │ │ ├── community/ │ │ │ │ └── x86/ │ │ │ │ └── index.txt │ │ │ └── main/ │ │ │ └── x86/ │ │ │ └── index.txt │ │ ├── v3.18/ │ │ │ ├── community/ │ │ │ │ └── x86/ │ │ │ │ └── index.txt │ │ │ └── main/ │ │ │ └── x86/ │ │ │ └── index.txt │ │ └── v3.19/ │ │ ├── community/ │ │ │ └── x86/ │ │ │ └── index.txt │ │ └── main/ │ │ └── x86/ │ │ └── index.txt │ ├── clone-linux.sh │ ├── config.h │ ├── kconfig-fragment.sh │ ├── libarchive.xcodeproj/ │ │ ├── project.pbxproj │ │ └── project.xcworkspace/ │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata/ │ │ └── IDEWorkspaceChecks.plist │ ├── linux-build.sh │ ├── linux-sparse.txt │ ├── linux.config │ ├── makefilter.py │ └── meson.build ├── emu/ │ ├── cpu.h │ ├── cpuid.h │ ├── decode.h │ ├── float80-test.c │ ├── float80.c │ ├── float80.h │ ├── fpu.c │ ├── fpu.h │ ├── interrupt.h │ ├── mmu.h │ ├── mmx.c │ ├── modrm.h │ ├── tlb.c │ ├── tlb.h │ ├── vec.c │ └── vec.h ├── fastlane/ │ ├── Appfile │ ├── Deliverfile │ ├── Fastfile │ ├── Matchfile │ ├── README.md │ ├── Snapfile │ ├── SnapshotHelper.swift │ └── footer.txt ├── fs/ │ ├── adhoc.c │ ├── dev.c │ ├── dev.h │ ├── devices.h │ ├── dir.c │ ├── dyndev.c │ ├── dyndev.h │ ├── fake-db.c │ ├── fake-db.h │ ├── fake-migrate.c │ ├── fake-rebuild.c │ ├── fake.c │ ├── fake.h │ ├── fd.c │ ├── fd.h │ ├── fix_path.h │ ├── generic.c │ ├── inode.c │ ├── inode.h │ ├── lock.c │ ├── mem.c │ ├── mem.h │ ├── mount.c │ ├── path.c │ ├── path.h │ ├── pipe.c │ ├── poll.c │ ├── poll.h │ ├── proc/ │ │ ├── entry.c │ │ ├── ish.c │ │ ├── ish.h │ │ ├── pid.c │ │ └── root.c │ ├── proc.c │ ├── proc.h │ ├── pty.c │ ├── real.c │ ├── real.h │ ├── sock.c │ ├── sock.h │ ├── sockrestart.c │ ├── sockrestart.h │ ├── sqlutil.h │ ├── stat.c │ ├── stat.h │ ├── tmp.c │ ├── tty-real.c │ ├── tty.c │ └── tty.h ├── iSH.xcodeproj/ │ ├── project.pbxproj │ ├── project.xcworkspace/ │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata/ │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata/ │ ├── xcdebugger/ │ │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes/ │ ├── Screenshots.xcscheme │ ├── iSH+Linux.xcscheme │ ├── iSH.xcscheme │ └── ish-cli.xcscheme ├── ish-gdb.gdb ├── ish-lldb.lldb ├── kernel/ │ ├── calls.c │ ├── calls.h │ ├── elf.h │ ├── epoll.c │ ├── errno.c │ ├── errno.h │ ├── eventfd.c │ ├── exec.c │ ├── exit.c │ ├── fork.c │ ├── fs.c │ ├── fs.h │ ├── fs_info.c │ ├── futex.c │ ├── futex.h │ ├── getset.c │ ├── group.c │ ├── init.c │ ├── init.h │ ├── ipc.c │ ├── log.c │ ├── memory.c │ ├── memory.h │ ├── misc.c │ ├── mm.h │ ├── mmap.c │ ├── personality.h │ ├── poll.c │ ├── ptrace.c │ ├── ptrace.h │ ├── random.c │ ├── random.h │ ├── resource.c │ ├── resource.h │ ├── signal.c │ ├── signal.h │ ├── task.c │ ├── task.h │ ├── time.c │ ├── time.h │ ├── tls.c │ ├── uname.c │ ├── user.c │ ├── vdso.c │ └── vdso.h ├── linux/ │ ├── emu_asbestos.c │ ├── emu_unicorn.c │ ├── emu_unicorn.h │ ├── emu_unicorn_kernel.c │ ├── fakefs.c │ └── main.c ├── main.c ├── meson.build ├── meson_options.txt ├── misc.h ├── platform/ │ ├── darwin.c │ ├── linux.c │ └── platform.h ├── shell.nix ├── tests/ │ ├── .gitignore │ ├── e2e/ │ │ ├── e2e.bash │ │ ├── fpu/ │ │ │ ├── expected.txt │ │ │ ├── test.sh │ │ │ └── test_fpu.c │ │ ├── hello/ │ │ │ ├── expected.txt │ │ │ ├── test.sh │ │ │ ├── test_c.c │ │ │ ├── test_python2.py │ │ │ └── test_python3.py │ │ ├── qemu/ │ │ │ ├── expected.txt │ │ │ ├── qemu-test-muldiv.h │ │ │ ├── qemu-test-shift.h │ │ │ ├── qemu-test.c │ │ │ ├── qemu-test.h │ │ │ └── test.sh │ │ ├── shell/ │ │ │ ├── expected.txt │ │ │ └── test.sh │ │ └── sse2/ │ │ ├── expected.txt │ │ ├── movaps.c │ │ ├── movss.c │ │ ├── paddq.c │ │ ├── psllq.c │ │ ├── psrlq.c │ │ ├── test.sh │ │ └── xorps.c │ └── manual/ │ ├── cat.c │ ├── fibbonaci.c │ ├── forkexec.c │ ├── get-busybox.sh │ ├── getdents.c │ ├── hello-clib.c │ ├── hello.c │ ├── looper.c │ ├── meson.build │ ├── modify.c │ ├── signal.c │ ├── stat.c │ └── thread.c ├── tools/ │ ├── fakefs.c │ ├── fakefs.h │ ├── fakefsify.c │ ├── meson.build │ ├── ptraceomatic-config.h │ ├── ptraceomatic-gdb.gdb │ ├── ptraceomatic.c │ ├── ptutil.c │ ├── ptutil.h │ ├── staticdefine.h │ ├── staticdefine.sh │ ├── transplant.h │ ├── undefined-flags.c │ ├── undefined-flags.h │ ├── unicornomatic.c │ ├── vdso-dump.c │ ├── vdso-transplant-main.c │ └── vdso-transplant.c ├── util/ │ ├── bits.h │ ├── fchdir.c │ ├── fchdir.h │ ├── fifo.c │ ├── fifo.h │ ├── list.h │ ├── refcount.h │ ├── sync.c │ ├── sync.h │ ├── timer.c │ └── timer.h ├── vdso/ │ ├── check-cc.sh │ ├── meson.build │ ├── note.S │ ├── vdso.S │ ├── vdso.c │ └── vdso.lds └── xX_main_Xx.h ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true [*] charset = utf-8 end_of_line = lf indent_style = space indent_size = 4 ================================================ FILE: .github/FUNDING.yml ================================================ patreon: tbodt github: tbodt custom: https://www.paypal.me/tbodt ================================================ FILE: .github/workflows/autolabel.yml ================================================ name: Auto-Label on: issues: types: [opened] jobs: label: runs-on: ubuntu-latest steps: - uses: satackey/action-js-inline@bf6fcaf35de1ed03bcfd25a0a8b1fa4c551ec908 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: script: | const core = require('@actions/core'); const github = require('@actions/github'); const octokit = github.getOctokit(process.env.GITHUB_TOKEN); const {repo, owner, number} = github.context.issue; await octokit.rest.issues.addLabels({repo, owner, issue_number: number, labels: ['unconfirmed']}); ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: pull_request: branches: [master] jobs: build-linux: runs-on: ubuntu-20.04 strategy: matrix: cc: [clang, gcc] kernel: [ish, linux] steps: - uses: actions/checkout@v2 with: submodules: true - uses: actions/setup-python@v2 with: python-version: '3.x' - name: Install dependencies run: | sudo apt-get update sudo apt-get install libarchive-dev pip3 install meson ninja - name: Clone Linux if: matrix.kernel == 'linux' run: deps/clone-linux.sh - name: Build run: | meson build -Dengine=asbestos -Dkernel=${{matrix.kernel}} ninja -C build env: CC: ${{matrix.cc}} - name: Test if: matrix.kernel == 'ish' run: ninja -C build test build-mac: runs-on: macos-15 strategy: matrix: kernel: [ish, linux] steps: - uses: actions/checkout@v2 with: submodules: true - name: Install dependencies run: | brew install llvm lld ninja libarchive meson - name: Clone Linux if: matrix.kernel == 'linux' run: deps/clone-linux.sh - name: Build if: matrix.kernel == 'ish' run: xcodebuild -project iSH.xcodeproj -scheme iSH -arch arm64 -sdk iphoneos CODE_SIGNING_ALLOWED=NO - name: Build if: matrix.kernel == 'linux' run: xcodebuild -project iSH.xcodeproj -scheme iSH+Linux -arch x86_64 -sdk iphonesimulator CODE_SIGNING_ALLOWED=NO ================================================ FILE: .github/workflows/deploy-site.yml ================================================ name: Deploy Site on: release: jobs: deploy: runs-on: ubuntu-latest steps: - name: Trigger Deploy run: curl -d {} $NETLIFY_DEPLOY_HOOK env: NETLIFY_DEPLOY_HOOK: ${{ secrets.NETLIFY_DEPLOY_HOOK }} ================================================ FILE: .github/workflows/update-alpine-repo.yml ================================================ name: "Update Repo" on: workflow_dispatch: schedule: - cron: "0 0 * * 6" jobs: update: if: github.repository == 'ish-app/ish' runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Setup dependencies run: | curl https://rclone.org/install.sh | sudo bash mkdir -p ~/.config/rclone echo "$RCLONE_CONFIG" > ~/.config/rclone/rclone.conf env: RCLONE_CONFIG: ${{ secrets.RCLONE_CONFIG }} - name: Update run: | deps/aports/sync-archive.sh v3.14 main/x86 deps/aports deps/aports/sync-archive.sh v3.14 community/x86 deps/aports deps/aports/sync-archive.sh v3.17 main/x86 deps/aports deps/aports/sync-archive.sh v3.17 community/x86 deps/aports deps/aports/sync-archive.sh v3.18 main/x86 deps/aports deps/aports/sync-archive.sh v3.18 community/x86 deps/aports deps/aports/sync-archive.sh v3.19 main/x86 deps/aports deps/aports/sync-archive.sh v3.19 community/x86 deps/aports - name: Commit id: commit run: | git config user.name github-actions[bot] git config user.email 41898282+github-actions[bot]@users.noreply.github.com git add deps/aports git commit -m "Update Alpine repositories" # TODO add a summary of changes in the commit description continue-on-error: true - name: Push if: ${{ steps.commit.outcome == 'success' }} run: git push ================================================ FILE: .github/workflows/upload-build.yml ================================================ name: Upload Build on: workflow_dispatch: schedule: - cron: "0 6 * * *" jobs: upload-build: if: github.repository == 'ish-app/ish' runs-on: macos-15 timeout-minutes: 720 steps: - uses: actions/checkout@v2 with: submodules: true fetch-depth: 0 - name: Install deps run: | brew install ninja lld llvm meson bundle install git config user.name github-actions[bot] git config user.email 41898282+github-actions[bot]@users.noreply.github.com - name: Fastlane timeout-minutes: 720 run: script fastlane.log bundle exec fastlane upload_build env: APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }} APP_STORE_CONNECT_API_KEY_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }} APP_STORE_CONNECT_API_KEY_KEY: ${{ secrets.APP_STORE_CONNECT_KEY }} MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GITHUB_AUTH }} MATCH_PASSWORD: ${{ secrets.MATCH_PASSPHRASE }} GH_TOKEN: ${{ secrets.GH_TOKEN }} SLACK_URL: ${{ secrets.SLACK_URL }} FASTLANE_SKIP_UPDATE_CHECK: 1 - uses: actions/upload-artifact@v4 with: name: App path: | iSH.ipa iSH.app.dSYM.zip - uses: actions/upload-artifact@v4 if: always() with: name: Fastlane Logs path: | fastlane.log ~/Library/Logs/gym ================================================ FILE: .gitignore ================================================ build/ xcuserdata/ .floo cross-*.txt root.tar.gz node_modules app/xtermjs/xterm-dist app/xtermjs/.cache subprojects/ !subprojects/*.wrap fastlane/report.xml fastlane/Preview.html fastlane/screenshots fastlane/test_output e2e_out/ ================================================ FILE: .gitmodules ================================================ [submodule "libapps"] path = deps/libapps url = https://github.com/ish-app/libapps [submodule "deps/libarchive"] path = deps/libarchive url = https://github.com/libarchive/libarchive [submodule "deps/linux"] path = deps/linux url = https://github.com/ish-app/linux update = none shallow = true ================================================ FILE: Gemfile ================================================ source "https://rubygems.org" gem "fastlane" gem "dotenv" gem "pry" gem "abbrev" gem "irb" gem "rdoc" gem "mutex_m" gem "ostruct" gem "logger" ================================================ FILE: ISSUE_TEMPLATE.md ================================================ ================================================ FILE: LICENSE.IOS ================================================ The iSH developers are aware that the terms of service that apply to apps distributed via Apple's App Store services may conflict with rights granted under the iSH license, the GNU General Public License, version 2 or 3. The copyright holders of the iSH app do not wish this conflict to prevent the otherwise-compliant distribution of derived apps via the App Store. Therefore, we have committed not to pursue any license violation that results solely from the conflict between the GNU GPLv2 or v3 and the Apple App Store terms of service. In other words, as long as you comply with the GPL in all other respects, including its requirements to provide users with source code and the text of the license, we will not object to your distribution of the iSH app through the App Store. ================================================ FILE: LICENSE.md ================================================ iSH is licensed under the [GPLv3][]. The additional terms in LICENSE.IOS also apply. Contributions made after commit 0e3a4144f93135c4fd618c8397d2cfd87194f69f are additionally licensed under the [GPLv2][]. This is intended to allow linking with GPLv2 licensed projects such as Linux and QEMU. The following authors have agreed to relicense their past contributions under GPLv2: - Theodore Dubois - Saagar Jha - Christoffer Tønnessen - Philipp Wallisch - Ed Luff - David Southgate - Charlie Melbye - David <0b101@users.noreply.github.com> - [as@irc](https://gist.github.com/tbodt/45ccbea8d3c095258d63f611654f05b4) - asdfugil (name was "Assfugil" when last contributed) <42699250+Assfugil@users.noreply.github.com> - AngeloHYang <38714377+AngeloHYang@users.noreply.github.com> - Matthew Merrill - Siddharth Dushantha - Lorenzo De Linares - Christopher Albert - Stephen Leaf - Noah Peeters - Alexis Marquis - Brian Almeida - Viktor Oreshkin - Ryan Hileman - Christoforos Charalambous - Kenta Kubo - Zhuowei Zhang - never_released <24752637+woachk@users.noreply.github.com> [GPLv3]: https://www.gnu.org/licenses/gpl-3.0.html [GPLv2]: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html ================================================ FILE: README.md ================================================ # [iSH](https://ish.app) [![Build Status](https://github.com/ish-app/ish/actions/workflows/ci.yml/badge.svg)](https://github.com/ish-app/ish/actions) [![goto counter](https://img.shields.io/github/search/ish-app/ish/goto.svg)](https://github.com/ish-app/ish/search?q=goto) [![fuck counter](https://img.shields.io/github/search/ish-app/ish/fuck.svg)](https://github.com/ish-app/ish/search?q=fuck) [![shit counter](https://img.shields.io/github/search/ish-app/ish/shit.svg)](https://github.com/ish-app/ish/search?q=shit)

A project to get a Linux shell running on iOS, using usermode x86 emulation and syscall translation. For the current status of the project, check the issues tab, and the commit logs. - [App Store page](https://apps.apple.com/us/app/ish-shell/id1436902243) - [TestFlight beta](https://testflight.apple.com/join/97i7KM8O) - [Discord server](https://discord.gg/HFAXj44) - [Wiki with help and tutorials](https://github.com/ish-app/ish/wiki) - [README中文](https://github.com/ish-app/ish/blob/master/README_ZH.md) (如若未能保持最新,请提交PR以更新) # Hacking This project has a git submodule, make sure to clone with `--recurse-submodules` or run `git submodule update --init` after cloning. You'll need these things to build the project: - Python 3 + Meson (`pip3 install meson`) - Ninja - Clang and LLD (on mac, `brew install llvm`, on linux, `sudo apt install clang lld` or `sudo pacman -S clang lld` or whatever) - sqlite3 (this is so common it may already be installed on linux and is definitely already installed on mac. if not, do something like `sudo apt install libsqlite3-dev`) - libarchive (`brew install libarchive`, `sudo port install libarchive`, `sudo apt install libarchive-dev`) TODO: bundle this dependency ## Build for iOS Open the project in Xcode, open iSH.xcconfig, and change `ROOT_BUNDLE_IDENTIFIER` to something unique. You'll also need to update the development team ID in the project (not target!) build settings. Then click Run. There are scripts that should do everything else automatically. If you run into any problems, open an issue and I'll try to help. ## Build command line tool for testing To set up your environment, cd to the project and run `meson build` to create a build directory in `build`. Then cd to the build directory and run `ninja`. To set up a self-contained Alpine linux filesystem, download the Alpine minirootfs tarball for i386 from the [Alpine website](https://alpinelinux.org/downloads/) and run `./tools/fakefsify`, with the minirootfs tarball as the first argument and the name of the output directory as the second argument. Then you can run things inside the Alpine filesystem with `./ish -f alpine /bin/sh`, assuming the output directory is called `alpine`. If `tools/fakefsify` doesn't exist for you in your build directory, that might be because it couldn't find libarchive on your system (see above for ways to install it.) You can replace `ish` with `tools/ptraceomatic` to run the program in a real process and single step and compare the registers at each step. I use it for debugging. Requires 64-bit Linux 4.11 or later. ## Logging iSH has several logging channels which can be enabled at build time. By default, all of them are disabled. To enable them: - In Xcode: Set the `ISH_LOG` setting in iSH.xcconfig to a space-separated list of log channels. - With Meson (command line tool for testing): Run `meson configure -Dlog=""`. Available channels: - `strace`: The most useful channel, logs the parameters and return value of almost every system call. - `instr`: Logs every instruction executed by the emulator. This slows things down a lot. - `verbose`: Debug logs that don't fit into another category. - Grep for `DEFAULT_CHANNEL` to see if more log channels have been added since this list was updated. # A note on the interpreter Possibly the most interesting thing I wrote as part of iSH is the interpreter. It's not quite a JIT since it doesn't target machine code. Instead it generates an array of pointers to functions called gadgets, and each gadget ends with a tailcall to the next function; like the threaded code technique used by some Forth interpreters. The result is a speedup of roughly 3-5x compared to emulation using a simpler switch dispatch. Unfortunately, I made the decision to write nearly all of the gadgets in assembly language. This was probably a good decision with regards to performance (though I'll never know for sure), but a horrible decision with regards to readability, maintainability, and my sanity. The amount of bullshit I've had to put up with from the compiler/assembler/linker is insane. It's like there's a demon in there that makes sure my code is sufficiently deformed, and if not, makes up stupid reasons why it shouldn't compile. In order to stay sane while writing this code, I've had to ignore best practices in code structure and naming. You'll find macros and variables with such descriptive names as `ss` and `s` and `a`. Assembler macros nested beyond belief. And to top it off, there are almost no comments. So a warning: Long-term exposure to this code may cause loss of sanity, nightmares about GAS macros and linker errors, or any number of other debilitating side effects. This code is known to the State of California to cause cancer, birth defects, and reproductive harm. ================================================ FILE: README_JP.md ================================================ # [iSH](https://ish.app) [![Build Status](https://github.com/ish-app/ish/actions/workflows/ci.yml/badge.svg)](https://github.com/ish-app/ish/actions) [![goto counter](https://img.shields.io/github/search/ish-app/ish/goto.svg)](https://github.com/ish-app/ish/search?q=goto) [![fuck counter](https://img.shields.io/github/search/ish-app/ish/fuck.svg)](https://github.com/ish-app/ish/search?q=fuck) [![shit counter](https://img.shields.io/github/search/ish-app/ish/shit.svg)](https://github.com/ish-app/ish/search?q=shit)

iSHは、ユーザーモードのx86エミュレーションとシステムコールの翻訳を使用して、iOS上でLinuxシェルを実行するプロジェクトです。 プロジェクトの現状については、issueタブとコミットログを確認してください。 - [App Storeページ](https://apps.apple.com/us/app/ish-shell/id1436902243) - [TestFlightベータ](https://testflight.apple.com/join/97i7KM8O) - [Discordサーバー](https://discord.gg/HFAXj44) - [ヘルプとチュートリアルのWiki](https://github.com/ish-app/ish/wiki) # ハッキング このプロジェクトにはgitサブモジュールがあります。`--recurse-submodules`を使用してクローンするか、クローン後に`git submodule update --init`を実行してください。 プロジェクトをビルドするには、以下のものが必要です: - Python 3 + Meson (`pip3 install meson`) - Ninja - ClangとLLD(macでは`brew install llvm`、linuxでは`sudo apt install clang lld`または`sudo pacman -S clang lld`など) - sqlite3(これは非常に一般的で、linuxではすでにインストールされているかもしれませんし、macでは確実にインストールされています。もしインストールされていない場合は、`sudo apt install libsqlite3-dev`などを実行してください) - libarchive(`brew install libarchive`、`sudo port install libarchive`、`sudo apt install libarchive-dev`など) ## iOS用にビルドする プロジェクトをXcodeで開き、iSH.xcconfigを開いて、`ROOT_BUNDLE_IDENTIFIER`を一意の値に変更します。また、プロジェクト(ターゲットではなく!)のビルド設定で開発チームIDを更新する必要があります。その後、実行をクリックします。他のすべてを自動的に行うスクリプトがあります。問題が発生した場合は、issueを開いてください。お手伝いします。 ## テスト用のコマンドラインツールをビルドする 環境を設定するには、プロジェクトディレクトリに移動し、`meson build`を実行して`build`ディレクトリを作成します。その後、buildディレクトリに移動し、`ninja`を実行します。 自己完結型のAlpine linuxファイルシステムを設定するには、[Alpineウェブサイト](https://alpinelinux.org/downloads/)からi386用のAlpine minirootfs tarballをダウンロードし、`./tools/fakefsify`を実行します。minirootfs tarballを最初の引数として、出力ディレクトリの名前を2番目の引数として指定します。その後、`./ish -f alpine /bin/sh`を使用して、Alpineファイルシステム内でコマンドを実行できます。出力ディレクトリの名前が`alpine`であると仮定します。`tools/fakefsify`がbuildディレクトリに存在しない場合、それはシステム上でlibarchiveを見つけられなかったためかもしれません(インストール方法については上記を参照してください)。 `ish`を`tools/ptraceomatic`に置き換えることで、実際のプロセスでプログラムを実行し、各ステップでレジスタを比較しながらシングルステップ実行できます。デバッグに使用します。64ビットLinux 4.11以降が必要です。 ## ロギング iSHには、ビルド時に有効にできるいくつかのロギングチャネルがあります。デフォルトでは、すべて無効になっています。有効にするには: - Xcodeで:iSH.xcconfigの`ISH_LOG`設定をスペースで区切られたログチャネルのリストに設定します。 - Meson(テスト用のコマンドラインツール)で:`meson configure -Dlog="<ログチャネルのスペース区切りリスト>"`を実行します。 利用可能なチャネル: - `strace`:最も有用なチャネルで、ほぼすべてのシステムコールのパラメータと戻り値をログに記録します。 - `instr`:エミュレータが実行するすべての命令をログに記録します。これにより、実行速度が大幅に低下します。 - `verbose`:他のカテゴリに該当しないデバッグログを記録します。 - `DEFAULT_CHANNEL`をgrepして、このリストが更新された後に追加されたログチャネルがあるかどうかを確認します。 # JITに関する注意事項 iSHの一部として書いた中で最も興味深いものの1つはJITです。実際には、マシンコードをターゲットにしていないため、実際のJITではありません。代わりに、ガジェットと呼ばれる関数へのポインタの配列を生成し、各ガジェットは次の関数へのテールコールで終了します。これは、一部のForthインタープリタが使用するスレッド化コード技術に似ています。その結果、純粋なエミュレーションと比較して、速度が約3〜5倍向上します。 残念ながら、ほぼすべてのガジェットをアセンブリ言語で書くという決定を下しました。これは、パフォーマンスに関してはおそらく良い決定でしたが(確かではありませんが)、可読性、保守性、および私の正気に関してはひどい決定でした。コンパイラ、アセンブラ、リンカからのたくさんの問題に対処しなければなりませんでした。コードが十分に変形していることを確認し、そうでない場合は、コンパイルできない理由をでっち上げる悪魔がいるようなものです。このコードを書いている間に正気を保つために、コード構造と命名のベストプラクティスを無視しなければなりませんでした。`ss`、`s`、`a`などの説明的な名前を持つマクロや変数が見つかるでしょう。信じられないほどネストされたアセンブラマクロ。そして、ほとんどコメントがありません。 したがって、警告です:このコードに長期間さらされると、正気を失い、GASマクロやリンカエラーについての悪夢に悩まされる可能性があります。カリフォルニア州では、このコードが癌、先天性欠損症、および生殖障害を引き起こすことが知られています。 ================================================ FILE: README_KO.md ================================================ # [iSH](https://ish.app) [![Build Status](https://github.com/ish-app/ish/actions/workflows/ci.yml/badge.svg)](https://github.com/ish-app/ish/actions) [![goto counter](https://img.shields.io/github/search/ish-app/ish/goto.svg)](https://github.com/ish-app/ish/search?q=goto) [![fuck counter](https://img.shields.io/github/search/ish-app/ish/fuck.svg)](https://github.com/ish-app/ish/search?q=fuck) [![shit counter](https://img.shields.io/github/search/ish-app/ish/shit.svg)](https://github.com/ish-app/ish/search?q=shit)

사용자 모드 x86 에뮬레이션과 시스템 call 번역을 사용하여 iOS 에서 리눅스 쉘을 실행할 수 있게 해줍니다. 프로젝트의 현황을 알고 싶으시면 커밋 로그와 이슈 탭을 참고해주세요. - [애플 앱스토어](https://apps.apple.com/us/app/ish-shell/id1436902243) - [TestFlight beta](https://testflight.apple.com/join/97i7KM8O) - [Discord server](https://discord.gg/HFAXj44) - [도움 문서 Wiki](https://github.com/ish-app/ish/wiki) - [README중문](https://github.com/ish-app/ish/blob/master/README_ZH.md) # Hacking 해당 프로젝트는 깃의 서브 모듈이 있습니다. 해당 저장소를 받은 후 `--recurse-submodules` 또는 `git submodule update --init` 을 입력하여 깃 서브 모듈을 클론하세요. 아래 사항은 이 프로젝트를 빌드하기 위해 필요한 것들 입니다: - Python 3 + Meson (`pip3 install meson`) - Ninja - Clang and LLD (맥에서는, `brew install llvm`, 리눅스에서는, `sudo apt install clang lld` 또는 `sudo pacman -S clang lld` 을 실행하세요) - sqlite3 (맥에서는 이미 제공 되어 있을 확률이 높습니다. 만약 그렇지 않다면 `sudo apt install libsqlite3-dev`) - libarchive (`brew install libarchive`, `sudo port install libarchive`, `sudo apt install libarchive-dev`) TODO: 앞에 dependency를 번들링 하기 ## iOS 로 빌드하는 법 Xcode로 프로젝트를 열고, iSH.xcconfig 연 후에 `ROOT_BUNDLE_IDENTIFIER`를 해당 프로젝트에 유일한 값으로 바꾸세요. 그후 실행을 누르면 자동으로 나머지를 세팅해줄 스크립트가 제공되어 있습니다. 만약 문제가 생긴다면, issue open을 해주시면 도와드리겠습니다. ## 테스트를 위한 cli 도구 빌드하는 법 환경을 세팅하기 위해서는 프로젝트 디렉토리로 이동하고 `meson build`를 커맨드 라인에 입력하세요. 그 후 빌드 된 디렉토리로 cd 후 `ninja` 커맨드를 입력해 실행하세요. 자체적으로 컨테이너 화 된 Alpine 리눅스 파일 시스템으로 실행하고 싶다면, [Alpine 웹사이트](https://alpinelinux.org/downloads/) 에서 i386을 위한 Alpine minirootfs(Mini Root Filesystem) tarball 을 다운로드 받고 `./tools/fakefsify`으로 실행하세요. 매개인자로 다운로드 받은 minirootfs tarball 파일을 입력하고 출력 받을 디렉토리의 이름을 두번째 인자로 입력하면 됩니다. 그 후에는 `./ish -f {출력받을 디렉토리 이름} /bin/sh` 명령어를 사용하여 Alpine 시스템 내에서 원하는 것을 실행할 수 있습니다. 만약 `tools/fakefsify` 가 빌드 디렉토리에 존재하지 않는다면, libarchive를 찾을 수 없어서 그런 것일 수 있습니다. 위를 참고하여 시스템에 설치하는 방법을 참고해주세요. 실제 프로세스로 프로그램을 실행하고 각 단계의 레지스터를 비교하기 위해서 `ish`를 `tools/ptraceomatic`로 바꿔 실행할 수 있습니다. 디버깅을 위해 저는 사용합니다. 64-bit Linux 4.11 이후 버전이 필요합니다. ## 로깅 iSH 는 빌드 시간에 허용될 수 있는 다수의 로깅 채널을 갖고 있습니다. 기본 값으로는 모두 꺼놨는데, 사용을 위해서는: - Xcode에서: iSH.xcconfig에 있는 `ISH_LOG` 값을 스페이스로 나뉜 로그 채널 리스트로 설정해주세요. - Meson에서 (테스트를 위한 커맨드 라인 도구): `meson configure -Dlog="<스페이스로 나뉜 로그 채널 리스트>"`을 실행하세요. 제공되는 로그 채널: - `strace`: 가장 쓸모있는 채널입니다. 매개변수와 거의 모든 시스템 호출의 반환 값을 로깅합니다. - `instr`: 에뮬레이터에서 실행된 모든 명령어를 로깅합니다. 이로인해 성능저하가 일어날 수 있습니다. - `verbose`: 다른 카타고리에 들지 않는 로그를 디버깅합니다. - `DEFAULT_CHANNEL`을 찾아보면 리스트가 업데이트 이후 새로 추가된 로그 채널을 볼수 있습니다. # JIT(Just In Time 컴파일러)에 대한 추가사항 iSH에서 추가한 것 중 가장 흥미로운 것은 JIT 컴파일러 일 것입니다. 기계 코드를 목적으로 하지 않기 때문에 JIT 실질적으로는 아니긴 합니다. Gadget 이라고 불리는 포인터 배열을 생성하는데, 각각의 이것은 다음 함수를 호출하는 꼬리물기를 합니다. 몇몇 Forth 언어 인터프리터에서 사용된 스레드 코드처럼 말이죠. 결과적으로 순수 에뮬보다 3-5배 더 빨라졌습니다. 불행하게도 저는 어셈블리어로 대부분의 이러한 gadget을 작성했습니다. 이것은 성능적으로는 좋은 선택이었을 지 몰라도(실제로는 알 도리가 없지만), 가독성, 유지보수, 그리고 제 정신상태에 대해서는 좋지 않은 선택이 되었습니다. 컴파일러/어셈블러/링커로 인한 여러 고충은 말도 할 수 없을 정도입니다. 거의 무슨 제 코드의 가독성을 해치지 않으면 컴파일을 막는 그러한 악마가 있는 것 같았습니다. 이 코드를 작성하는 도중 제정신을 유지하기 위해서 저는 네이밍과 코드 구조론을 따른 최적의 선택을 하지 못하였습니다. `ss`, `s` 그리고 `a`와 같은 매크로 그리고 변수 명을 찾을 수 있을 것입니다. 주석 또한 찾기 힘들 것입니다. 그렇기에 주의 하세요: 해당 코드를 장기간 접할 경우 정신질환을 앓게되거나 GAS 매크로와 링커오류에 대한 악몽에 시달리고 또다른 부작용이 있을 수 있습니다. 암, 선천적 결함, 또는 생식기 질환을 야기한다고 질병관리청에서 인정했습니다. 암튼 그랬습니다. ================================================ FILE: README_ZH.md ================================================ # [iSH](https://ish.app) [![Build Status](https://github.com/ish-app/ish/actions/workflows/ci.yml/badge.svg)](https://github.com/ish-app/ish/actions) [![goto counter](https://img.shields.io/github/search/ish-app/ish/goto.svg)](https://github.com/ish-app/ish/search?q=goto) [![fuck counter](https://img.shields.io/github/search/ish-app/ish/fuck.svg)](https://github.com/ish-app/ish/search?q=fuck) [![shit counter](https://img.shields.io/github/search/ish-app/ish/shit.svg)](https://github.com/ish-app/ish/search?q=shit)

iSH 是一个运行在 iOS 上的 Linux shell。本项目使用了 x86 用户模式仿真和系统调用翻译转换。 请查看 issue 和提交记录以了解本项目当前的状态。 - [App Store 页面](https://apps.apple.com/us/app/ish-shell/id1436902243) - [Testflight 测试](https://testflight.apple.com/join/97i7KM8O) - [Discord 服务器](https://discord.gg/HFAXj44) - [维基帮助与教程](https://github.com/ish-app/ish/wiki) # 上手 本项目下包含了其他 git 项目作为子模块,请确保在克隆时使用参数`--recurse-submodules`,即 `git clone --recurse-submodules https://github.com/ish-app/ish.git`。或是在克隆好了之后执行 `git submodule update --init`。 编译此项目需要以下依赖: - Python 3 + Meson (`pip3 install meson`) - Ninja 请查看[此处](https://ninja-build.org/) - Clang and LLD (在安装了 `brew` 的 macOS 系统上运行 `brew install llvm`。在 Linux 系统上请根据你的包管理器,选择运行相应的安装命令 `sudo apt install clang lld` 或者 `sudo pacman -S clang lld`) - sqlite3 (通常 sqlite3 在 macOS 上是预安装的,但它或许没有安装在你的 Linux 上,运行 `which sqlite3` 以查看它是否存在。如果没有,你可以根据你的包管理器运行 `sudo apt install libsqlite3-dev` 之类的安装命令) - libarchive (在 macOS 系统上使用 `brew install libarchive` 或 `sudo port install libarchive` 来安装。在 Linux 系统上请根据你的包管理器,选择运行相应的安装命令如 `sudo apt install libarchive-dev` 来安装) ## 创建iOS应用 使用 Xcode 打开项目,选择 iSH.xcconfig,并且修改 `ROOT_BUNDLE_IDENTIFIER` 为你的[唯一值](https://help.apple.com/xcode/mac/current/#/dev91fe7130a)。此外,还需要在项目(project)的构建设置(build settings)中更新开发团队 ID,注意这里指的不是目标(target)的构建设置(build settings)。然后点击 `运行`,之后应该有脚本帮你自动执行相关操作。如果遇到了任何问题,请提交 issue,我们会帮你解决。 ## 为测试构建命令行工具 在项目目录中运行命令 `meson build`,之后 `build` 目录会被创建。进入到 `build` 目录并运行命令 `ninja`。 为了建立一个自有的 Alpine linux 文件系统,请从 [Alpine 网站](https://alpinelinux.org/downloads/) 下载 `Alpine minirotfs tarball for i386` 并运行 `tools/fakefsify` 。将 minirotfs tarball 指定为第一个参数,将输出目录的名称(如`alpine`)指定为第二个参数,即 `tools/fakefsify $MinirotfsTarballFilename alpine` 然后在 Alpine 文件系统中运行 `/ish -f alpine/bin/sh`。如果 `build` 目录下找不到 `tools/fakefsify`,可能是系统上找不到 `libarchive` 的依赖(请参照前面的章节进行安装)。 除了可以使用 `ish`,你也可以使用 `tools/ptraceomatic` 替代它,以便在某个真实进程中单步比较寄存器。我通常使用它来进行调试(需要 64 位 Linux 4.11 或更高版本)。 ## 日志 在编译过程中,iSH 提供数种日志类型,默认情况下它们都被禁用,想要启用它们需要: - 在 Xcode 中将 iSH.xcconfig 中 `ISH_LOG` 设置为以空格分隔的日志类型列表。 - 在 Meson (测试使用的命令行工具) 中执行命令 `meson configure -Dlog=""`。 可用的日志类型: - `strace`: 最有用的类型,记录几乎每个系统调用的参数和返回值。 - `instr`: 记录模拟器执行的每个指令,这会让所有执行变得很慢。 - `verbose`: 记录不属于其他类别的调试日志。 - 使用 `grep` 命令查看 `DEFAULT_CHANNEL` 变量,以确认在更新此列表后是否添加了更多日志频道。 # 关于 JIT 可能我在写 iSH 中最有趣的部分就是 JIT 了。实际上它不是真正的 JIT,因为它不并以机器代码为目标,而是生成一个称为 gadgets 的函数指针数组,并且每个 gadget 都以对下一个函数的尾调用结束,类似于一些 Forth 解释器使用的线程化代码技术。好处就是,与纯仿真相比,它的速度提高了 3-5 倍。 但不幸的是,我最开始决定用汇编语言编写几乎所有的 gadgets。这可能从性能方面来说是一个好的决定(虽然我永远也无法确定),但是对可读性、可维护性和我的理智来说,这是一个可怕的决定。我承受了大量来自编译器、汇编程序以及链接器的乱七八糟的东西。那里面就像有一个魔鬼,把我的代码搞得畸形,就算没有畸形,也会编造一些愚蠢的理由说它不能够编译。为了在编写代码时保持理智,我不得不忽略代码结构和命名方面的最佳实践。你会发现宏和变量具有诸如 `ss`、`s` 和 `a` 等描述性的名称,并且汇编器的宏嵌套层数超乎你的想象。最重要的是,代码中几乎没有任何注释。 所以这是一个警告: 长期接触此代码可能会使你失去理智,对 GAS 宏和链接器错误产生噩梦,或是任何其他使人虚弱的副作用。在加利福尼亚,众所周知这样的代码会导致癌症、生产缺陷和重复伤害。 ================================================ FILE: SECURITY.md ================================================ # iSH is not a security boundary! The goal of this project is to support a Linux shell on iOS. As such, its security model assumes that the app is running in another sandbox and is used by a single user. The project is focused on compatibility, and very little thought has been put into internal security. Permissions are only loosely checked. Memory corruption in edge cases is common. Please do not use iSH for any sort of secure containerization or production use case. As such, most types of bugs that are security issues in most projects are not security issues in iSH. Insufficient permission checks, memory corruption, and thread safety issues are generally considered correctness bugs and would be best filed as GitHub issues. We will prioritize bugs encountered by real programs in typical use. In our security model, we expect real security bugs to be very rare. It's not completely impossible, e.g. a bug allowing remote code execution without user consent would be a security bug. If you think you found one, you can send it to security@ish.app. We'll work with you to resolve it appropriately. ================================================ FILE: app/AboutAppearanceViewController.h ================================================ // // ThemeViewController.h // iSH // // Created by Charlie Melbye on 11/12/18. // #import NS_ASSUME_NONNULL_BEGIN @interface AboutAppearanceViewController : UITableViewController @end NS_ASSUME_NONNULL_END ================================================ FILE: app/AboutAppearanceViewController.m ================================================ // // ThemeViewController.m // iSH // // Created by Charlie Melbye on 11/12/18. // #import "AboutAppearanceViewController.h" #import "FontPickerViewController.h" #import "TerminalView.h" #import "ThemesViewController.h" #import "UserPreferences.h" #import "NSObject+SaneKVO.h" @interface AboutAppearanceViewController () @property (strong, nonatomic) IBOutlet UISwitch *blinkCursor; @property (strong, nonatomic) IBOutlet UISegmentedControl *cursorStyle; @property (strong, nonatomic) IBOutlet UISwitch *hideStatusBar; @property UIFontPickerViewController *fontPicker API_AVAILABLE(ios(13)); @end char *previewString = "# cat /proc/ish/colors\r\n" "\x1B[30m" "iSH" "\x1B[39m " "\x1B[31m" "iSH" "\x1B[39m " "\x1B[32m" "iSH" "\x1B[39m " "\x1B[33m" "iSH" "\x1B[39m " "\x1B[34m" "iSH" "\x1B[39m " "\x1B[35m" "iSH" "\x1B[39m " "\x1B[36m" "iSH" "\x1B[39m " "\x1B[37m" "iSH" "\x1B[39m" "\r\n\x1B[7m" "\x1B[40m" "iSH" "\x1B[39m " "\x1B[41m" "iSH" "\x1B[39m " "\x1B[42m" "iSH" "\x1B[39m " "\x1B[43m" "iSH" "\x1B[39m " "\x1B[44m" "iSH" "\x1B[39m " "\x1B[45m" "iSH" "\x1B[39m " "\x1B[46m" "iSH" "\x1B[39m " "\x1B[47m" "iSH" "\x1B[39m" "\x1B[0m\x1B[1m\r\n" "\x1B[90m" "iSH" "\x1B[39m " "\x1B[91m" "iSH" "\x1B[39m " "\x1B[92m" "iSH" "\x1B[39m " "\x1B[93m" "iSH" "\x1B[39m " "\x1B[94m" "iSH" "\x1B[39m " "\x1B[95m" "iSH" "\x1B[39m " "\x1B[96m" "iSH" "\x1B[39m " "\x1B[97m" "iSH" "\x1B[39m" "\r\n\x1B[7m" "\x1B[100m" "iSH" "\x1B[39m " "\x1B[101m" "iSH" "\x1B[39m " "\x1B[102m" "iSH" "\x1B[39m " "\x1B[103m" "iSH" "\x1B[39m " "\x1B[104m" "iSH" "\x1B[39m " "\x1B[105m" "iSH" "\x1B[39m " "\x1B[106m" "iSH" "\x1B[39m " "\x1B[107m" "iSH" "\x1B[39m" "\x1B[0m\r\n" "# "; @implementation AboutAppearanceViewController { TerminalView *_terminalView; Terminal *_terminal; struct tty *_tty; } - (void)viewDidLoad { [super viewDidLoad]; [UserPreferences.shared observe:@[@"theme", @"fontSize", @"fontFamily", @"colorScheme"] options:0 owner:self usingBlock:^(typeof(self) self) { dispatch_async(dispatch_get_main_queue(), ^{ [self.tableView reloadData]; }); }]; [UserPreferences.shared observe:@[@"cursorStyle", @"blinkCursor", @"hideStatusBar"] options:0 owner:self usingBlock:^(typeof(self) self) { dispatch_async(dispatch_get_main_queue(), ^{ [self updateOtherControls]; }); }]; [self updateOtherControls]; #if !ISH_LINUX if (![NSUserDefaults.standardUserDefaults boolForKey:@"recovery"]) { _terminal = [Terminal createPseudoTerminal:&_tty]; [_terminal sendOutput:previewString length:(int)strlen(previewString)]; } #endif } - (void)viewDidAppear:(BOOL)animated { if (@available(iOS 13, *)) { // Initialize the font picker ASAP, as it takes about a quarter second to initialize (XPC crap) and appears invisible until then. // Re-initialize it after navigating away from it, to reset the table view highlight. UIFontPickerViewControllerConfiguration *config = [UIFontPickerViewControllerConfiguration new]; config.filteredTraits = UIFontDescriptorTraitMonoSpace; self.fontPicker = [[UIFontPickerViewController alloc] initWithConfiguration:config]; // Prevent the font picker from resizing the popup when it appears self.fontPicker.preferredContentSize = CGSizeZero; self.fontPicker.navigationItem.title = @"Font"; self.fontPicker.delegate = self; self.fontPicker.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Reset" style:UIBarButtonItemStylePlain target:self action:@selector(resetFont:)]; } } #pragma mark - Table view data source enum { PreviewSection, MainSection, ColorSchemeSection, CursorSection, StatusBarSection, NumberOfSections, }; - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return NumberOfSections; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { switch (section) { case PreviewSection: return 2; case MainSection: return 3; case ColorSchemeSection: return 3; case CursorSection: return 2; case StatusBarSection: return 1; default: NSAssert(NO, @"unhandled section"); return 0; } } - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { switch (section) { case PreviewSection: return @"Preview"; case ColorSchemeSection: return @"Color Scheme"; case CursorSection: return @"Cursor"; case StatusBarSection: return @"Status Bar"; default: return nil; } } - (NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section { switch (section) { case PreviewSection: return @"Change the color scheme used for the preview."; default: return nil; } } - (NSString *)reuseIdentifierForIndexPath:(NSIndexPath *)indexPath { switch (indexPath.section) { case PreviewSection: return @[@"Preview", @"Color Scheme Preview"][indexPath.row]; case MainSection: return @[@"Theme Name", @"Font", @"Font Size"][indexPath.row]; case ColorSchemeSection: return @"Color Scheme"; case CursorSection: return @[@"Cursor Style", @"Blink Cursor"][indexPath.row]; case StatusBarSection: return @"Status Bar"; default: return nil; } } - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { if (indexPath.section == PreviewSection && indexPath.row == 0) { // Try a best-effort guess as to how big the preview should be. return [@"\n\n\n\n\n\n" sizeWithAttributes:@{NSFontAttributeName: UserPreferences.shared.approximateFont}].height + 10; } else { return UITableViewAutomaticDimension; } } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:[self reuseIdentifierForIndexPath:indexPath] forIndexPath:indexPath]; cell.selectionStyle = UITableViewCellSelectionStyleDefault; switch (indexPath.section) { case PreviewSection: switch (indexPath.row) { case 0: _terminalView = [cell viewWithTag:1]; _terminalView.userInteractionEnabled = NO; _terminalView.terminal = _terminal; break; case 1: { UISegmentedControl *segmentedControl = [cell viewWithTag:1]; [segmentedControl addTarget:self action:@selector(changePreviewTheme:) forControlEvents:UIControlEventValueChanged]; [self changePreviewTheme:segmentedControl]; cell.selectionStyle = UITableViewCellSelectionStyleNone; break; } } break; case MainSection: switch (indexPath.row) { case 0: cell.detailTextLabel.text = UserPreferences.shared.theme.name; break; case 1: cell.detailTextLabel.text = UserPreferences.shared.fontFamilyUserFacingName; cell.detailTextLabel.font = [UIFont fontWithName:UserPreferences.shared.fontFamily size:cell.detailTextLabel.font.pointSize]; break; case 2: { UserPreferences *prefs = [UserPreferences shared]; UILabel *label = [cell viewWithTag:1]; UIStepper *stepper = [cell viewWithTag:2]; label.text = prefs.fontSize.stringValue; stepper.value = prefs.fontSize.doubleValue; cell.selectionStyle = UITableViewCellSelectionStyleNone; break; } } break; case ColorSchemeSection: switch (indexPath.row) { case 0: cell.textLabel.text = @"Match System"; break; case 1: cell.textLabel.text = @"Light"; break; case 2: cell.textLabel.text = @"Dark"; break; } cell.accessoryType = indexPath.row == UserPreferences.shared.colorScheme ? UITableViewCellAccessoryCheckmark : UITableViewCellAccessoryNone; break; case CursorSection: case StatusBarSection: cell.selectionStyle = UITableViewCellSelectionStyleNone; break; } return cell; } - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { [tableView deselectRowAtIndexPath:indexPath animated:YES]; switch (indexPath.section) { case MainSection: switch (indexPath.row) { case 0: { // theme ThemesViewController *themesViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"Themes"]; [self.navigationController pushViewController:themesViewController animated:YES]; break; } case 1: // font family [self selectFont:nil]; break; } break; case ColorSchemeSection: [UserPreferences.shared setColorScheme:indexPath.row]; } } - (void)updateOtherControls { self.hideStatusBar.on = UserPreferences.shared.hideStatusBar; self.cursorStyle.selectedSegmentIndex = UserPreferences.shared.cursorStyle; self.blinkCursor.on = UserPreferences.shared.blinkCursor; [self setNeedsStatusBarAppearanceUpdate]; } - (void)changePreviewTheme:(UISegmentedControl *)sender { _terminalView.overrideAppearance = sender.selectedSegmentIndex ? OverrideAppearanceDark : OverrideAppearanceLight; _terminalView.backgroundColor = [[UIColor alloc] ish_initWithHexString:(sender.selectedSegmentIndex ? UserPreferences.shared.theme.darkPalette : UserPreferences.shared.theme.lightPalette).backgroundColor]; } - (void)selectFont:(id)sender { if (@available(iOS 13, *)) { [self.navigationController pushViewController:self.fontPicker animated:YES]; return; } FontPickerViewController *fontPicker = [self.storyboard instantiateViewControllerWithIdentifier:@"FontPicker"]; [self.navigationController pushViewController:fontPicker animated:YES]; } - (void)fontPickerViewControllerDidPickFont:(UIFontPickerViewController *)viewController API_AVAILABLE(ios(13.0)) { UserPreferences.shared.fontFamily = viewController.selectedFontDescriptor.fontAttributes[UIFontDescriptorFamilyAttribute]; [self.navigationController popToViewController:self animated:YES]; } - (IBAction)resetFont:(UIBarButtonItem *)sender API_AVAILABLE(ios(13)) { UserPreferences.shared.fontFamily = nil; [self.navigationController popToViewController:self animated:YES]; } - (IBAction)fontSizeChanged:(UIStepper *)sender { UserPreferences.shared.fontSize = @((int) sender.value); } - (IBAction)hideStatusBarChanged:(UISwitch *)sender { UserPreferences.shared.hideStatusBar = sender.on; [self setNeedsStatusBarAppearanceUpdate]; } - (IBAction)cursorStyleChanged:(UISegmentedControl *)sender { [UserPreferences.shared setCursorStyle:sender.selectedSegmentIndex]; } - (IBAction)blinkCursorChanged:(UISwitch *)sender { [UserPreferences.shared setBlinkCursor:sender.on]; } @end ================================================ FILE: app/AboutExternalKeyboardViewController.h ================================================ // // CapsLockMappingViewController.h // iSH // // Created by Theodore Dubois on 12/2/18. // #import NS_ASSUME_NONNULL_BEGIN @interface AboutExternalKeyboardViewController : UITableViewController @end NS_ASSUME_NONNULL_END ================================================ FILE: app/AboutExternalKeyboardViewController.m ================================================ // // CapsLockMappingViewController.m // iSH // // Created by Theodore Dubois on 12/2/18. // #import "AboutExternalKeyboardViewController.h" #import "UserPreferences.h" #import "NSObject+SaneKVO.h" const int kCapsLockMappingSection = 0; @interface AboutExternalKeyboardViewController () @property (weak, nonatomic) IBOutlet UISwitch *optionMetaSwitch; @property (weak, nonatomic) IBOutlet UISwitch *backtickEscapeSwitch; @property (weak, nonatomic) IBOutlet UISwitch *overrideControlSpaceSwitch; @property (weak, nonatomic) IBOutlet UISwitch *hideExtraKeysWithExternalKeyboardSwitch; @end @implementation AboutExternalKeyboardViewController - (void)viewDidLoad { [super viewDidLoad]; [UserPreferences.shared observe:@[@"capsLockMapping", @"optionMapping"] options:0 owner:self usingBlock:^(typeof(self) self) { dispatch_async(dispatch_get_main_queue(), ^{ [self.tableView reloadData]; }); }]; [self _update]; } - (void)_update { self.optionMetaSwitch.on = UserPreferences.shared.optionMapping == OptionMapEsc; self.backtickEscapeSwitch.on = UserPreferences.shared.backtickMapEscape; self.overrideControlSpaceSwitch.on = UserPreferences.shared.overrideControlSpace; self.hideExtraKeysWithExternalKeyboardSwitch.on = UserPreferences.shared.hideExtraKeysWithExternalKeyboard; } - (IBAction)optionMetaToggle:(UISwitch *)sender { UserPreferences.shared.optionMapping = sender.on ? OptionMapEsc : OptionMapNone; } - (IBAction)backtickEscapeToggle:(UISwitch *)sender { UserPreferences.shared.backtickMapEscape = sender.on; } - (IBAction)overrideControlSpaceToggle:(UISwitch *)sender { UserPreferences.shared.overrideControlSpace = sender.on; } - (IBAction)hideExtraKeysToggle:(UISwitch *)sender { UserPreferences.shared.hideExtraKeysWithExternalKeyboard = sender.on; } - (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath { if (indexPath.section == kCapsLockMappingSection && cell.tag == UserPreferences.shared.capsLockMapping) cell.accessoryType = UITableViewCellAccessoryCheckmark; else cell.accessoryType = UITableViewCellAccessoryNone; } - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { if (indexPath.section == kCapsLockMappingSection) { UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath]; UserPreferences.shared.capsLockMapping = cell.tag; } } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { if (section == 0 && ![self.class capsLockMappingSupported]) return 0; return [super tableView:tableView numberOfRowsInSection:section]; } - (NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section { if (section == 0 && ![self.class capsLockMappingSupported]) return @"Caps Lock mapping is broken in iOS 13.\n\n" @"Since iOS 13.4, Caps Lock can be remapped system-wide in Settings → General → Keyboard → Hardware Keyboard → Modifier Keys."; return [super tableView:tableView titleForFooterInSection:section]; } - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { if (section == 0 && ![self.class capsLockMappingSupported]) return @""; return [super tableView:tableView titleForHeaderInSection:section]; } + (BOOL)capsLockMappingSupported { if (@available(iOS 13, *)) { return NO; } return YES; } @end ================================================ FILE: app/AboutNavigationController.h ================================================ // // AboutNavigationController.h // iSH // // Created by Theodore Dubois on 10/6/19. // #import NS_ASSUME_NONNULL_BEGIN @interface AboutNavigationController : UINavigationController @end NS_ASSUME_NONNULL_END ================================================ FILE: app/AboutNavigationController.m ================================================ // // AboutNavigationController.m // iSH // // Created by Theodore Dubois on 10/6/19. // #import "AboutNavigationController.h" #import "UserPreferences.h" #import "NSObject+SaneKVO.h" @interface AboutNavigationController () @end @implementation AboutNavigationController - (void)viewDidLoad { [super viewDidLoad]; [UserPreferences.shared observe:@[@"colorScheme"] options:NSKeyValueObservingOptionInitial owner:self usingBlock:^(typeof(self) self) { dispatch_async(dispatch_get_main_queue(), ^{ if (@available(iOS 13, *)) { self.overrideUserInterfaceStyle = UserPreferences.shared.userInterfaceStyle; } }); }]; } @end ================================================ FILE: app/AboutViewController.h ================================================ // // AboutViewController.h // iSH // // Created by Theodore Dubois on 9/23/18. // #import NS_ASSUME_NONNULL_BEGIN @interface AboutViewController : UITableViewController @property BOOL includeDebugPanel; @property BOOL recoveryMode; @end NS_ASSUME_NONNULL_END ================================================ FILE: app/AboutViewController.m ================================================ // // AboutViewController.m // iSH // // Created by Theodore Dubois on 9/23/18. // #import "AboutViewController.h" #import "AppDelegate.h" #import "CurrentRoot.h" #import "AppGroup.h" #import "UserPreferences.h" #import "iOSFS.h" #import "UIApplication+OpenURL.h" #import "NSObject+SaneKVO.h" @interface AboutViewController () @property (weak, nonatomic) IBOutlet UITableViewCell *capsLockMappingCell; @property (weak, nonatomic) IBOutlet UITableViewCell *themeCell; @property (weak, nonatomic) IBOutlet UISwitch *disableDimmingSwitch; @property (weak, nonatomic) IBOutlet UITextField *launchCommandField; @property (weak, nonatomic) IBOutlet UITextField *bootCommandField; @property (weak, nonatomic) IBOutlet UITableViewCell *sendFeedback; @property (weak, nonatomic) IBOutlet UITableViewCell *openGithub; @property (weak, nonatomic) IBOutlet UITableViewCell *openFediverse; @property (weak, nonatomic) IBOutlet UITableViewCell *openDiscord; @property (weak, nonatomic) IBOutlet UITableViewCell *upgradeApkCell; @property (weak, nonatomic) IBOutlet UILabel *upgradeApkLabel; @property (weak, nonatomic) IBOutlet UIView *upgradeApkBadge; @property (weak, nonatomic) IBOutlet UITableViewCell *exportContainerCell; @property (weak, nonatomic) IBOutlet UITableViewCell *resetMountsCell; @property (weak, nonatomic) IBOutlet UILabel *versionLabel; @property (weak, nonatomic) IBOutlet NSLayoutConstraint *saddamHussein; @end @implementation AboutViewController - (void)viewDidLoad { [super viewDidLoad]; [self _updateUI]; if (self.recoveryMode) { self.includeDebugPanel = YES; self.navigationItem.title = @"Recovery Mode"; self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Exit" style:UIBarButtonItemStyleDone target:self action:@selector(exitRecovery:)]; self.navigationItem.leftBarButtonItem = nil; } _versionLabel.text = [NSString stringWithFormat:@"iSH %@ (Build %@)", [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"], [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]]; [UserPreferences.shared observe:@[@"capsLockMapping", @"fontSize", @"launchCommand", @"bootCommand"] options:0 owner:self usingBlock:^(typeof(self) self) { dispatch_async(dispatch_get_main_queue(), ^{ [self _updateUI]; }); }]; [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(_updateUI:) name:FsUpdatedNotification object:nil]; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; [self _updateUI]; } - (void)updateViewConstraints { self.saddamHussein.constant = UIEdgeInsetsInsetRect(self.tableView.frame, self.tableView.adjustedContentInset).size.height; [super updateViewConstraints]; } - (IBAction)dismiss:(id)sender { [self dismissViewControllerAnimated:self completion:nil]; } - (void)exitRecovery:(id)sender { [NSUserDefaults.standardUserDefaults setBool:NO forKey:@"recovery"]; exit(0); } - (void)_updateUI:(NSNotification *)notification { [self _updateUI]; } - (void)_updateUI { NSAssert(NSThread.isMainThread, @"This method needs to be called on the main thread"); self.disableDimmingSwitch.on = UserPreferences.shared.shouldDisableDimming; self.launchCommandField.text = [UserPreferences.shared.launchCommand componentsJoinedByString:@" "]; self.bootCommandField.text = [UserPreferences.shared.bootCommand componentsJoinedByString:@" "]; self.upgradeApkCell.userInteractionEnabled = FsNeedsRepositoryUpdate(); self.upgradeApkLabel.enabled = FsNeedsRepositoryUpdate(); self.upgradeApkBadge.hidden = !FsNeedsRepositoryUpdate(); [self.tableView reloadData]; } - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath]; if (cell == self.sendFeedback) { [UIApplication openURL:@"mailto:tblodt@icloud.com?subject=Feedback%20for%20iSH"]; } else if (cell == self.openGithub) { [UIApplication openURL:@"https://github.com/ish-app/ish"]; } else if (cell == self.openFediverse) { [UIApplication openURL:@"https://publ.ish.app/ish"]; } else if (cell == self.openDiscord) { [UIApplication openURL:@"https://discord.gg/HFAXj44"]; } else if (cell == self.exportContainerCell) { // copy the files to the app container so they can be extracted from iTunes file sharing NSURL *container = ContainerURL(); NSURL *documents = [NSFileManager.defaultManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask][0]; [NSFileManager.defaultManager removeItemAtURL:[documents URLByAppendingPathComponent:@"roots copy"] error:nil]; [NSFileManager.defaultManager copyItemAtURL:[container URLByAppendingPathComponent:@"roots"] toURL:[documents URLByAppendingPathComponent:@"roots copy"] error:nil]; } else if (cell == self.resetMountsCell) { #if !ISH_LINUX iosfs_clear_all_bookmarks(); #endif } [tableView deselectRowAtIndexPath:indexPath animated:YES]; } - (NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section { if (section == 1) { // filesystems / upgrade if (!FsIsManaged()) { return @"The current filesystem is not managed by iSH."; } else if (!FsNeedsRepositoryUpdate()) { return [NSString stringWithFormat:@"The current filesystem is using %s, which is the latest version.", CURRENT_APK_VERSION_STRING]; } else { return [NSString stringWithFormat:@"An upgrade to %s is available.", CURRENT_APK_VERSION_STRING]; } } return [super tableView:tableView titleForFooterInSection:section]; } - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { NSInteger sections = [super numberOfSectionsInTableView:tableView]; if (!self.includeDebugPanel) sections--; return sections; } - (IBAction)disableDimmingChanged:(id)sender { UserPreferences.shared.shouldDisableDimming = self.disableDimmingSwitch.on; } - (IBAction)textBoxSubmit:(id)sender { [sender resignFirstResponder]; } - (IBAction)launchCommandChanged:(id)sender { UserPreferences.shared.launchCommand = [self.launchCommandField.text componentsSeparatedByString:@" "]; } - (IBAction)bootCommandChanged:(id)sender { UserPreferences.shared.bootCommand = [self.bootCommandField.text componentsSeparatedByString:@" "]; } @end ================================================ FILE: app/AccessibilityFixes.m ================================================ // // AccessibilityFixes.m // iSH // // Created by Saagar Jha on 12/31/22. // #import "hook.h" #import #import #import #import // Work around https://bugs.webkit.org/show_bug.cgi?id=249976, which causes // https://github.com/ish-app/ish/issues/1937. static void replacement(void) { NSLog(@"Hooked PageClientImpl::assistiveTechnologyMakeFirstResponder"); } static bool patched; static void patch_if_needed(void) { if (!patched && UIAccessibilityIsVoiceOverRunning()) { patched = true; // This can take a little while. dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{ Dl_info info; dladdr((__bridge void *)objc_getClass("WKWebView"), &info); void *symbol = find_symbol(info.dli_fbase, "__ZN6WebKit14PageClientImpl37assistiveTechnologyMakeFirstResponderEv"); bool hooked = hook((void *)symbol, (void *)replacement); assert(hooked); }); } } __attribute__((constructor)) void accessibilityfixes_init(void) { if (@available(iOS 15.7, *)) { [NSNotificationCenter.defaultCenter addObserverForName:UIAccessibilityVoiceOverStatusDidChangeNotification object:nil queue:nil usingBlock:^(NSNotification *notification) { patch_if_needed(); }]; patch_if_needed(); } } ================================================ FILE: app/AltIconViewController.h ================================================ // // IconViewController.h // iSH // // Created by Theodore Dubois on 12/13/19. // #import NS_ASSUME_NONNULL_BEGIN @interface AltIconViewController : UIViewController @end NS_ASSUME_NONNULL_END ================================================ FILE: app/AltIconViewController.m ================================================ // // IconViewController.m // iSH // // Created by Theodore Dubois on 12/13/19. // #import "AltIconViewController.h" #import "UIApplication+OpenURL.h" @interface AltIconViewController () @property (weak) IBOutlet UICollectionView *collectionView; @property NSDictionary *altIcons; @property NSArray *altIconNames; @end @interface AltIconCell : UICollectionViewCell @property (weak, nonatomic) IBOutlet UIImageView *imageView; @property (weak, nonatomic) IBOutlet UIImageView *checkboxImageView; @property (weak, nonatomic) IBOutlet UIButton *authorButton; @property (nonatomic) NSString *link; - (void)updateImage:(UIImage *)image description:(NSString *)description author:(NSString *)author link:(NSURL *)link; @end @implementation AltIconViewController - (void)viewDidLoad { [super viewDidLoad]; self.altIcons = [NSDictionary dictionaryWithContentsOfURL: [NSBundle.mainBundle URLForResource:@"Icons" withExtension:@"plist"]]; self.altIconNames = [self.altIcons.allKeys sortedArrayUsingSelector:@selector(compare:)]; NSString *iconName = UIApplication.sharedApplication.alternateIconName; if (iconName == nil) iconName = @""; NSIndexPath *indexPath = [NSIndexPath indexPathForItem:[self.altIconNames indexOfObject:iconName] inSection:0]; [self.collectionView selectItemAtIndexPath:indexPath animated:NO scrollPosition:UICollectionViewScrollPositionTop]; } - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { return self.altIconNames.count; } - (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath { return [collectionView dequeueReusableSupplementaryViewOfKind:kind withReuseIdentifier:@"footer" forIndexPath:indexPath]; } - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { AltIconCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"icon" forIndexPath:indexPath]; NSString *iconName = self.altIconNames[indexPath.item]; [cell updateImage:[UIImage imageNamed:iconName.length == 0 ? @"icon" : iconName] description:self.altIcons[iconName][@"description"] author:self.altIcons[iconName][@"author"] link:self.altIcons[iconName][@"link"]]; return cell; } - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath { NSString *iconName = self.altIconNames[indexPath.item]; if (iconName.length == 0) iconName = nil; [UIApplication.sharedApplication setAlternateIconName:iconName completionHandler:^(NSError *err) { if (err != nil) NSLog(@"%@", err); }]; } - (IBAction)openSubmissions:(id)sender { [UIApplication openURL:@"https://github.com/tbodt/ish/issues/578"]; } - (CGFloat)sideInset:(UICollectionViewFlowLayout *)layout { // For maximum aesthetics, there should be a decent amount of spacing between cells static const CGFloat kMinSpacer = 20; // The insets should be somewhat smaller than the spacer static const CGFloat kInsetToSpacerRatio = 0.75; CGFloat total = layout.collectionView.frame.size.width; CGFloat item = layout.itemSize.width; NSUInteger count = (int) (total / item); CGFloat spacer; CGFloat inset; do { CGFloat slack = total - (item * count); spacer = slack / (2 * kInsetToSpacerRatio + count - 1); inset = spacer * kInsetToSpacerRatio; count--; } while (spacer < kMinSpacer); return inset; } - (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewFlowLayout *)layout insetForSectionAtIndex:(NSInteger)section { CGFloat sideInset = [self sideInset:layout]; return UIEdgeInsetsMake(sideInset, sideInset, 20, sideInset); } @end @implementation AltIconCell - (void)awakeFromNib { [super awakeFromNib]; CAShapeLayer *iconMask = [CAShapeLayer new]; iconMask.frame = self.imageView.bounds; iconMask.path = [UIBezierPath bezierPathWithRoundedRect:self.imageView.bounds cornerRadius:self.imageView.bounds.size.width * 0.225].CGPath; self.imageView.layer.mask = iconMask; self.imageView.layer.minificationFilter = kCAFilterTrilinear; if (@available(iOS 13, *)) { self.checkboxImageView.image = UIImage.checkmarkImage; } else { // self.checkboxImageView.backgroundColor = UIColor.whiteColor; // self.checkboxImageView.layer.cornerRadius = self.checkboxImageView.bounds.size.width / 2; } self.authorButton.titleLabel.adjustsFontForContentSizeCategory = YES; self.isAccessibilityElement = YES; self.accessibilityCustomActions = @[[[UIAccessibilityCustomAction alloc] initWithName:@"Open link" target:self selector:@selector(openSource:)]]; } - (void)updateImage:(UIImage *)image description:(NSString *)description author:(NSString *)author link:(NSString *)url { self.imageView.image = image; [self.authorButton setTitle:[NSString stringWithFormat:@"by %@", author] forState:UIControlStateNormal]; self.link = url; self.accessibilityLabel = [NSString stringWithFormat:@"%@ by %@", description, author]; } - (IBAction)openSource:(id)sender { [UIApplication openURL:self.link]; } - (void)setSelected:(BOOL)selected { [super setSelected:selected]; self.checkboxImageView.hidden = !selected; self.accessibilityTraits = selected ? UIAccessibilityTraitSelected : 0; } @end ================================================ FILE: app/App.xcconfig ================================================ #include "iOS.xcconfig" PRODUCT_NAME = iSH PRODUCT_BUNDLE_IDENTIFIER = $(PRODUCT_BUNDLE_IDENTIFIER) INFOPLIST_FILE = app/Info.plist INFOPLIST_PREPROCESS = YES INFOPLIST_PREFIX_HEADER = $(BUILT_PRODUCTS_DIR)/infoplisticons.h ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon CODE_SIGN_ENTITLEMENTS = app/iSH.entitlements HEADER_SEARCH_PATHS = $(inherited) $(SRCROOT) $(SRCROOT)/deps/libarchive/libarchive // AccessibilityFixes.m contains a static constructor that we don't want removed OTHER_LDFLAGS = -ObjC $(LINUX_APP_LDFLAGS) -u _accessibilityfixes_init ================================================ FILE: app/AppDelegate.h ================================================ // // AppDelegate.h // iSH // // Created by Theodore Dubois on 10/17/17. // #import @interface AppDelegate : UIResponder @property (strong, nonatomic) UIWindow *window; - (void)exitApp; #if !ISH_LINUX + (int)bootError; #endif + (void)maybePresentStartupMessageOnViewController:(UIViewController *)vc; @end #if !ISH_LINUX extern NSString *const ProcessExitedNotification; #else extern NSString *const KernelPanicNotification; #endif ================================================ FILE: app/AppDelegate.m ================================================ // // AppDelegate.m // iSH // // Created by Theodore Dubois on 10/17/17. // #include #include #include #import #import "AboutViewController.h" #import "AppDelegate.h" #import "AppGroup.h" #import "CurrentRoot.h" #import "ExceptionExfiltrator.h" #import "iOSFS.h" #import "SceneDelegate.h" #import "PasteboardDevice.h" #import "LocationDevice.h" #import "NSObject+SaneKVO.h" #import "Roots.h" #import "TerminalViewController.h" #import "UserPreferences.h" #import "UIApplication+OpenURL.h" #include "kernel/init.h" #include "kernel/calls.h" #include "fs/dyndev.h" #include "fs/devices.h" #include "fs/path.h" #if ISH_LINUX #import "LinuxInterop.h" #endif @interface AppDelegate () @property BOOL exiting; @property SCNetworkReachabilityRef reachability; @end #if !ISH_LINUX static void ios_handle_exit(struct task *task, int code) { // we are interested in init and in children of init // this is called with pids_lock as an implementation side effect, please do not cite as an example of good API design if (task->parent != NULL && task->parent->parent != NULL) return; // pid should be saved now since task would be freed pid_t pid = task->pid; dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:ProcessExitedNotification object:nil userInfo:@{@"pid": @(pid), @"code": @(code)}]; }); } static void ios_handle_die(const char *msg) { NSString *message = [NSString stringWithFormat:@"%s: %s", __func__, msg]; iSHExceptionHandler([[NSException alloc] initWithName:NSGenericException reason:message userInfo:nil]); } #elif ISH_LINUX void ReportPanic(const char *message) { [NSNotificationCenter.defaultCenter postNotificationName:KernelPanicNotification object:nil userInfo:@{@"message":@(message)}]; } #endif static int bootError; static NSString *const kSkipStartupMessage = @"Skip Startup Message"; @implementation AppDelegate - (int)boot { #if !ISH_LINUX NSURL *root = [Roots.instance rootUrl:Roots.instance.defaultRoot]; int err = mount_root(&fakefs, [root URLByAppendingPathComponent:@"data"].fileSystemRepresentation); if (err < 0) return err; fs_register(&iosfs); fs_register(&iosfs_unsafe); // need to do this first so that we can have a valid current for the generic_mknod calls err = become_first_process(); if (err < 0) return err; FsInitialize(); // create some device nodes // this will do nothing if they already exist generic_mknodat(AT_PWD, "/dev/tty1", S_IFCHR|0666, dev_make(TTY_CONSOLE_MAJOR, 1)); generic_mknodat(AT_PWD, "/dev/tty2", S_IFCHR|0666, dev_make(TTY_CONSOLE_MAJOR, 2)); generic_mknodat(AT_PWD, "/dev/tty3", S_IFCHR|0666, dev_make(TTY_CONSOLE_MAJOR, 3)); generic_mknodat(AT_PWD, "/dev/tty4", S_IFCHR|0666, dev_make(TTY_CONSOLE_MAJOR, 4)); generic_mknodat(AT_PWD, "/dev/tty5", S_IFCHR|0666, dev_make(TTY_CONSOLE_MAJOR, 5)); generic_mknodat(AT_PWD, "/dev/tty6", S_IFCHR|0666, dev_make(TTY_CONSOLE_MAJOR, 6)); generic_mknodat(AT_PWD, "/dev/tty7", S_IFCHR|0666, dev_make(TTY_CONSOLE_MAJOR, 7)); generic_mknodat(AT_PWD, "/dev/tty", S_IFCHR|0666, dev_make(TTY_ALTERNATE_MAJOR, DEV_TTY_MINOR)); generic_mknodat(AT_PWD, "/dev/console", S_IFCHR|0666, dev_make(TTY_ALTERNATE_MAJOR, DEV_CONSOLE_MINOR)); generic_mknodat(AT_PWD, "/dev/ptmx", S_IFCHR|0666, dev_make(TTY_ALTERNATE_MAJOR, DEV_PTMX_MINOR)); generic_mknodat(AT_PWD, "/dev/null", S_IFCHR|0666, dev_make(MEM_MAJOR, DEV_NULL_MINOR)); generic_mknodat(AT_PWD, "/dev/zero", S_IFCHR|0666, dev_make(MEM_MAJOR, DEV_ZERO_MINOR)); generic_mknodat(AT_PWD, "/dev/full", S_IFCHR|0666, dev_make(MEM_MAJOR, DEV_FULL_MINOR)); generic_mknodat(AT_PWD, "/dev/random", S_IFCHR|0666, dev_make(MEM_MAJOR, DEV_RANDOM_MINOR)); generic_mknodat(AT_PWD, "/dev/urandom", S_IFCHR|0666, dev_make(MEM_MAJOR, DEV_URANDOM_MINOR)); generic_mkdirat(AT_PWD, "/dev/pts", 0755); // Permissions on / have been broken for a while, let's fix them generic_setattrat(AT_PWD, "/", (struct attr) {.type = attr_mode, .mode = 0755}, false); // Register clipboard device driver and create device node for it err = dyn_dev_register(&clipboard_dev, DEV_CHAR, DYN_DEV_MAJOR, DEV_CLIPBOARD_MINOR); if (err != 0) { return err; } generic_mknodat(AT_PWD, "/dev/clipboard", S_IFCHR|0666, dev_make(DYN_DEV_MAJOR, DEV_CLIPBOARD_MINOR)); err = dyn_dev_register(&location_dev, DEV_CHAR, DYN_DEV_MAJOR, DEV_LOCATION_MINOR); if (err != 0) return err; generic_mknodat(AT_PWD, "/dev/location", S_IFCHR|0666, dev_make(DYN_DEV_MAJOR, DEV_LOCATION_MINOR)); do_mount(&procfs, "proc", "/proc", "", 0); do_mount(&devptsfs, "devpts", "/dev/pts", "", 0); iosfs_init(); // let it mount any filesystems from user defaults [self configureDns]; exit_hook = ios_handle_exit; die_handler = ios_handle_die; #if !TARGET_OS_SIMULATOR NSString *sockTmp = [NSTemporaryDirectory() stringByAppendingString:@"ishsock"]; sock_tmp_prefix = strdup(sockTmp.UTF8String); #endif tty_drivers[TTY_CONSOLE_MAJOR] = &ios_console_driver; set_console_device(TTY_CONSOLE_MAJOR, 1); err = create_stdio("/dev/console", TTY_CONSOLE_MAJOR, 1); if (err < 0) return err; NSArray *command; command = UserPreferences.shared.bootCommand; NSLog(@"%@", command); char argv[4096]; [Terminal convertCommand:command toArgs:argv limitSize:sizeof(argv)]; const char *envp = "TERM=xterm-256color\0"; err = do_execve(command[0].UTF8String, command.count, argv, envp); if (err < 0) return err; task_start(current); #else // On first launch, this will trigger the import of the default root. Make sure to do this before entering the kernel, because it needs to run something on the main thread, and that would deadlock. [Roots instance]; NSArray *args = @[]; actuate_kernel([args componentsJoinedByString:@" "].UTF8String); #endif return 0; } #if ISH_LINUX const char *DefaultRootPath() { return [Roots.instance rootUrl:Roots.instance.defaultRoot].fileSystemRepresentation; } void SyncHostname(void) { async_do_in_workqueue(^{ char hostname[256]; if (gethostname(hostname, sizeof(hostname)) < 0) return; linux_sethostname(hostname); }); } #endif - (void)configureDns { #if !ISH_LINUX struct __res_state res; if (EXIT_SUCCESS != res_ninit(&res)) { exit(2); } NSMutableString *resolvConf = [NSMutableString new]; if (res.dnsrch[0] != NULL) { [resolvConf appendString:@"search"]; for (int i = 0; res.dnsrch[i] != NULL; i++) { [resolvConf appendFormat:@" %s", res.dnsrch[i]]; } [resolvConf appendString:@"\n"]; } union res_sockaddr_union servers[NI_MAXSERV]; int serversFound = res_getservers(&res, servers, NI_MAXSERV); char address[NI_MAXHOST]; for (int i = 0; i < serversFound; i ++) { union res_sockaddr_union s = servers[i]; if (s.sin.sin_len == 0) continue; getnameinfo((struct sockaddr *) &s.sin, s.sin.sin_len, address, sizeof(address), NULL, 0, NI_NUMERICHOST); [resolvConf appendFormat:@"nameserver %s\n", address]; } current = pid_get_task(1); struct fd *fd = generic_open("/etc/resolv.conf", O_WRONLY_ | O_CREAT_ | O_TRUNC_, 0666); if (!IS_ERR(fd)) { fd->ops->write(fd, resolvConf.UTF8String, [resolvConf lengthOfBytesUsingEncoding:NSUTF8StringEncoding]); fd_close(fd); } #endif } + (int)bootError { return bootError; } + (void)maybePresentStartupMessageOnViewController:(UIViewController *)vc { if ([NSUserDefaults.standardUserDefaults integerForKey:kSkipStartupMessage] >= 1) return; if (!FsIsManaged()) { UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Install iSH’s built-in APK?" message:@"iSH now includes the APK package manager, but it must be manually activated." preferredStyle:UIAlertControllerStyleAlert]; [alert addAction:[UIAlertAction actionWithTitle:@"Show me how" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { [UIApplication openURL:@"https://go.ish.app/get-apk"]; }]]; [alert addAction:[UIAlertAction actionWithTitle:@"Don't show again" style:UIAlertActionStyleDefault handler:nil]]; [vc presentViewController:alert animated:YES completion:nil]; } [NSUserDefaults.standardUserDefaults setInteger:1 forKey:kSkipStartupMessage]; } - (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions { NSUserDefaults *defaults = NSUserDefaults.standardUserDefaults; if ([defaults boolForKey:@"hail mary"]) { [defaults removeObjectForKey:kPreferenceBootCommandKey]; [defaults removeObjectForKey:kPreferenceLaunchCommandKey]; [defaults setBool:NO forKey:@"hail mary"]; } if ([NSUserDefaults.standardUserDefaults boolForKey:@"recovery"]) return YES; bootError = [self boot]; #if ISH_LINUX [NSNotificationCenter.defaultCenter addObserverForName:UIApplicationWillEnterForegroundNotification object:UIApplication.sharedApplication queue:nil usingBlock:^(NSNotification * _Nonnull note) { SyncHostname(); }]; SyncHostname(); #endif return YES; } void NetworkReachabilityCallback(SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags, void *info) { AppDelegate *self = (__bridge AppDelegate *) info; [self configureDns]; } - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // get the network permissions popup to appear on chinese devices [[NSURLSession.sharedSession dataTaskWithURL:[NSURL URLWithString:@"http://captive.apple.com"]] resume]; if ([NSUserDefaults.standardUserDefaults boolForKey:@"FASTLANE_SNAPSHOT"]) [UIView setAnimationsEnabled:NO]; #if !ISH_LINUX NSString *ishVersion = [NSString stringWithFormat:@"iSH %@ (%@)", [NSBundle.mainBundle objectForInfoDictionaryKey:@"CFBundleShortVersionString"], [NSBundle.mainBundle objectForInfoDictionaryKey:(NSString *) kCFBundleVersionKey]]; extern const char *proc_ish_version; proc_ish_version = strdup(ishVersion.UTF8String); // this defaults key is set when taking app store screenshots extern const char *uname_hostname_override; NSString *hostnameOverride = UserPreferences.shared._hostnameOverride; if (@available(iOS 16.0, *)) { // Hostname obfuscation is in effect hostnameOverride = hostnameOverride ? hostnameOverride : UserPreferences.shared.hostnameOverride; } if (hostnameOverride) { uname_hostname_override = strdup(hostnameOverride.UTF8String); } #endif [UserPreferences.shared observe:@[@"shouldDisableDimming"] options:NSKeyValueObservingOptionInitial owner:self usingBlock:^(typeof(self) self) { dispatch_async(dispatch_get_main_queue(), ^{ UIApplication.sharedApplication.idleTimerDisabled = UserPreferences.shared.shouldDisableDimming; }); }]; // This code is IPv4 and IPv6 aware: see https://developer.apple.com/library/archive/samplecode/Reachability/Listings/ReadMe_md.html struct sockaddr_in address = { .sin_len = sizeof(address), .sin_family = AF_INET, }; self.reachability = SCNetworkReachabilityCreateWithAddress(kCFAllocatorDefault, (struct sockaddr *) &address); SCNetworkReachabilityContext context = { .info = (__bridge void *) self, }; SCNetworkReachabilitySetCallback(self.reachability, NetworkReachabilityCallback, &context); SCNetworkReachabilityScheduleWithRunLoop(self.reachability, CFRunLoopGetMain(), kCFRunLoopCommonModes); if (self.window != nil) { // For iOS <13, where the app delegate owns the window instead of the scene if ([NSUserDefaults.standardUserDefaults boolForKey:@"recovery"]) { UINavigationController *vc = [[UIStoryboard storyboardWithName:@"About" bundle:nil] instantiateInitialViewController]; AboutViewController *avc = (AboutViewController *) vc.topViewController; avc.recoveryMode = YES; self.window.rootViewController = vc; return YES; } TerminalViewController *vc = (TerminalViewController *) self.window.rootViewController; currentTerminalViewController = vc; [vc startNewSession]; } return YES; } - (void)application:(UIApplication *)application didDiscardSceneSessions:(NSSet *)sceneSessions API_AVAILABLE(ios(13.0)) { for (UISceneSession *sceneSession in sceneSessions) { NSString *terminalUUID = sceneSession.stateRestorationActivity.userInfo[@"TerminalUUID"]; [[Terminal terminalWithUUID:[[NSUUID alloc] initWithUUIDString:terminalUUID]] destroy]; } } - (void)dealloc { if (self.reachability != NULL) { SCNetworkReachabilityUnscheduleFromRunLoop(self.reachability, CFRunLoopGetMain(), kCFRunLoopCommonModes); CFRelease(self.reachability); } } - (void)exitApp { self.exiting = YES; id app = [UIApplication sharedApplication]; [app suspend]; } - (void)applicationDidEnterBackground:(UIApplication *)application { if (self.exiting) exit(0); } @end #if !ISH_LINUX NSString *const ProcessExitedNotification = @"ProcessExitedNotification"; #else NSString *const KernelPanicNotification = @"KernelPanicNotification"; #endif ================================================ FILE: app/AppGroup.h ================================================ // // AppGroup.h // iSH // // Created by Theodore Dubois on 2/28/20. // #import NSURL *ContainerURL(void); ================================================ FILE: app/AppGroup.m ================================================ // // AppGroup.m // iSH // // Created by Theodore Dubois on 2/28/20. // #import "AppGroup.h" #import #import #import #import #import #define CSMAGIC_EMBEDDED_SIGNATURE 0xfade0cc0 #define CSMAGIC_EMBEDDED_ENTITLEMENTS 0xfade7171 struct cs_blob_index { uint32_t type; uint32_t offset; }; struct cs_superblob { uint32_t magic; uint32_t length; uint32_t count; struct cs_blob_index index[]; }; struct cs_entitlements { uint32_t magic; uint32_t length; char entitlements[]; }; static NSDictionary *AppEntitlements(void) { static NSDictionary *entitlements; if (entitlements != nil) return entitlements; // Inspired by codesign.c in Darwin sources for Security.framework const struct mach_header_64 *header = &_mh_execute_header; // Simulator executables have fake entitlements in the code signature. The real entitlements can be found in an __entitlements section. size_t entitlements_size; char *entitlements_data = (char *) getsectiondata(header, "__TEXT", "__entitlements", &entitlements_size); if (entitlements_data != NULL) { NSData *data = [NSData dataWithBytesNoCopy:entitlements_data length:entitlements_size freeWhenDone:NO]; return entitlements = [NSPropertyListSerialization propertyListWithData:data options:NSPropertyListImmutable format:nil error:nil]; } // Find the LC_CODE_SIGNATURE struct load_command *lc = (void *) (header + 1); struct linkedit_data_command *cs_lc = NULL; for (uint32_t i = 0; i < header->ncmds; i++) { if (lc->cmd == LC_CODE_SIGNATURE) { cs_lc = (void *) lc; break; } lc = (void *) ((char *) lc + lc->cmdsize); } if (cs_lc == NULL) return nil; // Read the code signature off disk, as it's apparently not loaded into memory NSFileHandle *fileHandle = [NSFileHandle fileHandleForReadingFromURL:NSBundle.mainBundle.executableURL error:nil]; if (fileHandle == nil) return nil; [fileHandle seekToFileOffset:cs_lc->dataoff]; NSData *csData = [fileHandle readDataOfLength:cs_lc->datasize]; [fileHandle closeFile]; const struct cs_superblob *cs = csData.bytes; if (ntohl(cs->magic) != CSMAGIC_EMBEDDED_SIGNATURE) return nil; // Find the entitlements in the code signature NSData *entitlementsData = nil; for (uint32_t i = 0; i < ntohl(cs->count); i++) { struct cs_entitlements *ents = (void *) ((char *) cs + ntohl(cs->index[i].offset)); if (ntohl(ents->magic) == CSMAGIC_EMBEDDED_ENTITLEMENTS) { entitlementsData = [NSData dataWithBytes:ents->entitlements length:ntohl(ents->length) - offsetof(struct cs_entitlements, entitlements)]; } } if (entitlementsData == nil) return nil; return entitlements = [NSPropertyListSerialization propertyListWithData:entitlementsData options:NSPropertyListImmutable format:nil error:nil]; } NSArray *CurrentAppGroups(void) { return AppEntitlements()[@"com.apple.security.application-groups"]; } NSURL *ContainerURL(void) { NSString *appGroup = CurrentAppGroups()[0]; return [NSFileManager.defaultManager containerURLForSecurityApplicationGroupIdentifier:appGroup]; } ================================================ FILE: app/AppLib.xcconfig ================================================ #include "StaticLib.xcconfig" HEADER_SEARCH_PATHS = $(inherited) $(SRCROOT) $(SRCROOT)/deps/libarchive/libarchive ================================================ FILE: app/ArrowBarButton.h ================================================ // // ArrowBarButton.h // iSH // // Created by Theodore Dubois on 9/23/18. // #import NS_ASSUME_NONNULL_BEGIN typedef enum : NSUInteger { ArrowNone = 0, ArrowUp, ArrowDown, ArrowLeft, ArrowRight, } ArrowDirection; @interface ArrowBarButton : UIControl @property (nonatomic, readonly) ArrowDirection direction; @property (nonatomic) UIKeyboardAppearance keyAppearance; @end NS_ASSUME_NONNULL_END ================================================ FILE: app/ArrowBarButton.m ================================================ // // ArrowBarButton.m // iSH // // Created by Theodore Dubois on 9/23/18. // #import "ArrowBarButton.h" @interface ArrowBarButton () { CALayer *arrowLayers[5]; } @property CGPoint startPoint; @property (nonatomic) ArrowDirection direction; @property BOOL accessibilityUpDown; @property NSTimer *timer; @end @implementation ArrowBarButton - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { [self setup]; } return self; } - (instancetype)initWithCoder:(NSCoder *)aDecoder { if (self = [super initWithCoder:aDecoder]) { [self setup]; } return self; } static CGPoint anchors[] = { {}, {.5, .95}, // ArrowUp {.5, .05}, // ArrowDown {1.05, .5}, // ArrowLeft {-.05, .5}, // ArrowRight }; - (void)setup { self.layer.delegate = self; [self addTextLayer:@"↑" direction:ArrowUp]; [self addTextLayer:@"↓" direction:ArrowDown]; [self addTextLayer:@"←" direction:ArrowLeft]; [self addTextLayer:@"→" direction:ArrowRight]; [self layoutSublayersOfLayer:self.layer]; self.layer.cornerRadius = 5; self.layer.shadowOffset = CGSizeMake(0, 1); self.layer.shadowOpacity = 0.4; self.layer.shadowRadius = 0; self.accessibilityUpDown = YES; } - (BOOL)isAccessibilityElement { return YES; } - (UIAccessibilityTraits)accessibilityTraits { return UIAccessibilityTraitAdjustable; } - (NSString *)accessibilityLabel { return self.accessibilityUpDown ? @"Arrow Keys Up or Down" : @"Arrow Keys Left or Right"; } - (NSString *)accessibilityHint { return @"Double tap to toggle direction"; } - (BOOL)accessibilityActivate { self.accessibilityUpDown = !self.accessibilityUpDown; return TRUE; } - (void)accessibilityIncrement { if (self.accessibilityUpDown) self.direction = ArrowUp; else self.direction = ArrowLeft; // this might be wrong in RTL self.direction = ArrowNone; } - (void)accessibilityDecrement { if (self.accessibilityUpDown) self.direction = ArrowDown; else self.direction = ArrowRight; self.direction = ArrowNone; } - (void)addTextLayer:(NSString *)text direction:(ArrowDirection)direction { CATextLayer *layer = [CATextLayer new]; layer.contentsScale = UIScreen.mainScreen.scale; layer.string = text; layer.fontSize = 15; UIFont *font = [UIFont systemFontOfSize:layer.fontSize]; layer.font = (__bridge CFTypeRef _Nullable) font; CGSize textSize = [[NSAttributedString alloc] initWithString:text attributes:@{NSFontAttributeName: font}].size; layer.bounds = CGRectMake(0, 0, textSize.width, textSize.height); layer.foregroundColor = UIColor.blackColor.CGColor; layer.alignmentMode = kCAAlignmentCenter; layer.anchorPoint = anchors[direction]; [self.layer addSublayer:layer]; self->arrowLayers[direction] = layer; } - (void)layoutSublayersOfLayer:(CALayer *)superlayer { NSParameterAssert(superlayer == self.layer); for (CALayer *layer in superlayer.sublayers) { layer.position = CGPointMake(self.bounds.size.width / 2, self.bounds.size.height / 2); } } - (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event { [super beginTrackingWithTouch:touch withEvent:event]; self.startPoint = [touch locationInView:self]; self.selected = YES; return YES; } - (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event { [super continueTrackingWithTouch:touch withEvent:event]; CGPoint currentPoint = [touch locationInView:self]; CGPoint diff = CGPointMake(currentPoint.x - self.startPoint.x, currentPoint.y - self.startPoint.y); if (hypot(diff.x, diff.y) < 20) { self.direction = ArrowNone; } else if (fabs(diff.x) > fabs(diff.y)) { // more to the side if (diff.x > 0) self.direction = ArrowRight; else self.direction = ArrowLeft; } else { // more up and down if (diff.y > 0) self.direction = ArrowDown; else self.direction = ArrowUp; } return YES; } - (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event { [super endTrackingWithTouch:touch withEvent:event]; self.selected = NO; self.direction = ArrowNone; } - (void)cancelTrackingWithEvent:(UIEvent *)event { [super cancelTrackingWithEvent:event]; self.selected = NO; self.direction = ArrowNone; } - (void)animateLayerUpdates { [UIView animateWithDuration:0.25 animations:^{ for (int d = ArrowUp; d <= ArrowRight; d++) { CATextLayer *layer = (CATextLayer *) self->arrowLayers[d]; if (self.direction == ArrowNone || self.direction != d) { layer.opacity = self.selected ? 0.25 : 1; } else { layer.opacity = 1; } } }]; } - (void)setDirection:(ArrowDirection)direction { ArrowDirection oldDirection = _direction; _direction = direction; if (direction != oldDirection) { [self animateLayerUpdates]; [self.timer invalidate]; if (direction != ArrowNone) { [self sendActionsForControlEvents:UIControlEventValueChanged]; self.timer = [NSTimer scheduledTimerWithTimeInterval:0.5 repeats:NO block:^(NSTimer *timer) { [self sendActionsForControlEvents:UIControlEventValueChanged]; self.timer = [NSTimer scheduledTimerWithTimeInterval:0.1 repeats:YES block:^(NSTimer *timer) { [self sendActionsForControlEvents:UIControlEventValueChanged]; }]; }]; } } } - (UIColor *)textColor { if (self.keyAppearance == UIKeyboardAppearanceLight) return UIColor.blackColor; else return UIColor.whiteColor; } // copy pasted code :dab: - (UIColor *)defaultColor { if (self.keyAppearance == UIKeyboardAppearanceLight) return UIColor.whiteColor; else return [UIColor colorWithRed:1 green:1 blue:1 alpha:77/255.]; } - (UIColor *)highlightedColor { if (self.keyAppearance == UIKeyboardAppearanceLight) return [UIColor colorWithRed:172/255. green:180/255. blue:190/255. alpha:1]; else return [UIColor colorWithRed:147/255. green:147/255. blue:147/255. alpha:66/255.]; } - (void)setColors { if (self.selected) { self.backgroundColor = self.highlightedColor; } else { [UIView animateWithDuration:0 delay:0.1 options:UIViewAnimationOptionAllowUserInteraction animations:^{ self.backgroundColor = self.defaultColor; } completion:nil]; } for (int d = ArrowUp; d <= ArrowRight; d++) { CATextLayer *layer = (CATextLayer *) arrowLayers[d]; if (self.keyAppearance == UIKeyboardAppearanceLight) layer.foregroundColor = UIColor.blackColor.CGColor; else layer.foregroundColor = UIColor.whiteColor.CGColor; } [self animateLayerUpdates]; } - (void)setSelected:(BOOL)selected { [super setSelected:selected]; [self setColors]; } - (void)setKeyAppearance:(UIKeyboardAppearance)keyAppearance { _keyAppearance = keyAppearance; [self setColors]; } @end ================================================ FILE: app/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "size" : "20x20", "idiom" : "iphone", "filename" : "Icon20x20@2x.png", "scale" : "2x" }, { "size" : "20x20", "idiom" : "iphone", "filename" : "Icon20x20@3x.png", "scale" : "3x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "Icon29x29@2x.png", "scale" : "2x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "Icon29x29@3x.png", "scale" : "3x" }, { "size" : "40x40", "idiom" : "iphone", "filename" : "Icon40x40@2x.png", "scale" : "2x" }, { "size" : "40x40", "idiom" : "iphone", "filename" : "Icon40x40@3x.png", "scale" : "3x" }, { "size" : "60x60", "idiom" : "iphone", "filename" : "Icon60x60@2x.png", "scale" : "2x" }, { "size" : "60x60", "idiom" : "iphone", "filename" : "Icon60x60@3x.png", "scale" : "3x" }, { "size" : "20x20", "idiom" : "ipad", "filename" : "Icon20x20@1x.png", "scale" : "1x" }, { "size" : "20x20", "idiom" : "ipad", "filename" : "Icon20x20@2x-1.png", "scale" : "2x" }, { "size" : "29x29", "idiom" : "ipad", "filename" : "Icon29x29@1x.png", "scale" : "1x" }, { "size" : "29x29", "idiom" : "ipad", "filename" : "Icon29x29@2x-1.png", "scale" : "2x" }, { "size" : "40x40", "idiom" : "ipad", "filename" : "Icon40x40@1x.png", "scale" : "1x" }, { "size" : "40x40", "idiom" : "ipad", "filename" : "Icon40x40@2x-1.png", "scale" : "2x" }, { "size" : "76x76", "idiom" : "ipad", "filename" : "Icon76x76@1x.png", "scale" : "1x" }, { "size" : "76x76", "idiom" : "ipad", "filename" : "Icon76x76@2x.png", "scale" : "2x" }, { "size" : "83.5x83.5", "idiom" : "ipad", "filename" : "Icon83.5x83.5@2x.png", "scale" : "2x" }, { "size" : "1024x1024", "idiom" : "ios-marketing", "filename" : "App Store.png", "scale" : "1x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: app/Assets.xcassets/Checkbox.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "checkbox.pdf" } ], "info" : { "version" : 1, "author" : "xcode" }, "properties" : { "template-rendering-intent" : "template", "preserves-vector-representation" : true } } ================================================ FILE: app/Assets.xcassets/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: app/Assets.xcassets/Hide Keyboard.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "Hide Keyboard.pdf" } ], "info" : { "version" : 1, "author" : "xcode" }, "properties" : { "template-rendering-intent" : "template" } } ================================================ FILE: app/Assets.xcassets/Paste.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "Paste.pdf" } ], "info" : { "version" : 1, "author" : "xcode" }, "properties" : { "template-rendering-intent" : "template" } } ================================================ FILE: app/Assets.xcassets/Saddam Hussein.imageset/Contents.json ================================================ { "images" : [ { "filename" : "satanusen.png", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: app/Assets.xcassets/X.imageset/Contents.json ================================================ { "images" : [ { "filename" : "xmark.circle.fill.regular.large.pdf", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "preserves-vector-representation" : true, "template-rendering-intent" : "template" } } ================================================ FILE: app/BarButton.h ================================================ // // AccessoryButton.h // iSH // // Created by Theodore Dubois on 9/22/18. // #import NS_ASSUME_NONNULL_BEGIN @interface BarButton : UIButton @property (nonatomic) UIKeyboardAppearance keyAppearance; @property IBInspectable BOOL secondary; @property IBInspectable BOOL toggleable; @end NS_ASSUME_NONNULL_END ================================================ FILE: app/BarButton.m ================================================ // // AccessoryButton.m // iSH // // Created by Theodore Dubois on 9/22/18. // #import "BarButton.h" @interface BarButton () @end extern UIAccessibilityTraits UIAccessibilityTraitToggle; @implementation BarButton - (void)awakeFromNib { [super awakeFromNib]; self.layer.cornerRadius = 5; self.layer.shadowOffset = CGSizeMake(0, 1); self.layer.shadowOpacity = 0.4; self.layer.shadowRadius = 0; self.backgroundColor = self.defaultColor; self.keyAppearance = UIKeyboardAppearanceLight; self.accessibilityTraits |= UIAccessibilityTraitKeyboardKey; if (self.toggleable) { self.accessibilityTraits |= 0x20000000000000; } } - (UIColor *)primaryColor { if (self.keyAppearance == UIKeyboardAppearanceLight) return UIColor.whiteColor; else return [UIColor colorWithRed:1 green:1 blue:1 alpha:77/255.]; } - (UIColor *)secondaryColor { if (self.keyAppearance == UIKeyboardAppearanceLight) return [UIColor colorWithRed:172/255. green:180/255. blue:190/255. alpha:1]; else return [UIColor colorWithRed:147/255. green:147/255. blue:147/255. alpha:66/255.]; } - (UIColor *)defaultColor { if (self.secondary) return self.secondaryColor; return self.primaryColor; } - (UIColor *)highlightedColor { if (!self.secondary) return self.secondaryColor; return self.primaryColor; } - (void)chooseBackground { if (self.selected || self.highlighted) { self.backgroundColor = self.highlightedColor; } else { [UIView animateWithDuration:0 delay:0.1 options:UIViewAnimationOptionAllowUserInteraction animations:^{ self.backgroundColor = self.defaultColor; } completion:nil]; } if (self.keyAppearance == UIKeyboardAppearanceLight) { self.tintColor = UIColor.blackColor; } else { self.tintColor = UIColor.whiteColor; } [self setTitleColor:self.tintColor forState:UIControlStateNormal]; } - (void)setHighlighted:(BOOL)highlighted { [super setHighlighted:highlighted]; [self chooseBackground]; } - (void)setSelected:(BOOL)selected { [super setSelected:selected]; [self chooseBackground]; } - (void)setKeyAppearance:(UIKeyboardAppearance)keyAppearance { _keyAppearance = keyAppearance; [self chooseBackground]; } - (NSString *)accessibilityValue { if (self.toggleable) { return self.selected ? @"1" : @"0"; } return nil; } @end ================================================ FILE: app/Base.lproj/About.storyboard ================================================ ================================================ FILE: app/Base.lproj/LaunchScreen.storyboard ================================================ ================================================ FILE: app/Base.lproj/Terminal.storyboard ================================================ ================================================ FILE: app/CLI.xcconfig ================================================ CODE_SIGN_IDENTITY = - ENABLE_HARDENED_RUNTIME = YES HEADER_SEARCH_PATHS = $(SRCROOT) PRODUCT_NAME = $(TARGET_NAME) SDKROOT = macosx SUPPORTED_PLATFORMS = macosx ================================================ FILE: app/CurrentRoot.h ================================================ // // CurrentRoot.h // iSH // // Created by Theodore Dubois on 11/4/21. // #import NS_ASSUME_NONNULL_BEGIN extern int fs_ish_version; extern int fs_ish_apk_version; void FsInitialize(void); bool FsIsManaged(void); bool FsNeedsRepositoryUpdate(void); void FsUpdateOnlyRepositoriesFile(void); void FsUpdateRepositories(void); /// An integer representing the current major version of the apk repositories. An upgrade will be run if the number in /ish/apk-version is smaller. After a successful upgrade, the newer number is copied into /ish/apk-version. /// To upgrade: /// - update the default rootfs to the same version /// - update gen_apk_repositories.py to generate the new version of /etc/apk/repositories /// - set both of the following constants appropriately, making sure to use a larger number than the previous one #define CURRENT_APK_VERSION 31900 #define CURRENT_APK_VERSION_STRING "Alpine v3.19" extern NSString *const FsUpdatedNotification; NS_ASSUME_NONNULL_END ================================================ FILE: app/CurrentRoot.m ================================================ // // CurrentRoot.m // iSH // // Created by Theodore Dubois on 11/4/21. // #import "CurrentRoot.h" #include "kernel/calls.h" #include "fs/path.h" #ifdef ISH_LINUX #import "LinuxInterop.h" #endif int fs_ish_version; int fs_ish_apk_version; #if !ISH_LINUX static ssize_t read_file(const char *path, char *buf, size_t size) { struct fd *fd = generic_open(path, O_RDONLY_, 0); if (IS_ERR(fd)) return PTR_ERR(fd); ssize_t n = fd->ops->read(fd, buf, size); fd_close(fd); if (n == size) return _ENAMETOOLONG; return n; } static ssize_t write_file(const char *path, const char *buf, size_t size) { struct fd *fd = generic_open(path, O_WRONLY_|O_CREAT_|O_TRUNC_, 0644); if (IS_ERR(fd)) return PTR_ERR(fd); ssize_t n = fd->ops->write(fd, buf, size); fd_close(fd); return n; } static int remove_directory(const char *path) { return generic_rmdirat(AT_PWD, path); } #else #define read_file linux_read_file #define write_file linux_write_file #define remove_directory linux_remove_directory #endif void FsInitialize(void) { // /ish/version is the last ish version that opened this root. Used to migrate the filesystem. char buf[1000]; ssize_t n = read_file("/ish/version", buf, sizeof(buf)); if (n >= 0) { NSString *currentVersion = NSBundle.mainBundle.infoDictionary[(__bridge NSString *) kCFBundleVersionKey]; NSString *currentVersionFile = [NSString stringWithFormat:@"%@\n", currentVersion]; NSString *version = [[NSString alloc] initWithBytesNoCopy:buf length:n encoding:NSUTF8StringEncoding freeWhenDone:NO]; version = [version stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet]; fs_ish_version = version.intValue; version = nil; n = read_file("/ish/apk-version", buf, sizeof(buf)); if (n >= 0) { NSString *version = [[NSString alloc] initWithBytesNoCopy:buf length:n encoding:NSUTF8StringEncoding freeWhenDone:NO]; version = [version stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet]; fs_ish_apk_version = version.intValue; } // If no newer value for CURRENT_APK_VERSION, do silent update. if (fs_ish_apk_version >= CURRENT_APK_VERSION) FsUpdateRepositories(); if (currentVersion.intValue > fs_ish_version) { fs_ish_version = currentVersion.intValue; write_file("/ish/version", currentVersionFile.UTF8String, [currentVersionFile lengthOfBytesUsingEncoding:NSUTF8StringEncoding]); } } } bool FsIsManaged(void) { return fs_ish_version != 0; } bool FsNeedsRepositoryUpdate(void) { return FsIsManaged() && fs_ish_apk_version < CURRENT_APK_VERSION; } void FsUpdateOnlyRepositoriesFile(void) { NSURL *repositories = [NSBundle.mainBundle URLForResource:@"repositories" withExtension:@"txt"]; if (repositories != nil) { NSMutableData *repositoriesData = [@"# This file contains pinned repositories managed by iSH. If the /ish directory\n" @"# exists, iSH uses the metadata stored in it to keep this file up to date (by\n" @"# overwriting the contents on boot.)\n" dataUsingEncoding:NSUTF8StringEncoding].mutableCopy; [repositoriesData appendData:[NSData dataWithContentsOfURL:repositories]]; write_file("/etc/apk/repositories", repositoriesData.bytes, repositoriesData.length); } } void FsUpdateRepositories(void) { FsUpdateOnlyRepositoriesFile(); fs_ish_apk_version = CURRENT_APK_VERSION; NSString *currentVersionFile = [NSString stringWithFormat:@"%d\n", fs_ish_apk_version]; write_file("/ish/apk-version", currentVersionFile.UTF8String, [currentVersionFile lengthOfBytesUsingEncoding:NSUTF8StringEncoding]); remove_directory("/ish/apk"); dispatch_async(dispatch_get_main_queue(), ^{ [NSNotificationCenter.defaultCenter postNotificationName:FsUpdatedNotification object:nil]; }); } NSString *const FsUpdatedNotification = @"FsUpdatedNotification"; ================================================ FILE: app/DelayedUITask.h ================================================ // // DelayedUITask.h // iSH // // Created by Theodore Dubois on 11/8/17. // #import @interface DelayedUITask : NSObject - (instancetype)initWithTarget:(id)target action:(SEL)action; - (void)schedule; @end ================================================ FILE: app/DelayedUITask.m ================================================ // // DelayedUITask.m // iSH // // Created by Theodore Dubois on 11/8/17. // #import "DelayedUITask.h" @interface DelayedUITask () @property id target; @property SEL action; @property NSTimer *timer; @end @implementation DelayedUITask - (instancetype)initWithTarget:(id)target action:(SEL)action { if (self = [super init]) { self.target = target; self.action = action; } return self; } - (void)schedule { if (!self.timer.valid) { self.timer = [NSTimer timerWithTimeInterval:1./60 repeats:NO block:^(NSTimer * _Nonnull timer) { self.timer = nil; ((void (*)(id, SEL)) [self.target methodForSelector:self.action])(self.target, self.action); }]; [NSRunLoop.mainRunLoop addTimer:self.timer forMode:NSDefaultRunLoopMode]; } } @end ================================================ FILE: app/ExceptionExfiltrator.h ================================================ // // ExceptionExfiltrator.h // iSH // // Created by Saagar Jha on 5/5/23. // #ifndef ExceptionExfiltrator_h #define ExceptionExfiltrator_h #import void iSHExceptionHandler(NSException *exception); #endif /* ExceptionExfiltrator_h */ ================================================ FILE: app/ExceptionExfiltrator.m ================================================ // // ExceptionExfiltrator.m // libiSHApp // // Created by Saagar Jha on 5/5/23. // #import "ExceptionExfiltrator.h" #import #import #import #define f(name, character) \ __asm__("\" " name "\": nop"); \ void ish_exception_exfiltrate_##character(void) __asm__(" " name) f("unprintable", unprintable); f(" ", space); f("!", exclamation_mark); f("quotation_mark", quotation_mark); f("#", number_sign); f("$", dollar_sign); f("%", percent_sign); f("&", ampersand); f("'", apostrophe); f("(", left_parenthesis); f(")", right_parenthesis); f("*", asterisk); f("+", plus_sign); f(",", comma); f("-", hyphen_minus); f(".", full_stop); f("/", solidus); f("0", 0); f("1", 1); f("2", 2); f("3", 3); f("4", 4); f("5", 5); f("6", 6); f("7", 7); f("8", 8); f("9", 9); f(":", colon); f(";", semicolon); f("<", less_than_sign); f("=", equals_sign); f(">", greater_than_sign); f("?", question_mark); f("@", commercial_at); f("A", A); f("B", B); f("C", C); f("D", D); f("E", E); f("F", F); f("G", G); f("H", H); f("I", I); f("J", J); f("K", K); f("L", L); f("M", M); f("N", N); f("O", O); f("P", P); f("Q", Q); f("R", R); f("S", S); f("T", T); f("U", U); f("V", V); f("W", W); f("X", X); f("Y", Y); f("Z", Z); f("[", left_square_bracket); f("reverse_solidus", reverse_solidus); f("]", right_square_bracket); f("^", circumflex_accent); f("_", low_line); f("`", grave_accent); f("a", a); f("b", b); f("c", c); f("d", d); f("e", e); f("f", f); f("g", g); f("h", h); f("i", i); f("j", j); f("k", k); f("l", l); f("m", m); f("n", n); f("o", o); f("p", p); f("q", q); f("r", r); f("s", s); f("t", t); f("u", u); f("v", v); f("w", w); f("x", x); f("y", y); f("z", z); f("{", left_curly_bracket); f("|", vertical_line); f("}", right_curly_bracket); f("~", tilde); #undef f #define f(character, name) [character] = ish_exception_exfiltrate_##name static void (*character2function[256])(void) = { f(' ', space), f('!', exclamation_mark), f('"', quotation_mark), f('#', number_sign), f('$', dollar_sign), f('%', percent_sign), f('&', ampersand), f('\'', apostrophe), f('(', left_parenthesis), f(')', right_parenthesis), f('*', asterisk), f('+', plus_sign), f(',', comma), f('-', hyphen_minus), f('.', full_stop), f('/', solidus), f('0', 0), f('1', 1), f('2', 2), f('3', 3), f('4', 4), f('5', 5), f('6', 6), f('7', 7), f('8', 8), f('9', 9), f(':', colon), f(';', semicolon), f('<', less_than_sign), f('=', equals_sign), f('>', greater_than_sign), f('?', question_mark), f('@', commercial_at), f('A', A), f('B', B), f('C', C), f('D', D), f('E', E), f('F', F), f('G', G), f('H', H), f('I', I), f('J', J), f('K', K), f('L', L), f('M', M), f('N', N), f('O', O), f('P', P), f('Q', Q), f('R', R), f('S', S), f('T', T), f('U', U), f('V', V), f('W', W), f('X', X), f('Y', Y), f('Z', Z), f('[', left_square_bracket), f('\\', reverse_solidus), f(']', right_square_bracket), f('^', circumflex_accent), f('_', low_line), f('`', grave_accent), f('a', a), f('b', b), f('c', c), f('d', d), f('e', e), f('f', f), f('g', g), f('h', h), f('i', i), f('j', j), f('k', k), f('l', l), f('m', m), f('n', n), f('o', o), f('p', p), f('q', q), f('r', r), f('s', s), f('t', t), f('u', u), f('v', v), f('w', w), f('x', x), f('y', y), f('z', z), f('{', left_curly_bracket), f('|', vertical_line), f('}', right_curly_bracket), f('~', tilde), }; #undef f void __ish_exception_exfiltrate_NAME__(void) { __asm__("nop"); } void __ish_exception_exfiltrate_REASON__(void) { __asm__("nop"); } void __ish_exception_exfiltrate_BACKTRACE__(void) { __asm__("nop"); } static void (*function_for_character(unichar character))(void) { return character < sizeof(character2function) / sizeof(*character2function) ? (character2function[character] ? character2function[character] : ish_exception_exfiltrate_unprintable) : ish_exception_exfiltrate_unprintable; } static void *address_for_function(void (*function)(void)) { return (void *)((uintptr_t)function + 1); } struct frame { void *next; void *address; }; static void *__ish_exception_exfiltrate_THREAD__(void *frames) { *(void **)__builtin_frame_address(0) = frames; __builtin_trap(); } void iSHExceptionHandler(NSException *exception) { NSArray *backtrace = exception.callStackReturnAddresses; NSString *name = exception.name; NSString *reason = exception.reason; size_t size = backtrace.count + /* backtrace frames */ 1 + /* __ish_exception_exfiltrate_BACKTRACE__ */ name.length + /* name */ 1 + /* __ish_exception_exfiltrate__NAME__ */ reason.length + /* reason */ 1; /* __ish_exception_exfiltrate__REASON__ */ struct frame *frames = malloc(sizeof(struct frame) * size); frames[0].next = NULL; for (size_t i = 1; i < size; ++i) { frames[i].next = frames + i - 1; } size_t index = 0; for (NSNumber *address in backtrace.reverseObjectEnumerator) { frames[index++].address = address.pointerValue; } frames[index++].address = address_for_function(__ish_exception_exfiltrate_BACKTRACE__); for (NSUInteger i = 0; i < reason.length; ++i, ++index) { frames[index].address = address_for_function(function_for_character([reason characterAtIndex:reason.length - i - 1])); } frames[index++].address = address_for_function(__ish_exception_exfiltrate_REASON__); for (NSUInteger i = 0; i < name.length; ++i, ++index) { frames[index].address = address_for_function(function_for_character([name characterAtIndex:name.length - i - 1])); } frames[index++].address = address_for_function(__ish_exception_exfiltrate_NAME__); pthread_t thread; pthread_create(&thread, NULL, __ish_exception_exfiltrate_THREAD__, frames + size - 1); pthread_join(thread, NULL); } ================================================ FILE: app/FileProvider/FileProviderEnumerator.h ================================================ // // FileProviderEnumerator.h // iSHFiles // // Created by Theodore Dubois on 9/20/18. // #import #import "FileProviderItem.h" @interface FileProviderEnumerator : NSObject - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithItem:(FileProviderItem *)item; @end ================================================ FILE: app/FileProvider/FileProviderEnumerator.m ================================================ // // FileProviderEnumerator.m // iSHFiles // // Created by Theodore Dubois on 9/20/18. // #import #include #import "FileProviderExtension.h" #import "FileProviderEnumerator.h" #import "FileProviderItem.h" #import "NSError+ISHErrno.h" #include "fs/fake-db.h" @interface FileProviderEnumerator () @property FileProviderItem *item; @end @implementation FileProviderEnumerator - (instancetype)initWithItem:(FileProviderItem *)item { if (self = [super init]) { self.item = item; } return self; } - (void)enumerateItemsForObserver:(id)observer startingAtPage:(NSFileProviderPage)page { NSLog(@"enumeration start %@", self.item.itemIdentifier); // if we're asked to enumerate the working set if (self.item == nil) { [observer finishEnumeratingUpToPage:page]; return; } // if we're asked to enumerate a file if (![self.item.typeIdentifier isEqualToString:(NSString *) kUTTypeFolder]) { NSLog(@"not enumerating a file (%@)", self.item.typeIdentifier); [observer finishEnumeratingUpToPage:page]; return; } NSError *error; int fd = [self.item openNewFDWithError:&error]; if (fd == -1) { [observer finishEnumeratingWithError:error]; return; } DIR *dir = fdopendir(fd); NSMutableArray *items = [NSMutableArray new]; struct dirent *dirent; errno = 0; while ((dirent = readdir(dir))) { if (strcmp(dirent->d_name, ".") == 0 || strcmp(dirent->d_name, "..") == 0) continue; // this is annoying NSString *path = _item.path; NSString *childIdent; if (strcmp(dirent->d_name, "..") == 0) { childIdent = _item.parentItemIdentifier; } else if (strcmp(dirent->d_name, ".") != 0) { db_begin_read(&_item.mount->db); inode_t inode = path_get_inode(&_item.mount->db, [path stringByAppendingFormat:@"/%@", [NSString stringWithUTF8String:dirent->d_name]].fileSystemRepresentation); db_commit(&_item.mount->db); if (inode == 0) { NSLog(@"could not find %s in database, assuming nonexistent", dirent->d_name); continue; } childIdent = [NSString stringWithFormat:@"%lu", (unsigned long) inode]; } NSLog(@"returning %s %@", dirent->d_name, childIdent); FileProviderItem *item = [[FileProviderItem alloc] initWithIdentifier:childIdent mount:_item.mount error:&error]; if (item == nil) { [observer finishEnumeratingWithError:error]; closedir(dir); return; } [items addObject:item]; errno = 0; } if (errno != 0) { NSError *error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:nil]; NSLog(@"readdir returned %@", error); [observer finishEnumeratingWithError:error]; closedir(dir); return; } closedir(dir); NSLog(@"returning %@", items); [observer didEnumerateItems:items]; [observer finishEnumeratingUpToPage:nil]; } - (void)enumerateChangesForObserver:(id)observer fromSyncAnchor:(NSFileProviderSyncAnchor)anchor { NSLog(@"saying no file changes"); // TODO implement by having the sync anchor be a serialized list of files [observer finishEnumeratingChangesUpToSyncAnchor:anchor moreComing:NO]; } - (void)invalidate { } @end ================================================ FILE: app/FileProvider/FileProviderExtension.h ================================================ // // FileProviderExtension.h // iSHFiles // // Created by Theodore Dubois on 9/20/18. // #import #include "fs/fake-db.h" struct fakefs_mount { struct fakefs_db db; int root_fd; const char *source; }; @interface FileProviderExtension : NSFileProviderExtension - (struct fakefs_mount *)mount; @end ================================================ FILE: app/FileProvider/FileProviderExtension.m ================================================ // // FileProviderExtension.m // iSHFiles // // Created by Theodore Dubois on 9/20/18. // #import "FileProviderExtension.h" #import "FileProviderItem.h" #import "FileProviderEnumerator.h" #import "NSError+ISHErrno.h" #import "../AppGroup.h" #import "../ExceptionExfiltrator.h" #include "fs/fake-db.h" @interface FileProviderExtension () { BOOL _mounted; struct fakefs_mount _mount; }; @property NSURL *root; @end @implementation FileProviderExtension - (struct fakefs_mount *)mount { NSAssert(_mounted, @""); return &_mount; } - (BOOL)getMount:(struct fakefs_mount **)mount error:(NSError **)error { @synchronized (self) { if (!_mounted) { if (self.domain == nil) { *error = [NSError errorWithDomain:NSFileProviderErrorDomain code:NSFileProviderErrorNotAuthenticated userInfo:nil]; return NO; } NSURL *container = ContainerURL(); NSURL *fs_dir = [[container URLByAppendingPathComponent:@"roots"] URLByAppendingPathComponent:self.domain.identifier]; _root = [fs_dir URLByAppendingPathComponent:@"data"]; _mount.source = strdup(_root.fileSystemRepresentation); _mount.root_fd = open(_mount.source, O_RDONLY | O_DIRECTORY); int err = fake_db_init(&_mount.db, [fs_dir URLByAppendingPathComponent:@"meta.db"].fileSystemRepresentation, _mount.root_fd); if (err < 0) { NSLog(@"error opening root: %d", err); close(_mount.root_fd); *error = [NSError errorWithISHErrno:err itemIdentifier:NSFileProviderRootContainerItemIdentifier]; return NO; } *mount = &_mount; _mounted = YES; } // Run a cleanup every once in a while. The idea here is that this // function gets called while the file provider is being interacted // with, so this should generally get time to run at that point, but we // don't want to do this when the user is not interacting with the file // provider. NSDate *lastCleanup = [NSUserDefaults.standardUserDefaults objectForKey:@"LastCleanup"]; lastCleanup = lastCleanup ? lastCleanup : NSDate.distantPast; if ([lastCleanup timeIntervalSinceDate:NSDate.date] > 60 * 60 /* 1 hour */) { [self cleanupStorage]; } [NSUserDefaults.standardUserDefaults setObject:NSDate.date forKey:@"LastCleanup"]; return YES; } } - (NSURL *)storageURL { NSURL *storage = NSFileProviderManager.defaultManager.documentStorageURL; if (self.domain != nil) storage = [storage URLByAppendingPathComponent:self.domain.pathRelativeToDocumentStorage isDirectory:YES]; return storage; } - (nullable NSFileProviderItem)itemForIdentifier:(NSFileProviderItemIdentifier)identifier error:(NSError * _Nullable *)error { struct fakefs_mount *mount; if (![self getMount:&mount error:error]) return nil; NSLog(@"item for id %@", identifier); NSError *err; FileProviderItem *item = [[FileProviderItem alloc] initWithIdentifier:identifier mount:&_mount error:&err]; if (item == nil) { if (error != nil) *error = err; return nil; } return item; } - (nullable NSURL *)URLForItemWithPersistentIdentifier:(NSFileProviderItemIdentifier)identifier { if ([identifier isEqualToString:NSFileProviderRootContainerItemIdentifier]) return self.storageURL; FileProviderItem *item = [self itemForIdentifier:identifier error:nil]; if (item == nil) return nil; NSURL *url = [self.storageURL URLByAppendingPathComponent:identifier isDirectory:YES]; url = [url URLByAppendingPathComponent:item.path.lastPathComponent isDirectory:NO]; NSLog(@"url for id %@ = %@", identifier, url); return url; } - (nullable NSFileProviderItemIdentifier)persistentIdentifierForItemAtURL:(NSURL *)url { if ([url.URLByDeletingLastPathComponent isEqual:NSFileProviderManager.defaultManager.documentStorageURL]) { NSAssert([self.domain.identifier isEqualToString:url.lastPathComponent], @"url isn't the same as our domain"); return NSFileProviderRootContainerItemIdentifier; } NSString *identifier = url.pathComponents[url.pathComponents.count - 2]; if (identifier.longLongValue == 0) return nil; // something must be screwed I guess NSLog(@"id for url %@ = %@", url, identifier); return identifier; } - (BOOL)enhanceSanityOfURL:(NSURL *)url error:(NSError **)error { NSURL *dir = url.URLByDeletingLastPathComponent; NSFileManager *manager = NSFileManager.defaultManager; BOOL isDir; if ([manager fileExistsAtPath:dir.path isDirectory:&isDir] && !isDir) [manager removeItemAtURL:dir error:nil]; return [manager createDirectoryAtURL:dir withIntermediateDirectories:YES attributes:nil error:error]; } - (void)providePlaceholderAtURL:(NSURL *)url completionHandler:(void (^)(NSError * _Nullable error))completionHandler { NSError *err; FileProviderItem *item = [self itemForIdentifier:[self persistentIdentifierForItemAtURL:url] error:&err]; if (item == nil) { completionHandler(err); return; } if (![self enhanceSanityOfURL:url error:&err]) { completionHandler(err); return; } if (![NSFileProviderManager writePlaceholderAtURL:[NSFileProviderManager placeholderURLForURL:url] withMetadata:item error:&err]) { completionHandler(err); return; } completionHandler(nil); } - (void)startProvidingItemAtURL:(NSURL *)url completionHandler:(void (^)(NSError *))completionHandler { // Should ensure that the actual file is in the position returned by URLForItemWithIdentifier:, then call the completion handler NSError *err; FileProviderItem *item = [self itemForIdentifier:[self persistentIdentifierForItemAtURL:url] error:&err]; if (item == nil) { completionHandler(err); return; } if (![self enhanceSanityOfURL:url error:&err]) { completionHandler(err); return; } [item loadToURL:url]; completionHandler(nil); } - (void)itemChangedAtURL:(NSURL *)url { FileProviderItem *item = [self itemForIdentifier:[self persistentIdentifierForItemAtURL:url] error:nil]; if (item == nil) return; [item saveFromURL:url]; } #pragma mark - Action helpers // FIXME: not dry enough // It's ok to use _mount in these because in each case the caller has already invoked itemForIdentifier:error: at least once - (BOOL)doCreateDirectoryAt:(NSString *)path inode:(ino_t *)inode error:(NSError **)error { NSURL *url = [[NSURL fileURLWithPath:[NSString stringWithUTF8String:_mount.source]] URLByAppendingPathComponent:path]; db_begin_write(&_mount.db); if (![NSFileManager.defaultManager createDirectoryAtURL:url withIntermediateDirectories:NO attributes:@{NSFilePosixPermissions: @0777} error:error]) { db_rollback(&_mount.db); return nil; } struct ish_stat stat; NSString *parentPath = [path substringToIndex:[path rangeOfString:@"/" options:NSBackwardsSearch].location]; if (!path_read_stat(&_mount.db, parentPath.fileSystemRepresentation, &stat, NULL)) { db_rollback(&_mount.db); *error = [NSError errorWithDomain:NSFileProviderErrorDomain code:NSFileProviderErrorNoSuchItem userInfo:nil]; return nil; } stat.mode = (stat.mode & ~S_IFMT) | S_IFDIR; path_create(&_mount.db, path.fileSystemRepresentation, &stat); if (inode != NULL) *inode = path_get_inode(&_mount.db, path.fileSystemRepresentation); db_commit(&_mount.db); return YES; } - (BOOL)doCreateFileAt:(NSString *)path importFrom:(NSURL *)importURL inode:(ino_t *)inode error:(NSError **)error { NSURL *url = [[NSURL fileURLWithPath:[NSString stringWithUTF8String:_mount.source]] URLByAppendingPathComponent:path]; db_begin_write(&_mount.db); if (![NSFileManager.defaultManager copyItemAtURL:importURL toURL:url error:error]) { db_rollback(&_mount.db); return nil; } struct ish_stat stat; NSString *parentPath = [path substringToIndex:[path rangeOfString:@"/" options:NSBackwardsSearch].location]; if (!path_read_stat(&_mount.db, parentPath.fileSystemRepresentation, &stat, NULL)) { db_rollback(&_mount.db); *error = [NSError errorWithDomain:NSFileProviderErrorDomain code:NSFileProviderErrorNoSuchItem userInfo:nil]; return nil; } stat.mode = (stat.mode & ~S_IFMT & ~0111) | S_IFREG; path_create(&_mount.db, path.fileSystemRepresentation, &stat); if (inode != NULL) *inode = path_get_inode(&_mount.db, path.fileSystemRepresentation); db_commit(&_mount.db); return YES; } - (NSString *)pathOfItemWithIdentifier:(NSFileProviderItemIdentifier)identifier error:(NSError **)error { FileProviderItem *parent = [self itemForIdentifier:identifier error:error]; if (parent == nil) return nil; return parent.path; } #pragma mark - Actions /* TODO: implement the actions for items here each of the actions follows the same pattern: - make a note of the change in the local model - schedule a server request as a background task to inform the server of the change - call the completion block with the modified item in its post-modification state */ - (void)createDirectoryWithName:(NSString *)directoryName inParentItemIdentifier:(NSFileProviderItemIdentifier)parentItemIdentifier completionHandler:(void (^)(NSFileProviderItem _Nullable, NSError * _Nullable))completionHandler { NSError *error; NSString *parentPath = [self pathOfItemWithIdentifier:parentItemIdentifier error:&error]; if (parentPath == nil) { completionHandler(nil, error); return; } ino_t inode; if (![self doCreateDirectoryAt:[parentPath stringByAppendingFormat:@"/%@", directoryName] inode:&inode error:&error]) { completionHandler(nil, error); return; } FileProviderItem *item = [self itemForIdentifier:[NSString stringWithFormat:@"%lu", (unsigned long) inode] error:&error]; if (item == nil) completionHandler(nil, error); else completionHandler(item, nil); } - (void)importDocumentAtURL:(NSURL *)fileURL toParentItemIdentifier:(NSFileProviderItemIdentifier)parentItemIdentifier completionHandler:(void (^)(NSFileProviderItem _Nullable, NSError * _Nullable))completionHandler { NSError *error; NSString *parentPath = [self pathOfItemWithIdentifier:parentItemIdentifier error:&error]; if (parentPath == nil) { completionHandler(nil, error); return; } [fileURL startAccessingSecurityScopedResource]; BOOL isDir; assert([NSFileManager.defaultManager fileExistsAtPath:fileURL.path isDirectory:&isDir] && !isDir); ino_t inode; BOOL worked = [self doCreateFileAt:[parentPath stringByAppendingFormat:@"/%@", fileURL.lastPathComponent] importFrom:fileURL inode:&inode error:&error]; [fileURL stopAccessingSecurityScopedResource]; if (!worked) { completionHandler(nil, error); return; } FileProviderItem *item = [self itemForIdentifier:[NSString stringWithFormat:@"%lu", (unsigned long) inode] error:&error]; if (item == nil) completionHandler(nil, error); else completionHandler(item, nil); } - (NSString *)pathFromURL:(NSURL *)url { NSURL *root = [NSURL fileURLWithPath:[NSString stringWithUTF8String:_mount.source]]; assert([url.path hasPrefix:root.path]); NSString *path = [url.path substringFromIndex:root.path.length]; assert([path hasPrefix:@"/"]); if ([path hasSuffix:@"/"]) path = [path substringToIndex:path.length - 1]; return path; } - (BOOL)doDelete:(NSString *)path itemIdentifier:(NSFileProviderItemIdentifier)identifier error:(NSError **)error { NSURL *url = [[NSURL fileURLWithPath:[NSString stringWithUTF8String:_mount.source]] URLByAppendingPathComponent:path]; NSDirectoryEnumerator *enumerator = [NSFileManager.defaultManager enumeratorAtURL:url includingPropertiesForKeys:nil options:NSDirectoryEnumerationSkipsSubdirectoryDescendants errorHandler:nil]; for (NSURL *suburl in enumerator) { if (![self doDelete:[self pathFromURL:suburl] itemIdentifier:identifier error:error]) return NO; } db_begin_write(&_mount.db); path_unlink(&_mount.db, path.fileSystemRepresentation); int err = unlinkat(_mount.root_fd, fix_path(path.fileSystemRepresentation), 0); if (err < 0) err = unlinkat(_mount.root_fd, fix_path(path.fileSystemRepresentation), AT_REMOVEDIR); if (err < 0) { db_rollback(&_mount.db); *error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:nil]; return NO; } db_commit(&_mount.db); return YES; } - (void)deleteItemWithIdentifier:(NSFileProviderItemIdentifier)itemIdentifier completionHandler:(void (^)(NSError * _Nullable))completionHandler { NSError *error; NSString *path = [self pathOfItemWithIdentifier:itemIdentifier error:&error]; if (path == nil) { completionHandler(error); return; } if (![self doDelete:path itemIdentifier:itemIdentifier error:&error]) completionHandler(error); else completionHandler(nil); } - (BOOL)doRename:(NSString *)src to:(NSString *)dst itemIdentifier:(NSFileProviderItemIdentifier)identifier error:(NSError **)error { db_begin_write(&_mount.db); path_rename(&_mount.db, src.fileSystemRepresentation, dst.fileSystemRepresentation); int err = renameat(_mount.root_fd, fix_path(src.fileSystemRepresentation), _mount.root_fd, fix_path(dst.fileSystemRepresentation)); if (err < 0) { db_rollback(&_mount.db); *error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:nil]; return NO; } db_commit(&_mount.db); return YES; } - (void)renameItemWithIdentifier:(NSFileProviderItemIdentifier)itemIdentifier toName:(NSString *)itemName completionHandler:(void (^)(NSFileProviderItem _Nullable, NSError * _Nullable))completionHandler { NSError *error; FileProviderItem *item = [self itemForIdentifier:itemIdentifier error:&error]; if (item == nil) { completionHandler(nil, error); return; } NSString *dstPath = [item.path.stringByDeletingLastPathComponent stringByAppendingPathComponent:itemName]; if (![self doRename:item.path to:dstPath itemIdentifier:itemIdentifier error:&error]) { completionHandler(nil, error); return; } completionHandler(item, nil); } - (void)reparentItemWithIdentifier:(NSFileProviderItemIdentifier)itemIdentifier toParentItemWithIdentifier:(NSFileProviderItemIdentifier)parentItemIdentifier newName:(NSString *)newName completionHandler:(void (^)(NSFileProviderItem _Nullable, NSError * _Nullable))completionHandler { NSError *error; FileProviderItem *item = [self itemForIdentifier:itemIdentifier error:&error]; if (item == nil) { completionHandler(nil, error); return; } FileProviderItem *parent = [self itemForIdentifier:parentItemIdentifier error:&error]; if (parent == nil) { completionHandler(nil, error); return; } if (newName == nil) newName = item.path.lastPathComponent; if (![self doRename:item.path to:[parent.path stringByAppendingPathComponent:newName] itemIdentifier:itemIdentifier error:&error]) { completionHandler(nil, error); return; } completionHandler(item, nil); } #pragma mark - Enumeration - (nullable id)enumeratorForContainerItemIdentifier:(NSFileProviderItemIdentifier)containerItemIdentifier error:(NSError **)error { FileProviderItem *item = [self itemForIdentifier:containerItemIdentifier error:error]; if (item == nil) return nil; return [[FileProviderEnumerator alloc] initWithItem:item]; } - (void)dealloc { if (_mounted) { free((void *) _mount.source); close(_mount.root_fd); fake_db_deinit(&_mount.db); } } #pragma mark - Storage deletion // According to an engineer I talked to at WWDC, -stopProvidingItemAtURL: is never ever called, so that can't be used to free up disk space. // Solution for now is to periodically look for and delete files in file provider storage where the original is missing. // TODO: Delete files in file provider storage when the original file is deleted // TODO: Create hardlinks into file provider storage instead of copies // - (void)cleanupStorage { NSAssert(_mounted, @"Mount should exist by this point"); NSFileManager *manager = NSFileManager.defaultManager; NSArray *storageDirs = [manager contentsOfDirectoryAtURL:self.storageURL includingPropertiesForKeys:nil options:0 error:nil]; for (NSURL *dir in storageDirs) { inode_t inode = dir.lastPathComponent.longLongValue; if (inode == 0) continue; // TODO: make this a function in fake-db.c db_begin_read(&_mount.db); sqlite3_bind_int64(_mount.db.stmt.inode_read_stat, 1, inode); BOOL exists = db_exec(&_mount.db, _mount.db.stmt.inode_read_stat); db_reset(&_mount.db, _mount.db.stmt.inode_read_stat); db_rollback(&_mount.db); if (!exists) { NSLog(@"removing dead inode %llu", inode); NSError *err; if (![manager removeItemAtURL:dir error:&err]) NSLog(@"failed to remove dead inode: %@", err); } } } // Dead code, leaving it here just in case - (void)stopProvidingItemAtURL:(NSURL *)url { FileProviderItem *item = [self itemForIdentifier:[self persistentIdentifierForItemAtURL:url] error:nil]; if (item == nil) return; [item saveFromURL:url]; [[NSFileManager defaultManager] removeItemAtURL:url error:nil]; [NSFileProviderManager writePlaceholderAtURL:[NSFileProviderManager placeholderURLForURL:url] withMetadata:item error:nil]; } + (void)load { NSSetUncaughtExceptionHandler(iSHExceptionHandler); } @end void die(const char *msg, ...) { va_list args; va_start(args, msg); [NSException raise:@"ish die" format:[NSString stringWithUTF8String:msg] arguments:args]; abort(); va_end(args); } void ish_printk(const char *msg, ...) { va_list args; va_start(args, msg); NSLogv([NSString stringWithUTF8String:msg], args); va_end(args); } ================================================ FILE: app/FileProvider/FileProviderItem.h ================================================ // // FileProviderItem.h // iSHFiles // // Created by Theodore Dubois on 9/20/18. // #import #include "fs/fake-db.h" NS_ASSUME_NONNULL_BEGIN @interface FileProviderItem : NSObject - (instancetype)initWithIdentifier:(NSFileProviderItemIdentifier)identifier mount:(struct fakefs_mount *)mount error:(NSError *_Nullable *)err; - (void)loadToURL:(NSURL *)url; - (void)saveFromURL:(NSURL *)url; - (int)openNewFDWithError:(NSError *_Nullable *)err; @property (readonly) NSString *path; @property (readonly) struct fakefs_mount *mount; @end NS_ASSUME_NONNULL_END ================================================ FILE: app/FileProvider/FileProviderItem.m ================================================ // // FileProviderItem.m // iSHFiles // // Created by Theodore Dubois on 9/20/18. // #import #include #include #import "FileProviderExtension.h" #import "FileProviderItem.h" #include "fs/fake-db.h" #include "kernel/errno.h" @interface FileProviderItem () @property (readonly) NSFileProviderItemIdentifier identifier; @property (readonly) int fd; @property (readonly) BOOL isRoot; @end @implementation FileProviderItem - (instancetype)initWithIdentifier:(NSFileProviderItemIdentifier)identifier mount:(struct fakefs_mount *)mount error:(NSError *__autoreleasing _Nullable *)error { if (self = [super init]) { _identifier = identifier; _mount = mount; _fd = [self openNewFDWithError:error]; if (_fd == -1) return nil; } return self; } - (BOOL)isRoot { return [self.identifier isEqualToString:NSFileProviderRootContainerItemIdentifier]; } - (int)openNewFDWithError:(NSError *__autoreleasing _Nullable *)error { int fd = -1; if (self.isRoot) { fd = open(_mount->source, O_DIRECTORY | O_RDONLY); } else { db_begin_read(&_mount->db); sqlite3_stmt *stmt = _mount->db.stmt.path_from_inode; sqlite3_bind_int64(_mount->db.stmt.path_from_inode, 1, _identifier.longLongValue); while (db_exec(&_mount->db, stmt)) { const char *path = (const char *) sqlite3_column_text(stmt, 0); fd = openat(_mount->root_fd, fix_path(path), O_RDWR); if (fd == -1 && errno == EISDIR) fd = openat(_mount->root_fd, fix_path(path), O_RDONLY); if (fd == -1 && errno != ENOENT) break; } db_reset(&_mount->db, stmt); db_commit(&_mount->db); } if (fd == -1) { if (error != nil) { if (errno == ENOENT) *error = [NSError fileProviderErrorForNonExistentItemWithIdentifier:_identifier]; else *error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:nil]; } NSLog(@"opening %@ failed: %@", self.identifier, *error); return -1; } return fd; } - (NSString *)path { char path[PATH_MAX] = ""; int err = fcntl(_fd, F_GETPATH, path); [self handleError:err inFunction:@"getpath"]; const char *myPath = path + strlen(_mount->source); return [NSFileManager.defaultManager stringWithFileSystemRepresentation:myPath length:strlen(myPath)]; } - (NSURL *)URL { NSURL *rootURL = [NSURL fileURLWithPath:[NSString stringWithUTF8String:_mount->source]]; if (self.isRoot) return rootURL; return [rootURL URLByAppendingPathComponent:self.path]; } - (struct ish_stat)ishStat { struct ish_stat stat = {}; db_begin_read(&_mount->db); inode_t inode = _identifier.longLongValue; if ([_identifier isEqualToString:NSFileProviderRootContainerItemIdentifier]) inode = path_get_inode(&_mount->db, ""); inode_read_stat_or_die(&_mount->db, inode, &stat); db_commit(&_mount->db); return stat; } - (struct stat)realStat { struct stat statbuf; int err = fstat(_fd, &statbuf); [self handleError:err inFunction:@"realStat"]; return statbuf; } - (NSFileProviderItemIdentifier)itemIdentifier { if (self.isRoot) return NSFileProviderRootContainerItemIdentifier; return _identifier; } - (NSFileProviderItemIdentifier)parentItemIdentifier { if (self.isRoot) { NSLog(@"parent of root %@ is %@", self.path, NSFileProviderRootContainerItemIdentifier); return NSFileProviderRootContainerItemIdentifier; } NSString *parentPath = self.path.stringByDeletingLastPathComponent; if ([parentPath isEqualToString:@"/"]) return NSFileProviderRootContainerItemIdentifier; db_begin_read(&_mount->db); inode_t parentInode = path_get_inode(&_mount->db, parentPath.UTF8String); db_commit(&_mount->db); assert(parentInode != 0); NSString *parent = [NSString stringWithFormat:@"%lu", (unsigned long) parentInode]; NSLog(@"parent of %@ is %@", self.path, parent); return parent; } - (NSFileProviderItemCapabilities)capabilities { NSFileProviderItemCapabilities caps = NSFileProviderItemCapabilitiesAllowsDeleting | NSFileProviderItemCapabilitiesAllowsRenaming | NSFileProviderItemCapabilitiesAllowsReparenting; if (S_ISREG(self.ishStat.mode)) caps |= NSFileProviderItemCapabilitiesAllowsReading | NSFileProviderItemCapabilitiesAllowsWriting; else if (S_ISDIR(self.ishStat.mode)) caps |= NSFileProviderItemCapabilitiesAllowsAddingSubItems | NSFileProviderItemCapabilitiesAllowsContentEnumerating; else return 0; return caps; } - (NSString *)filename { NSString *filename = self.path.lastPathComponent; if ([filename isEqualToString:@""]) filename = @"/"; NSLog(@"filename %@", filename); return filename; } - (NSNumber *)documentSize { struct stat statbuf; int err = fstat(_fd, &statbuf); [self handleError:err inFunction:@"documentSize"]; return [NSNumber numberWithUnsignedLongLong:statbuf.st_size]; } - (NSNumber *)childItemCount { if (!S_ISDIR(self.ishStat.mode)) return @0; int fd = [self openNewFDWithError:nil]; if (fd == -1) return @0; unsigned n = 0; DIR *dir = fdopendir(fd); struct dirent *dirent; while ((dirent = readdir(dir))) { if (strcmp(dirent->d_name, ".") == 0 || strcmp(dirent->d_name, "..") == 0) continue; n++; } closedir(dir); return @(n); } - (NSDate *)contentModificationDate { struct stat statbuf = self.realStat; return [NSDate dateWithTimeIntervalSince1970:statbuf.st_mtimespec.tv_sec + (NSTimeInterval) statbuf.st_mtimespec.tv_nsec / 1000000000]; } - (NSString *)typeIdentifier { if (self.isRoot) { NSLog(@"uti of %@ is %@", self.path, (NSString *) kUTTypeFolder); return (NSString *) kUTTypeFolder; } mode_t_ mode = self.ishStat.mode; if ((mode & S_IFMT) == S_IFDIR) return (NSString *) kUTTypeFolder; if ((mode & S_IFMT) == S_IFLNK) return (NSString *) kUTTypeSymLink; NSString *uti = CFBridgingRelease(UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef _Nonnull) self.path.pathExtension, nil)); if ([uti hasPrefix:@"dyn."]) uti = (NSString *) kUTTypePlainText; NSLog(@"uti of %@ is %@", self.path, uti); return uti; } // locking on these keeps the remove/copy operation atomic // or at least tries to - (void)loadToURL:(NSURL *)url { NSLog(@"copying %@ to %@", self.path, url); NSURL *itemURL = self.URL; NSError *err; sqlite3_mutex_enter(_mount->db.lock); [NSFileManager.defaultManager removeItemAtURL:url error:nil]; BOOL success = [NSFileManager.defaultManager copyItemAtURL:itemURL toURL:url error:&err]; sqlite3_mutex_leave(_mount->db.lock); if (!success) { NSLog(@"error copying to %@: %@", url, err); } } - (void)saveFromURL:(NSURL *)url { NSLog(@"copying %@ from %@", self.path, url); NSURL *itemURL = self.URL; NSError *err; sqlite3_mutex_enter(_mount->db.lock); [NSFileManager.defaultManager removeItemAtURL:itemURL error:nil]; BOOL success = [NSFileManager.defaultManager copyItemAtURL:url toURL:itemURL error:&err]; sqlite3_mutex_leave(_mount->db.lock); if (!success) { NSLog(@"error copying to %@: %@", url, err); } } - (void)dealloc { if (self.fd != -1) close(self.fd); } - (void)handleError:(long)err inFunction:(NSString *)func { if (err < 0) { [NSException raise:NSGenericException format:@"%@ returned %ld %d", func, err, errno]; } } @end ================================================ FILE: app/FileProvider/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName iSH CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType XPC! CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion $(CURRENT_PROJECT_VERSION) NSExtension NSExtensionFileProviderDocumentGroup $(PRODUCT_APP_GROUP_IDENTIFIER) NSExtensionFileProviderSupportsEnumeration NSExtensionPointIdentifier com.apple.fileprovider-nonui NSExtensionPrincipalClass FileProviderExtension ================================================ FILE: app/FileProvider/NSError+ISHErrno.h ================================================ // // NSError+ISHErrno.h // iSH // // Created by Theodore Dubois on 12/15/18. // #import NS_ASSUME_NONNULL_BEGIN @interface NSError (ISHErrno) + (NSError *)errorWithISHErrno:(long)err itemIdentifier:(NSFileProviderItemIdentifier)identifier; @end extern NSString *const ISHErrnoDomain; NS_ASSUME_NONNULL_END ================================================ FILE: app/FileProvider/NSError+ISHErrno.m ================================================ // // NSError+ISHErrno.m // iSH // // Created by Theodore Dubois on 12/15/18. // #import #import "NSError+ISHErrno.h" #include "kernel/errno.h" @implementation NSError (ISHErrno) + (NSError *)errorWithISHErrno:(long)err itemIdentifier:(nonnull NSFileProviderItemIdentifier)identifier { switch (err) { case _ENOENT: return [NSError fileProviderErrorForNonExistentItemWithIdentifier:identifier]; } return [NSError errorWithDomain:ISHErrnoDomain code:err userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"error code %ld", err]}]; } @end NSString *const ISHErrnoDomain = @"ISHErrnoDomain"; ================================================ FILE: app/FileProvider/iSHFileProvider.entitlements ================================================ com.apple.security.application-groups $(PRODUCT_APP_GROUP_IDENTIFIER) ================================================ FILE: app/FontPickerViewController.h ================================================ // // FontPickerViewController.h // iSH // // Created by Theodore Dubois on 10/26/19. // #import NS_ASSUME_NONNULL_BEGIN NS_CLASS_DEPRECATED_IOS(10_0, 12_0, "UIFontPickerViewController is better") @interface FontPickerViewController : UITableViewController @end NS_ASSUME_NONNULL_END ================================================ FILE: app/FontPickerViewController.m ================================================ // // FontPickerViewController.m // iSH // // Created by Theodore Dubois on 10/26/19. // #import "FontPickerViewController.h" #import "UserPreferences.h" @interface FontPickerViewController () @property NSArray *fontFamilies; @end @implementation FontPickerViewController - (void)viewDidLoad { [super viewDidLoad]; NSMutableArray *families = [NSMutableArray new]; for (NSString *family in UIFont.familyNames) { UIFont *font = [UIFont fontWithName:family size:1]; if (font.fontDescriptor.symbolicTraits & UIFontDescriptorTraitMonoSpace) { [families addObject:family]; } } self.fontFamilies = families; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return self.fontFamilies.count; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Font"]; NSString *family = self.fontFamilies[indexPath.row]; UIFont *font = [UIFont fontWithName:family size:18]; cell.textLabel.font = [[UIFontMetrics metricsForTextStyle:UIFontTextStyleBody] scaledFontForFont:font]; cell.textLabel.adjustsFontForContentSizeCategory = YES; cell.textLabel.text = family; if ([family isEqualToString:UserPreferences.shared.fontFamily]) cell.accessoryType = UITableViewCellAccessoryCheckmark; return cell; } - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { [tableView deselectRowAtIndexPath:indexPath animated:YES]; UserPreferences.shared.fontFamily = self.fontFamilies[indexPath.row]; [self.navigationController popViewControllerAnimated:YES]; } @end ================================================ FILE: app/IOSCalls.m ================================================ // // IOSCalls.m // libiSHApp // // Created by Theodore Dubois on 8/15/21. // #if ISH_LINUX #include #include #include "Roots.h" #include "LinuxInterop.h" void async_do_in_ios(void (^block)(void)) { dispatch_async(dispatch_get_main_queue(), block); } void ConsoleLog(const char *data, unsigned len) { NSLog(@"%.*s", len, data); } nsobj_t objc_get(nsobj_t object) { CFBridgingRetain((__bridge id) object); return object; } void objc_put(nsobj_t object) { CFBridgingRelease(object); } void sync_do_in_workqueue(void (^block)(void (^done)(void))) { __block pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; __block pthread_cond_t cond = PTHREAD_COND_INITIALIZER; __block bool flag = false; async_do_in_workqueue(^{ block(^{ pthread_mutex_lock(&mutex); flag = true; pthread_mutex_unlock(&mutex); pthread_cond_broadcast(&cond); }); }); pthread_mutex_lock(&mutex); while (!flag) pthread_cond_wait(&cond, &mutex); pthread_mutex_unlock(&mutex); } long UIPasteboard_changeCount(void) { return UIPasteboard.generalPasteboard.changeCount; } nsobj_t UIPasteboard_get(void) { return (__bridge nsobj_t) [UIPasteboard.generalPasteboard.string dataUsingEncoding:NSUTF8StringEncoding]; } void UIPasteboard_set(const char *data, size_t len) { UIPasteboard.generalPasteboard.string = [[NSString alloc] initWithBytes:data length:len encoding:NSUTF8StringEncoding]; } size_t NSData_length(nsobj_t data) { return [(__bridge NSData *) data length]; } const void *NSData_bytes(nsobj_t data) { return [(__bridge NSData *) data bytes]; } #endif ================================================ FILE: app/Icons/Icons.plist ================================================ link https://mastodon.social/@tbodt author @tbodt description Default ihash1 link https://github.com/ish-app/ish/issues/578#issuecomment-562960935 author @01010101lzy description i# uninspired link https://github.com/ish-app/ish/issues/578#issuecomment-562906800 author @saagarjha description uninspired 3d link https://github.com/ish-app/ish/issues/578#issuecomment-567060974 author @ricardohnn description 3D icon1337 link https://github.com/ish-app/ish/issues/578#issuecomment-675671406 author @AudioBra4n description 1337 pydann2 link https://github.com/ish-app/ish/issues/578#issuecomment-562897067 author @PyDann description >| Light pydann1 link https://github.com/ish-app/ish/issues/578#issuecomment-562897067 author @PyDann description >| Dark colontildehash link https://github.com/ish-app/ish/issues/578#issuecomment-566759722 author @TechUpdateGuy description :~# idollarhash link https://github.com/ish-app/ish/issues/578#issuecomment-566855431 author @TechUpdateGuy description i$# iinhash link https://github.com/ish-app/ish/issues/578#issuecomment-576352583 author @relikd description i in # metal link https://github.com/ish-app/ish/issues/578#issuecomment-676845860 author @heronwr description iS̈H notsurewhatthisis link https://github.com/ish-app/ish/issues/578#issuecomment-725585045 author @wack-inc description Not sure what this is reworked link https://github.com/ish-app/ish/issues/578#issuecomment-717395400 author @peterlewis description iSH: Reworked rgb link https://github.com/ish-app/ish/issues/578#issuecomment-713075148 author @FelixTornqvist description RGB sprite64 link https://github.com/ish-app/ish/issues/578#issuecomment-716465657 author @peterlewis description Sprite 64 is link https://github.com/ish-app/ish/issues/578#issuecomment-828967195 author @MelinaCodesInMinecraft description iS circular link https://github.com/ish-app/ish/issues/578#issuecomment-716465657 author @haohailong description Circular dollarblock1 link https://github.com/ish-app/ish/issues/578#issuecomment-968200645 author @omduggineni description $+block light dollarblock2 link https://github.com/ish-app/ish/issues/578#issuecomment-968201479 author @omduggineni description $+block dark freeiosterminal link https://github.com/ish-app/ish/issues/578#issuecomment-971898997 author @huxl3 description iOS iSH Free Ios Terminal ishcolontildehash link https://github.com/ish-app/ish/issues/578#issuecomment-1038424724 author @moontr3 description iSH:~# dark ================================================ FILE: app/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion $(CURRENT_PROJECT_VERSION) LSRequiresIPhoneOS NSLocalNetworkUsageDescription This is required for connecting to localhost and using the ping command. NSLocationAlwaysAndWhenInUseUsageDescription Programs running in iSH will be allowed to track your location in the background. NSLocationAlwaysUsageDescription Programs running in iSH will be allowed to track your location in the background. NSLocationWhenInUseUsageDescription Programs running in iSH will be allowed to see your location. NSUserActivityTypes app.ish.scene UIApplicationSceneManifest UIApplicationSupportsMultipleScenes UISceneConfigurations UIWindowSceneSessionRoleApplication UISceneClassName UIWindowScene UISceneConfigurationName Main App UISceneDelegateClassName SceneDelegate UISceneStoryboardFile Terminal UIBackgroundModes location UIFileSharingEnabled UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Terminal UIRequiredDeviceCapabilities armv7 UIStatusBarHidden UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight fuc ICON_STUFF ================================================ FILE: app/Linux.xcconfig ================================================ ISH_KERNEL = linux NINJA_TARGETS = deps/liblinux.a libfakefs.a libish_emu.a LINUX_HOSTCC = env -u SDKROOT -u IPHONEOS_DEPLOYMENT_TARGET xcrun clang GCC_PREPROCESSOR_DEFINITIONS = ISH_LINUX=1 LINUX_APP_LDFLAGS = -Wl,-ld_classic -sectalign __DATA __percpu_first 1000 -sectalign __DATA __tracepoints 20 -force_load $(BUILT_PRODUCTS_DIR)/liblinux.a -force_load $(BUILT_PRODUCTS_DIR)/libiSHLinux.a ================================================ FILE: app/LinuxInterop.c ================================================ // // LinuxInterop.c // iSH // // Created by Theodore Dubois on 7/3/21. // #include "LinuxInterop.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include extern void run_kernel(void); void actuate_kernel(const char *cmdline) { strcpy(boot_command_line, cmdline); run_kernel(); } static int panic_report(struct notifier_block *nb, unsigned long action, void *data) { const char *message = data; async_do_in_ios(^{ ReportPanic(message); }); return 0; } static struct notifier_block panic_report_block = { .notifier_call = panic_report, .priority = INT_MAX, }; static int __init panic_report_init(void) { atomic_notifier_chain_register(&panic_notifier_list, &panic_report_block); return 0; } core_initcall(panic_report_init); static int block_request_read; static int block_request_write; static irqreturn_t call_block_irq(int irq, void *dev) { void (^block)(void); for (;;) { int err = host_read(block_request_read, &block, sizeof(block)); if (err <= 0) break; block(); Block_release(block); } return IRQ_HANDLED; } void async_do_in_irq(void (^block)(void)) { block = Block_copy(block); int err = host_write(block_request_write, &block, sizeof(block)); if (err < 0) __builtin_trap(); trigger_irq(CALL_BLOCK_IRQ); } struct ios_work { void (^block)(void); struct work_struct work; }; static void do_ios_work(struct work_struct *work) { struct ios_work *ios_work = container_of(work, struct ios_work, work); ios_work->block(); Block_release(ios_work->block); kfree(ios_work); } void async_do_in_workqueue(void (^block)(void)) { async_do_in_irq(^{ struct ios_work *work = kzalloc(sizeof(*work), GFP_ATOMIC); work->block = Block_copy(block); INIT_WORK(&work->work, do_ios_work); schedule_work(&work->work); }); } static int __init call_block_init(void) { int err = host_pipe(&block_request_read, &block_request_write); if (err < 0) return err; err = fd_set_nonblock(block_request_read); if (err < 0) return err; err = request_irq(CALL_BLOCK_IRQ, call_block_irq, 0, "block", NULL); if (err < 0) return err; return 0; } subsys_initcall(call_block_init); struct ish_session { struct file *tty; nsobj_t terminal; int pid; StartSessionDoneBlock callback; }; static int session_init(struct subprocess_info *info, struct cred *cred) { struct ish_session *session = info->data; int err = ksys_setsid(); if (err < 0) return err; err = vfs_ioctl(session->tty, TIOCSCTTY, 0); if (err < 0) return err; for (int fd = 0; fd <= 2; fd++) { int err = replace_fd(fd, session->tty, 0); if (err < 0) return err; } session->pid = task_pid_nr(current); return 0; } static void session_cleanup(struct subprocess_info *info) { struct ish_session *session = info->data; if (session->pid != 0 || info->retval != 0) session->callback(info->retval, session->pid, objc_get(session->terminal)); else; // otherwise, there was a synchronous failure, returned directly from call_usermodehelper_exec if (session->tty != NULL) fput(session->tty); objc_put(session->terminal); kfree(session); } void linux_start_session(const char *exe, const char *const *argv, const char *const *envp, StartSessionDoneBlock done) { struct ish_session *session = kzalloc(sizeof(*session), GFP_KERNEL); session->tty = ios_pty_open(&session->terminal); session->callback = done; struct subprocess_info *proc = call_usermodehelper_setup(exe, (char **) argv, (char **) envp, GFP_KERNEL, session_init, session_cleanup, session); int err = call_usermodehelper_exec(proc, UMH_WAIT_EXEC); if (err < 0) done(err, 0, NULL); } void linux_sethostname(const char *hostname) { int len = strlen(hostname); if (len > __NEW_UTS_LEN) len = __NEW_UTS_LEN; down_write(&uts_sem); struct new_utsname *u = utsname(); if (strncmp(u->nodename, hostname, len) != 0) { memcpy(u->nodename, hostname, len); memset(u->nodename + len, 0, sizeof(u->nodename) - len); uts_proc_notify(UTS_PROC_HOSTNAME); } up_write(&uts_sem); } ssize_t linux_read_file(const char *path, char *buf, size_t size) { struct file *filp = filp_open(path, O_RDONLY, 0); if (IS_ERR(filp)) return PTR_ERR(filp); ssize_t res = vfs_read(filp, buf, size, NULL); filp_close(filp, NULL); if (res >= size) return -ENAMETOOLONG; return res; } ssize_t linux_write_file(const char *path, const char *buf, size_t size) { struct file *filp = filp_open(path, O_WRONLY, 0); ssize_t res = vfs_write(filp, buf, size, NULL); filp_close(filp, NULL); return res; } int linux_remove_directory(const char *path) { return init_rmdir(path); } ================================================ FILE: app/LinuxInterop.h ================================================ // // LinuxInterop.h // iSH // // Created by Theodore Dubois on 7/3/21. // #ifndef LinuxInterop_h #define LinuxInterop_h #ifndef __KERNEL__ #include #else #include #include #endif void async_do_in_irq(void (^block)(void)); void async_do_in_workqueue(void (^block)(void)); void async_do_in_ios(void (^block)(void)); void sync_do_in_workqueue(void (^block)(void (^done)(void))); // call into ios from kernel: void actuate_kernel(const char *cmdline); void ReportPanic(const char *message); void ConsoleLog(const char *data, unsigned len); const char *DefaultRootPath(void); typedef const void *nsobj_t; nsobj_t objc_get(nsobj_t object); void objc_put(nsobj_t object); struct linux_tty { struct linux_tty_callbacks *ops; }; struct linux_tty_callbacks { void (*can_output)(struct linux_tty *tty); void (*send_input)(struct linux_tty *tty, const char *data, size_t length); void (*resize)(struct linux_tty *tty, int cols, int rows); void (*hangup)(struct linux_tty *tty); }; #ifdef __KERNEL__ struct file *ios_pty_open(nsobj_t *terminal_out); #endif nsobj_t Terminal_terminalWithType_number(int type, int number); void Terminal_setLinuxTTY(nsobj_t _self, struct linux_tty *tty); int Terminal_sendOutput_length(nsobj_t _self, const char *data, int size); int Terminal_roomForOutput(nsobj_t _self); nsobj_t UIPasteboard_get(void); long UIPasteboard_changeCount(void); void UIPasteboard_set(const char *data, size_t len); size_t NSData_length(nsobj_t data); const void *NSData_bytes(nsobj_t data); // call into kernel from ios: typedef void (^StartSessionDoneBlock)(int retval, int pid, nsobj_t terminal); void linux_start_session(const char *exe, const char *const *argv, const char *const *envp, StartSessionDoneBlock done); void linux_sethostname(const char *hostname); ssize_t linux_read_file(const char *path, char *buf, size_t size); ssize_t linux_write_file(const char *path, const char *buf, size_t size); int linux_remove_directory(const char *path); #endif /* LinuxInterop_h */ ================================================ FILE: app/LinuxPTY.c ================================================ // // LinuxPTY.c // libiSHLinux // // Created by Theodore Dubois on 12/30/21. // #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "LinuxInterop.h" static struct path ptmx_path; struct ios_pty_wq { struct ios_pty *pty; struct wait_queue_entry wq; struct wait_queue_head *head; }; struct ios_pty { dev_t pts_rdev; struct file *ptm; nsobj_t terminal; struct linux_tty linux_tty; // pseudoterminals have multiple wait queues and you need a different wait_queue_entry for each one. fun fact! int n_wqs; struct ios_pty_wq wqs[4]; poll_table pt; struct work_struct poll_cb_work; struct work_struct output_work; }; static void ios_pty_output_work(struct work_struct *output_work) { struct ios_pty *pty = container_of(output_work, struct ios_pty, output_work); char *buf = kvmalloc(PAGE_SIZE, GFP_KERNEL); ssize_t size; for (;;) { size_t room = Terminal_roomForOutput(pty->terminal); if (room == 0) { printk(KERN_WARNING "ios: no room for pty output\n"); break; } size = kernel_read(pty->ptm, buf, room, NULL); if (size < 0) { if (size != -EAGAIN) printk(KERN_WARNING "ios: pty read failed: %s\n", errname(size)); break; } int sent = Terminal_sendOutput_length(pty->terminal, buf, size); if (sent != size) { printk(KERN_WARNING "ios: dropped %ld bytes of pty output\n", size - sent); break; } } kvfree(buf); } static void ios_pty_cleanup(struct ios_pty *pty) { for (int i = 0; i < pty->n_wqs; i++) remove_wait_queue(pty->wqs[i].head, &pty->wqs[i].wq); fput(pty->ptm); nsobj_t terminal = pty->terminal; Terminal_setLinuxTTY(terminal, NULL); objc_put(terminal); kfree(pty); } static void ios_pty_cb_can_output(struct linux_tty *linux_tty) { struct ios_pty *pty = container_of(linux_tty, struct ios_pty, linux_tty); schedule_work(&pty->output_work); } static void ios_pty_cb_send_input(struct linux_tty *linux_tty, const char *data, size_t length) { struct ios_pty *pty = container_of(linux_tty, struct ios_pty, linux_tty); ssize_t written = kernel_write(pty->ptm, data, length, NULL); if (written < 0) printk(KERN_WARNING "ios: pty input failed: %s\n", errname(written)); else if (written != length) printk(KERN_WARNING "ios: dropped %ld bytes of pty input\n", length - written); } static void ios_pty_cb_resize(struct linux_tty *linux_tty, int cols, int rows) { struct ios_pty *pty = container_of(linux_tty, struct ios_pty, linux_tty); struct winsize ws = { .ws_row = rows, .ws_col = cols, }; vfs_ioctl(pty->ptm, TIOCSWINSZ, (unsigned long) &ws); } static void ios_pty_cb_hangup(struct linux_tty *linux_tty) { // TODO: figure out what this should be doing } static struct linux_tty_callbacks ios_pty_callbacks = { .can_output = ios_pty_cb_can_output, .send_input = ios_pty_cb_send_input, .resize = ios_pty_cb_resize, .hangup = ios_pty_cb_hangup, }; static void ios_pty_poll_cb_work(struct work_struct *work) { struct ios_pty *pty = container_of(work, struct ios_pty, poll_cb_work); __poll_t events = vfs_poll(pty->ptm, NULL); if (events & EPOLLIN) ios_pty_output_work(&pty->output_work); if (events & EPOLLHUP) ios_pty_cleanup(pty); } static int ptm_callback(struct wait_queue_entry *wq_entry, unsigned mode, int flags, void *key) { struct ios_pty *pty = container_of(wq_entry, struct ios_pty_wq, wq)->pty; schedule_work(&pty->poll_cb_work); return 0; } static void poll_callback(struct file *file, wait_queue_head_t *whead, poll_table *pt) { struct ios_pty *pty = container_of(pt, struct ios_pty, pt); if (pty->n_wqs >= ARRAY_SIZE(pty->wqs)) panic("ios pty: too many wait queues!"); struct ios_pty_wq *pty_wq = &pty->wqs[pty->n_wqs++]; pty_wq->pty = pty; pty_wq->head = whead; init_waitqueue_func_entry(&pty_wq->wq, ptm_callback); add_wait_queue(whead, &pty_wq->wq); } struct file *ios_pty_open(nsobj_t *terminal_out) { struct file *ptm_file = dentry_open(&ptmx_path, O_RDWR, current_cred()); if (IS_ERR(ptm_file)) return ptm_file; int lock_pty = 0; vfs_ioctl(ptm_file, TIOCSPTLCK, (unsigned long) &lock_pty); spin_lock(&ptm_file->f_lock); ptm_file->f_flags |= O_NONBLOCK; spin_unlock(&ptm_file->f_lock); // sadly this api can't just return a struct file * int fd = vfs_ioctl(ptm_file, TIOCGPTPEER, O_RDWR); if (fd < 0) { fput(ptm_file); return ERR_PTR(fd); } struct file *pts_file = fget(fd); close_fd(fd); struct ios_pty *pty = kzalloc(sizeof(*pty), GFP_KERNEL); if (pty == NULL) { fput(pts_file); fput(ptm_file); return ERR_PTR(-ENOMEM); } pty->ptm = ptm_file; INIT_WORK(&pty->poll_cb_work, ios_pty_poll_cb_work); INIT_WORK(&pty->output_work, ios_pty_output_work); pty->pts_rdev = pts_file->f_inode->i_rdev; pty->terminal = Terminal_terminalWithType_number(MAJOR(pty->pts_rdev), MINOR(pty->pts_rdev)); pty->linux_tty.ops = &ios_pty_callbacks; Terminal_setLinuxTTY(pty->terminal, &pty->linux_tty); *terminal_out = pty->terminal; init_poll_funcptr(&pty->pt, poll_callback); __poll_t revents = vfs_poll(pty->ptm, &pty->pt); if (revents) ptm_callback(&pty->wqs[pty->n_wqs-1].wq, 0, 0, NULL); return pts_file; } static __init int ios_pty_init(void) { init_mkdir("/dev/pts", 0755); int err = do_mount("devpts", "/dev/pts", "devpts", MS_SILENT, NULL); if (err < 0) { panic("ish: failed to mount devpts: %s", errname(err)); } err = kern_path("/dev/pts/ptmx", 0, &ptmx_path); if (err < 0) { panic("ish: failed to acquire ptmx: %s", errname(err)); } return 0; } device_initcall(ios_pty_init); ================================================ FILE: app/LinuxRoot.c ================================================ // // LinuxRoot.c // libiSHLinux // // Created by Theodore Dubois on 12/29/21. // #include #include #include #include #include #include #include #include "LinuxInterop.h" void FsInitialize(void); static __init int ish_rootfs(void) { rootfs_mounted = true; const char *fakefs_path = DefaultRootPath(); int err = do_mount(fakefs_path, "/root", "fakefs", MS_SILENT, NULL); if (err < 0) { pr_emerg("ish: failed to mount fakefs root from %s: %s\n", fakefs_path, errname(err)); return err; } init_chdir("/root"); devtmpfs_mount(); err = do_mount("proc", "proc", "proc", MS_SILENT, NULL); if (err < 0) { pr_warn("ish: failed to mount procfs: %s", errname(err)); } do_mount(".", "/", NULL, MS_MOVE, NULL); init_chroot("."); FsInitialize(); return 0; } rootfs_initcall(ish_rootfs); ================================================ FILE: app/LinuxTTY.c ================================================ // // LinuxTTY.c // libiSHLinux // // Created by Theodore Dubois on 8/15/21. // #include "LinuxInterop.h" #include #include #include #include #include #include #include static void nslog_console_write(struct console *console, const char *data, unsigned len) { ConsoleLog(data, len); } static struct console nslog_console = { .name = "nslog", .write = nslog_console_write, .flags = CON_PRINTBUFFER|CON_ANYTIME, .index = -1, }; static __init int nslog_init(void) { register_console(&nslog_console); return 0; } device_initcall(nslog_init); struct ios_tty { struct linux_tty linux_tty; struct tty_port port; }; #define NUM_TTYS 6 static struct tty_driver *ios_tty_driver; static struct ios_tty ios_ttys[NUM_TTYS]; static int ios_tty_port_activate(struct tty_port *port, struct tty_struct *tty) { BUG_ON(port != &ios_ttys[tty->index].port); port->client_data = (void *) Terminal_terminalWithType_number(TTY_MAJOR, tty->index); Terminal_setLinuxTTY(port->client_data, &container_of(port, struct ios_tty, port)->linux_tty); return 0; } static void ios_tty_port_destruct(struct tty_port *port) { async_do_in_ios(^{ objc_put(port->client_data); async_do_in_irq(^{ kfree(port); }); }); } static struct tty_port_operations ios_tty_port_ops = { .activate = ios_tty_port_activate, .destruct = ios_tty_port_destruct, }; static void ios_tty_cb_can_output(struct linux_tty *linux_tty) { struct ios_tty *tty = container_of(linux_tty, struct ios_tty, linux_tty); tty_port_tty_wakeup(&tty->port); } static void ios_tty_cb_send_input(struct linux_tty *linux_tty, const char *data, size_t length) { struct ios_tty *tty = container_of(linux_tty, struct ios_tty, linux_tty); tty_insert_flip_string(&tty->port, data, length); tty_flip_buffer_push(&tty->port); } static void ios_tty_cb_resize(struct linux_tty *linux_tty, int cols, int rows) { struct ios_tty *tty = container_of(linux_tty, struct ios_tty, linux_tty); struct winsize ws = { .ws_row = rows, .ws_col = cols, }; tty_do_resize(tty->port.tty, &ws); } static void ios_tty_cb_hangup(struct linux_tty *linux_tty) { // nah } static struct linux_tty_callbacks ios_tty_callbacks = { .can_output = ios_tty_cb_can_output, .send_input = ios_tty_cb_send_input, .resize = ios_tty_cb_resize, .hangup = ios_tty_cb_hangup, }; static int ios_tty_open(struct tty_struct *tty, struct file *filp) { return tty_port_open(tty->port, tty, filp); } static int ios_tty_write(struct tty_struct *tty, const unsigned char *data, int len) { return Terminal_sendOutput_length(tty->port->client_data, data, len); } static unsigned int ios_tty_write_room(struct tty_struct *tty) { return Terminal_roomForOutput(tty->port->client_data); } static struct tty_operations ios_tty_ops = { .open = ios_tty_open, .write = ios_tty_write, .write_room = ios_tty_write_room, }; static int ios_tty_console_setup(struct console *console, char *what) { console->data = (void *) Terminal_terminalWithType_number(TTY_MAJOR, 1); return 0; } static void ios_tty_console_write(struct console *console, const char *data, unsigned len) { nsobj_t tty = console->data; while (len) { const char *newline = memchr(data, '\n', len); if (newline != NULL) { Terminal_sendOutput_length(tty, data, newline - data); Terminal_sendOutput_length(tty, "\r\n", 2); len -= newline - data + 1; data = newline + 1; } else { Terminal_sendOutput_length(tty, data, len); len = 0; } } } static struct tty_driver *ios_tty_console_device(struct console *console, int *index) { *index = console->index; return ios_tty_driver; } static struct console ios_tty_console = { .name = "tty", .setup = ios_tty_console_setup, .write = ios_tty_console_write, .device = ios_tty_console_device, .flags = CON_PRINTBUFFER|CON_ANYTIME, .index = -1, }; static __init int ios_tty_init(void) { for (int i = 0; i < NUM_TTYS; i++) { ios_ttys[i].linux_tty.ops = &ios_tty_callbacks; tty_port_init(&ios_ttys[i].port); ios_ttys[i].port.ops = &ios_tty_port_ops; } ios_tty_driver = tty_alloc_driver(NUM_TTYS, TTY_DRIVER_REAL_RAW | TTY_DRIVER_RESET_TERMIOS); ios_tty_driver->driver_name = "ios"; ios_tty_driver->name = "tty"; ios_tty_driver->name_base = 1; ios_tty_driver->major = TTY_MAJOR; ios_tty_driver->minor_start = 1; ios_tty_driver->type = TTY_DRIVER_TYPE_CONSOLE; ios_tty_driver->subtype = SYSTEM_TYPE_CONSOLE; ios_tty_driver->init_termios = tty_std_termios; tty_set_operations(ios_tty_driver, &ios_tty_ops); for (int i = 0; i < NUM_TTYS; i++) { tty_port_link_device(&ios_ttys[i].port, ios_tty_driver, i); } if (tty_register_driver(ios_tty_driver)) panic("ios tty: failed to tty_register_driver"); register_console(&ios_tty_console); return 0; } device_initcall(ios_tty_init); ================================================ FILE: app/LocationDevice.h ================================================ // // LocationDevice.h // iSH // // Created by Theodore Dubois on 10/20/19. // extern struct dev_ops location_dev; ================================================ FILE: app/LocationDevice.m ================================================ // // LocationDevice.m // iSH // // Created by Theodore Dubois on 10/20/19. // #import #import #include "kernel/fs.h" #include "fs/dev.h" #include "util/sync.h" @interface LocationTracker : NSObject + (LocationTracker *)instance; @property CLLocationManager *locationManager; @property (nonatomic) CLLocation *latest; @property lock_t lock; @property cond_t updateCond; - (int)waitForUpdate; @end BOOL CLIsAuthorized(CLAuthorizationStatus status) { return status == kCLAuthorizationStatusAuthorizedWhenInUse || status == kCLAuthorizationStatusAuthorizedAlways; } @implementation LocationTracker + (LocationTracker *)instance { static __weak LocationTracker *tracker; if (tracker == nil) { __block LocationTracker *newTracker; dispatch_sync(dispatch_get_main_queue(), ^{ if (tracker == nil) { newTracker = [LocationTracker new]; tracker = newTracker; } }); return newTracker; } return tracker; } - (instancetype)init { if (self = [super init]) { self.locationManager = [CLLocationManager new]; self.locationManager.delegate = self; self.locationManager.allowsBackgroundLocationUpdates = YES; if (CLIsAuthorized([CLLocationManager authorizationStatus])) { [self.locationManager startUpdatingLocation]; [self.locationManager requestLocation]; } else { [self.locationManager requestAlwaysAuthorization]; } lock_init(&_lock); cond_init(&_updateCond); } return self; } - (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations { lock(&_lock); self.latest = locations.lastObject; notify(&_updateCond); unlock(&_lock); } - (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error { NSLog(@"location failed %@", error); } - (void)dealloc { [self.locationManager stopUpdatingLocation]; cond_destroy(&_updateCond); } - (int)waitForUpdate { lock(&_lock); CLLocation *oldLatest = self.latest; int err = 0; while (self.latest == oldLatest) { err = wait_for(&_updateCond, &_lock, NULL); if (err < 0) break; } unlock(&_lock); return err; } - (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status { if (status == kCLAuthorizationStatusAuthorizedAlways || status == kCLAuthorizationStatusAuthorizedWhenInUse) { NSLog(@"got auth, starting updates"); [manager startUpdatingLocation]; } } @end @interface LocationFile : NSObject { NSData *buffer; size_t bufferOffset; } @property LocationTracker *tracker; - (ssize_t)readIntoBuffer:(void *)buf size:(size_t)size; @end @implementation LocationFile - (instancetype)init { if (self = [super init]) { self.tracker = [LocationTracker instance]; } return self; } - (int)waitForUpdate { if (buffer != nil) return 0; int err = [self.tracker waitForUpdate]; if (err < 0) return err; CLLocation *location = self.tracker.latest; NSString *output = [NSString stringWithFormat:@"%+f,%+f\n", location.coordinate.latitude, location.coordinate.longitude]; buffer = [output dataUsingEncoding:NSUTF8StringEncoding]; bufferOffset = 0; return 0; } - (ssize_t)readIntoBuffer:(void *)buf size:(size_t)size { @synchronized (self) { int err = [self waitForUpdate]; if (err < 0) return err; size_t remaining = buffer.length - bufferOffset; if (size > remaining) size = remaining; [buffer getBytes:buf range:NSMakeRange(bufferOffset, size)]; bufferOffset += size; if (bufferOffset == buffer.length) buffer = nil; return size; } } @end static int location_open(int major, int minor, struct fd *fd) { fd->data = (void *) CFBridgingRetain([LocationFile new]); return 0; } static int location_close(struct fd *fd) { CFBridgingRelease(fd->data); return 0; } static ssize_t location_read(struct fd *fd, void *buf, size_t size) { LocationFile *file = (__bridge LocationFile *) fd->data; return [file readIntoBuffer:buf size:size]; } const struct dev_ops location_dev = { .open = location_open, .fd.close = location_close, .fd.read = location_read, }; ================================================ FILE: app/NSObject+SaneKVO.h ================================================ // // NSObject+SaneKVO.h // iSH // // Created by Theodore Dubois on 11/10/20. // #import NS_ASSUME_NONNULL_BEGIN @interface KVOObservation : NSObject { BOOL _enabled; __weak id _object; NSString *_keyPath; void (^_block)(void); } - (void)disable; @end @interface NSObject (SaneKVO) - (KVOObservation *)observe:(NSString *)keyPath options:(NSKeyValueObservingOptions)options usingBlock:(void (^)(void))block; - (void)observe:(NSArray *)keyPaths options:(NSKeyValueObservingOptions)options owner:(id)owner usingBlock:(void (^)(id))block; @end NS_ASSUME_NONNULL_END ================================================ FILE: app/NSObject+SaneKVO.m ================================================ // // NSObject+SaneKVO.m // iSH // // Created by Theodore Dubois on 11/10/20. // #import #import "NSObject+SaneKVO.h" static void *kKVOObservations = &kKVOObservations; @interface KVOObservation () - (instancetype)initWithKeyPath:(NSString *)keyPath object:(id)object block:(void (^)(void))block; @end @implementation NSObject (SaneKVO) - (KVOObservation *)observe:(NSString *)keyPath options:(NSKeyValueObservingOptions)options usingBlock:(void (^)(void))block { KVOObservation *observation = [[KVOObservation alloc] initWithKeyPath:keyPath object:self block:block]; [self addObserver:observation forKeyPath:keyPath options:options context:NULL]; return observation; } - (void)observe:(NSArray *)keyPaths options:(NSKeyValueObservingOptions)options owner:(id)owner usingBlock:(void (^)(id self))block { __weak id weakOwner = owner; void (^newBlock)(void) = ^{ id owner = weakOwner; NSAssert(owner, @"kvo notification shouldn't come to dead object"); block(owner); }; @synchronized (owner) { for (NSString *keyPath in keyPaths) { NSMutableSet *observations = objc_getAssociatedObject(owner, kKVOObservations); if (observations == nil) { observations = [NSMutableSet new]; objc_setAssociatedObject(owner, kKVOObservations, observations, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } [observations addObject:[self observe:keyPath options:options usingBlock:newBlock]]; } } } @end @implementation KVOObservation - (instancetype)initWithKeyPath:(NSString *)keyPath object:(id)object block:(void (^)(void))block { if (self = [super init]) { _keyPath = keyPath; _object = object; _block = block; _enabled = YES; } return self; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { _block(); } - (void)disable { if (_enabled) { [_object removeObserver:self forKeyPath:_keyPath context:NULL]; _enabled = NO; } } - (void)dealloc { [self disable]; } @end ================================================ FILE: app/NotLinux.xcconfig ================================================ NINJA_TARGETS = libish.a libish_emu.a libfakefs.a ================================================ FILE: app/PassthroughView.h ================================================ // // PassthroughView.h // iSH // // Created by Theodore Dubois on 11/24/20. // #import NS_ASSUME_NONNULL_BEGIN @interface PassthroughView : UIView @end NS_ASSUME_NONNULL_END ================================================ FILE: app/PassthroughView.m ================================================ // // PassthroughView.m // iSH // // Created by Theodore Dubois on 11/24/20. // #import "PassthroughView.h" @implementation PassthroughView - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { for (UIView *subview in self.subviews) { if (subview.userInteractionEnabled && [subview pointInside:[self convertPoint:point toView:subview] withEvent:event]) return YES; } return NO; } @end ================================================ FILE: app/PasteboardDevice.h ================================================ // Pasteboard is implementation of /dev/clipboard device extern struct dev_ops clipboard_dev; ================================================ FILE: app/PasteboardDevice.m ================================================ #include #import #include "fs/poll.h" #include "fs/dyndev.h" #include "kernel/errno.h" #include "debug.h" /** * buffer is dynamically sized buffer of size buffer_cap * All writes go to it, and buffer_len is length of data held in buffer */ // Prepare for fd separation #define fd_priv(fd) fd->clipboard typedef struct fd clip_fd; #define INITIAL_BUFFER_CAP 1024 // 8MB: https://stackoverflow.com/a/3523175 #define MAXIMAL_BUFFER_CAP 8*1024*1024 // If pasteboard contents were changed since file was opened, // all read operations on in return error static int check_read_generation(clip_fd *fd) { UIPasteboard *pb = UIPasteboard.generalPasteboard; uint64_t pb_gen = (uint64_t) pb.changeCount; uint64_t fd_gen = fd_priv(fd).generation; if (fd_gen == 0 || fd->offset == 0) { fd_priv(fd).generation = pb_gen; } else if (fd_gen != pb_gen) { return -1; } return 0; } static const char *get_data(clip_fd *fd, size_t *len) { if (fd_priv(fd).buffer != NULL) { *len = fd_priv(fd).buffer_len; return fd_priv(fd).buffer; } if (check_read_generation(fd) != 0) { return NULL; } NSString __autoreleasing *contents = UIPasteboard.generalPasteboard.string; *len = contents.length; return contents.UTF8String; } static int realloc_to_fit(clip_fd* fd, size_t fit_len) { // (Re)allocate buffer if there's not enough space to fit fit_len if (fit_len <= fd_priv(fd).buffer_cap) { return 0; } if (fit_len > MAXIMAL_BUFFER_CAP) { return 1; } size_t size = fd_priv(fd).buffer_cap * 2; if (size == 0) { size = INITIAL_BUFFER_CAP; } while (size < fit_len) size *= 2; void *new_buf = realloc(fd_priv(fd).buffer, size); if (new_buf == NULL) { return 1; } fd_priv(fd).buffer = new_buf; fd_priv(fd).buffer_cap = size; return 0; } // buffer => UIPasteboard static int clipboard_write_sync(clip_fd *fd) { if (fd_priv(fd).buffer == NULL) { return 0; } void *data = fd_priv(fd).buffer; size_t len = fd_priv(fd).buffer_len; // FIXME(stek29): This logs "Returning local object of class NSString" // and I have no idea why (or how to fix it) UIPasteboard.generalPasteboard.string = [[NSString alloc] initWithBytes:data length:len encoding:NSUTF8StringEncoding]; // Reset generation since we've just updated UIPasteboard // note: offset doesn't change fd_priv(fd).generation = 0; return 0; } // UIPasteboard => buffer, return len static ssize_t clipboard_read_sync(clip_fd *fd) { if (fd_priv(fd).buffer != NULL) { free(fd_priv(fd).buffer); fd_priv(fd).buffer = NULL; fd_priv(fd).buffer_cap = 0; fd_priv(fd).buffer_len = 0; } @autoreleasepool { size_t len; const void *data = get_data(fd, &len); // Make sure size is still INITIAL_BUFFER_CAP based if (realloc_to_fit(fd, len)) { return _ENOMEM; } memcpy(fd_priv(fd).buffer, data, len); fd_priv(fd).buffer_len = len; return len; } } static int clipboard_poll(clip_fd *fd) { return POLL_READ | POLL_WRITE; } static ssize_t clipboard_read(clip_fd *fd, void *buf, size_t bufsize) { @autoreleasepool { size_t length = 0; const char *data = get_data(fd, &length); if (data == NULL) { return _EIO; } size_t remaining = length - fd->offset; if ((size_t) fd->offset > length) remaining = 0; size_t n = bufsize; if (n > remaining) n = remaining; if (n != 0) { memcpy(buf, data + fd->offset, n); fd->offset += n; } return n; } } static ssize_t clipboard_write(clip_fd *fd, const void *buf, size_t bufsize) { size_t new_len = fd->offset + bufsize; size_t old_len = fd_priv(fd).buffer_len; if (old_len > new_len) { new_len = old_len; } if (realloc_to_fit(fd, new_len)) { return _ENOMEM; } // fill the hole between new offset and old len if (old_len < fd->offset) { memset(fd_priv(fd).buffer + old_len, '\0', fd->offset - old_len); } memcpy(fd_priv(fd).buffer + fd->offset, buf, bufsize); fd->offset += bufsize; fd_priv(fd).buffer_len = new_len; return bufsize; } static off_t_ clipboard_lseek(clip_fd *fd, off_t_ off, int whence) { off_t_ old_off = fd->offset; size_t length = 0; if (whence != LSEEK_SET || off != 0) { @autoreleasepool { if (get_data(fd, &length) == NULL) { return _EIO; } } } switch (whence) { case LSEEK_SET: fd->offset = off; break; case LSEEK_CUR: fd->offset += off; break; case LSEEK_END: fd->offset = length + off; break; default: return _EINVAL; } if (fd->offset < 0) { fd->offset = old_off; return _EINVAL; } return fd->offset; } static int clipboard_close(clip_fd *fd) { clipboard_write_sync(fd); if (fd_priv(fd).buffer != NULL) { free(fd_priv(fd).buffer); } return 0; } static int clipboard_open(int major, int minor, clip_fd *fd) { // Zero fd_priv data memset(&fd_priv(fd), 0, sizeof(fd_priv(fd))); // If O_TRUNC is not set, initialize buffer with current pasteboard contents if (!(fd->flags & O_TRUNC_)) { ssize_t len = clipboard_read_sync(fd); if (len < 0) { return (int) len; } if (fd->flags & O_APPEND_) { fd->offset = (size_t) len; } } return 0; } struct dev_ops clipboard_dev = { .open = clipboard_open, .fd.read = clipboard_read, .fd.write = clipboard_write, .fd.lseek = clipboard_lseek, .fd.poll = clipboard_poll, .fd.close = clipboard_close, .fd.fsync = clipboard_write_sync, }; ================================================ FILE: app/PasteboardDeviceLinux.c ================================================ // // PasteboardDeviceLinux.c // iSH+Linux // // Created by Theodore Dubois on 2/19/22. // #include #include #include #include #include #include #include "LinuxInterop.h" #define INITIAL_BUFFER_CAP 1024 // 8MB: https://stackoverflow.com/a/3523175 #define MAXIMAL_BUFFER_CAP 8*1024*1024 struct pasteboard_file { char *buffer; size_t cap; size_t len; long generation; }; static int realloc_buffer_to_fit(struct file *file, size_t fit_len) { struct pasteboard_file *pb = file->private_data; // (Re)allocate buffer if there's not enough space to fit fit_len if (fit_len <= pb->cap) return 0; if (fit_len > MAXIMAL_BUFFER_CAP) return -ENOSPC; size_t size = pb->cap * 2; if (size == 0) size = INITIAL_BUFFER_CAP; while (size < fit_len) size *= 2; void *new_buf = krealloc(pb->buffer, size, GFP_KERNEL); if (new_buf == NULL) return -ENOMEM; pb->buffer = new_buf; pb->cap = size; return 0; } static int read_pasteboard_to_buffer(struct file *file) { struct pasteboard_file *pb = file->private_data; nsobj_t data = UIPasteboard_get(); int err = realloc_buffer_to_fit(file, NSData_length(data)); if (err < 0) { objc_put(data); return err; } pb->len = NSData_length(data); memcpy(pb->buffer, NSData_bytes(data), pb->len); objc_put(data); return 0; } static int pasteboard_open(struct inode *ino, struct file *file) { struct pasteboard_file *pb = kzalloc(sizeof(struct pasteboard_file), GFP_KERNEL); int err = -ENOMEM; if (pb == NULL) goto fail; file->private_data = pb; if (!(file->f_flags & O_TRUNC)) { err = read_pasteboard_to_buffer(file); if (err < 0) goto fail_free_pb; } return 0; fail_free_pb: if (pb->buffer != NULL) kfree(pb->buffer); kfree(pb); fail: file->private_data = NULL; return err; } static loff_t pasteboard_llseek(struct file *file, loff_t off, int whence) { struct pasteboard_file *pb = file->private_data; return generic_file_llseek_size(file, off, whence, MAXIMAL_BUFFER_CAP, pb->len); } static ssize_t pasteboard_read(struct file *file, char __user *buf, size_t count, loff_t *ppos) { struct pasteboard_file *pb = file->private_data; return simple_read_from_buffer(buf, count, ppos, pb->buffer, pb->len); } static ssize_t pasteboard_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) { struct pasteboard_file *pb = file->private_data; if (file->f_flags & O_APPEND) *ppos = pb->len; loff_t new_len = *ppos + count; int err = realloc_buffer_to_fit(file, new_len); if (err < 0) return err; ssize_t result = simple_write_to_buffer(pb->buffer, pb->cap, ppos, buf, count); if (result < 0) return result; pb->len = new_len; return result; } static int pasteboard_fsync(struct file *file, loff_t start, loff_t end, int datasync) { struct pasteboard_file *pb = file->private_data; UIPasteboard_set(pb->buffer, pb->len); return 0; } static int pasteboard_release(struct inode *inode, struct file *file) { struct pasteboard_file *pb = file->private_data; pasteboard_fsync(file, 0, 0, 0); if (pb->buffer != NULL) kfree(pb->buffer); kfree(pb); return 0; } static struct file_operations pasteboard_fops = { .owner = THIS_MODULE, .open = pasteboard_open, .read = pasteboard_read, .write = pasteboard_write, .llseek = pasteboard_llseek, .fsync = pasteboard_fsync, .release = pasteboard_release, }; static struct miscdevice pasteboard_dev = { .name = "clipboard", .minor = MISC_DYNAMIC_MINOR, .fops = &pasteboard_fops, }; int __init pasteboard_init(void) { return misc_register(&pasteboard_dev); } device_initcall(pasteboard_init); ================================================ FILE: app/ProgressReportViewController.h ================================================ // // ProgressReportViewController.h // iSH // // Created by Theodore Dubois on 6/18/20. // #import #import "Roots.h" NS_ASSUME_NONNULL_BEGIN @interface ProgressReportViewController : UIViewController - (void)updateProgress:(double)progressFraction message:(NSString *)progressMessage; @end NS_ASSUME_NONNULL_END ================================================ FILE: app/ProgressReportViewController.m ================================================ // // ProgressReportViewController.m // iSH // // Created by Theodore Dubois on 6/18/20. // #import "ProgressReportViewController.h" @interface ProgressReportViewController () @property (weak, nonatomic) IBOutlet UIView *popupView; @property (weak, nonatomic) IBOutlet UIVisualEffectView *backdrop; @property (weak, nonatomic) IBOutlet UILabel *titleLabel; @property (weak, nonatomic) IBOutlet UILabel *statusLabel; @property (weak, nonatomic) IBOutlet UIProgressView *bar; @property (weak, nonatomic) IBOutlet UIButton *cancelButton; @property (nonatomic) double progress; @property (nonatomic) NSString *message; @property (nonatomic) BOOL cancelled; @property CADisplayLink *timer; @end @implementation ProgressReportViewController - (void)viewDidLoad { self.titleLabel.text = self.title; if (@available(iOS 13, *)) { self.backdrop.effect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleSystemMaterial]; } } - (void)viewDidLayoutSubviews { CAShapeLayer *mask = [CAShapeLayer new]; mask.path = [UIBezierPath bezierPathWithRoundedRect:self.popupView.bounds cornerRadius:13].CGPath; self.popupView.layer.mask = mask; self.popupView.layer.masksToBounds = YES; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; self.timer = [CADisplayLink displayLinkWithTarget:self selector:@selector(update)]; [self.timer addToRunLoop:NSRunLoop.mainRunLoop forMode:NSRunLoopCommonModes]; } - (void)viewDidDisappear:(BOOL)animated { [super viewDidDisappear:animated]; [self.timer invalidate]; } - (void)updateProgress:(double)progressFraction message:(NSString *)progressMessage { @synchronized (self) { _progress = progressFraction; _message = progressMessage; } } - (BOOL)shouldCancel { @synchronized (self) { return _cancelled; } } - (void)update { @synchronized (self) { self.bar.progress = _progress; self.statusLabel.text = _message; } } - (IBAction)cancel:(id)sender { @synchronized (self) { self.cancelled = YES; self.cancelButton.enabled = NO; } } @end ================================================ FILE: app/Project.xcconfig ================================================ #include "iSH.xcconfig" MARKETING_VERSION = 1.3.3 ENABLE_BITCODE = NO // no idea why PRODUCT_BUNDLE_IDENTIFIER = $(ROOT_BUNDLE_IDENTIFIER) PRODUCT_APP_GROUP_IDENTIFIER = group.$(ROOT_BUNDLE_IDENTIFIER) IPHONEOS_DEPLOYMENT_TARGET = 11.0 VERSIONING_SYSTEM = apple-generic MESON_BUILD_DIR = $(CONFIGURATION_BUILD_DIR)/meson NINJA_TARGETS = libish.a libish_emu.a libfakefs.a SUPPORTED_PLATFORMS = iphonesimulator iphoneos macosx ================================================ FILE: app/ProjectDebug.xcconfig ================================================ #include "XcodeDebug.xcconfig" #include "Project.xcconfig" #include "NotLinux.xcconfig" ================================================ FILE: app/ProjectDebugLinux.xcconfig ================================================ #include "XcodeDebug.xcconfig" #include "Project.xcconfig" #include "Linux.xcconfig" ================================================ FILE: app/ProjectRelease.xcconfig ================================================ #include "XcodeRelease.xcconfig" #include "Project.xcconfig" #include "NotLinux.xcconfig" ================================================ FILE: app/ProjectReleaseLinux.xcconfig ================================================ #include "XcodeRelease.xcconfig" #include "Project.xcconfig" #include "Linux.xcconfig" ================================================ FILE: app/Roots.h ================================================ // // Roots.h // iSH // // Created by Theodore Dubois on 6/7/20. // #import NS_ASSUME_NONNULL_BEGIN @protocol ProgressReporter - (void)updateProgress:(double)progressFraction message:(NSString *)progressMessage; - (BOOL)shouldCancel; @end @interface Roots : NSObject + (instancetype)instance; @property (readonly) NSOrderedSet *roots; @property NSString *defaultRoot; @property (readonly) BOOL wantsVersionFile; - (NSURL *)rootUrl:(NSString *)name; - (BOOL)importRootFromArchive:(NSURL *)archive name:(NSString *)name error:(NSError **)error progressReporter:(id _Nullable)progress; - (BOOL)exportRootNamed:(NSString *)name toArchive:(NSURL *)archive error:(NSError **)error progressReporter:(id _Nullable)progress; - (BOOL)destroyRootNamed:(NSString *)name error:(NSError **)error; - (BOOL)renameRoot:(NSString *)name toName:(NSString *)newName error:(NSError **)error; @end NS_ASSUME_NONNULL_END ================================================ FILE: app/Roots.m ================================================ // // Roots.m // iSH // // Created by Theodore Dubois on 6/7/20. // #import #import "Roots.h" #import "AppGroup.h" #import "NSObject+SaneKVO.h" #include "tools/fakefs.h" static NSURL *RootsDir(void) { static NSURL *rootsDir; static dispatch_once_t token; dispatch_once(&token, ^{ rootsDir = [ContainerURL() URLByAppendingPathComponent:@"roots"]; NSFileManager *manager = [NSFileManager defaultManager]; [manager createDirectoryAtURL:rootsDir withIntermediateDirectories:YES attributes:@{} error:nil]; }); return rootsDir; } static NSString *kDefaultRoot = @"Default Root"; @interface Roots () @property NSMutableOrderedSet *roots; @property BOOL updatingDomains; @property BOOL domainsNeedUpdate; @property BOOL wantsVersionFile; @end @implementation Roots - (instancetype)init { if (self = [super init]) { NSError *error = nil; NSArray *rootNames = [NSFileManager.defaultManager contentsOfDirectoryAtPath:RootsDir().path error:&error]; NSAssert(error == nil, @"couldn't list roots: %@", error); self.roots = [rootNames mutableCopy]; if (!self.roots.count) { // import default root NSError *error; if (![self importRootFromArchive:[NSBundle.mainBundle URLForResource:@"root" withExtension:@"tar.gz"] name:@"default" error:&error progressReporter:nil]) { NSAssert(NO, @"failed to import default root, error %@", error); } _wantsVersionFile = YES; } [self observe:@[@"roots"] options:0 owner:self usingBlock:^(typeof(self) self) { if (self.defaultRoot == nil && self.roots.count) self.defaultRoot = self.roots[0]; [self syncFileProviderDomains]; }]; [self syncFileProviderDomains]; if ((!self.defaultRoot || ![self.roots containsObject:self.defaultRoot]) && self.roots.count) self.defaultRoot = self.roots.firstObject; } return self; } - (NSString *)defaultRoot { return [NSUserDefaults.standardUserDefaults stringForKey:kDefaultRoot]; } - (void)setDefaultRoot:(NSString *)defaultRoot { [NSUserDefaults.standardUserDefaults setObject:defaultRoot forKey:kDefaultRoot]; } - (NSURL *)rootUrl:(NSString *)name { return [RootsDir() URLByAppendingPathComponent:name]; } - (void)syncFileProviderDomains { if (self.updatingDomains) { self.domainsNeedUpdate = YES; return; } self.updatingDomains = YES; self.domainsNeedUpdate = NO; [NSFileProviderManager getDomainsWithCompletionHandler:^(NSArray *domains, NSError *error) { void (^onError)(NSError *error) = ^(NSError *error) { if (error != nil) NSLog(@"error adjusting domains: %@", error); }; onError(error); NSMutableOrderedSet *missingRoots = [self.roots mutableCopy]; for (NSFileProviderDomain *domain in domains) { if ([missingRoots containsObject:domain.identifier]) { [missingRoots removeObject:domain.identifier]; } else { [NSFileManager.defaultManager removeItemAtURL: [NSFileProviderManager.defaultManager.documentStorageURL URLByAppendingPathComponent:domain.pathRelativeToDocumentStorage] error:nil]; [NSFileProviderManager removeDomain:domain completionHandler:onError]; } } for (NSString *rootId in missingRoots) { [NSFileProviderManager addDomain:[[NSFileProviderDomain alloc] initWithIdentifier:rootId displayName:rootId pathRelativeToDocumentStorage:rootId] completionHandler:onError]; } if (self.domainsNeedUpdate) [self syncFileProviderDomains]; self.updatingDomains = NO; }]; } - (BOOL)accessInstanceVariablesDirectly { return YES; } void root_progress_callback(void *cookie, double progress, const char *message, bool *should_cancel) { id reporter = (__bridge id) cookie; [reporter updateProgress:progress message:[NSString stringWithUTF8String:message]]; if ([reporter shouldCancel]) *should_cancel = true; } - (BOOL)importRootFromArchive:(NSURL *)archive name:(NSString *)name error:(NSError **)error progressReporter:(id _Nullable)progress { NSAssert(![self.roots containsObject:name], @"root already exists: %@", name); struct fakefsify_error fs_err; NSURL *destination = [self rootUrl:name]; NSURL *tempDestination = [NSFileManager.defaultManager.temporaryDirectory URLByAppendingPathComponent:[NSProcessInfo.processInfo globallyUniqueString]]; if (tempDestination == nil) return NO; if (!fakefs_import(archive.fileSystemRepresentation, tempDestination.fileSystemRepresentation, &fs_err, (struct progress) {(__bridge void *) progress, root_progress_callback})) { NSString *domain = NSPOSIXErrorDomain; if (fs_err.type == ERR_SQLITE) domain = @"SQLite"; *error = [NSError errorWithDomain:domain code:fs_err.code userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"%s, line %d", fs_err.message, fs_err.line]}]; if (fs_err.type == ERR_CANCELLED) *error = nil; free(fs_err.message); [NSFileManager.defaultManager removeItemAtURL:tempDestination error:nil]; return NO; } if (![NSFileManager.defaultManager moveItemAtURL:tempDestination toURL:destination error:error]) return NO; void (^addRoot)(void) = ^{ [[self mutableOrderedSetValueForKey:@"roots"] addObject:name]; }; if (!NSThread.isMainThread) dispatch_sync(dispatch_get_main_queue(), addRoot); else addRoot(); return YES; } - (BOOL)exportRootNamed:(NSString *)name toArchive:(NSURL *)archive error:(NSError **)error progressReporter:(id _Nullable)progress { NSAssert([self.roots containsObject:name], @"trying to export a root that doesn't exist: %@", name); struct fakefsify_error fs_err; if (!fakefs_export([self rootUrl:name].fileSystemRepresentation, archive.fileSystemRepresentation, &fs_err, (struct progress) {(__bridge void *) progress, root_progress_callback})) { // TODO: dedup with above method NSString *domain = NSPOSIXErrorDomain; if (fs_err.type == ERR_SQLITE) domain = @"SQLite"; *error = [NSError errorWithDomain:domain code:fs_err.code userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithUTF8String:fs_err.message]}]; if (fs_err.type == ERR_CANCELLED) *error = nil; free(fs_err.message); return NO; } return YES; } - (BOOL)destroyRootNamed:(NSString *)name error:(NSError **)error { if ([name isEqualToString:self.defaultRoot]) { *error = [NSError errorWithDomain:@"iSH" code:0 userInfo:@{NSLocalizedDescriptionKey: @"Cannot delete the default filesystem"}]; return NO; } NSAssert([self.roots containsObject:name], @"root does not exist: %@", name); if (![NSFileManager.defaultManager removeItemAtURL:[self rootUrl:name] error:error]) return NO; [[self mutableOrderedSetValueForKey:@"roots"] removeObject:name]; return YES; } - (BOOL)renameRoot:(NSString *)name toName:(NSString *)newName error:(NSError **)error { if (name.length == 0) { *error = [NSError errorWithDomain:@"iSH" code:0 userInfo:@{NSLocalizedDescriptionKey: @"Filesystem name can't be empty"}]; return NO; } if ([name containsString:@"/"]) { *error = [NSError errorWithDomain:@"iSH" code:0 userInfo:@{NSLocalizedDescriptionKey: @"Filesystem name can't contain /"}]; return NO; } if ([name isEqualToString:@"."] || [name isEqualToString:@".."]) { *error = [NSError errorWithDomain:@"iSH" code:0 userInfo:@{NSLocalizedDescriptionKey: @"Filesystem name can't be . or .."}]; return NO; } if ([name isEqualToString:self.defaultRoot]) { *error = [NSError errorWithDomain:@"iSH" code:0 userInfo:@{NSLocalizedDescriptionKey: @"Cannot rename the default filesystem"}]; return NO; } NSAssert([self.roots containsObject:name], @"root does not exist: %@", name); if (![NSFileManager.defaultManager moveItemAtURL:[self rootUrl:name] toURL:[self rootUrl:newName] error:error]) return NO; NSUInteger index = [self.roots indexOfObject:name]; [[self mutableOrderedSetValueForKey:@"roots"] replaceObjectAtIndex:index withObject:newName]; return YES; } + (instancetype)instance { static Roots *instance; static dispatch_once_t token; dispatch_once(&token, ^{ instance = [Roots new]; }); return instance; } @end ================================================ FILE: app/Roots.storyboard ================================================ ================================================ FILE: app/RootsTableViewController.h ================================================ // // RootsTableViewController.h // iSH // // Created by Theodore Dubois on 6/7/20. // #import NS_ASSUME_NONNULL_BEGIN @interface RootsTableViewController : UITableViewController @end NS_ASSUME_NONNULL_END ================================================ FILE: app/RootsTableViewController.m ================================================ // // RootsTableViewController.m // iSH // // Created by Theodore Dubois on 6/7/20. // #import "Roots.h" #import "RootsTableViewController.h" #import "ProgressReportViewController.h" #import "UIApplication+OpenURL.h" #import "UIViewController+Extras.h" #import "NSObject+SaneKVO.h" @interface RootsTableViewController () @end @interface RootDetailViewController : UITableViewController @property (nonatomic) NSString *rootName; @property (nonatomic) NSURL *exportURL; @property (weak, nonatomic) IBOutlet UITextField *nameField; @property (weak, nonatomic) IBOutlet UILabel *deleteLabel; @property (weak, nonatomic) IBOutlet UITableViewCell *deleteCell; @end @implementation RootsTableViewController - (void)viewDidLoad { [super viewDidLoad]; [Roots.instance observe:@[@"roots", @"defaultRoot"] options:0 owner:self usingBlock:^(typeof(self) self) { [self.tableView reloadData]; }]; } - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return Roots.instance.roots.count; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { NSString *ident = @"Root"; if ([Roots.instance.roots[indexPath.row] isEqual:Roots.instance.defaultRoot]) ident = @"Default Root"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ident forIndexPath:indexPath]; cell.textLabel.text = Roots.instance.roots[indexPath.row]; return cell; } - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { [tableView deselectRowAtIndexPath:indexPath animated:YES]; } - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { RootDetailViewController *vc = segue.destinationViewController; vc.rootName = Roots.instance.roots[self.tableView.indexPathForSelectedRow.row]; } - (IBAction)importFilesystem:(id)sender { UIDocumentPickerViewController *picker = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:@[@"public.tar-archive", @"org.gnu.gnu-zip-archive"] inMode:UIDocumentPickerModeImport]; [self presentViewController:picker animated:YES completion:nil]; if (@available(iOS 13, *)) { picker.shouldShowFileExtensions = YES; } picker.delegate = self; } - (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentsAtURLs:(NSArray *)urls { NSAssert(urls.count == 1, @"somehow picked multiple documents"); NSURL *url = urls.firstObject; NSString *fileName = url.lastPathComponent.stringByDeletingPathExtension; if ([fileName hasSuffix:@".tar"]) fileName = fileName.stringByDeletingPathExtension; unsigned i = 2; NSString *name = fileName; while ([Roots.instance.roots containsObject:name]) { name = [NSString stringWithFormat:@"%@ %u", fileName, i++]; } ProgressReportViewController *progressVC = [self.storyboard instantiateViewControllerWithIdentifier:@"progress"]; progressVC.title = [NSString stringWithFormat:@"Importing %@", name]; [self presentViewController:progressVC animated:YES completion:nil]; dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{ NSError *error; [url startAccessingSecurityScopedResource]; BOOL success = [Roots.instance importRootFromArchive:url name:name error:&error progressReporter:progressVC]; [url stopAccessingSecurityScopedResource]; dispatch_async(dispatch_get_main_queue(), ^{ [progressVC dismissViewControllerAnimated:YES completion:^{ if (!success && error != nil) [self presentError:error title:@"Import failed"]; }]; }); }); } @end @implementation RootDetailViewController - (void)viewWillAppear:(BOOL)animated { self.nameField.text = self.rootName; [self update]; } - (void)update { self.navigationItem.title = self.rootName; self.nameField.enabled = !self.isDefaultRoot; self.nameField.clearButtonMode = self.isDefaultRoot ? UITextFieldViewModeNever : UITextFieldViewModeAlways; self.deleteLabel.enabled = !self.isDefaultRoot; self.deleteCell.selectionStyle = !self.isDefaultRoot ? UITableViewCellSelectionStyleDefault : UITableViewCellSelectionStyleNone; [self.tableView reloadData]; } - (IBAction)nameChanged:(id)sender { NSString *newName = self.nameField.text; NSError *err; if (![Roots.instance renameRoot:self.rootName toName:newName error:&err]) { self.nameField.text = self.rootName; [self presentError:err title:@"Rename failed"]; return; } self.rootName = newName; [self update]; } - (BOOL)textFieldShouldReturn:(UITextField *)textField { [textField resignFirstResponder]; return NO; } - (BOOL)isDefaultRoot { return [self.rootName isEqualToString:Roots.instance.defaultRoot]; } - (NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section { if (section == 2) { // delete if (self.isDefaultRoot) return @"This filesystem can't be deleted because it's currently mounted as the root."; } return [super tableView:tableView titleForFooterInSection:section]; } - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { if (indexPath.section == 0 && indexPath.row == 1) [self browseFiles]; if (indexPath.section == 0 && indexPath.row == 2) [self exportFilesystem]; if (indexPath.section == 1 && indexPath.row == 0) [self bootThis]; if (indexPath.section == 2 && indexPath.row == 0) [self deleteFilesystem]; [tableView deselectRowAtIndexPath:indexPath animated:YES]; } - (void)browseFiles { NSURL *url = [NSFileProviderManager.defaultManager.documentStorageURL URLByAppendingPathComponent:self.rootName]; NSURLComponents *components = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO]; components.scheme = @"shareddocuments"; [UIApplication openURL:components.string]; } - (void)exportFilesystem { self.exportURL = [[NSFileManager.defaultManager.temporaryDirectory URLByAppendingPathComponent:[NSProcessInfo.processInfo globallyUniqueString]] URLByAppendingPathComponent:[NSString stringWithFormat:@"%@.tar.gz", self.rootName]]; [NSFileManager.defaultManager createDirectoryAtURL:self.exportURL.URLByDeletingLastPathComponent withIntermediateDirectories:YES attributes:nil error:nil]; ProgressReportViewController *progressVC = [self.storyboard instantiateViewControllerWithIdentifier:@"progress"]; progressVC.title = [NSString stringWithFormat:@"Exporting %@", self.rootName]; [self presentViewController:progressVC animated:YES completion:nil]; // witness the callback hell dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{ NSError *err; BOOL success = [Roots.instance exportRootNamed:self.rootName toArchive:self.exportURL error:&err progressReporter:progressVC]; dispatch_async(dispatch_get_main_queue(), ^{ [progressVC dismissViewControllerAnimated:YES completion:^{ if (!success) { if (err != nil) [self presentError:err title:@"Export failed"]; return; } UIDocumentPickerViewController *picker = [[UIDocumentPickerViewController alloc] initWithURL:self.exportURL inMode:UIDocumentPickerModeExportToService]; picker.delegate = self; if (@available(iOS 13, *)) { picker.shouldShowFileExtensions = YES; } [self presentViewController:picker animated:YES completion:nil]; }]; }); }); } - (void)setExportURL:(NSURL *)exportURL { [NSFileManager.defaultManager removeItemAtURL:_exportURL.URLByDeletingLastPathComponent error:nil]; _exportURL = exportURL; } - (void)bootThis { Roots.instance.defaultRoot = self.rootName; exit(0); } - (void)deleteFilesystem { if (self.isDefaultRoot) return; UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Really delete?" message:@"I can't be bothered to implement any undo or regret UI so this is irreversable." preferredStyle:UIAlertControllerStyleAlert]; [alert addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]]; [alert addAction:[UIAlertAction actionWithTitle:@"Delete" style:UIAlertActionStyleDestructive handler:^(UIAlertAction *action) { NSError *error; if (![Roots.instance destroyRootNamed:self.rootName error:&error]) { [self presentError:error title:@"Delete failed"]; } else { [self.navigationController popViewControllerAnimated:YES]; } }]]; [self presentViewController:alert animated:YES completion:nil]; } - (void)dealloc { self.exportURL = nil; // get it deleted } @end ================================================ FILE: app/SceneDelegate.h ================================================ // // SceneDelegate.h // iSH // // Created by Theodore Dubois on 10/26/19. // #import #import "TerminalViewController.h" NS_ASSUME_NONNULL_BEGIN extern TerminalViewController *currentTerminalViewController; API_AVAILABLE(ios(13)) @interface SceneDelegate : UIResponder @property (nonatomic) UIWindow *window; @end NS_ASSUME_NONNULL_END ================================================ FILE: app/SceneDelegate.m ================================================ // // SceneDelegate.m // iSH // // Created by Theodore Dubois on 10/26/19. // #import "SceneDelegate.h" #import "AboutViewController.h" TerminalViewController *currentTerminalViewController = NULL; @interface SceneDelegate () @property NSString *terminalUUID; @end static NSString *const TerminalUUID = @"TerminalUUID"; @implementation SceneDelegate - (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions { if ([NSUserDefaults.standardUserDefaults boolForKey:@"recovery"]) { UINavigationController *vc = [[UIStoryboard storyboardWithName:@"About" bundle:nil] instantiateInitialViewController]; AboutViewController *avc = (AboutViewController *) vc.topViewController; avc.recoveryMode = YES; self.window.rootViewController = vc; return; } TerminalViewController *vc = (TerminalViewController *) self.window.rootViewController; vc.sceneSession = session; if (session.stateRestorationActivity == nil) { [vc startNewSession]; } else { self.terminalUUID = session.stateRestorationActivity.userInfo[TerminalUUID]; [vc reconnectSessionFromTerminalUUID: [[NSUUID alloc] initWithUUIDString:self.terminalUUID]]; } } - (NSUserActivity *)stateRestorationActivityForScene:(UIScene *)scene { NSUserActivity *activity = [[NSUserActivity alloc] initWithActivityType:@"app.ish.scene"]; TerminalViewController *vc = (TerminalViewController *) self.window.rootViewController; if ([vc isKindOfClass:TerminalViewController.class]) { self.terminalUUID = vc.sessionTerminalUUID.UUIDString; if (self.terminalUUID != nil) { [activity addUserInfoEntriesFromDictionary:@{TerminalUUID: self.terminalUUID}]; } } return activity; } - (void)sceneDidBecomeActive:(UIScene *)scene { TerminalViewController *terminalViewController = (TerminalViewController *) self.window.rootViewController;; currentTerminalViewController = terminalViewController; } - (void)sceneWillResignActive:(UIScene *)scene { TerminalViewController *terminalViewController = (TerminalViewController *) self.window.rootViewController; if (currentTerminalViewController == terminalViewController) { currentTerminalViewController = NULL; } } @end ================================================ FILE: app/ScrollbarView.h ================================================ // // ScrollbarView.h // iSH // // Created by Theodore Dubois on 9/2/19. // #import NS_ASSUME_NONNULL_BEGIN @interface ScrollbarView : UIScrollView @property (nonatomic, nullable) UIView *contentView; @end NS_ASSUME_NONNULL_END ================================================ FILE: app/ScrollbarView.m ================================================ // // ScrollbarView.m // iSH // // Created by Theodore Dubois on 9/2/19. // #import "ScrollbarView.h" @class ScrollbarViewDelegate; @interface ScrollbarView () @property CGPoint contentViewOrigin; @property ScrollbarViewDelegate *outerDelegate; @end @interface ScrollbarViewDelegate : NSObject @property (weak) id innerDelegate; @end @implementation ScrollbarViewDelegate - (void)scrollViewDidScroll:(ScrollbarView *)scrollView { CGRect frame = scrollView.contentView.frame; frame.origin.x = scrollView.contentOffset.x + scrollView.contentViewOrigin.x; frame.origin.y = scrollView.contentOffset.y + scrollView.contentViewOrigin.y; scrollView.contentView.frame = frame; [self.innerDelegate scrollViewDidScroll:scrollView]; } - (id)forwardingTargetForSelector:(SEL)aSelector { if ([self.innerDelegate respondsToSelector:aSelector]) return self.innerDelegate; return [super forwardingTargetForSelector:aSelector]; } @end @implementation ScrollbarView - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { self.outerDelegate = [ScrollbarViewDelegate new]; super.delegate = self.outerDelegate; } return self; } - (void)setContentView:(UIView *)contentView { _contentView = contentView; self.contentViewOrigin = contentView.frame.origin; } - (id)delegate { return self.outerDelegate.innerDelegate; } - (void)setDelegate:(id)delegate { self.outerDelegate.innerDelegate = delegate; } @end ================================================ FILE: app/Settings.bundle/Root.plist ================================================ PreferenceSpecifiers Type PSGroupSpecifier FooterText Opens the app straight to the settings menu. Useful if you changed anything there and need to change it back but the app won't start. Type PSToggleSwitchSpecifier Title Recovery Mode Key recovery DefaultValue ================================================ FILE: app/StaticLib.xcconfig ================================================ SKIP_INSTALL = YES EXECUTABLE_PREFIX = PRODUCT_NAME = $(TARGET_NAME) VERSIONING_SYSTEM = ================================================ FILE: app/StaticLibLinux.xcconfig ================================================ #include "StaticLib.xcconfig" HEADER_SEARCH_PATHS = $(SRCROOT)/deps/linux/arch/ish/include $(MESON_BUILD_DIR)/deps/linux/arch/ish/include/generated $(SRCROOT)/deps/linux/include $(MESON_BUILD_DIR)/deps/linux/include $(SRCROOT)/deps/linux/arch/ish/include/uapi $(MESON_BUILD_DIR)/deps/linux/arch/ish/include/generated/uapi $(SRCROOT)/deps/linux/include/uapi $(MESON_BUILD_DIR)/deps/linux/arch/ish/include/generated/uapi $(SRCROOT) GCC_PREPROCESSOR_DEFINITIONS = __KERNEL__ OTHER_CFLAGS = -U__weak -include linux/kconfig.h -include linux/compiler_types.h -Wno-gnu-variable-sized-type-not-at-end -Wno-conditional-uninitialized // disable modules because it results in clashes between linux and clang headers CLANG_ENABLE_MODULES = NO // these warnings are tripped by linux CLANG_WARN_DOCUMENTATION_COMMENTS = NO GCC_WARN_ABOUT_POINTER_SIGNEDNESS = NO GCC_WARN_64_TO_32_BIT_CONVERSION = NO ================================================ FILE: app/StaticLibLinuxUser.xcconfig ================================================ #include "StaticLib.xcconfig" HEADER_SEARCH_PATHS = $(SRCROOT)/deps/linux/arch/ish/include $(SRCROOT)/deps/linux/include $(MESON_BUILD_DIR)/deps/linux/include $(SRCROOT) OTHER_CFLAGS = -include user.h -include linux/kconfig.h ================================================ FILE: app/Terminal.h ================================================ // // Terminal.h // iSH // // Created by Theodore Dubois on 10/18/17. // #import #import struct tty; @interface Terminal : NSObject + (Terminal *)terminalWithType:(int)type number:(int)number; #if !ISH_LINUX // Returns a strong struct tty and a Terminal that has a weak reference to the same tty + (Terminal *)createPseudoTerminal:(struct tty **)tty; #endif + (Terminal *)terminalWithUUID:(NSUUID *)uuid; @property (readonly) NSUUID *uuid; + (void)convertCommand:(NSArray *)command toArgs:(char *)argv limitSize:(size_t)maxSize; - (int)sendOutput:(const void *)buf length:(int)len; - (void)sendInput:(NSData *)input; - (NSString *)arrow:(char)direction; // Make this terminal no longer be the singleton terminal with its type and number. Will happen eventually if all references go away, but sometimes you want it to happen now. - (void)destroy; @property (readonly) WKWebView *webView; @property (nonatomic) BOOL enableVoiceOverAnnounce; // Use KVO on this @property (readonly) BOOL loaded; @end extern struct tty_driver ios_console_driver; ================================================ FILE: app/Terminal.m ================================================ // // Terminal.m // iSH // // Created by Theodore Dubois on 10/18/17. // #import "Terminal.h" #import "DelayedUITask.h" #import "UserPreferences.h" #include "LinuxInterop.h" #include "fs/devices.h" #include "fs/tty.h" #include "fs/devices.h" extern struct tty_driver ios_pty_driver; #if !ISH_LINUX typedef struct tty *tty_t; #else typedef struct linux_tty *tty_t; #endif @interface Terminal () { #if !ISH_LINUX lock_t _dataLock; cond_t _dataConsumed; #endif } @property BOOL loaded; @property (nonatomic) tty_t tty; // lock with dataLock for !linux and @synchronized(self) for linux @property (nonatomic) NSMutableData *pendingData; // sending output is an asynchronous thing due to javascript, this is used to ensure it doesn't happen twice at once @property (nonatomic) BOOL outputInProgress; @property DelayedUITask *refreshTask; @property DelayedUITask *scrollToBottomTask; @property BOOL applicationCursor; @property NSNumber *terminalsKey; @property NSUUID *uuid; @end @interface CustomWebView : WKWebView @end @implementation CustomWebView - (BOOL)becomeFirstResponder { if (@available(iOS 13.4, *)) { return [super becomeFirstResponder]; } return NO; } - (BOOL)canPerformAction:(SEL)action withSender:(id)sender { if (action == @selector(copy:) || action == @selector(paste:)) { return NO; } return [super canPerformAction:action withSender:sender]; } @end @implementation Terminal @synthesize webView = _webView; static const int BUF_SIZE = 1<<14; static NSMapTable *terminals; static NSMapTable *terminalsByUUID; - (instancetype)initWithType:(int)type number:(int)num { @synchronized (Terminal.class) { self.terminalsKey = @(dev_make(type, num)); Terminal *terminal = [terminals objectForKey:self.terminalsKey]; if (terminal) return terminal; if (self = [super init]) { self.pendingData = [[NSMutableData alloc] initWithCapacity:BUF_SIZE]; self.refreshTask = [[DelayedUITask alloc] initWithTarget:self action:@selector(refresh)]; self.scrollToBottomTask = [[DelayedUITask alloc] initWithTarget:self action:@selector(scrollToBottom)]; #if !ISH_LINUX lock_init(&_dataLock); cond_init(&_dataConsumed); #endif [terminals setObject:self forKey:self.terminalsKey]; self.uuid = [NSUUID UUID]; [terminalsByUUID setObject:self forKey:self.uuid]; } return self; } } - (WKWebView *)webView { if (_webView == nil) { WKWebViewConfiguration *config = [WKWebViewConfiguration new]; [config.userContentController addScriptMessageHandler:self name:@"load"]; [config.userContentController addScriptMessageHandler:self name:@"log"]; [config.userContentController addScriptMessageHandler:self name:@"sendInput"]; [config.userContentController addScriptMessageHandler:self name:@"resize"]; [config.userContentController addScriptMessageHandler:self name:@"propUpdate"]; // Make the web view really big so that if a program tries to write to the terminal before it's displayed, the text probably won't wrap too badly. CGRect webviewSize = CGRectMake(0, 0, 10000, 10000); _webView = [[CustomWebView alloc] initWithFrame:webviewSize configuration:config]; if (@available(macOS 13.3, iOS 16.4, tvOS 16.4, *)) _webView.inspectable = YES; _webView.scrollView.scrollEnabled = NO; NSURL *xtermHtmlFile = [NSBundle.mainBundle URLForResource:@"term" withExtension:@"html"]; [_webView loadFileURL:xtermHtmlFile allowingReadAccessToURL:xtermHtmlFile]; } return _webView; } #if !ISH_LINUX + (Terminal *)createPseudoTerminal:(struct tty **)tty { *tty = pty_open_fake(&ios_pty_driver); if (IS_ERR(*tty)) return nil; return (__bridge Terminal *) (*tty)->data; } #endif - (void)setTty:(tty_t)tty { @synchronized (self) { _tty = tty; } dispatch_async(dispatch_get_main_queue(), ^{ [self syncWindowSize]; }); } - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message { if ([message.name isEqualToString:@"load"]) { self.loaded = YES; [self.refreshTask schedule]; // make sure this setting works if it's set before loading self.enableVoiceOverAnnounce = self.enableVoiceOverAnnounce; } else if ([message.name isEqualToString:@"log"]) { NSLog(@"%@", message.body); } else if ([message.name isEqualToString:@"sendInput"]) { NSData *data = [message.body dataUsingEncoding:NSUTF8StringEncoding]; [self sendInput:data]; } else if ([message.name isEqualToString:@"resize"]) { [self syncWindowSize]; } else if ([message.name isEqualToString:@"propUpdate"]) { [self setValue:message.body[1] forKey:message.body[0]]; } } - (void)syncWindowSize { [self.webView evaluateJavaScript:@"exports.getSize()" completionHandler:^(NSArray *dimensions, NSError *error) { int cols = dimensions[0].intValue; int rows = dimensions[1].intValue; if (self.tty == NULL) return; #if !ISH_LINUX lock(&self.tty->lock); tty_set_winsize(self.tty, (struct winsize_) {.col = cols, .row = rows}); unlock(&self.tty->lock); #else async_do_in_workqueue(^{ self->_tty->ops->resize(self->_tty, cols, rows); }); #endif }]; } - (void)setEnableVoiceOverAnnounce:(BOOL)enableVoiceOverAnnounce { _enableVoiceOverAnnounce = enableVoiceOverAnnounce; [self.webView evaluateJavaScript:[NSString stringWithFormat:@"term.setAccessibilityEnabled(%@)", enableVoiceOverAnnounce ? @"true" : @"false"] completionHandler:nil]; } - (int)sendOutput:(const void *)buf length:(int)len { #if !ISH_LINUX lock(&_dataLock); if (!NSThread.isMainThread) { // The main thread is the only one that can unblock this, so sleeping here would be a deadlock. // The only reason for this to be called on the main thread is if input is echoed. while (_pendingData.length > BUF_SIZE) wait_for_ignore_signals(&_dataConsumed, &_dataLock, NULL); } [_pendingData appendData:[NSData dataWithBytes:buf length:len]]; [self.refreshTask schedule]; unlock(&_dataLock); #else @synchronized (self) { int room = [self roomForOutput]; if (len > room) len = room; if (len > 0) { [_pendingData appendData:[NSData dataWithBytes:buf length:len]]; [_refreshTask schedule]; } } #endif return len; } #if ISH_LINUX - (int)roomForOutput { @synchronized (self) { if (_pendingData.length > BUF_SIZE) return 0; return BUF_SIZE - (int) _pendingData.length; } } #endif - (void)sendInput:(NSData *)input { if (self.tty == NULL) return; #if !ISH_LINUX tty_input(self.tty, input.bytes, input.length, 0); #else async_do_in_workqueue(^{ NSData *inputRef = input; self.tty->ops->send_input(self.tty, inputRef.bytes, inputRef.length); }); #endif [self.webView evaluateJavaScript:@"exports.setUserGesture()" completionHandler:nil]; [self.scrollToBottomTask schedule]; } - (void)scrollToBottom { [self.webView evaluateJavaScript:@"exports.scrollToBottom()" completionHandler:nil]; } - (NSString *)arrow:(char)direction { return [NSString stringWithFormat:@"\x1b%c%c", self.applicationCursor ? 'O' : '[', direction]; } - (void)refresh { if (!self.loaded) return; #if !ISH_LINUX lock(&_dataLock); if (_outputInProgress) { [self.refreshTask schedule]; unlock(&_dataLock); return; } NSData *data = _pendingData; _pendingData = [[NSMutableData alloc] initWithCapacity:BUF_SIZE]; _outputInProgress = YES; notify(&self->_dataConsumed); unlock(&_dataLock); #else NSData *data; @synchronized (self) { if (_outputInProgress) { [self.refreshTask schedule]; return; } data = _pendingData; _pendingData = [[NSMutableData alloc] initWithCapacity:BUF_SIZE]; _outputInProgress = YES; if (self->_tty) async_do_in_irq(^{ self->_tty->ops->can_output(self->_tty); }); } #endif NSString *dataString = [[NSString alloc] initWithBytes:data.bytes length:data.length encoding:NSISOLatin1StringEncoding]; // escape for javascript. only have to worry about the first 256 codepoints, because of the latin-1 encoding. dataString = [dataString stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"]; dataString = [dataString stringByReplacingOccurrencesOfString:@"\r" withString:@"\\r"]; dataString = [dataString stringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"]; dataString = [dataString stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""]; NSString *jsToEvaluate = [NSString stringWithFormat:@"exports.write(\"%@\")", dataString]; [self.webView evaluateJavaScript:jsToEvaluate completionHandler:^(id result, NSError *error) { #if !ISH_LINUX lock(&self->_dataLock); self->_outputInProgress = NO; unlock(&self->_dataLock); #else @synchronized (self) { self->_outputInProgress = NO; } #endif if (error != nil) { NSLog(@"error sending bytes to the terminal: %@", error); return; } }]; } + (void)convertCommand:(NSArray *)command toArgs:(char *)argv limitSize:(size_t)maxSize { char *p = argv; for (NSString *cmd in command) { const char *c = cmd.UTF8String; // Save space for the final NUL byte in argv while (p < argv + maxSize - 1 && (*p++ = *c++)); // If we reach the end of the buffer, the last string still needs to be // NUL terminated *p = '\0'; } // Add the final NUL byte to argv *++p = '\0'; } + (Terminal *)terminalWithType:(int)type number:(int)number { return [[Terminal alloc] initWithType:type number:number]; } + (Terminal *)terminalWithUUID:(NSUUID *)uuid { @synchronized (Terminal.class) { return [terminalsByUUID objectForKey:uuid]; } } - (void)destroy { tty_t tty = self.tty; if (tty != NULL) { #if !ISH_LINUX if (tty != NULL) { lock(&tty->lock); tty_hangup(tty); unlock(&tty->lock); } #else tty->ops->hangup(tty); #endif } @synchronized (Terminal.class) { [terminals removeObjectForKey:self.terminalsKey]; } } + (void)initialize { if (self == Terminal.class) { terminals = [NSMapTable strongToWeakObjectsMapTable]; terminalsByUUID = [NSMapTable strongToWeakObjectsMapTable]; } } @end #if ISH_LINUX nsobj_t Terminal_terminalWithType_number(int type, int number) { return CFBridgingRetain([Terminal terminalWithType:type number:number]); } int Terminal_sendOutput_length(nsobj_t _self, const char *data, int size) { return [(__bridge Terminal *) _self sendOutput:data length:size]; } int Terminal_roomForOutput(nsobj_t _self) { return [(__bridge Terminal *) _self roomForOutput]; } void Terminal_setLinuxTTY(nsobj_t _self, struct linux_tty *tty) { return [(__bridge Terminal *) _self setTty:tty]; } #endif #if !ISH_LINUX static int ios_tty_init(struct tty *tty) { // This is called with ttys_lock but that results in deadlock since the main thread can also acquire ttys_lock. So release it. unlock(&ttys_lock); void (^init_block)(void) = ^{ Terminal *terminal = [Terminal terminalWithType:tty->type number:tty->num]; tty->data = (void *) CFBridgingRetain(terminal); terminal.tty = tty; }; if ([NSThread isMainThread]) init_block(); else dispatch_sync(dispatch_get_main_queue(), init_block); lock(&ttys_lock); return 0; } static int ios_tty_write(struct tty *tty, const void *buf, size_t len, bool blocking) { Terminal *terminal = (__bridge Terminal *) tty->data; return [terminal sendOutput:buf length:(int) len]; } static void ios_tty_cleanup(struct tty *tty) { Terminal *terminal = CFBridgingRelease(tty->data); tty->data = NULL; terminal.tty = NULL; } struct tty_driver_ops ios_tty_ops = { .init = ios_tty_init, .write = ios_tty_write, .cleanup = ios_tty_cleanup, }; DEFINE_TTY_DRIVER(ios_console_driver, &ios_tty_ops, TTY_CONSOLE_MAJOR, 64); struct tty_driver ios_pty_driver = {.ops = &ios_tty_ops}; #endif ================================================ FILE: app/TerminalView.h ================================================ // // TerminalView.h // iSH // // Created by Theodore Dubois on 11/3/17. // #import #import "Terminal.h" enum OverrideAppearance { OverrideAppearanceNone, OverrideAppearanceLight, OverrideAppearanceDark, }; @interface TerminalView : UIView @property IBInspectable (nonatomic) BOOL canBecomeFirstResponder; @property (nonatomic) CGFloat overrideFontSize; @property (readonly) CGFloat effectiveFontSize; @property (nonatomic) enum OverrideAppearance overrideAppearance; @property (nonatomic) UIKeyboardAppearance keyboardAppearance; @property (weak) IBOutlet UIInputView *inputAccessoryView; @property (weak) IBOutlet UIButton *controlKey; @property (nonatomic) Terminal *terminal; @end ================================================ FILE: app/TerminalView.m ================================================ // // TerminalView.m // iSH // // Created by Theodore Dubois on 11/3/17. // #import "ScrollbarView.h" #import "TerminalView.h" #import "UserPreferences.h" #import "UIApplication+OpenURL.h" #import "NSObject+SaneKVO.h" struct rowcol { int row; int col; }; @interface WeakScriptMessageHandler : NSObject @property (weak) id handler; @end @implementation WeakScriptMessageHandler - (instancetype)initWithHandler:(id )handler { if (self = [super init]) { self.handler = handler; } return self; } - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message { [self.handler userContentController:userContentController didReceiveScriptMessage:message]; } @end @interface TerminalView () @property (nonatomic) NSMutableArray *keyCommands; @property ScrollbarView *scrollbarView; @property (nonatomic) BOOL terminalFocused; @property (nullable) NSString *markedText; @property (nullable) NSString *selectedText; @property UITextRange *markedRange; @property UITextRange *selectedRange; @property struct rowcol floatingCursor; @property CGSize floatingCursorSensitivity; @property CGSize actualFloatingCursorSensitivity; @end @implementation TerminalView @synthesize inputDelegate; @synthesize tokenizer; @synthesize canBecomeFirstResponder; - (void)awakeFromNib { [super awakeFromNib]; self.inputAssistantItem.leadingBarButtonGroups = @[]; self.inputAssistantItem.trailingBarButtonGroups = @[]; ScrollbarView *scrollbarView = self.scrollbarView = [[ScrollbarView alloc] initWithFrame:self.bounds]; scrollbarView.delegate = self; scrollbarView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; scrollbarView.bounces = NO; [self addSubview:scrollbarView]; UserPreferences *prefs = UserPreferences.shared; [prefs observe:@[@"capsLockMapping", @"optionMapping", @"backtickMapEscape", @"overrideControlSpace"] options:0 owner:self usingBlock:^(typeof(self) self) { dispatch_async(dispatch_get_main_queue(), ^{ self->_keyCommands = nil; }); }]; [prefs observe:@[@"colorScheme", @"fontFamily", @"fontSize", @"theme", @"cursorStyle", @"blinkCursor"] options:0 owner:self usingBlock:^(typeof(self) self) { dispatch_async(dispatch_get_main_queue(), ^{ [self _updateStyle]; }); }]; self.markedRange = [UITextRange new]; self.selectedRange = [UITextRange new]; } - (void)dealloc { self.terminal = nil; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (object == _terminal) { if (_terminal.loaded) { [self installTerminalView]; [self _updateStyle]; } } } static NSString *const HANDLERS[] = {@"syncFocus", @"focus", @"newScrollHeight", @"newScrollTop", @"openLink"}; - (void)setTerminal:(Terminal *)terminal { if (_terminal) { [_terminal removeObserver:self forKeyPath:@"loaded"]; [self uninstallTerminalView]; } _terminal = terminal; [_terminal addObserver:self forKeyPath:@"loaded" options:NSKeyValueObservingOptionInitial context:nil]; if (_terminal.loaded) [self installTerminalView]; } - (void)installTerminalView { NSAssert(_terminal.loaded, @"should probably not be installing a non-loaded terminal"); UIView *superview = self.terminal.webView.superview; if (superview != nil) { NSAssert(superview == self.scrollbarView, @"installing terminal that is already installed elsewhere"); return; } WKWebView *webView = _terminal.webView; _terminal.enableVoiceOverAnnounce = YES; webView.scrollView.scrollEnabled = NO; webView.scrollView.delaysContentTouches = NO; webView.scrollView.canCancelContentTouches = NO; webView.scrollView.panGestureRecognizer.enabled = NO; id handler = [[WeakScriptMessageHandler alloc] initWithHandler:self]; for (int i = 0; i < sizeof(HANDLERS)/sizeof(HANDLERS[0]); i++) { [webView.configuration.userContentController addScriptMessageHandler:handler name:HANDLERS[i]]; } webView.frame = self.bounds; self.opaque = webView.opaque = NO; webView.backgroundColor = UIColor.clearColor; webView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; self.scrollbarView.contentView = webView; [self.scrollbarView addSubview:webView]; } - (void)uninstallTerminalView { // remove old terminal UIView *superview = _terminal.webView.superview; if (superview != self.scrollbarView) { NSAssert(superview == nil, @"uninstalling terminal that is installed elsewhere"); return; } [_terminal.webView removeFromSuperview]; self.scrollbarView.contentView = nil; for (int i = 0; i < sizeof(HANDLERS)/sizeof(HANDLERS[0]); i++) { [_terminal.webView.configuration.userContentController removeScriptMessageHandlerForName:HANDLERS[i]]; } _terminal.enableVoiceOverAnnounce = NO; } #pragma mark Styling - (void)_updateStyle { NSAssert(NSThread.isMainThread, @"This method needs to be called on the main thread"); if (!self.terminal.loaded) return; UserPreferences *prefs = [UserPreferences shared]; if (_overrideFontSize == prefs.fontSize.doubleValue) _overrideFontSize = 0; Palette *palette = prefs.palette; if (self.overrideAppearance != OverrideAppearanceNone) { palette = self.overrideAppearance == OverrideAppearanceLight ? prefs.theme.lightPalette : prefs.theme.darkPalette; } NSMutableDictionary *themeInfo = [@{ @"fontFamily": prefs.fontFamily, @"fontSize": @(self.effectiveFontSize), @"foregroundColor": palette.foregroundColor, @"backgroundColor": palette.backgroundColor, @"blinkCursor": @(prefs.blinkCursor), @"cursorShape": prefs.htermCursorShape, } mutableCopy]; if (prefs.palette.colorPaletteOverrides) { themeInfo[@"colorPaletteOverrides"] = palette.colorPaletteOverrides; } NSString *json = [[NSString alloc] initWithData:[NSJSONSerialization dataWithJSONObject:themeInfo options:0 error:nil] encoding:NSUTF8StringEncoding]; [self.terminal.webView evaluateJavaScript:[NSString stringWithFormat:@"exports.updateStyle(%@)", json] completionHandler:^(id result, NSError *error){ [self updateFloatingCursorSensitivity]; }]; } - (void)setOverrideFontSize:(CGFloat)overrideFontSize { _overrideFontSize = overrideFontSize; [self _updateStyle]; } - (void)setOverrideAppearance:(enum OverrideAppearance)overrideAppearance { _overrideAppearance = overrideAppearance; [self _updateStyle]; } - (CGFloat)effectiveFontSize { if (self.overrideFontSize != 0) return self.overrideFontSize; return UserPreferences.shared.fontSize.doubleValue; } #pragma mark Focus and scrolling - (void)setTerminalFocused:(BOOL)terminalFocused { _terminalFocused = terminalFocused; NSString *script = terminalFocused ? @"exports.setFocused(true)" : @"exports.setFocused(false)"; [self.terminal.webView evaluateJavaScript:script completionHandler:nil]; } - (BOOL)becomeFirstResponder { self.terminalFocused = YES; [self reloadInputViews]; return [super becomeFirstResponder]; } - (BOOL)resignFirstResponder { self.terminalFocused = NO; return [super resignFirstResponder]; } - (void)windowDidBecomeKey:(NSNotification *)notif { self.terminalFocused = YES; } - (void)windowDidResignKey:(NSNotification *)notif { self.terminalFocused = NO; } - (IBAction)loseFocus:(id)sender { [self resignFirstResponder]; } - (void)willMoveToWindow:(UIWindow *)newWindow { NSNotificationCenter *center = NSNotificationCenter.defaultCenter; if (self.window != nil) { [center removeObserver:self name:UIWindowDidBecomeKeyNotification object:self.window]; [center removeObserver:self name:UIWindowDidResignKeyNotification object:self.window]; } if (newWindow != nil) { [center addObserver:self selector:@selector(windowDidBecomeKey:) name:UIWindowDidBecomeKeyNotification object:newWindow]; [center addObserver:self selector:@selector(windowDidResignKey:) name:UIWindowDidResignKeyNotification object:newWindow]; } } - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message { if ([message.name isEqualToString:@"syncFocus"]) { self.terminalFocused = self.terminalFocused; } else if ([message.name isEqualToString:@"focus"]) { if (!self.isFirstResponder) { [self becomeFirstResponder]; } } else if ([message.name isEqualToString:@"newScrollHeight"]) { self.scrollbarView.contentSize = CGSizeMake(0, [message.body doubleValue]); } else if ([message.name isEqualToString:@"newScrollTop"]) { CGFloat newOffset = [message.body doubleValue]; if (self.scrollbarView.contentOffset.y == newOffset) return; [self.scrollbarView setContentOffset:CGPointMake(0, newOffset) animated:NO]; } else if ([message.name isEqualToString:@"openLink"]) { [UIApplication openURL:message.body]; } } - (void)scrollViewDidScroll:(UIScrollView *)scrollView { [self.terminal.webView evaluateJavaScript:[NSString stringWithFormat:@"exports.newScrollTop(%f)", scrollView.contentOffset.y] completionHandler:nil]; } - (void)setKeyboardAppearance:(UIKeyboardAppearance)keyboardAppearance { BOOL needsFirstResponderDance = self.isFirstResponder && _keyboardAppearance != keyboardAppearance; if (needsFirstResponderDance) { [self resignFirstResponder]; } _keyboardAppearance = keyboardAppearance; if (needsFirstResponderDance) { [self becomeFirstResponder]; } if (keyboardAppearance == UIKeyboardAppearanceLight) { self.scrollbarView.indicatorStyle = UIScrollViewIndicatorStyleBlack; } else { self.scrollbarView.indicatorStyle = UIScrollViewIndicatorStyleWhite; } } #pragma mark Keyboard Input // implementing these makes a keyboard pop up when this view is first responder - (void)insertText:(NSString *)text { self.markedText = nil; if (self.controlKey.highlighted) self.controlKey.selected = YES; if (self.controlKey.selected) { if (!self.controlKey.highlighted) self.controlKey.selected = NO; if (text.length == 1) return [self insertControlChar:[text characterAtIndex:0]]; } text = [text stringByReplacingOccurrencesOfString:@"\n" withString:@"\r"]; NSData *data = [text dataUsingEncoding:NSUTF8StringEncoding]; [self.terminal sendInput:data]; } - (void)insertControlChar:(char)ch { if (strchr(controlKeys, ch) != NULL) { if (ch == ' ') ch = '\0'; if (ch == '2') ch = '@'; if (ch == '6') ch = '^'; if (ch != '\0') ch = toupper(ch) ^ 0x40; [self.terminal sendInput:[NSData dataWithBytes:&ch length:1]]; } } - (void)deleteBackward { [self insertText:@"\x7f"]; } - (BOOL)hasText { return YES; // it's always ok to send a "delete" } #pragma mark IME Input and Selection - (void)setMarkedText:(nullable NSString *)markedText selectedRange:(NSRange)selectedRange { self.markedText = markedText; } - (void)unmarkText { [self insertText:self.markedText]; } - (UITextRange *)markedTextRange { if (self.markedText != nil) return self.markedRange; return nil; } // The only reason to have this selected range is to prevent the "speak selection" context action from failing to get the current selection and falling back on calling copy:. It doesn't even have to work, it seems... - (UITextRange *)selectedTextRange { return self.selectedRange; } - (NSString *)textInRange:(UITextRange *)range { if (range == self.markedRange) return self.markedText; if (range == self.selectedRange) return @""; return nil; } - (id)insertDictationResultPlaceholder { return @""; } - (void)removeDictationResultPlaceholder:(id)placeholder willInsertResult:(BOOL)willInsertResult { } #pragma mark Keyboard Actions - (void)paste:(id)sender { NSString *string = UIPasteboard.generalPasteboard.string; if (string) { [self insertText:string]; } } - (void)copy:(id)sender { [self.terminal.webView evaluateJavaScript:@"exports.copy()" completionHandler:nil]; } - (void)clearScrollback:(UIKeyCommand *)command { [self.terminal.webView evaluateJavaScript:@"exports.clearScrollback()" completionHandler:nil]; } #pragma mark Floating cursor - (void)updateFloatingCursorSensitivity { [self.terminal.webView evaluateJavaScript:@"exports.getCharacterSize()" completionHandler:^(NSArray *charSizeRaw, NSError *error) { if (error != nil) { NSLog(@"error getting character size: %@", error); return; } CGSize charSize = CGSizeMake([charSizeRaw[0] doubleValue], [charSizeRaw[1] doubleValue]); double sensitivity = 0.5; self.floatingCursorSensitivity = CGSizeMake(charSize.width / sensitivity, charSize.height / sensitivity); }]; } - (struct rowcol)rowcolFromPoint:(CGPoint)point { CGSize sensitivity = self.actualFloatingCursorSensitivity; return (struct rowcol) { .row = (int) (-point.y / sensitivity.height), .col = (int) (point.x / sensitivity.width), }; } - (void)beginFloatingCursorAtPoint:(CGPoint)point { self.actualFloatingCursorSensitivity = self.floatingCursorSensitivity; self.floatingCursor = [self rowcolFromPoint:point]; } - (void)updateFloatingCursorAtPoint:(CGPoint)point { struct rowcol newPos = [self rowcolFromPoint:point]; int rowDiff = newPos.row - self.floatingCursor.row; int colDiff = newPos.col - self.floatingCursor.col; NSMutableString *arrows = [NSMutableString string]; for (int i = 0; i < abs(rowDiff); i++) { [arrows appendString:[self.terminal arrow:rowDiff > 0 ? 'A': 'B']]; } for (int i = 0; i < abs(colDiff); i++) { [arrows appendString:[self.terminal arrow:colDiff > 0 ? 'C': 'D']]; } [self insertText:arrows]; self.floatingCursor = newPos; } - (void)endFloatingCursor { self.floatingCursor = (struct rowcol) {}; } #pragma mark Keyboard Traits - (UITextSmartDashesType)smartDashesType API_AVAILABLE(ios(11)) { return UITextSmartDashesTypeNo; } - (UITextSmartQuotesType)smartQuotesType API_AVAILABLE(ios(11)) { return UITextSmartQuotesTypeNo; } - (UITextSmartInsertDeleteType)smartInsertDeleteType API_AVAILABLE(ios(11)) { return UITextSmartInsertDeleteTypeNo; } - (UITextAutocapitalizationType)autocapitalizationType { return UITextAutocapitalizationTypeNone; } - (UITextAutocorrectionType)autocorrectionType { return UITextAutocorrectionTypeNo; } // Apparently required on iOS 15+: https://stackoverflow.com/a/72359764 - (UITextSpellCheckingType)spellCheckingType { return UITextSpellCheckingTypeNo; } #pragma mark Hardware Keyboard - (void)handleKeyCommand:(UIKeyCommand *)command { NSString *key = command.input; if (command.modifierFlags == 0) { if ([key isEqualToString:@"`"] && UserPreferences.shared.backtickMapEscape) key = UIKeyInputEscape; if ([key isEqualToString:UIKeyInputEscape]) key = @"\x1b"; else if ([key isEqualToString:UIKeyInputUpArrow]) key = [self.terminal arrow:'A']; else if ([key isEqualToString:UIKeyInputDownArrow]) key = [self.terminal arrow:'B']; else if ([key isEqualToString:UIKeyInputLeftArrow]) key = [self.terminal arrow:'D']; else if ([key isEqualToString:UIKeyInputRightArrow]) key = [self.terminal arrow:'C']; [self insertText:key]; } else if (command.modifierFlags & UIKeyModifierShift) { [self insertText:[key uppercaseString]]; } else if (command.modifierFlags & UIKeyModifierAlternate) { [self insertText:[@"\x1b" stringByAppendingString:key]]; } else if (command.modifierFlags & UIKeyModifierAlphaShift) { [self handleCapsLockWithCommand:command]; } else if (command.modifierFlags & UIKeyModifierControl || command.modifierFlags & UIKeyModifierAlphaShift) { if (key.length == 0) return; if ([key isEqualToString:@"2"]) key = @"@"; else if ([key isEqualToString:@"6"]) key = @"^"; else if ([key isEqualToString:@"-"]) key = @"_"; [self insertControlChar:[key characterAtIndex:0]]; } } static const char *alphabet = "abcdefghijklmnopqrstuvwxyz"; static const char *controlKeys = "abcdefghijklmnopqrstuvwxyz@^26-=[]\\ "; static const char *metaKeys = "abcdefghijklmnopqrstuvwxyz0123456789-=[]\\;',./"; - (NSArray *)keyCommands { if (_keyCommands != nil) return _keyCommands; _keyCommands = [NSMutableArray new]; [self addKeys:controlKeys withModifiers:UIKeyModifierControl]; for (NSString *specialKey in @[UIKeyInputEscape, UIKeyInputUpArrow, UIKeyInputDownArrow, UIKeyInputLeftArrow, UIKeyInputRightArrow, @"\t"]) { [self addKey:specialKey withModifiers:0]; } if (UserPreferences.shared.capsLockMapping != CapsLockMapNone) { if (@available(iOS 13, *)); else { [self addKeys:controlKeys withModifiers:UIKeyModifierAlphaShift]; [self addKeys:alphabet withModifiers:0]; [self addKeys:alphabet withModifiers:UIKeyModifierShift]; [self addKey:@"" withModifiers:UIKeyModifierAlphaShift]; // otherwise tap of caps lock can switch layouts } } if (UserPreferences.shared.optionMapping == OptionMapEsc) { [self addKeys:metaKeys withModifiers:UIKeyModifierAlternate]; } if (UserPreferences.shared.backtickMapEscape) { [self addKey:@"`" withModifiers:0]; } [_keyCommands addObject:[UIKeyCommand keyCommandWithInput:@"k" modifierFlags:UIKeyModifierCommand|UIKeyModifierShift action:@selector(clearScrollback:) discoverabilityTitle:@"Clear Scrollback"]]; return _keyCommands; } - (void)addKeys:(const char *)keys withModifiers:(UIKeyModifierFlags)modifiers { for (size_t i = 0; keys[i] != '\0'; i++) { [self addKey:[NSString stringWithFormat:@"%c", keys[i]] withModifiers:modifiers]; } } - (void)addKey:(NSString *)key withModifiers:(UIKeyModifierFlags)modifiers { UIKeyCommand *command = [UIKeyCommand keyCommandWithInput:key modifierFlags:modifiers action:@selector(handleKeyCommand:)]; if (@available(iOS 15, *)) { command.wantsPriorityOverSystemBehavior = YES; } [_keyCommands addObject:command]; } - (void)keyCommandTriggered:(UIKeyCommand *)sender { dispatch_async(dispatch_get_main_queue(), ^{ [self handleKeyCommand:sender]; }); } - (void)handleCapsLockWithCommand:(UIKeyCommand *)command { CapsLockMapping target = UserPreferences.shared.capsLockMapping; NSString *newInput = command.input ? command.input : @""; UIKeyModifierFlags flags = command.modifierFlags; flags ^= UIKeyModifierAlphaShift; if(target == CapsLockMapEscape) { newInput = UIKeyInputEscape; } else if(target == CapsLockMapControl) { if([newInput length] == 0) { return; } flags |= UIKeyModifierControl; } else { return; } UIKeyCommand *newCommand = [UIKeyCommand keyCommandWithInput:newInput modifierFlags:flags action:@selector(keyCommandTriggered:)]; [self handleKeyCommand:newCommand]; } - (void)pressesBegan:(NSSet *)presses withEvent:(UIPressesEvent *)event { if (@available(iOS 13.4, *)) { UIKey *key = presses.anyObject.key; if (UserPreferences.shared.overrideControlSpace && key.keyCode == UIKeyboardHIDUsageKeyboardSpacebar && key.modifierFlags & UIKeyModifierControl) { return [self insertControlChar:' ']; } } return [super pressesBegan:presses withEvent:event]; } #pragma mark UITextInput stubs #if 0 #define LogStub() NSLog(@"%s", __func__) #else #define LogStub() #endif - (NSWritingDirection)baseWritingDirectionForPosition:(nonnull UITextPosition *)position inDirection:(UITextStorageDirection)direction { LogStub(); return NSWritingDirectionLeftToRight; } - (void)setBaseWritingDirection:(NSWritingDirection)writingDirection forRange:(nonnull UITextRange *)range { LogStub(); } - (UITextPosition *)beginningOfDocument { LogStub(); return nil; } - (CGRect)caretRectForPosition:(nonnull UITextPosition *)position { LogStub(); return CGRectZero; } - (nullable UITextRange *)characterRangeAtPoint:(CGPoint)point { LogStub(); return nil; } - (nullable UITextRange *)characterRangeByExtendingPosition:(nonnull UITextPosition *)position inDirection:(UITextLayoutDirection)direction { LogStub(); return nil; } - (nullable UITextPosition *)closestPositionToPoint:(CGPoint)point { LogStub(); return nil; } - (nullable UITextPosition *)closestPositionToPoint:(CGPoint)point withinRange:(nonnull UITextRange *)range { LogStub(); return nil; } - (NSComparisonResult)comparePosition:(nonnull UITextPosition *)position toPosition:(nonnull UITextPosition *)other { LogStub(); return NSOrderedSame; } - (UITextPosition *)endOfDocument { LogStub(); return nil; } - (CGRect)firstRectForRange:(nonnull UITextRange *)range { LogStub(); return CGRectZero; } - (NSDictionary *)markedTextStyle { LogStub(); return nil; } - (void)setMarkedTextStyle:(NSDictionary *)markedTextStyle { LogStub(); } - (NSInteger)offsetFromPosition:(nonnull UITextPosition *)from toPosition:(nonnull UITextPosition *)toPosition { LogStub(); return 0; } - (nullable UITextPosition *)positionFromPosition:(nonnull UITextPosition *)position inDirection:(UITextLayoutDirection)direction offset:(NSInteger)offset { LogStub(); return nil; } - (nullable UITextPosition *)positionFromPosition:(nonnull UITextPosition *)position offset:(NSInteger)offset { LogStub(); return nil; } - (nullable UITextPosition *)positionWithinRange:(nonnull UITextRange *)range farthestInDirection:(UITextLayoutDirection)direction { LogStub(); return nil; } - (void)replaceRange:(nonnull UITextRange *)range withText:(nonnull NSString *)text { LogStub(); } - (void)setSelectedTextRange:(UITextRange *)selectedTextRange { LogStub(); } - (nonnull NSArray *)selectionRectsForRange:(nonnull UITextRange *)range { LogStub(); return @[]; } - (nullable UITextRange *)textRangeFromPosition:(nonnull UITextPosition *)fromPosition toPosition:(nonnull UITextPosition *)toPosition { LogStub(); return nil; } // conforming to UITextInput makes this view default to being an accessibility element, which blocks selecting anything in it - (BOOL)isAccessibilityElement { return NO; } @end ================================================ FILE: app/TerminalViewController.h ================================================ // // ViewController.h // iSH // // Created by Theodore Dubois on 10/17/17. // #import #import "Terminal.h" @interface TerminalViewController : UIViewController @property (nonatomic) Terminal *terminal; - (void)startNewSession; - (void)reconnectSessionFromTerminalUUID:(NSUUID *)uuid; @property (readonly) NSUUID *sessionTerminalUUID; // 0 means invalid @property UISceneSession *sceneSession API_AVAILABLE(ios(13.0)); @end extern struct tty_driver ios_tty_driver; ================================================ FILE: app/TerminalViewController.m ================================================ // // ViewController.m // iSH // // Created by Theodore Dubois on 10/17/17. // #import "TerminalViewController.h" #import "AppDelegate.h" #import "TerminalView.h" #import "BarButton.h" #import "ArrowBarButton.h" #import "UserPreferences.h" #import "AboutViewController.h" #import "CurrentRoot.h" #import "NSObject+SaneKVO.h" #import "LinuxInterop.h" #include "kernel/init.h" #include "kernel/task.h" #include "kernel/calls.h" #include "fs/devices.h" @interface TerminalViewController () @property UITapGestureRecognizer *tapRecognizer; @property (weak, nonatomic) IBOutlet TerminalView *termView; @property (weak, nonatomic) IBOutlet NSLayoutConstraint *bottomConstraint; @property (weak, nonatomic) IBOutlet UIButton *tabKey; @property (weak, nonatomic) IBOutlet UIButton *controlKey; @property (weak, nonatomic) IBOutlet UIButton *escapeKey; @property (strong, nonatomic) IBOutletCollection(id) NSArray *barButtons; @property (strong, nonatomic) IBOutletCollection(id) NSArray *barControls; @property (weak, nonatomic) IBOutlet UIInputView *barView; @property (weak, nonatomic) IBOutlet UIStackView *bar; @property (weak, nonatomic) IBOutlet NSLayoutConstraint *barTop; @property (weak, nonatomic) IBOutlet NSLayoutConstraint *barBottom; @property (weak, nonatomic) IBOutlet NSLayoutConstraint *barLeading; @property (weak, nonatomic) IBOutlet NSLayoutConstraint *barTrailing; @property (weak, nonatomic) IBOutlet NSLayoutConstraint *barButtonWidth; @property (weak, nonatomic) IBOutlet NSLayoutConstraint *barHeight; @property (weak, nonatomic) IBOutlet UIView *settingsBadge; @property (weak, nonatomic) IBOutlet UIButton *infoButton; @property (weak, nonatomic) IBOutlet UIButton *pasteButton; @property (weak, nonatomic) IBOutlet UIButton *hideKeyboardButton; @property int sessionPid; @property (nonatomic) Terminal *sessionTerminal; @property BOOL ignoreKeyboardMotion; @property (nonatomic) BOOL hasExternalKeyboard; @end @implementation TerminalViewController - (void)viewDidLoad { [super viewDidLoad]; #if !ISH_LINUX int bootError = [AppDelegate bootError]; if (bootError < 0) { NSString *message = [NSString stringWithFormat:@"could not boot"]; NSString *subtitle = [NSString stringWithFormat:@"error code %d", bootError]; if (bootError == _EINVAL) subtitle = [subtitle stringByAppendingString:@"\n(try reinstalling the app, see release notes for details)"]; [self showMessage:message subtitle:subtitle]; NSLog(@"boot failed with code %d", bootError); } #endif self.terminal = self.terminal; [self.termView becomeFirstResponder]; NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; [center addObserver:self selector:@selector(keyboardDidSomething:) name:UIKeyboardWillChangeFrameNotification object:nil]; [center addObserver:self selector:@selector(keyboardDidSomething:) name:UIKeyboardDidChangeFrameNotification object:nil]; [center addObserver:self selector:@selector(_updateBadge) name:FsUpdatedNotification object:nil]; [self _updateStyleFromPreferences:NO]; if (UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad) { [self.bar removeArrangedSubview:self.hideKeyboardButton]; [self.hideKeyboardButton removeFromSuperview]; } if (UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPhone) { self.barHeight.constant = 36; } else { self.barHeight.constant = 43; } // SF Symbols is cool if (@available(iOS 13, *)) { [self.infoButton setImage:[UIImage systemImageNamed:@"gear"] forState:UIControlStateNormal]; [self.pasteButton setImage:[UIImage systemImageNamed:@"doc.on.clipboard"] forState:UIControlStateNormal]; [self.hideKeyboardButton setImage:[UIImage systemImageNamed:@"keyboard.chevron.compact.down"] forState:UIControlStateNormal]; [self.tabKey setTitle:nil forState:UIControlStateNormal]; [self.tabKey setImage:[UIImage systemImageNamed:@"arrow.right.to.line.alt"] forState:UIControlStateNormal]; [self.controlKey setTitle:nil forState:UIControlStateNormal]; [self.controlKey setImage:[UIImage systemImageNamed:@"control"] forState:UIControlStateNormal]; [self.escapeKey setTitle:nil forState:UIControlStateNormal]; [self.escapeKey setImage:[UIImage systemImageNamed:@"escape"] forState:UIControlStateNormal]; } [UserPreferences.shared observe:@[@"hideStatusBar"] options:0 owner:self usingBlock:^(typeof(self) self) { dispatch_async(dispatch_get_main_queue(), ^{ [self setNeedsStatusBarAppearanceUpdate]; }); }]; [UserPreferences.shared observe:@[@"colorScheme", @"theme", @"hideExtraKeysWithExternalKeyboard"] options:0 owner:self usingBlock:^(typeof(self) self) { dispatch_async(dispatch_get_main_queue(), ^{ [self _updateStyleFromPreferences:YES]; }); }]; [self _updateBadge]; } - (void)awakeFromNib { [super awakeFromNib]; #if !ISH_LINUX [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(processExited:) name:ProcessExitedNotification object:nil]; #else [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(kernelPanicked:) name:KernelPanicNotification object:nil]; #endif } - (void)viewDidAppear:(BOOL)animated { [AppDelegate maybePresentStartupMessageOnViewController:self]; [super viewDidAppear:animated]; } - (void)startNewSession { int err = [self startSession]; if (err < 0) { [self showMessage:@"could not start session" subtitle:[NSString stringWithFormat:@"error code %d", err]]; } } - (void)reconnectSessionFromTerminalUUID:(NSUUID *)uuid { self.sessionTerminal = [Terminal terminalWithUUID:uuid]; if (self.sessionTerminal == nil) [self startNewSession]; } - (NSUUID *)sessionTerminalUUID { return self.terminal.uuid; } - (int)startSession { NSArray *command = UserPreferences.shared.launchCommand; #if !ISH_LINUX int err = become_new_init_child(); if (err < 0) return err; struct tty *tty; self.sessionTerminal = nil; Terminal *terminal = [Terminal createPseudoTerminal:&tty]; if (terminal == nil) { NSAssert(IS_ERR(tty), @"tty should be error"); return (int) PTR_ERR(tty); } self.sessionTerminal = terminal; NSString *stdioFile = [NSString stringWithFormat:@"/dev/pts/%d", tty->num]; err = create_stdio(stdioFile.fileSystemRepresentation, TTY_PSEUDO_SLAVE_MAJOR, tty->num); if (err < 0) return err; tty_release(tty); char argv[4096]; [Terminal convertCommand:command toArgs:argv limitSize:sizeof(argv)]; const char *envp = "TERM=xterm-256color\0"; err = do_execve(command[0].UTF8String, command.count, argv, envp); if (err < 0) return err; self.sessionPid = current->pid; task_start(current); #else const char *argv_arr[command.count + 1]; for (NSUInteger i = 0; i < command.count; i++) argv_arr[i] = command[i].UTF8String; argv_arr[command.count] = NULL; const char *envp_arr[] = { "TERM=xterm-256color", NULL, }; const char *const *argv = argv_arr; const char *const *envp = envp_arr; __block Terminal *terminal = nil; __block int sessionPid = 0; __block int err = 1; sync_do_in_workqueue(^(void (^done)(void)) { linux_start_session(argv[0], argv, envp, ^(int retval, int pid, nsobj_t term) { err = retval; if (term) terminal = CFBridgingRelease(term); sessionPid = pid; done(); }); }); NSAssert(err <= 0, @"session start did not finish??"); if (err < 0) return err; self.sessionTerminal = terminal; self.sessionPid = sessionPid; #endif return 0; } #if !ISH_LINUX - (void)processExited:(NSNotification *)notif { int pid = [notif.userInfo[@"pid"] intValue]; if (pid != self.sessionPid) return; [self.sessionTerminal destroy]; // On iOS 13, there are multiple windows, so just close this one. if (@available(iOS 13, *)) { // On iPhone, destroying scenes will fail, but the error doesn't actually go to the error handler, which is really stupid. Apple doesn't fix bugs, so I'm forced to just add a check here. if (UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad && self.sceneSession != nil) { [UIApplication.sharedApplication requestSceneSessionDestruction:self.sceneSession options:nil errorHandler:^(NSError *error) { NSLog(@"scene destruction error %@", error); self.sceneSession = nil; [self processExited:notif]; }]; return; } } current = NULL; // it's been freed [self startNewSession]; } #endif #if ISH_LINUX - (void)kernelPanicked:(NSNotification *)notif { UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"panik" message:notif.userInfo[@"message"] preferredStyle:UIAlertControllerStyleAlert]; [alert addAction:[UIAlertAction actionWithTitle:@"k" style:UIAlertActionStyleDefault handler:nil]]; [self presentViewController:alert animated:YES completion:nil]; } #endif - (void)showMessage:(NSString *)message subtitle:(NSString *)subtitle { dispatch_async(dispatch_get_main_queue(), ^{ UIAlertController *alert = [UIAlertController alertControllerWithTitle:message message:subtitle preferredStyle:UIAlertControllerStyleAlert]; [alert addAction:[UIAlertAction actionWithTitle:@"k" style:UIAlertActionStyleDefault handler:nil]]; [self presentViewController:alert animated:YES completion:nil]; }); } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (object == [UserPreferences shared]) { [self _updateStyleFromPreferences:YES]; } else { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } } - (void)_updateStyleFromPreferences:(BOOL)animated { NSAssert(NSThread.isMainThread, @"This method needs to be called on the main thread"); NSTimeInterval duration = animated ? 0.1 : 0; [UIView animateWithDuration:duration animations:^{ self.view.backgroundColor = [[UIColor alloc] ish_initWithHexString:UserPreferences.shared.palette.backgroundColor]; UIKeyboardAppearance keyAppearance = UserPreferences.shared.keyboardAppearance; self.termView.keyboardAppearance = keyAppearance; for (BarButton *button in self.barButtons) { button.keyAppearance = keyAppearance; } UIColor *tintColor = keyAppearance == UIKeyboardAppearanceLight ? UIColor.blackColor : UIColor.whiteColor; for (UIControl *control in self.barControls) { control.tintColor = tintColor; } }]; UIView *oldBarView = self.termView.inputAccessoryView; if (UserPreferences.shared.hideExtraKeysWithExternalKeyboard && self.hasExternalKeyboard) { self.termView.inputAccessoryView = nil; } else { self.termView.inputAccessoryView = self.barView; } if (self.termView.inputAccessoryView != oldBarView && self.termView.isFirstResponder) { dispatch_async(dispatch_get_main_queue(), ^{ self.ignoreKeyboardMotion = YES; // avoid infinite recursion [self.termView reloadInputViews]; self.ignoreKeyboardMotion = NO; }); } } - (void)_updateStyleAnimated { [self _updateStyleFromPreferences:YES]; } - (void)_updateBadge { self.settingsBadge.hidden = !FsNeedsRepositoryUpdate(); } - (UIStatusBarStyle)preferredStatusBarStyle { return UserPreferences.shared.statusBarStyle; } - (BOOL)prefersStatusBarHidden { return UserPreferences.shared.hideStatusBar; } - (void)keyboardDidSomething:(NSNotification *)notification { if (self.ignoreKeyboardMotion) return; CGRect screenKeyboardFrame = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue]; UIScreen *screen = UIScreen.mainScreen; // notification.object is nil before iOS 16.1 and the correct UIScreen after iOS 16.1 if (notification.object != nil) screen = notification.object; CGRect keyboardFrame = [self.view convertRect:screenKeyboardFrame fromCoordinateSpace:screen.coordinateSpace]; if (CGRectEqualToRect(keyboardFrame, CGRectZero)) return; CGRect intersection = CGRectIntersection(keyboardFrame, self.view.bounds); keyboardFrame = intersection; NSLog(@"%@ %@", notification.name, @(keyboardFrame)); self.hasExternalKeyboard = keyboardFrame.size.height < 100; CGFloat pad = CGRectGetMaxY(self.view.bounds) - CGRectGetMinY(keyboardFrame); // The keyboard appears to be undocked. This means it can either be split or // truly floating. In the former case we want to keep the pad, but in the // latter we should fall back to the input accessory view instead of the // keyboard. if (pad != keyboardFrame.size.height && keyboardFrame.size.width != UIScreen.mainScreen.bounds.size.width) { pad = MAX(self.view.safeAreaInsets.bottom, self.termView.inputAccessoryView.frame.size.height); } // NSLog(@"pad %f", pad); self.bottomConstraint.constant = pad; BOOL initialLayout = self.termView.needsUpdateConstraints; [self.view setNeedsUpdateConstraints]; if (!initialLayout) { // if initial layout hasn't happened yet, the terminal view is going to be at a really weird place, so animating it is going to look really bad NSNumber *interval = notification.userInfo[UIKeyboardAnimationDurationUserInfoKey]; NSNumber *curve = notification.userInfo[UIKeyboardAnimationCurveUserInfoKey]; [UIView animateWithDuration:interval.doubleValue delay:0 options:curve.integerValue << 16 animations:^{ [self.view layoutIfNeeded]; } completion:nil]; } } - (void)setHasExternalKeyboard:(BOOL)hasExternalKeyboard { _hasExternalKeyboard = hasExternalKeyboard; [self _updateStyleFromPreferences:YES]; } - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { if ([segue.identifier isEqualToString:@"embed"]) { // You might want to check if this is your embed segue here // in case there are other segues triggered from this view controller. segue.destinationViewController.view.translatesAutoresizingMaskIntoConstraints = NO; } } - (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection { // Hack to resolve a layering mismatch between the UI and preferences. if (@available(iOS 12.0, *)) { if (previousTraitCollection.userInterfaceStyle != self.traitCollection.userInterfaceStyle) { // Ensure that the relevant things listening for this will update. UserPreferences.shared.colorScheme = UserPreferences.shared.colorScheme; } } } #pragma mark Bar - (IBAction)showAbout:(id)sender { UINavigationController *navigationController = [[UIStoryboard storyboardWithName:@"About" bundle:nil] instantiateInitialViewController]; if ([sender isKindOfClass:[UIGestureRecognizer class]]) { UIGestureRecognizer *recognizer = sender; if (recognizer.state == UIGestureRecognizerStateBegan) { AboutViewController *aboutViewController = (AboutViewController *) navigationController.topViewController; aboutViewController.includeDebugPanel = YES; } else { return; } } [self presentViewController:navigationController animated:YES completion:nil]; [self.termView resignFirstResponder]; } - (void)resizeBar { CGSize bar = self.barView.bounds.size; // set sizing parameters on bar // numbers stolen from iVim and modified somewhat if (UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPhone) { // phone [self setBarHorizontalPadding:6 verticalPadding:6 buttonWidth:32]; } else if (bar.width >= 450) { // wide ipad [self setBarHorizontalPadding:15 verticalPadding:8 buttonWidth:43]; } else { // narrow ipad (slide over) [self setBarHorizontalPadding:10 verticalPadding:8 buttonWidth:36]; } [UIView performWithoutAnimation:^{ [self.barView layoutIfNeeded]; }]; } - (void)setBarHorizontalPadding:(CGFloat)horizontal verticalPadding:(CGFloat)vertical buttonWidth:(CGFloat)buttonWidth { self.barLeading.constant = self.barTrailing.constant = horizontal; self.barTop.constant = self.barBottom.constant = vertical; self.barButtonWidth.constant = buttonWidth; } - (IBAction)pressEscape:(id)sender { [self pressKey:@"\x1b"]; } - (IBAction)pressTab:(id)sender { [self pressKey:@"\t"]; } - (void)pressKey:(NSString *)key { [self.termView insertText:key]; } - (IBAction)pressControl:(id)sender { self.controlKey.selected = !self.controlKey.selected; } - (IBAction)pressArrow:(ArrowBarButton *)sender { switch (sender.direction) { case ArrowUp: [self pressKey:[self.terminal arrow:'A']]; break; case ArrowDown: [self pressKey:[self.terminal arrow:'B']]; break; case ArrowLeft: [self pressKey:[self.terminal arrow:'D']]; break; case ArrowRight: [self pressKey:[self.terminal arrow:'C']]; break; case ArrowNone: break; } } - (void)switchTerminal:(UIKeyCommand *)sender { unsigned i = (unsigned) sender.input.integerValue; if (i == 7) self.terminal = self.sessionTerminal; else self.terminal = [Terminal terminalWithType:TTY_CONSOLE_MAJOR number:i]; } - (void)increaseFontSize:(UIKeyCommand *)command { self.termView.overrideFontSize = self.termView.effectiveFontSize + 1; } - (void)decreaseFontSize:(UIKeyCommand *)command { self.termView.overrideFontSize = self.termView.effectiveFontSize - 1; } - (void)resetFontSize:(UIKeyCommand *)command { self.termView.overrideFontSize = 0; } - (NSArray *)keyCommands { static NSMutableArray *commands = nil; if (commands == nil) { commands = [NSMutableArray new]; for (unsigned i = 1; i <= 7; i++) { [commands addObject: [UIKeyCommand keyCommandWithInput:[NSString stringWithFormat:@"%d", i] modifierFlags:UIKeyModifierCommand|UIKeyModifierAlternate|UIKeyModifierShift action:@selector(switchTerminal:)]]; } [commands addObject: [UIKeyCommand keyCommandWithInput:@"+" modifierFlags:UIKeyModifierCommand action:@selector(increaseFontSize:) discoverabilityTitle:@"Increase Font Size"]]; [commands addObject: [UIKeyCommand keyCommandWithInput:@"=" modifierFlags:UIKeyModifierCommand action:@selector(increaseFontSize:)]]; [commands addObject: [UIKeyCommand keyCommandWithInput:@"-" modifierFlags:UIKeyModifierCommand action:@selector(decreaseFontSize:) discoverabilityTitle:@"Decrease Font Size"]]; [commands addObject: [UIKeyCommand keyCommandWithInput:@"0" modifierFlags:UIKeyModifierCommand action:@selector(resetFontSize:) discoverabilityTitle:@"Reset Font Size"]]; [commands addObject: [UIKeyCommand keyCommandWithInput:@"," modifierFlags:UIKeyModifierCommand action:@selector(showAbout:) discoverabilityTitle:@"Settings"]]; } return commands; } - (void)setTerminal:(Terminal *)terminal { _terminal = terminal; self.termView.terminal = self.terminal; } - (void)setSessionTerminal:(Terminal *)sessionTerminal { if (_terminal == _sessionTerminal) self.terminal = sessionTerminal; _sessionTerminal = sessionTerminal; } @end @interface BarView : UIInputView @property (weak) IBOutlet TerminalViewController *terminalViewController; @property (nonatomic) IBInspectable BOOL allowsSelfSizing; @end @implementation BarView @dynamic allowsSelfSizing; - (void)layoutSubviews { [self.terminalViewController resizeBar]; } @end ================================================ FILE: app/Theme.h ================================================ // // Theme.h // iSH // // Created by Saagar Jha on 2/25/22. // #import NS_ASSUME_NONNULL_BEGIN @interface Palette : NSObject @property(readonly) NSString *foregroundColor; @property(readonly) NSString *backgroundColor; @property(readonly, nullable) NSString *cursorColor; @property(readonly, nullable) NSArray *colorPaletteOverrides; - (instancetype)initWithForegroundColor:(nonnull NSString *)foregroundColor backgroundColor:(nonnull NSString *)backgroundColor cursorColor:(nullable NSString *)cursorColor colorPaletteOverrides:(nullable NSArray *)colorPaletteOverrides; @end @interface ThemeAppearance : NSObject @property(readonly) BOOL lightOverride; @property(readonly) BOOL darkOverride; @property(class, readonly) ThemeAppearance *alwaysLight; @property(class, readonly) ThemeAppearance *alwaysDark; - (instancetype)initWithLightOverride:(BOOL)lightOverride darkOverride:(BOOL)darkOverride; @end @interface Theme : NSObject @property(class, readonly) NSArray *defaultThemes; @property(class, readonly) NSArray *userThemes; @property(readonly) NSString *name; @property(readonly) Palette *lightPalette; @property(readonly) Palette *darkPalette; @property(readonly, nullable) ThemeAppearance *appearance; + (nullable Theme *)themeForName:(NSString *)name includingDefaultThemes:(BOOL)includingDefaultThemes; - (instancetype)initWithName:(nonnull NSString *)name palette:(nonnull Palette *)palette appearance:(nullable ThemeAppearance *)appearance; - (instancetype)initWithName:(nonnull NSString *)name lightPalette:(nonnull Palette *)lightPalette darkPalette:(nonnull Palette *)darkPalette appearance:(nullable ThemeAppearance *)appearance; - (nullable instancetype)initWithName:(NSString *)name data:(NSData *)data; - (void)duplicateAsUserTheme; - (BOOL)addUserTheme; - (void)deleteUserTheme; - (void)replaceWithUserTheme:(Theme *)theme; @end @interface UIColor (iSH) - (nullable instancetype)ish_initWithHexString:(NSString *)string; @end extern NSString *const ThemesUpdatedNotification; extern NSString *const ThemeUpdatedNotification; NS_ASSUME_NONNULL_END ================================================ FILE: app/Theme.m ================================================ // // Theme.m // iSH // // Created by Saagar Jha on 2/25/22. // #import "Theme.h" #import "UserPreferences.h" #import "fs/proc/ish.h" char *get_documents_directory_impl(void) { return strdup(NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject.UTF8String); } #define THEME_VERSION 1 @implementation UIColor (iSH) - (nullable instancetype)ish_initWithHexString:(NSString *)string { if (![string hasPrefix:@"#"]) { return nil; } NSScanner *scanner = [NSScanner scannerWithString:string]; // Skip the leading # [scanner setScanLocation:1]; unsigned int value; if (![scanner scanHexInt:&value] || scanner.scanLocation != string.length) { return nil; } unsigned int red; unsigned int green; unsigned int blue; unsigned int alpha; if (string.length == 4) { // RGB blue = ((value & 0x00f) >> 0) * 0x11; green = ((value & 0x0f0) >> 4) * 0x11; red = ((value & 0xf00) >> 8) * 0x11; alpha = 0xff; } else if (string.length == 5) { // RGBA blue = ((value & 0x000f) >> 0) * 0x11; green = ((value & 0x00f0) >> 4) * 0x11; red = ((value & 0x0f00) >> 8) * 0x11; alpha = ((value & 0xf000) >> 12) * 0x11; } else if (string.length == 7) { // RRGGBB blue = (value & 0x0000ff) >> 0; green = (value & 0x00ff00) >> 8; red = (value & 0xff0000) >> 16; alpha = 0xff; } else if (string.length == 9) { // RRGGBBAA blue = (value & 0x000000ff) >> 0; green = (value & 0x0000ff00) >> 8; red = (value & 0x00ff0000) >> 16; alpha = (value & 0xff000000) >> 24; } else { return nil; } return [UIColor colorWithRed:1.0 * red / 0xff green:1.0 * green / 0xff blue:1.0 * blue / 0xff alpha:1.0 * alpha / 0xff]; } @end @interface DirectoryWatcher: NSObject @property(readonly, copy) NSURL *presentedItemURL; - (instancetype)initWithURL:(NSURL *)url handler:(void (^)(void))handler; @end @implementation DirectoryWatcher { void (^_handler)(void); } - (instancetype)initWithURL:(NSURL *)url handler:(void (^)(void))handler { if (self = [super init]) { self->_presentedItemURL = url; self->_handler = handler; } return self; } - (NSOperationQueue *)presentedItemOperationQueue { return NSOperationQueue.mainQueue; } - (void)presentedItemDidChange { self->_handler(); } @end @interface Palette () @property(readonly, nonnull) NSDictionary *serializedRepresentation; - (nullable instancetype)initWithSerializedRepresentation:(nonnull NSDictionary *)serializedRepresentation; @end @implementation Palette - (instancetype)initWithForegroundColor:(NSString *)foregroundColor backgroundColor:(NSString *)backgroundColor cursorColor:(NSString *)cursorColor colorPaletteOverrides:(NSArray *)colorPaletteOverrides { if (self = [super init]) { self->_foregroundColor = foregroundColor; self->_backgroundColor = backgroundColor; self->_cursorColor = cursorColor; self->_colorPaletteOverrides = colorPaletteOverrides; } return self; } - (instancetype)initWithSerializedRepresentation:(NSDictionary *)serializedRepresentation { #define VALID_COLOR(color) (color && [color isKindOfClass:NSString.class] && [[UIColor alloc] ish_initWithHexString:color]) id foregroundColor = serializedRepresentation[@"foregroundColor"]; id backgroundColor = serializedRepresentation[@"backgroundColor"]; id cursorColor = serializedRepresentation[@"cursorColor"]; id colorPaletteOverrides = serializedRepresentation[@"colorPaletteOverrides"]; BOOL validColorPalette = YES; if (colorPaletteOverrides) { if ([colorPaletteOverrides isKindOfClass:NSArray.class]) { for (id color in colorPaletteOverrides) { validColorPalette = validColorPalette || VALID_COLOR(color); } } else { validColorPalette = NO; } } if (VALID_COLOR(foregroundColor) && VALID_COLOR(backgroundColor) && (!cursorColor || VALID_COLOR(cursorColor)) && validColorPalette) { return [self initWithForegroundColor:foregroundColor backgroundColor:backgroundColor cursorColor:cursorColor colorPaletteOverrides:colorPaletteOverrides]; } else { return nil; } #undef VALID_COLOR } - (NSDictionary *)serializedRepresentation { NSMutableDictionary *representation = [@{ @"foregroundColor": self.foregroundColor, @"backgroundColor": self.backgroundColor, } mutableCopy]; if (self.cursorColor) { representation[@"cursorColor"] = self.cursorColor; } if (self.colorPaletteOverrides) { representation[@"colorPaletteOverrides"] = self.colorPaletteOverrides; } return representation; } @end @interface ThemeAppearance () @property(readonly, nonnull) NSDictionary *serializedRepresentation; - (nullable instancetype)initWithSerializedRepresentation:(nonnull NSDictionary *)serializedRepresentation; @end @implementation ThemeAppearance - (instancetype)initWithLightOverride:(BOOL)lightOverride darkOverride:(BOOL)darkOverride { if (self = [super init]) { self->_lightOverride = lightOverride; self->_darkOverride = darkOverride; } return self; } - (instancetype)initWithSerializedRepresentation:(NSDictionary *)serializedRepresentation { id lightOverride = serializedRepresentation[@"lightOverride"]; id darkOverride = serializedRepresentation[@"darkOverride"]; if ([lightOverride isKindOfClass:NSNumber.class] && [darkOverride isKindOfClass:NSNumber.class]) { return [self initWithLightOverride:[lightOverride boolValue] darkOverride:[darkOverride boolValue]]; } else { return nil; } } + (instancetype)alwaysLight { return [[self alloc] initWithLightOverride:NO darkOverride:YES]; } + (instancetype)alwaysDark { return [[self alloc] initWithLightOverride:YES darkOverride:NO]; } - (NSDictionary *)serializedRepresentation { return @{ @"lightOverride": @(self.lightOverride), @"darkOverride": @(self.darkOverride), }; } @end DirectoryWatcher *directoryWatcher; NSString *const ThemesUpdatedNotification = @"ThemesUpdatedNotification"; NSString *const ThemeUpdatedNotification = @"ThemeUpdatedNotification"; @interface Theme () @property(readonly, nonnull) NSData *data; @end // TODO: Move these to Linux #if ISH_LINUX char *(*get_documents_directory)(void); #endif @implementation Theme + (void)initialize { directoryWatcher = [[DirectoryWatcher alloc] initWithURL:self.themesDirectory handler:^{ [NSNotificationCenter.defaultCenter postNotificationName:ThemesUpdatedNotification object:nil]; }]; [NSFileCoordinator addFilePresenter:directoryWatcher]; get_documents_directory = get_documents_directory_impl; [NSFileManager.defaultManager createDirectoryAtURL:self.themesDirectory withIntermediateDirectories:YES attributes:nil error:nil]; } - (instancetype)initWithName:(NSString *)name palette:(Palette *)palette appearance:(ThemeAppearance *)appearance { Theme *theme = [self initWithName:name lightPalette:palette darkPalette:palette appearance:appearance]; return theme; } - (instancetype)initWithName:(NSString *)name lightPalette:(nonnull Palette *)lightPalette darkPalette:(nonnull Palette *)darkPalette appearance:(nullable ThemeAppearance *)appearance { if (self = [super init]) { self->_name = name; self->_lightPalette = lightPalette; self->_darkPalette = darkPalette; self->_appearance = appearance; } return self; } - (nullable instancetype)initWithName:(NSString *)name data:(NSData *)data { id json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; if (![json isKindOfClass:NSDictionary.class]) { return nil; } id version = json[@"version"]; if (![version isKindOfClass:NSNumber.class] || ((NSNumber *)version).integerValue <= 0 || ((NSNumber *)version).integerValue > THEME_VERSION) { NSLog(@"Rejecting theme %@ with invalid version number", name); return nil; } id _appearance = json[@"appearance"]; ThemeAppearance *appearance = [_appearance isKindOfClass:NSDictionary.class] ? [[ThemeAppearance alloc] initWithSerializedRepresentation:_appearance] : nil; id shared = json[@"shared"]; id light = json[@"light"]; id dark = json[@"dark"]; if ([shared isKindOfClass:NSDictionary.class]) { Palette *palette = [[Palette alloc] initWithSerializedRepresentation:shared]; return palette ? [self initWithName:name palette:palette appearance:appearance] : nil; } else if ([light isKindOfClass:NSDictionary.class] && [dark isKindOfClass:NSDictionary.class]) { Palette *lightPalette = [[Palette alloc] initWithSerializedRepresentation:light]; Palette *darkPalette = [[Palette alloc] initWithSerializedRepresentation:dark]; return lightPalette && darkPalette ? [self initWithName:name lightPalette:lightPalette darkPalette:darkPalette appearance:appearance] : nil; } else { NSLog(@"Rejecting theme %@ with invalid palette(s)", name); return nil; } } + (NSArray *)defaultThemes { static NSArray *defaultThemes; if (!defaultThemes) { defaultThemes = @[ [[self alloc] initWithName:@"Default" lightPalette:[[Palette alloc] initWithForegroundColor:@"#000" backgroundColor:@"#fff" cursorColor:nil colorPaletteOverrides:nil] darkPalette:[[Palette alloc] initWithForegroundColor:@"#fff" backgroundColor:@"#000" cursorColor:nil colorPaletteOverrides:nil] appearance:nil], [[self alloc] initWithName:@"1337" palette:[[Palette alloc] initWithForegroundColor:@"#0f0" backgroundColor:@"#000" cursorColor:nil colorPaletteOverrides:nil] appearance:ThemeAppearance.alwaysDark], [[self alloc] initWithName:@"Solarized" lightPalette:[[Palette alloc] initWithForegroundColor:@"#657b83" backgroundColor:@"#fdf6e3" cursorColor:nil colorPaletteOverrides:@[ @"#073642", @"#dc322f", @"#859900", @"#b58900", @"#268bd2", @"#d33682", @"#2aa198", @"#eee8d5", @"#002b36", @"#cb4b16", @"#586e75", @"#657b83", @"#839496", @"#6c71c4", @"#93a1a1", @"#fdf6e3", ]] darkPalette:[[Palette alloc] initWithForegroundColor:@"#839496" backgroundColor:@"#002b36" cursorColor:nil colorPaletteOverrides:@[ @"#073642", @"#dc322f", @"#859900", @"#b58900", @"#268bd2", @"#d33682", @"#2aa198", @"#eee8d5", @"#002b36", @"#cb4b16", @"#586e75", @"#657b83", @"#839496", @"#6c71c4", @"#93a1a1", @"#fdf6e3", ]] appearance:nil ], // Because this is a hidden theme, it needs to be last. There's // logic in UserPreferences and ThemesViewController which will not // work correctly otherwise. [[self alloc] initWithName:@"Hot Dog Stand" palette:[[Palette alloc] initWithForegroundColor:@"#ff0" backgroundColor:@"#f00" cursorColor:nil colorPaletteOverrides:nil] appearance:nil], ]; } return defaultThemes; } + (NSURL *)themesDirectory { return [[NSURL fileURLWithPath:NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject] URLByAppendingPathComponent:@"themes"]; } + (NSArray *)userThemes { NSMutableArray *themes = [NSMutableArray new]; for (NSURL *file in [NSFileManager.defaultManager contentsOfDirectoryAtURL:self.themesDirectory includingPropertiesForKeys:nil options:0 error:nil]) { NSData *data = [NSData dataWithContentsOfURL:file]; if (!data) { continue; } Theme *theme = [[Theme alloc] initWithName:file.lastPathComponent.stringByDeletingPathExtension data:data]; if (theme) { [themes addObject:theme]; } } [themes sortUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"name" ascending:YES selector:@selector(localizedStandardCompare:)]]]; return themes; } - (NSData *)data { NSMutableDictionary *representation = [@{ @"version": @(THEME_VERSION), } mutableCopy]; if (self.lightPalette == self.darkPalette) { representation[@"shared"] = self.lightPalette.serializedRepresentation; } else { representation[@"light"] = self.lightPalette.serializedRepresentation; representation[@"dark"] = self.darkPalette.serializedRepresentation; } if (self.appearance) { representation[@"appearance"] = self.appearance.serializedRepresentation; } return [NSJSONSerialization dataWithJSONObject:representation options:NSJSONWritingSortedKeys | NSJSONWritingPrettyPrinted error:nil]; } + (Theme *)themeForName:(NSString *)name includingDefaultThemes:(BOOL)includingDefaultThemes { // We should pick user themes over default ones, if they have the same name. NSMutableArray *themes = [Theme.userThemes mutableCopy]; if (includingDefaultThemes) { [themes addObjectsFromArray:Theme.defaultThemes]; } for (Theme *theme in themes) { if ([theme.name isEqualToString:name]) { return theme; } } return nil; } - (void)duplicateAsUserTheme { NSString *name; for (int suffix = 1; [self.class themeForName:name = [NSString stringWithFormat:@"%@-%d", self.name, suffix] includingDefaultThemes:NO]; ++suffix); [self.data writeToURL:[self.class.themesDirectory URLByAppendingPathComponent:[name stringByAppendingString:@".json"]] atomically:YES]; } - (BOOL)addUserTheme { if ([self.class themeForName:self.name includingDefaultThemes:NO]) { return NO; } else { [self.data writeToURL:[self.class.themesDirectory URLByAppendingPathComponent:[self.name stringByAppendingString:@".json"]] atomically:YES]; return YES; } } - (void)deleteUserTheme { [NSFileManager.defaultManager removeItemAtURL:[self.class.themesDirectory URLByAppendingPathComponent:[self.name stringByAppendingString:@".json"]] error:nil]; } - (void)replaceWithUserTheme:(Theme *)theme { [theme.data writeToURL:[self.class.themesDirectory URLByAppendingPathComponent:[theme.name stringByAppendingString:@".json"]] atomically:YES]; if (![self.name isEqualToString:theme.name]) { [self deleteUserTheme]; [NSNotificationCenter.defaultCenter postNotificationName:ThemeUpdatedNotification object:theme.name]; } } @end ================================================ FILE: app/ThemeViewController.h ================================================ // // ThemeViewController.h // libiSHApp // // Created by Saagar Jha on 7/16/22. // #import #import "Theme.h" NS_ASSUME_NONNULL_BEGIN @interface ThemeViewController : UITableViewController @property Theme *theme; @property BOOL isEditable; @end NS_ASSUME_NONNULL_END ================================================ FILE: app/ThemeViewController.m ================================================ // // ThemeViewController.m // libiSHApp // // Created by Saagar Jha on 7/16/22. // #import "ThemeViewController.h" #import "Theme.h" #define COLORS 16 static NSString *colorNames[] = { @"Black", @"Red", @"Green", @"Yellow", @"Blue", @"Magenta", @"Cyan", @"White", @"Bright Black", @"Bright Red", @"Bright Green", @"Bright Yellow", @"Bright Blue", @"Bright Magenta", @"Bright Cyan", @"Bright White", }; struct PaletteTextFields { UITextField *foregroundTextField; UITextField *backgroundTextField; UITextField *cursorTextField; NSArray *colorTextFields; }; @implementation ThemeViewController { UITextField *_nameTextField; UISwitch *_singlePaletteSwitch; UISwitch *_lightOverrideSwitch; UISwitch *_darkOverrideSwitch; BOOL _touchedOverrideSwitches; struct PaletteTextFields _paletteTextFields[2]; BOOL _duplicated; } - (UITextField *)detailTextFieldWithText:(NSString *)text monospaced:(BOOL)monospaced { UITextField *textField = [UITextField new]; textField.tag = 1; [textField addTarget:self action:@selector(textFieldChanged:) forControlEvents:UIControlEventEditingChanged]; textField.text = textField.placeholder = text; textField.translatesAutoresizingMaskIntoConstraints = NO; textField.textAlignment = NSTextAlignmentRight; if (@available(iOS 13.0, *)) { if (monospaced) { textField.font = [UIFont monospacedSystemFontOfSize:textField.font.pointSize weight:UIFontWeightRegular]; } } return textField; } - (void)viewDidLoad { [super viewDidLoad]; self.navigationItem.title = self.theme.name; _nameTextField = [self detailTextFieldWithText:_theme.name monospaced: NO]; _singlePaletteSwitch = [UISwitch new]; _singlePaletteSwitch.on = self.theme.lightPalette == self.theme.darkPalette; [_singlePaletteSwitch addTarget:self action:@selector(singlePaletteChanged:) forControlEvents:UIControlEventValueChanged]; for (int i = 0; i < sizeof(_paletteTextFields) / sizeof(*_paletteTextFields); ++i) { Palette *palette = i ? self.theme.darkPalette : self.theme.lightPalette; _paletteTextFields[i].foregroundTextField = [self detailTextFieldWithText:palette.foregroundColor monospaced:YES]; _paletteTextFields[i].backgroundTextField = [self detailTextFieldWithText:palette.backgroundColor monospaced:YES]; _paletteTextFields[i].cursorTextField = [self detailTextFieldWithText:palette.cursorColor monospaced:YES]; NSMutableArray *textFields = [NSMutableArray new]; for (int j = 0; j < COLORS; ++j) { UITextField *textField = [self detailTextFieldWithText:palette.colorPaletteOverrides ? palette.colorPaletteOverrides[j] : nil monospaced: YES]; textField.autocorrectionType = UITextAutocorrectionTypeNo; textField.autocapitalizationType = UITextAutocapitalizationTypeNone; [textFields addObject:textField]; } _paletteTextFields[i].colorTextFields = textFields; } if (!self.isEditable) { _singlePaletteSwitch.enabled = NO; } _lightOverrideSwitch = [UISwitch new]; _lightOverrideSwitch.on = self.theme.appearance.lightOverride; [_lightOverrideSwitch addTarget:self action:@selector(touchedOverrideSwitch:) forControlEvents:UIControlEventValueChanged]; _darkOverrideSwitch = [UISwitch new]; _darkOverrideSwitch.on = self.theme.appearance.darkOverride; [_darkOverrideSwitch addTarget:self action:@selector(touchedOverrideSwitch:) forControlEvents:UIControlEventValueChanged]; self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Duplicate" style:UIBarButtonItemStylePlain target:self action:@selector(duplicate:)]; } - (void)duplicate:(UIBarButtonItem *)sender { [self.theme duplicateAsUserTheme]; self->_duplicated = YES; [self.navigationController popViewControllerAnimated:YES]; } Palette *createPalette(struct PaletteTextFields *paletteTextFields) { NSMutableArray *colors = [NSMutableArray new]; for (UITextField *textField in paletteTextFields->colorTextFields) { if (textField.text.length) { [colors addObject:textField.text]; } } return [[Palette alloc] initWithForegroundColor:paletteTextFields->foregroundTextField.text backgroundColor:paletteTextFields->backgroundTextField.text cursorColor:paletteTextFields->cursorTextField.text.length ? paletteTextFields->cursorTextField.text : nil colorPaletteOverrides:colors.count == COLORS ? colors : nil]; } - (void)viewDidDisappear:(BOOL)animated { [super viewDidDisappear:animated]; if (self.isEditable && !self->_duplicated && [self validateTheme]) { Theme *theme; ThemeAppearance *appearance = self->_touchedOverrideSwitches ? [[ThemeAppearance alloc] initWithLightOverride:self->_lightOverrideSwitch.on darkOverride:self->_darkOverrideSwitch.on] : nil; if (_singlePaletteSwitch.on) { theme = [[Theme alloc] initWithName:_nameTextField.text palette:createPalette(_paletteTextFields + 0) appearance:appearance]; } else { theme = [[Theme alloc] initWithName:_nameTextField.text lightPalette:createPalette(_paletteTextFields + 0) darkPalette:createPalette(_paletteTextFields + 1) appearance:appearance]; } [self.theme replaceWithUserTheme:theme]; } } #pragma mark - Table view data source enum { NameSection, SinglePaletteSection, PaletteSection, PaletteSection2, UIOverrideSection, NumberOfSections, }; enum { ForegroundRow, BackgroundRow, CursorRow, NumberOfRows = CursorRow + COLORS + 1, }; - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return NumberOfSections; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { if ([self shouldHideSection:section]) { return 0; } switch (section) { case NameSection: return 1; case SinglePaletteSection: return 1; case PaletteSection: case PaletteSection2: return NumberOfRows; case UIOverrideSection: return 2; default: NSAssert(NO, @"unhandled section"); return 0; } } - (BOOL)shouldHideSection:(NSInteger)section { return section == PaletteSection2 && _singlePaletteSwitch.on; } - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { if ([self shouldHideSection:section]) { return nil; } switch (section) { case PaletteSection: return _singlePaletteSwitch.on ? @"Palette" : @"Light Palette"; case PaletteSection2: return @"Dark Palette"; case UIOverrideSection: return @"UI Overrides"; default: return nil; } } - (NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section { if ([self shouldHideSection:section]) { return nil; } switch (section) { case NameSection: return ![_nameTextField.text isEqualToString:self.theme.name] && [Theme themeForName:_nameTextField.text includingDefaultThemes:NO] ? @"A user theme with this name already exists." : nil; case SinglePaletteSection: return @"When this is enabled, light and dark color schemes will share a single palette."; case UIOverrideSection: return @"Use a customized color scheme for user interface elements (keyboard, status bar) rather than one that matches the current palette."; default: return nil; } } - (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section { return [self shouldHideSection:section] ? CGFLOAT_MIN : UITableViewAutomaticDimension; } - (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section { return [self shouldHideSection:section] ? CGFLOAT_MIN : UITableViewAutomaticDimension; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"ThemeSetting" forIndexPath:indexPath]; [[cell viewWithTag:1] removeFromSuperview]; if (self.isEditable) { cell.detailTextLabel.hidden = YES; } else { cell.detailTextLabel.hidden = NO; cell.detailTextLabel.enabled = NO; } cell.accessoryView = nil; switch (indexPath.section) { case NameSection: cell.textLabel.text = @"Name"; if (self.isEditable) { [cell.contentView addSubview:_nameTextField]; [NSLayoutConstraint activateConstraints:@[ [_nameTextField.leadingAnchor constraintEqualToSystemSpacingAfterAnchor:cell.textLabel.trailingAnchor multiplier:1], [_nameTextField.trailingAnchor constraintEqualToAnchor:cell.detailTextLabel.trailingAnchor], [_nameTextField.firstBaselineAnchor constraintEqualToAnchor:cell.detailTextLabel.firstBaselineAnchor], ]]; } else { cell.detailTextLabel.text = self.theme.name; if (@available(iOS 13.0, *)) { cell.detailTextLabel.font = [UIFont systemFontOfSize:cell.detailTextLabel.font.pointSize]; } } break; case SinglePaletteSection: cell.textLabel.text = @"Single Palette"; cell.detailTextLabel.hidden = YES; cell.accessoryView = _singlePaletteSwitch; break; case PaletteSection: case PaletteSection2: { UITextField *detailTextField; switch (indexPath.row) { case ForegroundRow: cell.textLabel.text = @"Foreground Color"; detailTextField = _paletteTextFields[indexPath.section - PaletteSection].foregroundTextField; break; case BackgroundRow: cell.textLabel.text = @"Background Color"; detailTextField = _paletteTextFields[indexPath.section - PaletteSection].backgroundTextField; break; case CursorRow: cell.textLabel.text = @"Cursor Color"; detailTextField = _paletteTextFields[indexPath.section - PaletteSection].cursorTextField; break; default: cell.textLabel.text = colorNames[indexPath.row - CursorRow - 1]; detailTextField = _paletteTextFields[indexPath.section - PaletteSection].colorTextFields[indexPath.row - CursorRow - 1]; break; } if (self.isEditable) { [cell.contentView addSubview:detailTextField]; [NSLayoutConstraint activateConstraints:@[ [detailTextField.leadingAnchor constraintEqualToSystemSpacingAfterAnchor:cell.textLabel.trailingAnchor multiplier:1], [detailTextField.trailingAnchor constraintEqualToAnchor:cell.detailTextLabel.trailingAnchor], [detailTextField.firstBaselineAnchor constraintEqualToAnchor:cell.detailTextLabel.firstBaselineAnchor], ]]; } else { cell.detailTextLabel.text = detailTextField.text; if (@available(iOS 13.0, *)) { cell.detailTextLabel.font = [UIFont monospacedSystemFontOfSize:cell.detailTextLabel.font.pointSize weight:UIFontWeightRegular]; } } break; } case UIOverrideSection: cell.detailTextLabel.hidden = YES; switch (indexPath.row) { case 0: cell.textLabel.text = @"Use Dark UI for Light Color Scheme"; cell.accessoryView = self->_lightOverrideSwitch; break; case 1: cell.textLabel.text = @"Use Light UI for Dark Color Scheme"; cell.accessoryView = self->_darkOverrideSwitch; break; default: NSAssert(NO, @"Invalid row"); } break; } if (!self.isEditable) { cell.textLabel.enabled = NO; } return cell; } - (BOOL)validateTheme { BOOL validName = _nameTextField.text.length && ([_nameTextField.text isEqualToString:self.theme.name] || ![Theme themeForName:_nameTextField.text includingDefaultThemes:NO]); _nameTextField.textColor = validName ? nil : UIColor.systemRedColor; [self.tableView reloadSections:[NSIndexSet indexSetWithIndex:NameSection] withRowAnimation:UITableViewRowAnimationNone]; BOOL validColors = YES; BOOL (^validColor)(UITextField *) = ^(UITextField *textField) { BOOL valid = !![[UIColor alloc] ish_initWithHexString:textField.text]; textField.textColor = valid ? nil : UIColor.systemRedColor; return valid; }; for (int i = 0; i < sizeof(_paletteTextFields) / sizeof(*_paletteTextFields) - _singlePaletteSwitch.on; ++i) { validColors &= validColor(_paletteTextFields[i].foregroundTextField); validColors &= validColor(_paletteTextFields[i].backgroundTextField); validColors &= !_paletteTextFields[i].cursorTextField.text.length || validColor(_paletteTextFields[i].cursorTextField); int empty = 0; int valid = 0; for (int j = 0; j < COLORS; ++j) { empty += !_paletteTextFields[i].colorTextFields[j].text.length; valid += validColor(_paletteTextFields[i].colorTextFields[j]); } validColors &= (empty == COLORS || valid == COLORS); } return !(self.navigationItem.hidesBackButton = !validName || !validColors); } - (void)textFieldChanged:(UITextField *)sender { // Hack to keep the keyboard up across a table view update UITextRange *selectedRange = sender.isFirstResponder ? sender.selectedTextRange : nil; [self validateTheme]; if (selectedRange) { [sender becomeFirstResponder]; [sender setSelectedTextRange:selectedRange]; } } - (void)singlePaletteChanged:(UISwitch *)sender { [self.tableView performBatchUpdates:^{ for (int i = 0; i < NumberOfRows; ++i) { if (sender.on) { [self.tableView deleteRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:i inSection:PaletteSection2]] withRowAnimation:UITableViewRowAnimationFade]; } else { [self.tableView insertRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:i inSection:PaletteSection2]] withRowAnimation:UITableViewRowAnimationFade]; } } [self.tableView reloadSections:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(PaletteSection, PaletteSection2 - PaletteSection)] withRowAnimation:UITableViewRowAnimationAutomatic]; } completion:nil]; [self validateTheme]; } - (void)touchedOverrideSwitch:(UISwitch *)sender { self->_touchedOverrideSwitches = YES; } @end ================================================ FILE: app/ThemesViewController.h ================================================ // // ThemesViewController.h // iSH // // Created by Saagar Jha on 2/25/22. // #import NS_ASSUME_NONNULL_BEGIN @interface ThemesViewController : UITableViewController @end NS_ASSUME_NONNULL_END ================================================ FILE: app/ThemesViewController.m ================================================ // // ThemesViewController.m // iSH // // Created by Saagar Jha on 2/25/22. // #import "ThemesViewController.h" #import "NSObject+SaneKVO.h" #import "Theme.h" #import "ThemeViewController.h" #import "UserPreferences.h" @implementation ThemesViewController { BOOL _singleRowEditing; BOOL _importButtonEditingMode; BOOL _pendingUpdate; Theme *_theme; NSMutableArray *_defaultThemes; NSMutableArray *_userThemes; BOOL _preferUserTheme; } - (void)viewDidLoad { [super viewDidLoad]; [UserPreferences.shared observe:@[@"theme"] options:0 owner:self usingBlock:^(typeof(self) self) { dispatch_async(dispatch_get_main_queue(), ^{ [self deferredReload]; }); }]; [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(updateThemes:) name:ThemesUpdatedNotification object:nil]; self.navigationItem.rightBarButtonItem = self.editButtonItem; [self deferredReload]; } - (void)updateThemes:(NSNotification *)notification { dispatch_async(dispatch_get_main_queue(), ^{ [self deferredReload]; }); } - (void)deferredReload { if (self.isEditing) { self->_pendingUpdate = YES; } else { self->_defaultThemes = [Theme.defaultThemes mutableCopy]; self->_userThemes = [Theme.userThemes mutableCopy]; [self updateTheme:UserPreferences.shared.theme]; [self.tableView reloadData]; self->_pendingUpdate = NO; } } - (void)updateTheme:(Theme *)theme { self->_theme = theme; self->_preferUserTheme = NO; for (Theme *theme in self->_defaultThemes) { if ([self->_theme.name isEqualToString:theme.name]) { for (Theme *theme in self->_userThemes) { if ([self->_theme.name isEqualToString:theme.name]) { self->_preferUserTheme = YES; } } break; } } } - (void)setEditing:(BOOL)editing animated:(BOOL)animated { self->_importButtonEditingMode = editing; [self.tableView reloadSections:[NSIndexSet indexSetWithIndex:ImportSection] withRowAnimation:UITableViewRowAnimationAutomatic]; if (!editing && self->_pendingUpdate) { [self deferredReload]; } [super setEditing:editing animated:animated]; } #pragma mark - Table view data source enum { DefaultSection, UserSection, ImportSection, NumberOfSections, }; - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return NumberOfSections; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { switch (section) { case DefaultSection: return self->_defaultThemes.count - ![self->_theme.name isEqualToString:@"Hot Dog Stand"]; case UserSection: return self->_userThemes.count; case ImportSection: return self->_importButtonEditingMode && !self->_singleRowEditing; default: NSAssert(NO, @"unhandled section"); return 0; } } - (BOOL)shouldHideSection:(NSInteger)section { return (section == UserSection && !self->_userThemes.count) || (section == ImportSection && (self->_singleRowEditing || !self->_importButtonEditingMode)); } - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { if ([self shouldHideSection:section]) { return nil; } switch (section) { case DefaultSection: return @"Default Themes"; case UserSection: return @"User Themes"; case ImportSection: return nil; default: NSAssert(NO, @"unhandled section"); return nil; } } - (NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section { if ([self shouldHideSection:section]) { return nil; } switch (section) { case DefaultSection: return self->_preferUserTheme ? [NSString stringWithFormat:@"The default theme \"%@\" is currently being overridden by a user theme.", self->_theme.name] : nil; case ImportSection: return @"User themes are stored in the iSH documents directory, under the \"themes\" folder. You can access them within iSH by running\n\n# mount -t real \"$(cat /proc/ish/documents)/themes\" [folder]\n\nand manipulating them from there."; default: return nil; } } - (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section { return [self shouldHideSection:section] ? CGFLOAT_MIN : UITableViewAutomaticDimension; } - (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section { return [self shouldHideSection:section] ? CGFLOAT_MIN : UITableViewAutomaticDimension; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Theme" forIndexPath:indexPath]; cell.textLabel.textColor = indexPath.section == ImportSection ? cell.tintColor : nil; cell.textLabel.enabled = YES; Theme *theme; switch (indexPath.section) { case DefaultSection: theme = self->_defaultThemes[indexPath.row]; break; case UserSection: theme = self->_userThemes[indexPath.row]; break; case ImportSection: cell.textLabel.text = @"Import Theme"; cell.editingAccessoryType = UITableViewCellAccessoryNone; return cell; } cell.textLabel.text = theme.name; cell.accessoryType = [theme.name isEqualToString:self->_theme.name] && (!self->_preferUserTheme || indexPath.section == UserSection) ? UITableViewCellAccessoryCheckmark : UITableViewCellAccessoryNone; cell.textLabel.enabled = ![theme.name isEqualToString:self->_theme.name] || indexPath.section != DefaultSection || !self->_preferUserTheme; cell.editingAccessoryType = UITableViewCellAccessoryDisclosureIndicator; return cell; } - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { if (tableView.isEditing) { ThemeViewController *themeViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"Theme"]; switch (indexPath.section) { case DefaultSection: themeViewController.theme = self->_defaultThemes[indexPath.row]; themeViewController.isEditable = NO; break; case UserSection: themeViewController.theme = self->_userThemes[indexPath.row]; themeViewController.isEditable = YES; break; case ImportSection: [self importTheme]; return; } [self.navigationController pushViewController:themeViewController animated:YES]; [self setEditing:NO animated:YES]; } else { Theme *theme; switch (indexPath.section) { case DefaultSection: theme = self->_defaultThemes[indexPath.row]; break; case UserSection: theme = self->_userThemes[indexPath.row]; break; } [self updateTheme:theme]; [self.tableView performBatchUpdates:^{ [self.tableView reloadSections:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(DefaultSection, UserSection - DefaultSection)] withRowAnimation:UITableViewRowAnimationAutomatic]; } completion:nil]; UserPreferences.shared.theme = theme; } } - (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath { switch (indexPath.section) { case UserSection: return UITableViewCellEditingStyleDelete; case ImportSection: return UITableViewCellEditingStyleInsert; default: return UITableViewCellEditingStyleNone; } } - (void)deleteUserThemeAtIndexPath:(NSIndexPath *)indexPath { [self->_userThemes[indexPath.row] deleteUserTheme]; [self->_userThemes removeObjectAtIndex:indexPath.row]; [self.tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; } - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { switch (editingStyle) { case UITableViewCellEditingStyleInsert: [self importTheme]; break; case UITableViewCellEditingStyleDelete: [self deleteUserThemeAtIndexPath:indexPath]; break; default: NSAssert(NO, @"Invalid editing style"); } } - (UISwipeActionsConfiguration *)tableView:(UITableView *)tableView trailingSwipeActionsConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath { if (self.isEditing) { return nil; } else { NSMutableArray *actions = [NSMutableArray arrayWithObject:[UIContextualAction contextualActionWithStyle:UIContextualActionStyleNormal title:@"Duplicate" handler:^(UIContextualAction *action, UIView *sourceView, void (^completionHandler)(BOOL)) { [(indexPath.section == DefaultSection ? self->_defaultThemes : self->_userThemes)[indexPath.row] duplicateAsUserTheme]; [tableView performBatchUpdates:^{ [tableView reloadSections:[NSIndexSet indexSetWithIndex:UserSection] withRowAnimation:UITableViewRowAnimationAutomatic]; } completion:nil]; completionHandler(YES); }]]; if (indexPath.section == UserSection) { [actions addObject:[UIContextualAction contextualActionWithStyle:UIContextualActionStyleDestructive title:@"Delete" handler:^(UIContextualAction *action, UIView *sourceView, void (^completionHandler)(BOOL)) { [self deleteUserThemeAtIndexPath:indexPath]; completionHandler(YES); }]]; } return [UISwipeActionsConfiguration configurationWithActions:actions]; } } - (void)tableView:(UITableView *)tableView willBeginEditingRowAtIndexPath:(NSIndexPath *)indexPath { self->_singleRowEditing = YES; [super tableView:tableView willBeginEditingRowAtIndexPath:indexPath]; } - (void)tableView:(UITableView *)tableView didEndEditingRowAtIndexPath:(NSIndexPath *)indexPath { [super tableView:tableView didEndEditingRowAtIndexPath:indexPath]; self->_singleRowEditing = NO; } - (void)importTheme { UIDocumentPickerViewController *picker = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:@[ @"public.json" ] inMode:UIDocumentPickerModeOpen]; picker.delegate = self; if (@available(iOS 13, *)) { } else { picker.allowsMultipleSelection = YES; } [self presentViewController:picker animated:true completion:nil]; } - (void)documentPickerWasCancelled:(UIDocumentPickerViewController *)controller { [self.tableView deselectRowAtIndexPath:self.tableView.indexPathForSelectedRow animated:YES]; } - (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentsAtURLs:(NSArray *)urls { for (NSURL *url in urls) { [url startAccessingSecurityScopedResource]; [[[Theme alloc] initWithName:url.lastPathComponent.stringByDeletingPathExtension data:[NSData dataWithContentsOfURL:url]] addUserTheme]; [url stopAccessingSecurityScopedResource]; } [self documentPickerWasCancelled:controller]; [self setEditing:NO animated:YES]; } @end ================================================ FILE: app/UIApplication+OpenURL.h ================================================ // // UIApplication+OpenURL.h // iSH // // Created by Theodore Dubois on 9/23/18. // #import NS_ASSUME_NONNULL_BEGIN @interface UIApplication (OpenURL) + (void)openURL:(NSString *)url; @end NS_ASSUME_NONNULL_END ================================================ FILE: app/UIApplication+OpenURL.m ================================================ // // UIApplication+OpenURL.m // iSH // // Created by Theodore Dubois on 9/23/18. // #import "UIApplication+OpenURL.h" @implementation UIApplication (OpenURL) + (void)openURL:(NSString *)url { [[self sharedApplication] openURL:[NSURL URLWithString:url] options:@{} completionHandler:nil]; } @end ================================================ FILE: app/UITests/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion $(CURRENT_PROJECT_VERSION) ================================================ FILE: app/UITests/Screenshots.m ================================================ // // Screenshots.m // iSHUITests // // Created by Theodore Dubois on 12/18/20. // #import #import #import "iSHUITests-Swift.h" @interface Screenshots : XCTestCase @property XCUIApplication *app; @end @implementation Screenshots - (void)setUp { self.continueAfterFailure = NO; XCUIApplication *app = self.app = [XCUIApplication new]; [Snapshot setupSnapshot:app waitForAnimations:NO]; NSString *hostnameOverride = nil; switch (UIDevice.currentDevice.userInterfaceIdiom) { case UIUserInterfaceIdiomPad: hostnameOverride = @"iPad"; break; case UIUserInterfaceIdiomPhone: hostnameOverride = @"iPhone"; break; default: XCTFail(@"unknown UI idiom"); } app.launchArguments = [app.launchArguments arrayByAddingObjectsFromArray:@[@"-hostnameOverride", hostnameOverride]]; [app launch]; XCTAssert([app.webViews.staticTexts.firstMatch waitForExistenceWithTimeout:10]); [self chooseTheme:@"Solarized"]; } - (XCUIElementQuery *)terminalLines { return self.app.webViews.staticTexts; } - (XCUIElementQuery *)terminalLinesContaining:(NSString *)text { return [self.terminalLines matchingPredicate:[NSPredicate predicateWithFormat:@"label CONTAINS %@", text]]; } - (void)waitForTerminalText:(NSString *)text timeout:(NSUInteger)timeout { XCTAssert([[self terminalLinesContaining:text].firstMatch waitForExistenceWithTimeout:timeout]); } - (void)waitForPromptWithTimeout:(NSUInteger)timeout { // Waits for the last text thing to be a prompt XCUIElementQuery *terminalLines = self.terminalLines; NSPredicate *pred = [NSPredicate predicateWithBlock:^BOOL(id _, NSDictionary *__) { XCUIElement *lastLine = nil; // Loop this because -count and -elementBoundByIndex will take two separate UI snapshots, so the index could become invalid. while (!lastLine.exists) lastLine = terminalLines.allElementsBoundByIndex.lastObject; NSString *lastLineText = lastLine.label; XCTAssertNotNil(lastLineText); return [lastLineText hasSuffix:@":~#"]; }]; [self waitForExpectations:@[[self expectationForPredicate:pred evaluatedWithObject:nil handler:nil]] timeout:timeout]; } - (void)runCommand:(NSString *)command timeout:(NSUInteger)timeout { [self.app typeText:[NSString stringWithFormat:@"%@\n", command]]; [self waitForPromptWithTimeout:timeout]; } - (void)chooseTheme:(NSString *)name { [self.app.buttons[@"Settings"] tap]; [self.app.tables.staticTexts[@"Appearance"] tap]; [self.app.tables.staticTexts[@"Theme"] tap]; [self.app.tables.staticTexts[name] tap]; [self.app.navigationBars[@"Themes"].buttons[@"Appearance"] tap]; [self.app.navigationBars[@"Appearance"].buttons[@"Settings"] tap]; [self.app.navigationBars[@"Settings"].buttons[@"Done"] tap]; } - (void)snapshot:(NSString *)name order:(NSUInteger)order { name = [NSString stringWithFormat:@"%02u%@", (unsigned) order, name]; [Snapshot snapshot:name timeWaitingForIdle:10]; } - (void)testSystemInfo { [self runCommand:@"uname -a" timeout:5]; [self snapshot:@"systeminfo" order:1]; } - (void)testLanguages { [self runCommand:@"apk add build-base python3" timeout:120]; [self runCommand:@"printf '#include \\nint main() { printf(\"Hello, iSH!\\\\n\"); }' > hello.c" timeout:5]; [self runCommand:@"gcc hello.c && ./a.out" timeout:5]; [self runCommand:@"python3 -c 'print(\"Hello, iSH!\")'" timeout:5]; [self snapshot:@"languages" order:2]; } - (void)testEditorsInTmux { [self runCommand:@"apk add vim nano tmux" timeout:120]; [self runCommand:@"tmux new-session -d -s foo nano" timeout:5]; [self runCommand:@"tmux split-window -v vim" timeout:5]; [self runCommand:@"tmux select-layout even-vertical" timeout:5]; [self.app typeText:@"tmux attach -t foo\n"]; [self waitForTerminalText:@"GNU nano" timeout:30]; [self snapshot:@"editorsintmux" order:3]; } - (void)testEmacs { [self runCommand:@"apk add emacs" timeout:120]; [self.app typeText:@"emacs\n"]; [self waitForTerminalText:@"Welcome to GNU Emacs" timeout:30]; [self snapshot:@"emacs" order:4]; } @end ================================================ FILE: app/UITests/Screenshots.xctestplan ================================================ { "configurations" : [ { "id" : "963A2374-D06F-45E9-AD7A-628EE51BB5BF", "name" : "Configuration 1", "options" : { } } ], "defaultOptions" : { "codeCoverage" : false }, "testTargets" : [ { "skippedTests" : [ "Screenshots\/testEditorsInTmux", "Screenshots\/testEmacs", "Screenshots\/testLanguages", "UITests" ], "target" : { "containerPath" : "container:iSH.xcodeproj", "identifier" : "BB41591B255EF9E300E0950C", "name" : "iSHUITests" } } ], "version" : 1 } ================================================ FILE: app/UITests/UITests.m ================================================ // // UITests.m // UITests // // Created by Theodore Dubois on 11/13/20. // #import @interface UITests : XCTestCase @end @implementation UITests - (void)setUp { self.continueAfterFailure = NO; } @end ================================================ FILE: app/UIViewController+Extras.h ================================================ // // UIViewController+Extras.h // iSH // // Created by Theodore Dubois on 9/23/18. // #import NS_ASSUME_NONNULL_BEGIN @interface UIViewController (Extras) - (void)presentError:(NSError *)error title:(NSString *)title; @end NS_ASSUME_NONNULL_END ================================================ FILE: app/UIViewController+Extras.m ================================================ // // UIViewController+Extras.m // iSH // // Created by Theodore Dubois on 9/23/18. // #import "UIViewController+Extras.h" @implementation UIViewController (Extras) - (void)presentError:(NSError *)error title:(NSString *)title { UIAlertController *alert = [UIAlertController alertControllerWithTitle:title message:error.localizedDescription preferredStyle:UIAlertControllerStyleAlert]; [alert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]]; [self presentViewController:alert animated:YES completion:nil]; } @end ================================================ FILE: app/UpgradeRootViewController.h ================================================ // // UpgradeRootViewController.h // iSH // // Created by Theodore Dubois on 11/27/21. // #import NS_ASSUME_NONNULL_BEGIN @interface UpgradeRootViewController : UIViewController @end NS_ASSUME_NONNULL_END ================================================ FILE: app/UpgradeRootViewController.m ================================================ // // UpgradeRootViewController.m // iSH // // Created by Theodore Dubois on 11/27/21. // #import "UpgradeRootViewController.h" #import "AppDelegate.h" #import "TerminalView.h" #import "CurrentRoot.h" #include "kernel/calls.h" #include "kernel/init.h" #include "fs/devices.h" @interface UpgradeRootViewController () @property (weak, nonatomic) IBOutlet TerminalView *terminalView; @property (weak, nonatomic) IBOutlet UIBarButtonItem *upgradeButton; @property (nonatomic) Terminal *terminal; @property (nonatomic) struct tty *tty; @property (nonatomic) int upgradePid; @end @implementation UpgradeRootViewController - (void)viewDidLoad { [super viewDidLoad]; #if !ISH_LINUX [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(processExited:) name:ProcessExitedNotification object:nil]; lock(&pids_lock); current = pid_get_task(1); // pray unlock(&pids_lock); self.terminal = [Terminal createPseudoTerminal:&self->_tty]; current = NULL; self.terminalView.terminal = self.terminal; #endif self.upgradeButton.enabled = NO; if (FsNeedsRepositoryUpdate()) { self.upgradeButton.enabled = YES; [self printToTerminal:@"# /sbin/apk upgrade"]; } else { [self showAlertWithTitle:@"fuck" message:@"No update needed. If you're seeing this message, there's a bug."]; } } - (void)printToTerminal:(NSString *)message, ... { va_list args; va_start(args, message); message = [[NSString alloc] initWithFormat:message arguments:args]; message = [message stringByReplacingOccurrencesOfString:@"\n" withString:@"\r\n"]; [self.terminal sendOutput:message.UTF8String length:(int)[message lengthOfBytesUsingEncoding:NSUTF8StringEncoding]]; } - (void)showAlertWithTitle:(NSString *)title message:(NSString *)message, ... { va_list args; va_start(args, message); message = [[NSString alloc] initWithFormat:message arguments:args]; UIAlertController *alert = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert]; [alert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]]; [self presentViewController:alert animated:YES completion:nil]; } #if !ISH_LINUX - (void)processExited:(NSNotification *)notif { int pid = [notif.userInfo[@"pid"] intValue]; if (pid != self.upgradePid) return; self.upgradePid = 0; [self setDismissable:YES]; int code = [notif.userInfo[@"code"] intValue]; [self printToTerminal:@"\n"]; if (code != 0) { [self printToTerminal:@"Upgrade failed with exit status %d.\nPlease send a bug report.\n", code]; } else { lock(&pids_lock); current = pid_get_task(1); // pray unlock(&pids_lock); FsUpdateRepositories(); current = NULL; [self printToTerminal:@"Upgrade complete!\nIf anything that was working before stops working, please send a bug report.\n"]; } [self.terminal destroy]; self.terminal = nil; } #endif - (int)startUpgrade { if (self.upgradePid != 0) return _EEXIST; #if !ISH_LINUX int err = become_new_init_child(); if (err < 0) return err; FsUpdateOnlyRepositoriesFile(); NSString *stdioFile = [NSString stringWithFormat:@"/dev/pts/%d", self.tty->num]; err = create_stdio(stdioFile.fileSystemRepresentation, TTY_PSEUDO_SLAVE_MAJOR, self.tty->num); if (err < 0) return err; err = do_execve("/sbin/apk", 2, "/sbin/apk\0upgrade\0", "TERM=xterm-256color\0"); if (err < 0) return err; self.upgradePid = current->pid; task_start(current); current = NULL; return 0; #else return _ENOSYS; #endif } - (IBAction)upgrade:(id)sender { self.upgradeButton.enabled = NO; [self setDismissable:NO]; [self printToTerminal:@"\n"]; int err = [self startUpgrade]; if (err < 0) { [self showAlertWithTitle:@"Failed to start upgrade" message:@"error %d", err]; } } - (void)setDismissable:(BOOL)dismissable { [self.navigationItem setHidesBackButton:!dismissable animated:YES]; self.navigationController.interactivePopGestureRecognizer.enabled = dismissable; if (@available(iOS 13, *)) { self.modalInPresentation = !dismissable; } } - (void)dealloc { [self.terminal destroy]; if (self.tty != NULL) tty_release(self.tty); } @end ================================================ FILE: app/UserPreferences.h ================================================ // // UserPreferences.h // iSH // // Created by Charlie Melbye on 11/12/18. // #import #import "Theme.h" typedef NS_ENUM(NSInteger, CapsLockMapping) { __CapsLockMapFirst = 0, CapsLockMapNone = 0, CapsLockMapControl, CapsLockMapEscape, __CapsLockMapLast, }; typedef enum : NSUInteger { __OptionMapFirst = 0, OptionMapNone = 0, OptionMapEsc, __OptionMapLast, } OptionMapping; typedef NS_ENUM(NSInteger, CursorStyle) { __CursorStyleFirst = 0, CursorStyleBlock = 0, CursorStyleBeam, CursorStyleUnderline, __CursorStyleLast, }; typedef NS_ENUM(NSInteger, ColorScheme) { __ColorSchemeFirst = 0, ColorSchemeMatchSystem = 0, ColorSchemeAlwaysLight, ColorSchemeAlwaysDark, __ColorSchemeLast, }; NS_ASSUME_NONNULL_BEGIN extern NSString *const kThemeForegroundColor; extern NSString *const kThemeBackgroundColor; @interface UserPreferences : NSObject @property CapsLockMapping capsLockMapping; @property OptionMapping optionMapping; @property BOOL backtickMapEscape; @property BOOL hideExtraKeysWithExternalKeyboard; @property BOOL overrideControlSpace; @property BOOL hideStatusBar; @property (nonatomic) Theme *theme; @property (nonatomic) Palette *palette; @property BOOL shouldDisableDimming; @property (null_resettable) NSString *fontFamily; @property (readonly) NSString *fontFamilyUserFacingName; @property (readonly) UIFont *approximateFont; @property NSNumber *fontSize; @property ColorScheme colorScheme; @property (readonly) BOOL requestingDarkAppearance; @property (readonly) UIUserInterfaceStyle userInterfaceStyle API_AVAILABLE(ios(12.0)); @property (readonly) UIKeyboardAppearance keyboardAppearance; @property CursorStyle cursorStyle; @property (readonly) NSString *htermCursorShape; @property BOOL blinkCursor; @property (readonly) UIStatusBarStyle statusBarStyle; @property NSArray *launchCommand; @property NSArray *bootCommand; @property NSString *hostnameOverride; // Same as above but returns nil if the user has never set the hostname @property (readonly) NSString *_hostnameOverride; + (instancetype)shared; - (BOOL)hasChangedLaunchCommand; @end extern NSString *const kPreferenceLaunchCommandKey; extern NSString *const kPreferenceBootCommandKey; extern NSString *const kHostnameOverrideKey; NS_ASSUME_NONNULL_END ================================================ FILE: app/UserPreferences.m ================================================ // // UserPreferences.m // iSH // // Created by Charlie Melbye on 11/12/18. // #import "UserPreferences.h" #import "fs/proc/ish.h" // IMPORTANT: If you add a constant here and expose it via UserPreferences, // consider if it also needs to be exposed as a friendly preference and included // in the KVO list below. (In most circumstances, the answer is "yes".) static NSString *const kPreferenceCapsLockMappingKey = @"Caps Lock Mapping"; static NSString *const kPreferenceOptionMappingKey = @"Option Mapping"; static NSString *const kPreferenceBacktickEscapeKey = @"Backtick Mapping Escape"; static NSString *const kPreferenceHideExtraKeysWithExternalKeyboardKey = @"Hide Extra Keys With External Keyboard"; static NSString *const kPreferenceOverrideControlSpaceKey = @"Override Control Space"; static NSString *const kPreferenceFontFamilyKey = @"Font Family"; static NSString *const kPreferenceFontSizeKey = @"Font Size"; static NSString *const kPreferenceThemeKey = @"ModernTheme"; static NSString *const kPreferenceDisableDimmingKey = @"Disable Dimming"; NSString *const kPreferenceLaunchCommandKey = @"Init Command"; NSString *const kPreferenceBootCommandKey = @"Boot Command"; static NSString *const kPreferenceCursorStyleKey = @"Cursor Style"; static NSString *const kPreferenceBlinkCursorKey = @"Blink Cursor"; NSString *const kPreferenceHideStatusBarKey = @"Status Bar"; static NSString *const kPreferenceColorSchemeKey = @"Color Scheme"; // This key has a different naming scheme because it's used in UI tests as a // CLI argument. NSString *const kHostnameOverrideKey = @"hostnameOverride"; NSDictionary *friendlyPreferenceMapping; NSDictionary *friendlyPreferenceReverseMapping; NSDictionary *kvoProperties; static NSString *const kSystemMonospacedFontName = @"ui-monospace"; @interface UserPreferences () { BOOL _hostnameIsOverridden; } - (void)updateTheme; @end char **get_all_defaults_keys_impl(void) { NSArray *preferenceKeys = NSUserDefaults.standardUserDefaults.dictionaryRepresentation.allKeys; char **entries = malloc((preferenceKeys.count + 1) * sizeof(*entries)); for (NSUInteger i = 0; i < preferenceKeys.count; ++i) entries[i] = strdup(preferenceKeys[i].UTF8String); entries[preferenceKeys.count] = NULL; return entries; } char *get_friendly_name_impl(const char *name) { const char *friendly_name = friendlyPreferenceReverseMapping[[NSString stringWithUTF8String:name]].UTF8String; if (friendly_name == NULL) return NULL; return strdup(friendly_name); } char *get_underlying_name_impl(const char *name) { return strdup(friendlyPreferenceMapping[[NSString stringWithUTF8String:name]].UTF8String); } bool get_user_default_impl(const char *name, char **buffer, size_t *size) { id value = [NSUserDefaults.standardUserDefaults objectForKey:[NSString stringWithUTF8String:name]]; // Since we are writing with fragments, wrap the object in an array to have // a top-level object to check. if (!value || ![NSJSONSerialization isValidJSONObject:@[value]]) { return false; } NSError *error; NSJSONWritingOptions options = NSJSONWritingFragmentsAllowed | NSJSONWritingSortedKeys | NSJSONWritingPrettyPrinted; if (@available(iOS 13.0, *)) { options |= NSJSONWritingWithoutEscapingSlashes; } NSData *data = [NSJSONSerialization dataWithJSONObject:value options:options error:&error]; if (error) { return false; } *buffer = malloc(data.length + 1); memcpy(*buffer, data.bytes, data.length); (*buffer)[data.length] = '\n'; *size = data.length + 1; return true; } bool set_user_default_impl(const char *name, char *buffer, size_t size) { NSString *key = [NSString stringWithUTF8String:name]; NSData *data = [NSData dataWithBytesNoCopy:buffer length:size freeWhenDone:NO]; NSError *error; id value = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingFragmentsAllowed error:&error]; if (error) { return false; } NSString *property = kvoProperties[key]; if (property) { if ([UserPreferences.shared validateValue:&value forKey:property error:nil]) { [UserPreferences.shared setValue:value forKey:property]; } else { return false; } } else { [NSUserDefaults.standardUserDefaults setValue:value forKey:key]; } return true; } bool remove_user_default_impl(const char *name) { NSString *key = [NSString stringWithUTF8String:name]; NSString *property = kvoProperties[key]; if (property) { [UserPreferences.shared willChangeValueForKey:property]; } [NSUserDefaults.standardUserDefaults removeObjectForKey:key]; if (property) { [UserPreferences.shared didChangeValueForKey:property]; } // This particular property needs special handling to stay up-to-date if ([property isEqualToString:@"userTheme"]) { [UserPreferences.shared updateTheme]; } return true; } // TODO: Move these to Linux #if ISH_LINUX char **(*get_all_defaults_keys)(void); char *(*get_friendly_name)(const char *name); char *(*get_underlying_name)(const char *name); bool (*get_user_default)(const char *name, char **buffer, size_t *size); bool (*set_user_default)(const char *name, char *buffer, size_t size); bool (*remove_user_default)(const char *name); #endif @implementation UserPreferences { NSUserDefaults *_defaults; } + (instancetype)shared { static UserPreferences *shared = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ shared = [[self alloc] init]; }); return shared; } - (instancetype)init { self = [super init]; self->_hostnameIsOverridden = !![NSUserDefaults.standardUserDefaults stringForKey:kHostnameOverrideKey]; if (self) { _defaults = [NSUserDefaults standardUserDefaults]; [_defaults registerDefaults:@{ kPreferenceFontSizeKey: @(12), kPreferenceCapsLockMappingKey: @(CapsLockMapControl), kPreferenceOptionMappingKey: @(OptionMapNone), kPreferenceBacktickEscapeKey: @(NO), kPreferenceHideExtraKeysWithExternalKeyboardKey: @(NO), kPreferenceOverrideControlSpaceKey: @(NO), kPreferenceDisableDimmingKey: @(NO), kPreferenceLaunchCommandKey: @[@"/bin/login", @"-f", @"root"], kPreferenceBootCommandKey: @[@"/sbin/init"], kPreferenceBlinkCursorKey: @(NO), kPreferenceCursorStyleKey: @(CursorStyleBlock), kPreferenceHideStatusBarKey: @(NO), kPreferenceColorSchemeKey: @(ColorSchemeMatchSystem), kPreferenceThemeKey: @"Default", kHostnameOverrideKey: UIDevice.currentDevice.name, }]; // https://webkit.org/blog/10247/new-webkit-features-in-safari-13-1/ if (@available(iOS 13.4, *)) { [_defaults registerDefaults:@{ kPreferenceFontFamilyKey: kSystemMonospacedFontName, }]; } else { [_defaults registerDefaults:@{ kPreferenceFontFamilyKey: @"Menlo", }]; } get_all_defaults_keys = get_all_defaults_keys_impl; get_friendly_name = get_friendly_name_impl; get_underlying_name = get_underlying_name_impl; get_user_default = get_user_default_impl; set_user_default = set_user_default_impl; remove_user_default = remove_user_default_impl; friendlyPreferenceMapping = @{ @"caps_lock_mapping": kPreferenceCapsLockMappingKey, @"option_mapping": kPreferenceOptionMappingKey, @"backtick_mapping_escape": kPreferenceBacktickEscapeKey, @"hide_extra_keys_with_external_keyboard": kPreferenceHideExtraKeysWithExternalKeyboardKey, @"override_control_space": kPreferenceOverrideControlSpaceKey, @"font_family": kPreferenceFontFamilyKey, @"font_size": kPreferenceFontSizeKey, @"disable_dimming": kPreferenceDisableDimmingKey, @"launch_command": kPreferenceLaunchCommandKey, @"boot_command": kPreferenceBootCommandKey, @"cursor_style": kPreferenceCursorStyleKey, @"blink_cursor": kPreferenceBlinkCursorKey, @"hide_status_bar": kPreferenceHideStatusBarKey, @"color_scheme": kPreferenceColorSchemeKey, @"theme": kPreferenceThemeKey, @"hostname_override": kHostnameOverrideKey, }; NSMutableDictionary *reverseMapping = [NSMutableDictionary new]; for (NSString *key in friendlyPreferenceMapping) { reverseMapping[friendlyPreferenceMapping[key]] = key; } friendlyPreferenceReverseMapping = reverseMapping; // Helps a bit with compile-time safety and autocompletion #define property(x) NSStringFromSelector(@selector(x)) kvoProperties = @{ kPreferenceCapsLockMappingKey: property(capsLockMapping), kPreferenceOptionMappingKey: property(optionMapping), kPreferenceBacktickEscapeKey: property(backtickMapEscape), kPreferenceHideExtraKeysWithExternalKeyboardKey: property(hideExtraKeysWithExternalKeyboard), kPreferenceOverrideControlSpaceKey: property(overrideControlSpace), kPreferenceFontFamilyKey: property(fontFamily), kPreferenceFontSizeKey: property(fontSize), kPreferenceDisableDimmingKey: property(shouldDisableDimming), kPreferenceLaunchCommandKey: property(launchCommand), kPreferenceBootCommandKey: property(bootCommand), kPreferenceCursorStyleKey: property(cursorStyle), kPreferenceBlinkCursorKey: property(blinkCursor), kPreferenceHideStatusBarKey: property(hideStatusBar), kPreferenceColorSchemeKey: property(colorScheme), // This one is a little bit special, so it needs extra handling. // The backing property for this is intentionally underscored. kPreferenceThemeKey: @"userTheme", }; #undef property [self updateTheme]; [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(updateTheme:) name:ThemesUpdatedNotification object:nil]; [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(updateTheme:) name:ThemeUpdatedNotification object:nil]; } return self; } // MARK: - Preference properties // MARK: capsLockMapping - (CapsLockMapping)capsLockMapping { return [_defaults integerForKey:kPreferenceCapsLockMappingKey]; } - (void)setCapsLockMapping:(CapsLockMapping)capsLockMapping { [_defaults setInteger:capsLockMapping forKey:kPreferenceCapsLockMappingKey]; } - (BOOL)validateCapsLockMapping:(id *)value error:(NSError **)error { if (![*value isKindOfClass:NSNumber.class]) { return NO; } int _value = [(NSNumber *)(*value) intValue]; return _value >= __CapsLockMapFirst && _value < __CapsLockMapLast; } // MARK: optionMapping - (OptionMapping)optionMapping { return [_defaults integerForKey:kPreferenceOptionMappingKey]; } - (void)setOptionMapping:(OptionMapping)optionMapping { [_defaults setInteger:optionMapping forKey:kPreferenceOptionMappingKey]; } - (BOOL)validateOptionMapping:(id *)value error:(NSError **)error { if (![*value isKindOfClass:NSNumber.class]) { return NO; } int _value = [(NSNumber *)(*value) intValue]; return _value >= __OptionMapFirst && _value < __OptionMapLast; } // MARK: backtickMapEscape - (BOOL)backtickMapEscape { return [_defaults boolForKey:kPreferenceBacktickEscapeKey]; } - (void)setBacktickMapEscape:(BOOL)backtickMapEscape { [_defaults setBool:backtickMapEscape forKey:kPreferenceBacktickEscapeKey]; } - (BOOL)validateBacktickMapEscape:(id *)value error:(NSError **)error { return [*value isKindOfClass:NSNumber.class]; } // MARK: hideExtraKeysWithExternalKeyboard - (BOOL)hideExtraKeysWithExternalKeyboard { return [_defaults boolForKey:kPreferenceHideExtraKeysWithExternalKeyboardKey]; } - (void)setHideExtraKeysWithExternalKeyboard:(BOOL)hideExtraKeysWithExternalKeyboard { [_defaults setBool:hideExtraKeysWithExternalKeyboard forKey:kPreferenceHideExtraKeysWithExternalKeyboardKey]; } - (BOOL)validateHideExtraKeysWithExternalKeyboard:(id *)value error:(NSError **)error { return [*value isKindOfClass:NSNumber.class]; } // MARK: overrideControlSpace - (BOOL)overrideControlSpace { return [_defaults boolForKey:kPreferenceOverrideControlSpaceKey]; } - (void)setOverrideControlSpace:(BOOL)overrideControlSpace { [_defaults setBool:overrideControlSpace forKey:kPreferenceOverrideControlSpaceKey]; } - (BOOL)validateOverrideControlSpace:(id *)value error:(NSError **)error { return [*value isKindOfClass:NSNumber.class]; } // MARK: fontSize - (NSNumber *)fontSize { return [_defaults objectForKey:kPreferenceFontSizeKey]; } - (void)setFontSize:(NSNumber *)fontSize { [_defaults setObject:fontSize forKey:kPreferenceFontSizeKey]; } - (BOOL)validateFontSize:(id *)value error:(NSError **)error { return [*value isKindOfClass:NSNumber.class]; } // MARK: fontFamily - (NSString *)fontFamily { return [_defaults objectForKey:kPreferenceFontFamilyKey]; } - (void)setFontFamily:(NSString *)fontFamily { if (fontFamily) { [_defaults setObject:fontFamily forKey:kPreferenceFontFamilyKey]; } else { [_defaults removeObjectForKey:kPreferenceFontFamilyKey]; } } - (BOOL)validateFontFamily:(id *)value error:(NSError **)error { return [*value isKindOfClass:NSString.class]; } - (NSString *)fontFamilyUserFacingName { return [self.fontFamily isEqualToString:kSystemMonospacedFontName] ? @"System" : self.fontFamily; } - (UIFont *)approximateFont { if (@available(iOS 13.4, *)) { if ([self.fontFamily isEqualToString:kSystemMonospacedFontName]) { return [UIFont monospacedSystemFontOfSize:self.fontSize.doubleValue weight:UIFontWeightRegular]; } } UIFont *font = [UIFont fontWithName:self.fontFamily size:self.fontSize.doubleValue]; return font ? font : [UIFont fontWithName:@"Menlo" size:self.fontSize.doubleValue]; } // MARK: theme - (void)setTheme:(Theme *)theme { _theme = theme; [_defaults setObject:theme.name forKey:kPreferenceThemeKey]; } // These are provided because user theme validation is done with strings - (NSString *)_userTheme { return self.theme.name; } - (void)_setUserTheme:(NSString *)userTheme { Theme *theme; if ((theme = [Theme themeForName:userTheme includingDefaultThemes:YES])) { self.theme = theme; } else { self.theme = Theme.defaultThemes.lastObject; } } - (BOOL)validateUserTheme:(id *)value error:(NSError **)error { return [*value isKindOfClass:NSString.class]; } - (void)updateTheme:(NSNotification *)notification { if (notification.object) { [_defaults setValue:notification.object forKey:kPreferenceThemeKey]; } [self updateTheme]; } - (void)updateTheme { [self _setUserTheme:[_defaults valueForKey:kPreferenceThemeKey]]; } - (Palette *)palette { switch (self.colorScheme) { case ColorSchemeMatchSystem: return self.class.systemThemeIsDark ? self.theme.darkPalette : self.theme.lightPalette; case ColorSchemeAlwaysDark: return self.theme.darkPalette; default: NSAssert(NO, @"invalid color scheme"); case ColorSchemeAlwaysLight: return self.theme.lightPalette; } } // MARK: shouldDisableDimming - (BOOL)shouldDisableDimming { return [_defaults boolForKey:kPreferenceDisableDimmingKey]; } - (void)setShouldDisableDimming:(BOOL)dim { [_defaults setBool:dim forKey:kPreferenceDisableDimmingKey]; } - (BOOL)validateShouldDisableDimming:(id *)value error:(NSError **)error { return [*value isKindOfClass:NSNumber.class]; } // MARK: launchCommand - (NSArray *)launchCommand { return [_defaults stringArrayForKey:kPreferenceLaunchCommandKey]; } - (void)setLaunchCommand:(NSArray *)launchCommand { [_defaults setObject:launchCommand forKey:kPreferenceLaunchCommandKey]; } - (BOOL)validateLaunchCommand:(id *)value error:(NSError **)error { if (![*value isKindOfClass:NSArray.class]) { return NO; } for (id element in (NSArray *)(*value)) { if (![element isKindOfClass:NSString.class]) { return NO; } } return YES; } - (BOOL)hasChangedLaunchCommand { NSArray *defaultLaunchCommand = [[[NSUserDefaults alloc] initWithSuiteName:NSRegistrationDomain] stringArrayForKey:kPreferenceLaunchCommandKey]; return ![self.launchCommand isEqual:defaultLaunchCommand]; } // MARK: bootCommand - (NSArray *)bootCommand { return [_defaults stringArrayForKey:kPreferenceBootCommandKey]; } - (void)setBootCommand:(NSArray *)bootCommand { [_defaults setObject:bootCommand forKey:kPreferenceBootCommandKey]; } - (BOOL)validateBootCommand:(id *)value error:(NSError **)error { if (![*value isKindOfClass:NSArray.class]) { return NO; } for (id element in (NSArray *)(*value)) { if (![element isKindOfClass:NSString.class]) { return NO; } } return YES; } // MARK: cursorStyle - (CursorStyle)cursorStyle { return [_defaults integerForKey:kPreferenceCursorStyleKey]; } - (void)setCursorStyle:(CursorStyle)cursorStyle { [_defaults setInteger:cursorStyle forKey:kPreferenceCursorStyleKey]; } - (BOOL)validateCursorStyle:(id *)value error:(NSError **)error { if (![*value isKindOfClass:NSNumber.class]) { return NO; } int _value = [(NSNumber *)(*value) intValue]; return _value >= __CursorStyleFirst && _value < __CursorStyleLast; } - (NSString *)htermCursorShape { switch (self.cursorStyle) { case CursorStyleBlock: return @"BLOCK"; case CursorStyleBeam: return @"BEAM"; case CursorStyleUnderline: return @"UNDERLINE"; default: NSAssert(NO, @"Invalid cursor style"); return nil; } } // MARK: blinkCursor - (BOOL)blinkCursor { return [_defaults boolForKey:kPreferenceBlinkCursorKey]; } - (void)setBlinkCursor:(BOOL)blinkCursor { [_defaults setBool:blinkCursor forKey:kPreferenceBlinkCursorKey]; } - (BOOL)validateBlinkCursor:(id *)value error:(NSError **)error { return [*value isKindOfClass:NSNumber.class]; } // MARK: hideStatusBar - (BOOL)hideStatusBar { return [_defaults boolForKey:kPreferenceHideStatusBarKey]; } - (void)setHideStatusBar:(BOOL)showStatusBar { [_defaults setBool:showStatusBar forKey:kPreferenceHideStatusBarKey]; } - (BOOL)validateHideStatusBar:(id *)value error:(NSError **)error { return [*value isKindOfClass:NSNumber.class]; } // MARK: colorScheme - (ColorScheme)colorScheme { return [_defaults integerForKey:kPreferenceColorSchemeKey]; } - (void)setColorScheme:(ColorScheme)colorScheme { [_defaults setInteger:colorScheme forKey:kPreferenceColorSchemeKey]; } - (BOOL)validateColorScheme:(id *)value error:(NSError **)error { if (![*value isKindOfClass:NSNumber.class]) { return NO; } int _value = [(NSNumber *)(*value) intValue]; return _value >= __ColorSchemeFirst && _value < __ColorSchemeLast; } // MARK: hostnameOverride - (NSString *)hostnameOverride { return [_defaults stringForKey:kHostnameOverrideKey]; } - (void)setHostnameOverride:(NSString *)hostnameOverride { [_defaults setValue:hostnameOverride forKey:kHostnameOverrideKey]; } - (BOOL)validateHostnameOverride:(id *)value error:(NSError **)error { return [*value isKindOfClass:NSString.class]; } - (NSString *)_hostnameOverride { return _hostnameIsOverridden ? self.hostnameOverride : nil; } + (BOOL)systemThemeIsDark { if (@available(iOS 12.0, *)) { switch (UIScreen.mainScreen.traitCollection.userInterfaceStyle) { case UIUserInterfaceStyleLight: return NO; case UIUserInterfaceStyleDark: return YES; default: break; } } return NO; } - (BOOL)requestingDarkAppearance { return (self.class.systemThemeIsDark && !self.theme.appearance.darkOverride) || (!self.class.systemThemeIsDark && self.theme.appearance.lightOverride); } - (UIUserInterfaceStyle)userInterfaceStyle { return self.requestingDarkAppearance ? UIUserInterfaceStyleDark : UIUserInterfaceStyleLight; } - (UIKeyboardAppearance)keyboardAppearance { return self.requestingDarkAppearance ? UIKeyboardAppearanceDark : UIKeyboardAppearanceLight; } - (UIStatusBarStyle)statusBarStyle { return self.requestingDarkAppearance ? UIStatusBarStyleLightContent : UIStatusBarStyleDefault; } @end ================================================ FILE: app/ViewController.h ================================================ // // ViewController.h // iSH // // Created by Theodore Dubois on 10/17/17. // #import @interface TerminalViewController : UIViewController @end ================================================ FILE: app/XcodeDebug.xcconfig ================================================ #include "XcodeDefault.xcconfig" DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_TESTABILITY = YES; GCC_DYNAMIC_NO_PIC = NO; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = $(inherited) DEBUG=1 MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; ONLY_ACTIVE_ARCH = YES; ================================================ FILE: app/XcodeDefault.xcconfig ================================================ ALWAYS_SEARCH_USER_PATHS = NO; CLANG_CXX_LANGUAGE_STANDARD = gnu++14; CLANG_CXX_LIBRARY = libc++; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; COPY_PHASE_STRIP = NO; GCC_C_LANGUAGE_STANDARD = gnu11; MTL_FAST_MATH = YES; // Compiler warnings that are not enabled by default for no apparent reason CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; DEAD_CODE_STRIPPING = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; ================================================ FILE: app/XcodeRelease.xcconfig ================================================ #include "XcodeDefault.xcconfig" DEBUG_INFORMATION_FORMAT = dwarf-with-dsym; ENABLE_NS_ASSERTIONS = NO; MTL_ENABLE_DEBUG_INFO = NO; VALIDATE_PRODUCT = YES; ================================================ FILE: app/gen_apk_repositories.py ================================================ import os def trim(x, start, end): assert x.startswith(start) assert x.endswith(end) return x[len(start):-len(end)] APK_REPOSITORIES = [ ('v3.19', 'main'), ('v3.19', 'community'), ] ARCH = 'x86' # TODO: support more archs repos_file = [] for version, repo in APK_REPOSITORIES: with open(f'{os.environ["SRCROOT"]}/deps/aports/{version}/{repo}/{ARCH}/index.txt') as f: index_name = f.read() index_name = trim(index_name, 'APKINDEX-', '.tar.gz\n') repos_file.append(f'http://apk.ish.app/{index_name}/{repo}') with open(os.path.join(os.environ['BUILT_PRODUCTS_DIR'], os.environ['CONTENTS_FOLDER_PATH'], 'repositories.txt'), 'w') as f: for line in repos_file: print(line, file=f) ================================================ FILE: app/hook.c ================================================ // // hook.c // iSH // // Created by Saagar Jha on 12/29/22. // #include "hook.h" #include "mach_excServer.h" #include #include #include #include #include #include #include #include #include #if __arm64__ // No Foundation.h extern void NSLog(CFStringRef, ...); kern_return_t catch_mach_exception_raise( mach_port_t exception_port, mach_port_t thread, mach_port_t task, exception_type_t exception, mach_exception_data_t code, mach_msg_type_number_t codeCnt) { abort(); } kern_return_t catch_mach_exception_raise_state_identity( mach_port_t exception_port, mach_port_t thread, mach_port_t task, exception_type_t exception, mach_exception_data_t code, mach_msg_type_number_t codeCnt, int *flavor, thread_state_t old_state, mach_msg_type_number_t old_stateCnt, thread_state_t new_state, mach_msg_type_number_t *new_stateCnt) { abort(); } static bool initialized; struct hook { uintptr_t old; uintptr_t new; }; static struct hook hooks[16]; static int active_hooks; static int breakpoints; mach_port_t server; kern_return_t catch_mach_exception_raise_state( mach_port_t exception_port, exception_type_t exception, const mach_exception_data_t code, mach_msg_type_number_t codeCnt, int *flavor, const thread_state_t old_state, mach_msg_type_number_t old_stateCnt, thread_state_t new_state, mach_msg_type_number_t *new_stateCnt) { arm_thread_state64_t *old = (arm_thread_state64_t *)old_state; arm_thread_state64_t *new = (arm_thread_state64_t *)new_state; for (int i = 0; i < active_hooks; ++i) { if (hooks[i].old == arm_thread_state64_get_pc(*old)) { *new = *old; *new_stateCnt = old_stateCnt; arm_thread_state64_set_pc_fptr(*new, hooks[i].new); return KERN_SUCCESS; } } return KERN_FAILURE; } void *exception_handler(void *unused) { mach_msg_server(mach_exc_server, sizeof(union __RequestUnion__catch_mach_exc_subsystem), server, MACH_MSG_OPTION_NONE); abort(); } static bool initialize_if_needed(void) { if (initialized) { return true; } #define CHECK(x) \ do { \ if (!(x)) { \ NSLog(CFSTR("hook failed: " #x)); \ return false; \ } \ } while (0) size_t size = sizeof(breakpoints); CHECK(!sysctlbyname("hw.optional.breakpoint", &breakpoints, &size, NULL, 0)); CHECK(mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &server) == KERN_SUCCESS); CHECK(mach_port_insert_right(mach_task_self(), server, server, MACH_MSG_TYPE_MAKE_SEND) == KERN_SUCCESS); // This will break any connected debuggers. Unfortunately the workarounds // for this are not very good, so we're not going to bother with them. CHECK(task_set_exception_ports(mach_task_self(), EXC_MASK_BREAKPOINT, server, EXCEPTION_STATE | MACH_EXCEPTION_CODES, ARM_THREAD_STATE64) == KERN_SUCCESS); pthread_t thread; CHECK(!pthread_create(&thread, NULL, exception_handler, NULL)); #undef CHECK return initialized = true; } // This is marked as available on iPhone in libproc.h but pulling in the header // on iOS is difficult, so we should just forward declare it. static int (*proc_regionfilename)(int pid, uint64_t address, void *buffer, uint32_t buffersize); // Pulled from https://github.com/apple-oss-distributions/dyld/blob/main/cache-builder/dyld_cache_format.h. // The format hasn't changed yet but we should check the magic value if it does, // since Apple's tools use it to detect if the layout will be updated. struct dyld_cache_header { char magic[16]; char padding[56]; uint64_t localSymbolsOffset; uint64_t localSymbolsSize; }; struct dyld_cache_local_symbols_info { uint32_t nlistOffset; uint32_t nlistCount; uint32_t stringsOffset; }; void *find_symbol(void *base, char *symbol) { if (!proc_regionfilename) { proc_regionfilename = dlsym(dlopen(NULL, RTLD_LAZY), "proc_regionfilename"); } #define CHECK(x) \ do { \ if (!(x)) { \ NSLog(CFSTR("hook failed: " #x)); \ return NULL; \ } \ } while (0) CHECK(proc_regionfilename); task_dyld_info_data_t dyld_info; mach_msg_type_number_t count = TASK_DYLD_INFO_COUNT; CHECK(task_info(mach_task_self(), TASK_DYLD_INFO, (task_info_t)&dyld_info, &count) == KERN_SUCCESS); struct dyld_all_image_infos *all_image_infos = (struct dyld_all_image_infos *)dyld_info.all_image_info_addr; // proc_regionfilename doesn't seem to produce results unless the shared // region has been modified in some way. This "no-op" forces the mapping // to be backed by a vnode whose path we can query. vm_protect(mach_task_self(), (vm_address_t)base, 1, false, VM_PROT_READ | VM_PROT_COPY); vm_protect(mach_task_self(), (vm_address_t)base, 1, false, VM_PROT_READ | VM_PROT_EXECUTE); char path[MAXPATHLEN]; int size = proc_regionfilename(getpid(), all_image_infos->sharedCacheBaseAddress, path, sizeof(path)); CHECK(size > 0); CFURLRef region = CFURLCreateWithBytes(NULL, (UInt8 *)path, size, kCFStringEncodingUTF8, NULL); CFURLRef shared_cache = CFURLCreateCopyDeletingPathExtension(NULL, region); CFURLRef symbols_file = CFURLCreateCopyAppendingPathExtension(NULL, shared_cache, CFSTR("symbols")); CFStringGetCString(CFURLGetString(symbols_file), path, sizeof(path), kCFStringEncodingUTF8); CFRelease(region); CFRelease(shared_cache); CFRelease(symbols_file); int fd = open(path, O_RDONLY); CHECK(fd >= 0); struct stat buffer; CHECK(!stat(path, &buffer)); void *file = mmap(NULL, buffer.st_size, PROT_READ, MAP_PRIVATE, fd, 0); close(fd); CHECK(file); #undef CHECK void *address = NULL; struct dyld_cache_header *header = (struct dyld_cache_header *)file; if (strcmp(header->magic, "dyld_v1 arm64") && strcmp(header->magic, "dyld_v1 arm64e")) { NSLog(CFSTR("hook failed: unknown shared cache magic %s"), header->magic); \ goto done; } struct dyld_cache_local_symbols_info *symbols = (struct dyld_cache_local_symbols_info *)(file + header->localSymbolsOffset); struct nlist_64 *list = (struct nlist_64 *)(file + header->localSymbolsOffset + symbols->nlistOffset); char *strings = (char *)(file + header->localSymbolsOffset + symbols->stringsOffset); for (size_t i = 0; i < symbols->nlistCount; ++i) { if (!strcmp(strings + list[i].n_un.n_strx, symbol)) { uintptr_t _address = list[i].n_value + all_image_infos->sharedCacheSlide; Dl_info info; dladdr((void *)_address, &info); if (info.dli_fbase == base) { address = (void *)_address; break; } } } done: munmap(file, buffer.st_size); return address; } bool hook(void *old, void *new) { initialize_if_needed(); #define CHECK(x) \ do { \ if (!(x)) { \ NSLog(CFSTR("hook failed: " #x)); \ return false; \ } \ } while (0) CHECK(active_hooks < breakpoints); arm_debug_state64_t state = {}; state.__bvr[active_hooks] = (uintptr_t)old; // DBGBCR_EL1 // .BT = 0b0000 << 20 (unlinked address match) // .BAS = 0xF << 5 (A64) // .PMC = 0b10 << 1 (user) // .E = 0b1 << 0 (enable) state.__bcr[active_hooks] = 0x1e5; CHECK(task_set_state(mach_task_self(), ARM_DEBUG_STATE64, (thread_state_t)&state, ARM_DEBUG_STATE64_COUNT) == KERN_SUCCESS); #undef CHECK bool success = true; thread_act_array_t threads; mach_msg_type_number_t thread_count = ARM_DEBUG_STATE64_COUNT; task_threads(mach_task_self(), &threads, &thread_count); for (int i = 0; i < thread_count; ++i) { if (thread_set_state(threads[i], ARM_DEBUG_STATE64, (thread_state_t)&state, ARM_DEBUG_STATE64_COUNT) != KERN_SUCCESS) { NSLog(CFSTR("hook failed: could not set thread 0x%x debug state"), threads[i]); success = false; goto done; } } hooks[active_hooks++] = (struct hook){(uintptr_t)old, (uintptr_t) new}; done: for (int i = 0; i < thread_count; ++i) { mach_port_deallocate(mach_task_self(), threads[i]); } vm_deallocate(mach_task_self(), (vm_address_t)threads, thread_count * sizeof(*threads)); return success; } #else void *find_symbol(void *base, char *symbol) { return NULL; } bool hook(void *old, void *new) { return false; } #endif ================================================ FILE: app/hook.h ================================================ // // hook.h // iSH // // Created by Saagar Jha on 12/29/22. // #ifndef hook_h #define hook_h #include void *find_symbol(void *base, char *symbol); bool hook(void *old, void *new); #endif /* hook_h */ ================================================ FILE: app/iOS.xcconfig ================================================ SDKROOT = iphoneos TARGETED_DEVICE_FAMILY = 1,2 // iPhone, iPad SUPPORTED_PLATFORMS = iphonesimulator iphoneos LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/Frameworks HEADER_SEARCH_PATHS = $(SRCROOT) ================================================ FILE: app/iOSFS.h ================================================ // // iOSFS.h // iSH // // Created by Noah Peeters on 26.10.19. // extern const struct fs_ops iosfs; extern const struct fs_ops iosfs_unsafe; void iosfs_init(void); void iosfs_clear_all_bookmarks(void); // for recovery ================================================ FILE: app/iOSFS.m ================================================ // // iOSFS.m // iSH // // Created by Noah Peeters on 26.10.19. // #import #import #include #include "SceneDelegate.h" #include "iOSFS.h" #include "kernel/fs.h" #include "kernel/errno.h" #include "fs/path.h" #include "fs/real.h" const NSFileCoordinatorWritingOptions NSFileCoordinatorWritingForCreating = NSFileCoordinatorWritingForMerging; @interface DirectoryPicker : NSObject @property NSArray *urls; @property lock_t lock; @property cond_t cond; @end @implementation DirectoryPicker - (instancetype)init { if (self = [super init]) { lock_init(&_lock); cond_init(&_cond); } return self; } - (void)documentPickerWasCancelled:(UIDocumentPickerViewController *)controller { [self documentPicker:controller didPickDocumentsAtURLs:@[]]; } - (void)presentationControllerDidDismiss:(UIPresentationController *)presentationController { [self documentPickerWasCancelled:(UIDocumentPickerViewController *)presentationController]; } - (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentsAtURLs:(NSArray *)urls { lock(&_lock); self.urls = urls; notify(&_cond); unlock(&_lock); } - (int)askForURL:(NSURL **)url { TerminalViewController *terminalViewController = currentTerminalViewController; if (!terminalViewController) return _ENODEV; dispatch_async(dispatch_get_main_queue(), ^(void) { UIDocumentPickerViewController *picker = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:@[ @"public.folder" ] inMode:UIDocumentPickerModeOpen]; picker.delegate = self; if (@available(iOS 13, *)) { } else { picker.allowsMultipleSelection = YES; } picker.presentationController.delegate = self; [terminalViewController presentViewController:picker animated:true completion:nil]; }); lock(&_lock); while (_urls == nil) { int err = wait_for(&_cond, &_lock, NULL); if (err < 0) { unlock(&_lock); return err; } } NSArray *urls = _urls; _urls = nil; unlock(&_lock); if (@available(iOS 13, *)) { assert(urls.count <= 1); } if (urls.count == 0) return _ECANCELED; *url = urls[0]; return 0; } - (void)dealloc { cond_destroy(&_cond); } @end static NSURL *url_for_mount(struct mount *mount) { return (__bridge NSURL *) mount->data; } static NSString *const kMountBookmarks = @"iOS Mount Bookmarks"; #define BOOKMARK_PATH_ENCODING NSISOLatin1StringEncoding // To avoid locking issues, only access from the main thread static NSMutableDictionary *ios_mount_bookmarks; static bool mount_from_bookmarks = false; // This is a hack because I am bad at parameter passing static void sync_bookmarks(void) { [NSUserDefaults.standardUserDefaults setObject:ios_mount_bookmarks forKey:kMountBookmarks]; } void iosfs_init(void) { ios_mount_bookmarks = [NSUserDefaults.standardUserDefaults dictionaryForKey:kMountBookmarks].mutableCopy; if (ios_mount_bookmarks == nil) ios_mount_bookmarks = [NSMutableDictionary new]; mount_from_bookmarks = true; for (NSString *path in ios_mount_bookmarks.allKeys) { const char *point = [path cStringUsingEncoding:BOOKMARK_PATH_ENCODING]; int err = do_mount(&iosfs, point, point, "", 0); if (err < 0) { NSLog(@"restoring bookmark %@ failed with error %d", path, err); [ios_mount_bookmarks removeObjectForKey:path]; } } mount_from_bookmarks = false; sync_bookmarks(); } void iosfs_clear_all_bookmarks(void) { [ios_mount_bookmarks removeAllObjects]; sync_bookmarks(); } static int iosfs_mount(struct mount *mount) { NSURL *url = nil; if (mount_from_bookmarks) { NSString *bookmarkName = [NSString stringWithCString:mount->source encoding:BOOKMARK_PATH_ENCODING]; url = [NSURL URLByResolvingBookmarkData:ios_mount_bookmarks[bookmarkName] options:0 relativeToURL:nil bookmarkDataIsStale:NULL error:nil]; if (url != nil && ![url startAccessingSecurityScopedResource]) { return _EPERM; } } if (url == nil) { DirectoryPicker *picker = [DirectoryPicker new]; int err = [picker askForURL:&url]; if (err) return err; if (![url startAccessingSecurityScopedResource]) return _EPERM; } // Overwrite url & base path mount->data = (void *) CFBridgingRetain(url); free((void *) mount->source); mount->source = strdup([[url path] UTF8String]); if (mount_param_flag(mount->info, "unsafe")) { mount->fs = &iosfs_unsafe; } if (!mount_from_bookmarks) { NSData *bookmark = [url bookmarkDataWithOptions:0 includingResourceValuesForKeys:nil relativeToURL:nil error:nil]; NSString *path = [NSString stringWithCString:mount->point encoding:BOOKMARK_PATH_ENCODING]; if (bookmark != nil) { dispatch_async(dispatch_get_main_queue(), ^{ ios_mount_bookmarks[path] = bookmark; sync_bookmarks(); }); } } return realfs.mount(mount); } static int iosfs_umount(struct mount *mount) { NSString *path = [NSString stringWithCString:mount->point encoding:BOOKMARK_PATH_ENCODING]; dispatch_async(dispatch_get_main_queue(), ^{ [ios_mount_bookmarks removeObjectForKey:path]; sync_bookmarks(); }); NSURL *url = url_for_mount(mount); [url stopAccessingSecurityScopedResource]; CFBridgingRelease(mount->data); return 0; } static NSURL *url_for_path_in_mount(struct mount *mount, const char *path) { if (path[0] == '/') path++; return [url_for_mount(mount) URLByAppendingPathComponent:[NSString stringWithUTF8String:path] isDirectory:NO]; } const char *path_for_url_in_mount(struct mount *mount, NSURL *url, const char *fallback) { NSString *mount_path = url_for_mount(mount).path; NSString *url_path = url.path; // The `/private` prefix is a special case as described in the documentation of `URLByStandardizingPath`. if ([mount_path hasPrefix:@"/private/"]) mount_path = [mount_path substringFromIndex:8]; if ([url_path hasPrefix:@"/private/"]) url_path = [url_path substringFromIndex:8]; if (![url_path hasPrefix:mount_path]) return fallback; return [url_path substringFromIndex:[mount_path length]].UTF8String; } static int iosfs_stat(struct mount *mount, const char *path, struct statbuf *fake_stat); extern const struct fd_ops iosfs_fdops; static int posixErrorFromNSError(NSError *error) { if (error == nil) return 0; while (error != nil) { if ([error.domain isEqualToString:NSPOSIXErrorDomain]) { return err_map((int) error.code); } error = error.userInfo[NSUnderlyingErrorKey]; } return _EINVAL; } static int combine_error(NSError *coordinatorError, int err) { int posix_error = posixErrorFromNSError(coordinatorError); return posix_error ? posix_error : err; } static struct fd *iosfs_open(struct mount *mount, const char *path, int flags, int mode) { NSURL *url = url_for_path_in_mount(mount, path); // FIXME: this does a redundant file coordination operation struct statbuf stats; int err = iosfs_stat(mount, path, &stats); if (err == 0 && S_ISREG(stats.mode)) { NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil]; __block NSError *error = nil; __block struct fd *fd; __block dispatch_semaphore_t file_opened = dispatch_semaphore_create(0); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void){ void (^operation)(NSURL *url) = ^(NSURL *url) { fd = realfs_open(mount, path_for_url_in_mount(mount, url, path), flags, mode); if (IS_ERR(fd)) { dispatch_semaphore_signal(file_opened); } else { fd->ops = &iosfs_fdops; dispatch_semaphore_t file_closed = dispatch_semaphore_create(0); fd->data = (__bridge void *) file_closed; dispatch_semaphore_signal(file_opened); dispatch_semaphore_wait(file_closed, DISPATCH_TIME_FOREVER); } }; int options; if (!(flags & O_WRONLY_) && !(flags & O_RDWR_)) { options = NSFileCoordinatorReadingWithoutChanges; } else if (flags & O_CREAT_) { options = NSFileCoordinatorWritingForCreating; } else { options = NSFileCoordinatorWritingForMerging; } [coordinator coordinateReadingItemAtURL:url options:options error:&error byAccessor:operation]; }); dispatch_semaphore_wait(file_opened, DISPATCH_TIME_FOREVER); int posix_error = posixErrorFromNSError(error); return posix_error ? ERR_PTR(posix_error) : fd; } struct fd *fd = realfs_open(mount, path, flags, mode); if (!IS_ERR(fd)) fd->ops = &iosfs_fdops; return fd; } int iosfs_close(struct fd *fd) { int err = realfs.close(fd); if (fd->data != NULL) { dispatch_semaphore_t file_closed = (__bridge dispatch_semaphore_t) fd->data; dispatch_semaphore_signal(file_closed); } return err; } static int iosfs_rename(struct mount *mount, const char *src, const char *dst) { NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil]; NSURL *src_url = url_for_path_in_mount(mount, src); NSURL *dst_url = url_for_path_in_mount(mount, dst); NSError *error; __block int err; [coordinator coordinateWritingItemAtURL:src_url options:NSFileCoordinatorWritingForMoving error:&error byAccessor:^(NSURL *url) { [coordinator itemAtURL:url willMoveToURL:dst_url]; err = realfs.rename(mount, path_for_url_in_mount(mount, url, src), dst); [coordinator itemAtURL:url didMoveToURL:dst_url]; }]; return combine_error(error, err); } static int iosfs_symlink(struct mount *mount, const char *target, const char *link) { NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil]; NSURL *dst_url = url_for_path_in_mount(mount, link); NSError *error; __block int err; [coordinator coordinateWritingItemAtURL:dst_url options:NSFileCoordinatorWritingForCreating error:&error byAccessor:^(NSURL *url) { err = realfs.symlink(mount, path_for_url_in_mount(mount, url, target), link); }]; return combine_error(error, err); } static int iosfs_mknod(struct mount *mount, const char *path, mode_t_ mode, dev_t_ dev) { NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil]; NSURL *in_url = url_for_path_in_mount(mount, path); NSError *error; __block int err; [coordinator coordinateWritingItemAtURL:in_url options:NSFileCoordinatorWritingForCreating error:&error byAccessor:^(NSURL *url) { err = realfs.mknod(mount, path_for_url_in_mount(mount, url, path), mode, dev); }]; return combine_error(error, err); } static int iosfs_setattr(struct mount *mount, const char *path, struct attr attr) { NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil]; NSURL *in_url = url_for_path_in_mount(mount, path); NSError *error; __block int err; [coordinator coordinateWritingItemAtURL:in_url options:NSFileCoordinatorWritingContentIndependentMetadataOnly error:&error byAccessor:^(NSURL *url) { err = realfs.setattr(mount, path_for_url_in_mount(mount, url, path), attr); }]; return combine_error(error, err); } static int iosfs_fsetattr(struct fd *fd, struct attr attr) { return realfs.fsetattr(fd, attr); } static ssize_t iosfs_readlink(struct mount *mount, const char *path, char *buf, size_t bufsize) { NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil]; NSURL *in_url = url_for_path_in_mount(mount, path); NSError *error; __block ssize_t size; [coordinator coordinateReadingItemAtURL:in_url options:NSFileCoordinatorReadingWithoutChanges error:&error byAccessor:^(NSURL *url) { size = realfs.readlink(mount, path_for_url_in_mount(mount, url, path), buf, bufsize); }]; int posix_error = posixErrorFromNSError(error); return posix_error ? posix_error : size; } static int iosfs_getpath(struct fd *fd, char *buf) { return realfs.getpath(fd, buf); } static int iosfs_link(struct mount *mount, const char *src, const char *dst) { NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil]; NSURL *dst_url = url_for_path_in_mount(mount, dst); NSError *error; __block int err; [coordinator coordinateWritingItemAtURL:dst_url options:NSFileCoordinatorWritingForCreating error:&error byAccessor:^(NSURL *url) { err = realfs.link(mount, src, path_for_url_in_mount(mount, url, dst)); }]; return combine_error(error, err); } static int iosfs_unlink(struct mount *mount, const char *path) { NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil]; NSURL *in_url = url_for_path_in_mount(mount, path); NSError *error; __block int err; [coordinator coordinateWritingItemAtURL:in_url options:NSFileCoordinatorWritingForDeleting error:&error byAccessor:^(NSURL *url) { err = realfs.unlink(mount, path_for_url_in_mount(mount, url, path)); }]; return combine_error(error, err); } static int iosfs_rmdir(struct mount *mount, const char *path) { NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil]; NSURL *in_url = url_for_path_in_mount(mount, path); NSError *error; __block int err; [coordinator coordinateWritingItemAtURL:in_url options:NSFileCoordinatorWritingForDeleting error:&error byAccessor:^(NSURL *url) { err = realfs.rmdir(mount, path_for_url_in_mount(mount, url, path)); }]; return combine_error(error, err); } static int iosfs_stat(struct mount *mount, const char *path, struct statbuf *fake_stat) { NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil]; NSURL *in_url = url_for_path_in_mount(mount, path); NSError *error; __block int err; [coordinator coordinateReadingItemAtURL:in_url options:NSFileCoordinatorReadingWithoutChanges error:&error byAccessor:^(NSURL *url) { err = realfs.stat(mount, path_for_url_in_mount(mount, url, path), fake_stat); }]; return combine_error(error, err); } static int iosfs_fstat(struct fd *fd, struct statbuf *fake_stat) { int err = realfs.fstat(fd, fake_stat); return err; } static int iosfs_utime(struct mount *mount, const char *path, struct timespec atime, struct timespec mtime) { NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil]; NSURL *in_url = url_for_path_in_mount(mount, path); NSError *error; __block int err; [coordinator coordinateWritingItemAtURL:in_url options:NSFileCoordinatorWritingContentIndependentMetadataOnly error:&error byAccessor:^(NSURL *url) { err = realfs.utime(mount, path_for_url_in_mount(mount, url, path), atime, mtime); }]; return combine_error(error, err); } static int iosfs_mkdir(struct mount *mount, const char *path, mode_t_ mode) { NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil]; NSURL *in_url = url_for_path_in_mount(mount, path); NSError *error; __block int err; [coordinator coordinateWritingItemAtURL:in_url options:NSFileCoordinatorWritingForCreating error:&error byAccessor:^(NSURL *url) { err = realfs.mkdir(mount, path_for_url_in_mount(mount, url, path), mode); }]; return combine_error(error, err); } static int iosfs_flock(struct fd *fd, int operation) { return realfs.flock(fd, operation); } const struct fs_ops iosfs = { .name = "ios", .magic = 0x694f5320, .mount = iosfs_mount, .umount = iosfs_umount, .statfs = realfs_statfs, .open = iosfs_open, .readlink = iosfs_readlink, .link = iosfs_link, .unlink = iosfs_unlink, .rmdir = iosfs_rmdir, .rename = iosfs_rename, .symlink = iosfs_symlink, .mknod = iosfs_mknod, .close = iosfs_close, .stat = iosfs_stat, .fstat = iosfs_fstat, .setattr = iosfs_setattr, .fsetattr = iosfs_fsetattr, .utime = iosfs_utime, .getpath = iosfs_getpath, .flock = iosfs_flock, .mkdir = iosfs_mkdir, }; const struct fs_ops iosfs_unsafe = { .name = "ios-unsafe", .magic = 0x694f5321, .mount = iosfs_mount, .umount = iosfs_umount, .statfs = realfs_statfs, .open = realfs_open, .readlink = realfs_readlink, .link = realfs_link, .unlink = realfs_unlink, .rmdir = realfs_rmdir, .rename = realfs_rename, .symlink = realfs_symlink, .mknod = realfs_mknod, .close = realfs_close, .stat = realfs_stat, .fstat = realfs_fstat, .setattr = realfs_setattr, .fsetattr = realfs_fsetattr, .utime = realfs_utime, .getpath = realfs_getpath, .flock = realfs_flock, .mkdir = realfs_mkdir, }; const struct fd_ops iosfs_fdops = { .read = realfs_read, .write = realfs_write, .readdir = realfs_readdir, .telldir = realfs_telldir, .seekdir = realfs_seekdir, .lseek = realfs_lseek, .mmap = realfs_mmap, .poll = realfs_poll, .fsync = realfs_fsync, .close = iosfs_close, .getflags = realfs_getflags, .setflags = realfs_setflags, }; ================================================ FILE: app/iSH.entitlements ================================================ com.apple.developer.user-fonts app-usage com.apple.security.application-groups $(PRODUCT_APP_GROUP_IDENTIFIER) ================================================ FILE: app/iSH.xcconfig ================================================ // Change this to change all the bundle IDs and app groups ROOT_BUNDLE_IDENTIFIER = app.ish.iSH // It's easiest to specify your development team ID in the project build settings, but you can alternatively put it here to reduce merge conflicts DEVELOPMENT_TEAM = // Choose logging channels to enable. Separate by spaces. Try "verbose strace". ISH_LOG = ISH_LOGGER = $(ISH_LOGGER_$(PLATFORM_NAME)) ISH_LOGGER_iphoneos = nslog ISH_LOGGER_iphonesimulator = nslog ISH_LOGGER_macosx = dprintf ROOTFS_URL = github.com/ish-app/roots/releases/download/g00712ff0a54b2839c5aa1a8ed758003ca65357dc/appstore-apk.tar.gz ================================================ FILE: app/main.m ================================================ // // main.m // iSH // // Created by Theodore Dubois on 10/17/17. // #import #import "AppDelegate.h" #import "ExceptionExfiltrator.h" int main(int argc, char * argv[]) { NSSetUncaughtExceptionHandler(iSHExceptionHandler); @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } } ================================================ FILE: app/terminal/term.css ================================================ body { margin: 0; background: transparent; overflow: hidden; } #terminal { position: absolute; top: 0; bottom: 0; left: 0; right: 0; } ================================================ FILE: app/terminal/term.html ================================================
================================================ FILE: app/terminal/term.js ================================================ hterm.defaultStorage = new lib.Storage.Memory(); window.onload = async function() { await lib.init(); window.term = new hterm.Terminal(); // make everything invisible so as to not be embarrassing term.getPrefs().set('background-color', 'transparent'); term.getPrefs().set('foreground-color', 'transparent'); term.getPrefs().set('cursor-color', 'transparent'); term.getPrefs().set('terminal-encoding', 'iso-2022'); term.getPrefs().set('enable-resize-status', false); term.getPrefs().set('copy-on-select', false); term.getPrefs().set('enable-clipboard-notice', false); term.getPrefs().set('user-css-text', termCss); term.getPrefs().set('screen-padding-size', 4); // Creating and preloading the