diff --git a/Cargo.lock b/Cargo.lock index 563f433..26f4ee0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,56 @@ dependencies = [ "memchr", ] +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + [[package]] name = "anyhow" version = "1.0.102" @@ -52,6 +102,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + [[package]] name = "deranged" version = "0.5.8" @@ -67,6 +123,29 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "env_filter" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -104,8 +183,6 @@ checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "fskit-rs" version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cacf239422def8840727438d526db61bcaff4d5e2342a89cc0fbba77d41a01b2" dependencies = [ "async-trait", "bytes", @@ -261,6 +338,12 @@ dependencies = [ "serde_core", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itertools" version = "0.14.0" @@ -276,6 +359,30 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "jiff" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f00b5dbd620d61dfdcb6007c9c1f6054ebd75319f163d886a9055cec1155073d" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e000de030ff8022ea1da3f466fbb0f3a809f5e51ed31f6dd931c35181ad8e6d7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "leb128fmt" version = "0.1.0" @@ -344,6 +451,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "parking_lot" version = "0.12.5" @@ -397,6 +510,21 @@ dependencies = [ "time", ] +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -639,8 +767,11 @@ name = "subtext" version = "0.1.0" dependencies = [ "async-trait", + "env_logger", "fskit-rs", "libc", + "tempfile", + "tokio", ] [[package]] @@ -758,6 +889,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index 9566002..2f63234 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,16 @@ name = "subtext" version = "0.1.0" edition = "2021" +[features] +default = [] +# Local development: extra logging and future dev-only tools (mock client, etc.). +dev = [] + [dependencies] async-trait = "0.1" -fskit-rs = "0.2" +env_logger = "0.11" +# Vendored fork: run ./scripts/bootstrap-vendor-fskit-rs.sh once (see VENDORED_FSKIT_RS_CHANGELOG.md). +fskit-rs = { path = "vendor/fskit-rs" } libc = "0.2" +tempfile = "3" +tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal"] } diff --git a/README.md b/README.md index 06cd9bb..48f0bd8 100644 --- a/README.md +++ b/README.md @@ -2,18 +2,28 @@ Stub FSKit backend: implements [`fskit-rs`](https://crates.io/crates/fskit-rs)’s `Filesystem` trait with `ENOSYS` for every call until you fill them in. -## Run +## Run (development) 1. **Rust** (current stable) and **Protocol Buffers compiler** — `protoc` must be on your `PATH` (e.g. `brew install protobuf` on macOS; `fskit-rs` needs it to generate code). -2. From the repo root: +2. **Vendor `fskit-rs` (first clone / after changing the fork):** we use a path dependency on `vendor/fskit-rs` (see [`VENDORED_FSKIT_RS_CHANGELOG.md`](VENDORED_FSKIT_RS_CHANGELOG.md)). From the repo root: ```sh - cargo build - cargo run + cargo fetch -p fskit-rs@0.2.0 + chmod +x scripts/bootstrap-vendor-fskit-rs.sh scripts/apply_subtext_fskit_rs_edits.py + ./scripts/bootstrap-vendor-fskit-rs.sh ``` -3. The binary currently only instantiates `StubFilesystem`. To serve a real FSKit session, call `fskit_rs::mount` with your handler and `MountOptions` (see the `fskit-rs` crate docs and its `basic_fs` example). +3. From the repo root, use the **`dev`** feature for local work (enables dev-only hooks; add more under `#[cfg(feature = "dev")]` / `src/dev.rs`): + + ```sh + cargo build --features dev + cargo run --features dev + ``` + +4. **What running does:** the binary calls `fskit_rs::mount` (same idea as [`basic_fs`](https://github.com/debox-network/fskit-rs/blob/main/examples/basic_fs.rs)) with [`StubFilesystem`](src/lib.rs) and a **temporary** mount point under the system temp directory, then blocks until **Ctrl+C** and unmounts. With **`--features dev`**, the vendored `fskit-rs` uses `MountOptions::skip_registration` so the TCP server can start **without** PlugInKit finding a registered appex (see changelog); you still need a full FSKit stack for `mount` to succeed in production. Optional: `RUST_LOG=info` for more logging from `fskit-rs`. + +5. Without `--features dev`, `cargo run` is the same flow minus dev-only hooks and without skipping registration. A build **without** `--features dev` is the default “non-dev” binary (e.g. `cargo build --release` for production-like artifacts). ## macOS diff --git a/VENDORED_FSKIT_RS_CHANGELOG.md b/VENDORED_FSKIT_RS_CHANGELOG.md new file mode 100644 index 0000000..8252ff1 --- /dev/null +++ b/VENDORED_FSKIT_RS_CHANGELOG.md @@ -0,0 +1,46 @@ +# Vendored `fskit-rs` — subtext delta log + +This document records **when** we took code from upstream **fskit-rs**, **what** we changed in the vendored copy under `vendor/fskit-rs/`, and **why**. The mechanical edits live in `scripts/apply_subtext_fskit_rs_edits.py` (run for you by `scripts/bootstrap-vendor-fskit-rs.sh`). + +## Upstream snapshot + +| Field | Value | +|--------|--------| +| **Crate** | [`fskit-rs`](https://crates.io/crates/fskit-rs) **0.2.0** | +| **Registry source** | `~/.cargo/registry/src/index.crates.io-*/fskit-rs-0.2.0` (from `cargo fetch -p fskit-rs@0.2.0`) | +| **Git tag (reference)** | [`v0.2.0`](https://github.com/debox-network/fskit-rs/releases/tag/v0.2.0) on [debox-network/fskit-rs](https://github.com/debox-network/fskit-rs) | +| **Vendored in subtext (first import)** | **2026-04-28** (date this workflow was added; re-run `bootstrap-vendor-fskit-rs.sh` after upstream bumps) | + +We do **not** use a separate git remote for the vendored tree; it is a **file copy** of the published 0.2.0 sources plus the edits below. Re-applying the script over a fresh 0.2.0 copy is the supported way to reconcile with upstream. + +## Why vendoring instead of a public fork + +- We needed **one optional code path** without waiting for an upstream API (`MountOptions` / `Session::new` behavior). +- A private or long-lived **git fork** would work, but a **path vendored crate** keeps all consumer changes in the **subtext repo** with a **single script** to reproduce the tree from crates.io. + +## Changes from upstream 0.2.0 (subtext patches) + +### 1. `SkipRegistration` and `MountOptions::skip_registration` + +- **Files:** `src/lib.rs` +- **What:** Introduced a public `SkipRegistration { server_port, fs_type }` and added `pub skip_registration: Option` to `MountOptions`, with `Default` setting `skip_registration: None` (preserving upstream behavior when unset). +- **Why:** The stock `Session::new` always calls `read_config()`, which uses PlugInKit + the appex `Info.plist` to discover the TCP port and FS type. That **fails** when no FSKit host extension is installed or elected (typical in CI or when experimenting without a paid signing setup). An explicit override lets the TCP listener start using **known** `server_port` and `fs_type` (e.g. `35367` and `fskitbridge`) for local testing. **System `mount(8)` still runs afterward**; skipping registration does not replace a full FSKit stack for real mounts. + +### 2. Session: honor `skip_registration` before `read_config()` + +- **Files:** `src/session.rs` +- **What:** If `opts.skip_registration` is `Some`, use that port and `fs_type`; otherwise call `read_config(&opts.fskit_id)?` as before. +- **Why:** Same as above: optional bypass of registration/plist discovery only. + +## Reconciliation checklist (future you / upstream) + +- [ ] Re-fetch registry: `cargo fetch -p fskit-rs@0.2.0` (or a newer version if you bump). +- [ ] Re-run `./scripts/bootstrap-vendor-fskit-rs.sh`. +- [ ] If upstream adds an official equivalent API, **drop** the Python edits, switch `Cargo.toml` back to a crates.io version, and delete this vendor tree. + +## Scripts + +| Script | Role | +|--------|------| +| `scripts/bootstrap-vendor-fskit-rs.sh` | Copy 0.2.0 from the cargo registry into `vendor/fskit-rs/`, then apply edits. | +| `scripts/apply_subtext_fskit_rs_edits.py` | Idempotent string edits; **this is the source of truth** for the diff vs. pristine 0.2.0. | diff --git a/scripts/bootstrap-vendor-fskit-rs.sh b/scripts/bootstrap-vendor-fskit-rs.sh new file mode 100755 index 0000000..61384a1 --- /dev/null +++ b/scripts/bootstrap-vendor-fskit-rs.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# Populates vendor/fskit-rs from the local cargo registry. +# Run from the repository root: ./scripts/bootstrap-vendor-fskit-rs.sh +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +VERSION="0.2.0" +REG_SRC="" + +shopt -s nullglob +for d in "${CARGO_HOME:-$HOME/.cargo}"/registry/src/index.crates.io-*/"fskit-rs-${VERSION}"; do + if [[ -d "$d" ]]; then + REG_SRC="$d" + break + fi +done +shopt -u nullglob + +if [[ -z "$REG_SRC" ]]; then + echo "fskit-rs-${VERSION} not found in the cargo registry." + echo "From ${ROOT}, run: cargo fetch -p fskit-rs@${VERSION}" + echo "then re-run this script." + exit 1 +fi + +echo "Using source: $REG_SRC" +rm -rf "${ROOT}/vendor/fskit-rs" +cp -a "$REG_SRC" "${ROOT}/vendor/fskit-rs" + +echo "Done: ${ROOT}/vendor/fskit-rs copied from crates.io source." +echo "Now re-apply subtext vendored edits manually and record them in:" +echo " vendor/fskit-rs/VENDORED_FSKIT_RS_CHANGELOG.md" diff --git a/src/dev.rs b/src/dev.rs new file mode 100644 index 0000000..c0de1af --- /dev/null +++ b/src/dev.rs @@ -0,0 +1,5 @@ +//! Development-only code (built with `cargo run --features dev`). + +pub fn init() { + eprintln!("subtext: dev mode enabled"); +} diff --git a/src/lib.rs b/src/lib.rs index 854e413..44747de 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,7 @@ //! FSKit backend: [`StubFilesystem`] implements [`fskit_rs::Filesystem`] with `ENOSYS` stubs. +pub mod backend; + use std::ffi::OsStr; use async_trait::async_trait; diff --git a/src/main.rs b/src/main.rs index 5242c47..3fd04b0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,56 @@ -//! Binary entry point. Wire up `fskit_rs::mount` and your [`subtext::StubFilesystem`] when ready. +//! Binary entry point: mounts like [`fskit-rs`’s `basic_fs` example](https://github.com/debox-network/fskit-rs/blob/main/examples/basic_fs.rs). -fn main() { - let _ = subtext::StubFilesystem::default(); +#[cfg(feature = "dev")] +mod dev; + +use fskit_rs::{session, MountOptions}; +use subtext::StubFilesystem; +use tokio::signal; + +#[tokio::main] +async fn main() -> session::Result<()> { + let _ = env_logger::try_init(); + + #[cfg(feature = "dev")] + dev::init(); + + let handler = StubFilesystem::default(); + + let temp = tempfile::tempdir()?; + let mut opts = MountOptions::default(); + opts.mount_point = temp.path().to_path_buf(); + // Without a registered FSKit appex, use fixed port/type (must match FSKitBridge when testing for real). + #[cfg(feature = "dev")] + { + opts.skip_registration = Some(fskit_rs::SkipRegistration { + server_port: 35367, + fs_type: "fskitbridge".into(), + }); + } + + println!( + "Mounting example filesystem at {}...", + opts.mount_point.display() + ); + + let session = match fskit_rs::mount(handler, opts.clone()).await { + Ok(session) => session, + Err(err) => { + eprintln!("Mount failed. Ensure the FSKit host app is installed and enabled."); + return Err(err); + } + }; + + println!( + "Mounted. Press Ctrl+C to unmount {}.", + opts.mount_point.display() + ); + + signal::ctrl_c().await?; + + drop(session); + + println!("Unmounted {}.", opts.mount_point.display()); + + Ok(()) } diff --git a/vendor/README.md b/vendor/README.md new file mode 100644 index 0000000..b085a54 --- /dev/null +++ b/vendor/README.md @@ -0,0 +1,21 @@ +# Vendored `fskit-rs` + +This directory holds a **copy of [`fskit-rs` 0.2.0](https://crates.io/crates/fskit-rs/0.2.0)** from your local Cargo registry, with **small subtext-specific changes** (see `../VENDORED_FSKIT_RS_CHANGELOG.md`). + +## Populate or refresh + +From the repository root (after `cargo fetch -p fskit-rs@0.2.0` if needed): + +```sh +./scripts/bootstrap-vendor-fskit-rs.sh +``` + +This copies `~/.cargo/registry/src/.../fskit-rs-0.2.0` to `vendor/fskit-rs` and runs `scripts/apply_subtext_fskit_rs_edits.py`. + +## Cargo layout + +`subtext` depends on the crate at `vendor/fskit-rs` via a **path dependency** in the workspace `Cargo.toml` (not `cargo vendor` of the full graph). That keeps a single forked crate in-tree while following the usual “vendor directory next to the app” layout. + +## License + +Upstream `fskit-rs` remains **MIT OR Apache-2.0**; vendored files keep the same `LICENSE-*` files from the crate. diff --git a/vendor/fskit-rs/.cargo-ok b/vendor/fskit-rs/.cargo-ok new file mode 100644 index 0000000..5f8b795 --- /dev/null +++ b/vendor/fskit-rs/.cargo-ok @@ -0,0 +1 @@ +{"v":1} \ No newline at end of file diff --git a/vendor/fskit-rs/.cargo_vcs_info.json b/vendor/fskit-rs/.cargo_vcs_info.json new file mode 100644 index 0000000..dc4ec47 --- /dev/null +++ b/vendor/fskit-rs/.cargo_vcs_info.json @@ -0,0 +1,6 @@ +{ + "git": { + "sha1": "4103aa2a76d4a078fc70b391df7db7e625d72ffc" + }, + "path_in_vcs": "" +} \ No newline at end of file diff --git a/vendor/fskit-rs/.gitignore b/vendor/fskit-rs/.gitignore new file mode 100644 index 0000000..0e5af40 --- /dev/null +++ b/vendor/fskit-rs/.gitignore @@ -0,0 +1,28 @@ +# Generated by Cargo +# will have compiled files and executables +debug +target + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +# Generated by cargo mutants +# Contains mutation testing data +**/mutants.out*/ + +# RustRover +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +.idea/ + +# macOS +.DS_Store diff --git a/vendor/fskit-rs/CHANGELOG.md b/vendor/fskit-rs/CHANGELOG.md new file mode 100644 index 0000000..d17424f --- /dev/null +++ b/vendor/fskit-rs/CHANGELOG.md @@ -0,0 +1,24 @@ +# Changelog + +## [Unreleased] + +## [v0.2.0] - 2026-04-20 + +### Added + +- `activate(app_name)` and `uninstall(app_name)`. + +### Changed + +- Refined the installer API around `/Applications/`. +- Changed `install(path, force)` to non-destructive `install(source)`. +- Improved activation diagnostics and session startup using the active registered appex. +- Strengthened uninstall cleanup for LaunchServices, PlugInKit, `Application Scripts`, and `Containers`. + +## [v0.1.0] - 2025-11-08 + +- Initial release of **fskit-rs**. See details in the [README](README.md). + +[Unreleased]: ../../compare/v0.2.0...HEAD +[v0.2.0]: ../../releases/tag/v0.2.0 +[v0.1.0]: ../../releases/tag/v0.1.0 diff --git a/vendor/fskit-rs/Cargo.toml b/vendor/fskit-rs/Cargo.toml new file mode 100644 index 0000000..481d107 --- /dev/null +++ b/vendor/fskit-rs/Cargo.toml @@ -0,0 +1,89 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2024" +name = "fskit-rs" +version = "0.2.0" +authors = [ + "Debox Team ", + "Pavel Denisov ", +] +build = "build.rs" +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "Rust crate for FSKitBridge protocol & socket layer (TCP + Protobuf)" +readme = "README.md" +keywords = [ + "macos", + "fskit", + "extensionkit", + "filesystem", +] +categories = [ + "filesystem", + "network-programming", + "os::macos-apis", +] +license = "MIT OR Apache-2.0" +repository = "https://github.com/debox-network/fskit-rs" + +[lib] +name = "fskit_rs" +path = "src/lib.rs" + +[[example]] +name = "basic_fs" +path = "examples/basic_fs.rs" + +[dependencies.async-trait] +version = "0.1" + +[dependencies.bytes] +version = "1.10" + +[dependencies.futures] +version = "0.3" + +[dependencies.libc] +version = "0.2" + +[dependencies.log] +version = "0.4" +default-features = false + +[dependencies.plist] +version = "1.8" + +[dependencies.prost] +version = "0.14" + +[dependencies.prost-types] +version = "0.14" + +[dependencies.regex] +version = "1.11" + +[dependencies.thiserror] +version = "2.0" + +[dependencies.tokio] +version = "1.47" +features = ["full"] + +[dev-dependencies.env_logger] +version = "0.11" + +[build-dependencies.prost-build] +version = "0.14" diff --git a/vendor/fskit-rs/Cargo.toml.orig b/vendor/fskit-rs/Cargo.toml.orig new file mode 100644 index 0000000..4030bfb --- /dev/null +++ b/vendor/fskit-rs/Cargo.toml.orig @@ -0,0 +1,37 @@ +[package] +name = "fskit-rs" +version = "0.2.0" +edition = "2024" +license = "MIT OR Apache-2.0" +readme = "README.md" +description = "Rust crate for FSKitBridge protocol & socket layer (TCP + Protobuf)" +repository = "https://github.com/debox-network/fskit-rs" +authors = [ + "Debox Team ", + "Pavel Denisov ", +] +keywords = ["macos", "fskit", "extensionkit", "filesystem"] +categories = ["filesystem", "network-programming", "os::macos-apis"] + +[dependencies] +async-trait = "0.1" +bytes = "1.10" +futures = "0.3" +libc = "0.2" +log = { version = "0.4", default-features = false } +plist = "1.8" +prost = "0.14" +prost-types = "0.14" +regex = "1.11" +thiserror = "2.0" +tokio = { version = "1.47", features = ["full"] } + +[build-dependencies] +prost-build = "0.14" + +[dev-dependencies] +env_logger = "0.11" + +[[example]] +name = "basic_fs" +path = "examples/basic_fs.rs" diff --git a/vendor/fskit-rs/LICENSE-APACHE b/vendor/fskit-rs/LICENSE-APACHE new file mode 100644 index 0000000..7e99f52 --- /dev/null +++ b/vendor/fskit-rs/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2025 Debox Network + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/fskit-rs/LICENSE-MIT b/vendor/fskit-rs/LICENSE-MIT new file mode 100644 index 0000000..6cd0eb5 --- /dev/null +++ b/vendor/fskit-rs/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Debox Network and contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/fskit-rs/NOTICE b/vendor/fskit-rs/NOTICE new file mode 100644 index 0000000..b3931fb --- /dev/null +++ b/vendor/fskit-rs/NOTICE @@ -0,0 +1,10 @@ +Copyright 2025 Debox Network + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed +under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. diff --git a/vendor/fskit-rs/README.md b/vendor/fskit-rs/README.md new file mode 100644 index 0000000..1514e6b --- /dev/null +++ b/vendor/fskit-rs/README.md @@ -0,0 +1,65 @@ +# fskit-rs + +A Rust companion to [**FSKitBridge**](https://github.com/debox-network/FSKitBridge) that provides the protocol & socket +layer and a trait for implementing your macOS FSKit backend in Rust. + +> **Requires macOS 15.4+** + +## Why fskit-rs? + +**FSKit** runs file systems in user space on macOS, but its public API is Swift. **fskit-rs** lets you keep the engine +in Rust while **FSKitBridge** (Swift/appex) handles FSKit/XPC. The two talk over `TCP localhost` using length-delimited +Protobuf messages defined in `protocol.proto`. + +> Keep your core in Rust; let FSKitBridge speak Swift to macOS. + +## FSKitBridge + +This crate pairs with the Swift-side [**FSKitBridge**](https://github.com/debox-network/FSKitBridge) project (host app + +FSKitExt appex). For the Swift/FSKit integration details, see: + +- [Architecture](https://github.com/debox-network/FSKitBridge/tree/main?tab=readme-ov-file#architecture) +- [How to use](https://github.com/debox-network/FSKitBridge/tree/main?tab=readme-ov-file#how-to-use) +- [Tests](https://github.com/debox-network/FSKitBridge/tree/main?tab=readme-ov-file#tests) + +## What’s in this crate? + +- **Trait-based backend API:** Implement the `Filesystem` trait to handle file system operations. +- **Types & schema (`protocol.proto`):** Common FS/OS types re-exported from code generated by the protocol schema. The + schema is the wire contract between the Swift appex and this Rust backend — defining all RPC messages/enums. +- **Handle errors:** Unified `Error` (includes POSIX via `libc::*`) and `Result`. +- **Session runner:** `session::mount(fs, opts)` mounts and serves requests until dropped. +- **Installer helpers:** `install(source)`, `activate(app_name)`, and `uninstall(app_name)` utilities for + host app lifecycle (optional). + +Observed on current macOS versions: +- The installer helpers manage host apps in `/Applications/`, which remains the supported installation target + for this crate. +- During local experiments, non-`/Applications` paths may also work after the extension is enabled, but runtime behavior + is stateful and not guaranteed across identities, paths, or prior registrations. +- `install(source)` is intentionally non-destructive: it does not overwrite an existing host app at the final + destination and returns `AppInstalled` instead. +- `activate(app_name)` verifies the installed app bundle, then re-runs the registration steps whenever the host app is + not already active. +- If activation still fails, the error now distinguishes between "not registered", "registered from a different app + path", and "registered but not elected" states. +- `uninstall(app_name)` removes the installed app and also performs best-effort cleanup of related LaunchServices, + PlugInKit, `Application Scripts`, and `Containers` state for the exact installed bundle identities. + +> This crate is the transport + protocol + trait layer. You bring the actual file system logic. + +## Quick start + +A minimal example is provided in `examples/basic_fs.rs`. It starts a stub backend with default `MountOptions`, mounts +the file system (e.g., at `/tmp/fskitbridge`), and serves requests until you press **Ctrl+C**. Use it as a skeleton and +replace the `ENOSYS` stubs with your real logic. + +## License + +This project is dual-licensed under Apache-2.0 and MIT: + +- Apache License, Version 2.0 — [`LICENSE-APACHE`](./LICENSE-APACHE) +- MIT License — [`LICENSE-MIT`](./LICENSE-MIT) + +**Contributions:** Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the +work shall be dual-licensed as above, without additional terms or conditions. diff --git a/vendor/fskit-rs/build.rs b/vendor/fskit-rs/build.rs new file mode 100644 index 0000000..7dbd6e3 --- /dev/null +++ b/vendor/fskit-rs/build.rs @@ -0,0 +1,6 @@ +use std::io::Result; + +fn main() -> Result<()> { + prost_build::compile_protos(&["src/protocol.proto"], &["src"])?; + Ok(()) +} diff --git a/vendor/fskit-rs/examples/basic_fs.rs b/vendor/fskit-rs/examples/basic_fs.rs new file mode 100644 index 0000000..c83eb18 --- /dev/null +++ b/vendor/fskit-rs/examples/basic_fs.rs @@ -0,0 +1,244 @@ +use std::ffi::OsStr; +use std::fs; + +use async_trait::async_trait; +use tokio::signal; + +use fskit_rs::{ + AccessMask, DirectoryEntries, Error, Filesystem, Item, ItemAttributes, ItemType, MountOptions, + OpenMode, PathConfOperations, PreallocateFlag, ResourceIdentifier, Result, SetXattrPolicy, + StatFsResult, SupportedCapabilities, SyncFlags, TaskOptions, VolumeBehavior, VolumeIdentifier, + Xattrs, session, +}; + +#[derive(Clone)] +struct FsHandler; + +#[async_trait] +impl Filesystem for FsHandler { + async fn get_resource_identifier(&mut self) -> Result { + Err(Error::Posix(libc::ENOSYS)) + } + + async fn get_volume_identifier(&mut self) -> Result { + Err(Error::Posix(libc::ENOSYS)) + } + + async fn get_volume_behavior(&mut self) -> Result { + Err(Error::Posix(libc::ENOSYS)) + } + + async fn get_path_conf_operations(&mut self) -> Result { + Err(Error::Posix(libc::ENOSYS)) + } + + async fn get_volume_capabilities(&mut self) -> Result { + Err(Error::Posix(libc::ENOSYS)) + } + + async fn get_volume_statistics(&mut self) -> Result { + Err(Error::Posix(libc::ENOSYS)) + } + + async fn mount(&mut self, _options: TaskOptions) -> Result<()> { + Err(Error::Posix(libc::ENOSYS)) + } + + async fn unmount(&mut self) -> Result<()> { + Err(Error::Posix(libc::ENOSYS)) + } + + async fn synchronize(&mut self, _flags: SyncFlags) -> Result<()> { + Err(Error::Posix(libc::ENOSYS)) + } + + async fn get_attributes(&mut self, _item_id: u64) -> Result { + Err(Error::Posix(libc::ENOSYS)) + } + + async fn set_attributes( + &mut self, + _item_id: u64, + _attributes: ItemAttributes, + ) -> Result { + Err(Error::Posix(libc::ENOSYS)) + } + + async fn lookup_item(&mut self, _name: &OsStr, _directory_id: u64) -> Result { + Err(Error::Posix(libc::ENOSYS)) + } + + async fn reclaim_item(&mut self, _item_id: u64) -> Result<()> { + Err(Error::Posix(libc::ENOSYS)) + } + + async fn read_symbolic_link(&mut self, _item_id: u64) -> Result> { + Err(Error::Posix(libc::ENOSYS)) + } + + async fn create_item( + &mut self, + _name: &OsStr, + _type: ItemType, + _directory_id: u64, + _attributes: ItemAttributes, + ) -> Result { + Err(Error::Posix(libc::ENOSYS)) + } + + async fn create_symbolic_link( + &mut self, + _name: &OsStr, + _directory_id: u64, + _new_attributes: ItemAttributes, + _contents: Vec, + ) -> Result { + Err(Error::Posix(libc::ENOSYS)) + } + + async fn create_link( + &mut self, + _item_id: u64, + _name: &OsStr, + _directory_id: u64, + ) -> Result> { + Err(Error::Posix(libc::ENOSYS)) + } + + async fn remove_item( + &mut self, + _item_id: u64, + _name: &OsStr, + _directory_id: u64, + ) -> Result<()> { + Err(Error::Posix(libc::ENOSYS)) + } + + async fn rename_item( + &mut self, + _item_id: u64, + _source_directory_id: u64, + _source_name: &OsStr, + _destination_name: &OsStr, + _destination_directory_id: u64, + _over_item_id: Option, + ) -> Result> { + Err(Error::Posix(libc::ENOSYS)) + } + + async fn enumerate_directory( + &mut self, + _directory_id: u64, + _cookie: u64, + _verifier: u64, + ) -> Result { + Err(Error::Posix(libc::ENOSYS)) + } + + async fn activate(&mut self, _options: TaskOptions) -> Result { + Err(Error::Posix(libc::ENOSYS)) + } + + async fn deactivate(&mut self) -> Result<()> { + Err(Error::Posix(libc::ENOSYS)) + } + + async fn get_supported_xattr_names(&mut self, _item_id: u64) -> Result { + Err(Error::Posix(libc::ENOSYS)) + } + + async fn get_xattr(&mut self, _name: &OsStr, _item_id: u64) -> Result> { + Err(Error::Posix(libc::ENOSYS)) + } + + async fn set_xattr( + &mut self, + _name: &OsStr, + _value: Option>, + _item_id: u64, + _policy: SetXattrPolicy, + ) -> Result<()> { + Err(Error::Posix(libc::ENOSYS)) + } + + async fn get_xattrs(&mut self, _item_id: u64) -> Result { + Err(Error::Posix(libc::ENOSYS)) + } + + async fn open_item(&mut self, _item_id: u64, _modes: Vec) -> Result<()> { + Err(Error::Posix(libc::ENOSYS)) + } + + async fn close_item(&mut self, _item_id: u64, _modes: Vec) -> Result<()> { + Err(Error::Posix(libc::ENOSYS)) + } + + async fn read(&mut self, _item_id: u64, _offset: i64, _length: i64) -> Result> { + Err(Error::Posix(libc::ENOSYS)) + } + + async fn write(&mut self, _contents: Vec, _item_id: u64, _offset: i64) -> Result { + Err(Error::Posix(libc::ENOSYS)) + } + + async fn check_access(&mut self, _item_id: u64, _access: Vec) -> Result { + Err(Error::Posix(libc::ENOSYS)) + } + + async fn set_volume_name(&mut self, _name: Vec) -> Result> { + Err(Error::Posix(libc::ENOSYS)) + } + + async fn preallocate_space( + &mut self, + _item_id: u64, + _offset: i64, + _length: i64, + _flags: Vec, + ) -> Result { + Err(Error::Posix(libc::ENOSYS)) + } + + async fn deactivate_item(&mut self, _item_id: u64) -> Result<()> { + Err(Error::Posix(libc::ENOSYS)) + } +} + +#[tokio::main] +async fn main() -> session::Result<()> { + let _ = env_logger::try_init(); + + let handler = FsHandler; + + let opts = MountOptions::default(); + + if !opts.mount_point.exists() { + let _ = fs::create_dir(opts.mount_point.clone()); + } + + println!( + "Mounting example filesystem at {}...", + opts.mount_point.display() + ); + + let session = match fskit_rs::mount(handler, opts.clone()).await { + Ok(session) => session, + Err(err) => { + eprintln!("Mount failed. Ensure the FSKit host app is installed and enabled."); + return Err(err); + } + }; + + println!( + "Mounted. Press Ctrl+C to unmount {}.", + opts.mount_point.display() + ); + + signal::ctrl_c().await?; + + drop(session); + + println!("Unmounted {}.", opts.mount_point.display()); + + Ok(()) +} diff --git a/vendor/fskit-rs/src/handler.rs b/vendor/fskit-rs/src/handler.rs new file mode 100644 index 0000000..bb7d4d0 --- /dev/null +++ b/vendor/fskit-rs/src/handler.rs @@ -0,0 +1,344 @@ +use std::ffi::OsString; +use std::os::unix::ffi::OsStringExt; + +use log::warn; + +use crate::pb::check_access::AccessMask; +use crate::pb::preallocate_space::PreallocateFlag; +use crate::pb::set_xattr::SetXattrPolicy; +use crate::pb::synchronize::SyncFlags; +use crate::pb::{Success, request, response}; +use crate::{Error, Filesystem, ItemType, OpenMode, Result}; + +#[derive(Clone, Debug)] +pub(super) struct Handler +where + FS: Filesystem + Send + Sync + Clone + 'static, +{ + filesystem: FS, +} + +impl Handler +where + FS: Filesystem + Send + Sync + Clone + 'static, +{ + pub(super) fn new(filesystem: FS) -> Self { + Self { filesystem } + } + + pub(super) async fn handle(&mut self, request: request::Content) -> Result { + Ok(match request { + request::Content::GetResourceIdentifier(_) => { + match self.filesystem.get_resource_identifier().await { + Ok(res) => response::Content::ResourceIdentifier(res), + Err(Error::Posix(code)) => response::Content::PosixError(code), + } + } + request::Content::GetVolumeIdentifier(_) => { + match self.filesystem.get_volume_identifier().await { + Ok(res) => response::Content::VolumeIdentifier(res), + Err(Error::Posix(code)) => response::Content::PosixError(code), + } + } + request::Content::GetVolumeBehavior(_) => { + match self.filesystem.get_volume_behavior().await { + Ok(res) => response::Content::VolumeBehavior(res), + Err(Error::Posix(code)) => response::Content::PosixError(code), + } + } + request::Content::GetPathConfOperations(_) => { + match self.filesystem.get_path_conf_operations().await { + Ok(res) => response::Content::PathConfOperations(res), + Err(Error::Posix(code)) => response::Content::PosixError(code), + } + } + request::Content::GetVolumeCapabilities(_) => { + match self.filesystem.get_volume_capabilities().await { + Ok(res) => response::Content::SupportedCapabilities(res), + Err(Error::Posix(code)) => response::Content::PosixError(code), + } + } + request::Content::GetVolumeStatistics(_) => { + match self.filesystem.get_volume_statistics().await { + Ok(res) => response::Content::StatFsResult(res), + Err(Error::Posix(code)) => response::Content::PosixError(code), + } + } + request::Content::Mount(msg) => { + let Some(options) = msg.options else { + warn!("Mount request missing options"); + return Ok(response::Content::PosixError(libc::EINVAL)); + }; + match self.filesystem.mount(options).await { + Ok(_) => response::Content::Success(Success {}), + Err(Error::Posix(code)) => response::Content::PosixError(code), + } + } + request::Content::Unmount(_) => match self.filesystem.unmount().await { + Ok(_) => response::Content::Success(Success {}), + Err(Error::Posix(code)) => response::Content::PosixError(code), + }, + request::Content::Synchronize(msg) => match self + .filesystem + .synchronize(SyncFlags::try_from(msg.flags).unwrap_or_default()) + .await + { + Ok(_) => response::Content::Success(Success {}), + Err(Error::Posix(code)) => response::Content::PosixError(code), + }, + request::Content::GetAttributes(msg) => { + match self.filesystem.get_attributes(msg.item_id).await { + Ok(attrs) => response::Content::ItemAttributes(attrs), + Err(Error::Posix(code)) => response::Content::PosixError(code), + } + } + request::Content::SetAttributes(msg) => { + let Some(attributes) = msg.attributes else { + warn!("SetAttributes request missing attributes"); + return Ok(response::Content::PosixError(libc::EINVAL)); + }; + match self + .filesystem + .set_attributes(msg.item_id, attributes) + .await + { + Ok(attrs) => response::Content::ItemAttributes(attrs), + Err(Error::Posix(code)) => response::Content::PosixError(code), + } + } + request::Content::LookupItem(msg) => match self + .filesystem + .lookup_item(&OsString::from_vec(msg.name), msg.directory_id) + .await + { + Ok(item) => response::Content::Item(item), + Err(Error::Posix(code)) => response::Content::PosixError(code), + }, + request::Content::ReclaimItem(msg) => { + match self.filesystem.reclaim_item(msg.item_id).await { + Ok(_) => response::Content::Success(Success {}), + Err(Error::Posix(code)) => response::Content::PosixError(code), + } + } + request::Content::ReadSymbolicLink(msg) => { + match self.filesystem.read_symbolic_link(msg.item_id).await { + Ok(data) => response::Content::Data(data), + Err(Error::Posix(code)) => response::Content::PosixError(code), + } + } + request::Content::CreateItem(msg) => { + let Ok(item_type) = ItemType::try_from(msg.r#type) else { + warn!("CreateItem request contained unknown type: {}", msg.r#type); + return Ok(response::Content::PosixError(libc::EINVAL)); + }; + let Some(attributes) = msg.attributes else { + warn!("CreateItem request missing attributes"); + return Ok(response::Content::PosixError(libc::EINVAL)); + }; + match self + .filesystem + .create_item( + &OsString::from_vec(msg.name), + item_type, + msg.directory_id, + attributes, + ) + .await + { + Ok(item) => response::Content::Item(item), + Err(Error::Posix(code)) => response::Content::PosixError(code), + } + } + request::Content::CreateSymbolicLink(msg) => { + let Some(attributes) = msg.new_attributes else { + warn!("CreateSymbolicLink request missing attributes"); + return Ok(response::Content::PosixError(libc::EINVAL)); + }; + match self + .filesystem + .create_symbolic_link( + &OsString::from_vec(msg.name), + msg.directory_id, + attributes, + msg.contents, + ) + .await + { + Ok(item) => response::Content::Item(item), + Err(Error::Posix(code)) => response::Content::PosixError(code), + } + } + request::Content::CreateLink(msg) => match self + .filesystem + .create_link(msg.item_id, &OsString::from_vec(msg.name), msg.directory_id) + .await + { + Ok(data) => response::Content::Data(data), + Err(Error::Posix(code)) => response::Content::PosixError(code), + }, + request::Content::RemoveItem(msg) => match self + .filesystem + .remove_item(msg.item_id, &OsString::from_vec(msg.name), msg.directory_id) + .await + { + Ok(_) => response::Content::Success(Success {}), + Err(Error::Posix(code)) => response::Content::PosixError(code), + }, + request::Content::RenameItem(msg) => match self + .filesystem + .rename_item( + msg.item_id, + msg.source_directory_id, + &OsString::from_vec(msg.source_name), + &OsString::from_vec(msg.destination_name), + msg.destination_directory_id, + msg.over_item_id, + ) + .await + { + Ok(data) => response::Content::Data(data), + Err(Error::Posix(code)) => response::Content::PosixError(code), + }, + request::Content::EnumerateDirectory(msg) => match self + .filesystem + .enumerate_directory(msg.directory_id, msg.cookie, msg.verifier) + .await + { + Ok(entries) => response::Content::DirectoryEntries(entries), + Err(Error::Posix(code)) => response::Content::PosixError(code), + }, + request::Content::Activate(msg) => { + let Some(options) = msg.options else { + warn!("Activate request missing options"); + return Ok(response::Content::PosixError(libc::EINVAL)); + }; + match self.filesystem.activate(options).await { + Ok(item) => response::Content::Item(item), + Err(Error::Posix(code)) => response::Content::PosixError(code), + } + } + request::Content::Deactivate(_) => match self.filesystem.deactivate().await { + Ok(_) => response::Content::Success(Success {}), + Err(Error::Posix(code)) => response::Content::PosixError(code), + }, + request::Content::GetSupportedXattrNames(msg) => { + match self.filesystem.get_supported_xattr_names(msg.item_id).await { + Ok(xattrs) => response::Content::Xattrs(xattrs), + Err(Error::Posix(code)) => response::Content::PosixError(code), + } + } + request::Content::GetXattr(msg) => match self + .filesystem + .get_xattr(&OsString::from_vec(msg.name), msg.item_id) + .await + { + Ok(data) => response::Content::Data(data), + Err(Error::Posix(code)) => response::Content::PosixError(code), + }, + request::Content::SetXattr(msg) => match self + .filesystem + .set_xattr( + &OsString::from_vec(msg.name), + msg.value, + msg.item_id, + SetXattrPolicy::try_from(msg.policy).unwrap_or_default(), + ) + .await + { + Ok(_) => response::Content::Success(Success {}), + Err(Error::Posix(code)) => response::Content::PosixError(code), + }, + request::Content::GetXattrs(msg) => match self.filesystem.get_xattrs(msg.item_id).await + { + Ok(xattrs) => response::Content::Xattrs(xattrs), + Err(Error::Posix(code)) => response::Content::PosixError(code), + }, + request::Content::OpenItem(msg) => match self + .filesystem + .open_item( + msg.item_id, + msg.modes + .iter() + .filter_map(|&raw| OpenMode::try_from(raw).ok()) + .collect(), + ) + .await + { + Ok(_) => response::Content::Success(Success {}), + Err(Error::Posix(code)) => response::Content::PosixError(code), + }, + request::Content::CloseItem(msg) => match self + .filesystem + .close_item( + msg.item_id, + msg.modes + .iter() + .filter_map(|&raw| OpenMode::try_from(raw).ok()) + .collect(), + ) + .await + { + Ok(_) => response::Content::Success(Success {}), + Err(Error::Posix(code)) => response::Content::PosixError(code), + }, + request::Content::Read(msg) => match self + .filesystem + .read(msg.item_id, msg.offset, msg.length) + .await + { + Ok(data) => response::Content::Data(data), + Err(Error::Posix(code)) => response::Content::PosixError(code), + }, + request::Content::Write(msg) => match self + .filesystem + .write(msg.contents, msg.item_id, msg.offset) + .await + { + Ok(count) => response::Content::ByteCount(count), + Err(Error::Posix(code)) => response::Content::PosixError(code), + }, + request::Content::CheckAccess(msg) => match self + .filesystem + .check_access( + msg.item_id, + msg.access + .iter() + .filter_map(|&raw| AccessMask::try_from(raw).ok()) + .collect(), + ) + .await + { + Ok(allow) => response::Content::Allow(allow), + Err(Error::Posix(code)) => response::Content::PosixError(code), + }, + request::Content::SetVolumeName(msg) => { + match self.filesystem.set_volume_name(msg.name).await { + Ok(data) => response::Content::Data(data), + Err(Error::Posix(code)) => response::Content::PosixError(code), + } + } + request::Content::PreallocateSpace(msg) => match self + .filesystem + .preallocate_space( + msg.item_id, + msg.offset, + msg.length, + msg.flags + .iter() + .filter_map(|&raw| PreallocateFlag::try_from(raw).ok()) + .collect(), + ) + .await + { + Ok(count) => response::Content::ByteCount(count), + Err(Error::Posix(code)) => response::Content::PosixError(code), + }, + request::Content::DeactivateItem(msg) => { + match self.filesystem.deactivate_item(msg.item_id).await { + Ok(_) => response::Content::Success(Success {}), + Err(Error::Posix(code)) => response::Content::PosixError(code), + } + } + }) + } +} diff --git a/vendor/fskit-rs/src/info.rs b/vendor/fskit-rs/src/info.rs new file mode 100644 index 0000000..f033958 --- /dev/null +++ b/vendor/fskit-rs/src/info.rs @@ -0,0 +1,68 @@ +use std::path::Path; + +use plist::{Dictionary, Value}; + +pub(super) type Result = std::result::Result; + +pub(super) struct Info { + root: Dictionary, +} + +impl Info { + pub(super) fn new(path: &Path) -> Result { + let root = Value::from_file(path.join("Contents/Info.plist"))? + .into_dictionary() + .ok_or(Error::Invalid)?; + Ok(Self { root }) + } + + pub(super) fn bundle_id(&self) -> Result { + self.root + .get("CFBundleIdentifier") + .and_then(Value::as_string) + .map(|s| s.to_string()) + .filter(|s| !s.is_empty()) + .ok_or(Error::Invalid) + } + + pub(super) fn server_port(&self) -> Result { + self.root + .get("Configuration") + .and_then(Value::as_dictionary) + .and_then(|d| d.get("serverPort")) + .and_then(Self::parse_u16) + .ok_or(Error::Invalid) + } + + pub(super) fn fs_type(&self) -> Result { + self.root + .get("EXAppExtensionAttributes") + .and_then(Value::as_dictionary) + .and_then(|d| d.get("FSFileSystemType")) + .and_then(Value::as_string) + .map(|s| s.to_string()) + .filter(|s| !s.is_empty()) + .ok_or(Error::Invalid) + } + + fn parse_u16(value: &Value) -> Option { + value + .as_unsigned_integer() + .and_then(|v| u16::try_from(v).ok()) + .or_else(|| { + value + .as_signed_integer() + .and_then(|v| u16::try_from(v).ok()) + }) + .or_else(|| value.as_string().and_then(|s| s.parse::().ok())) + } +} + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + Plist(#[from] plist::Error), + + #[error("invalid Info.plist configuration")] + Invalid, +} diff --git a/vendor/fskit-rs/src/installer.rs b/vendor/fskit-rs/src/installer.rs new file mode 100644 index 0000000..aa91120 --- /dev/null +++ b/vendor/fskit-rs/src/installer.rs @@ -0,0 +1,325 @@ +use std::ffi::OsStr; +use std::path::{Path, PathBuf}; +use std::process::{Command, Output}; +use std::sync::{LazyLock, Mutex}; +use std::{env, fs}; + +use super::info::Info; +use super::registration; + +pub(super) type Result = std::result::Result; + +const APPLICATIONS_DIR: &str = "/Applications"; +const APPLICATION_SCRIPTS_DIR: &str = "Library/Application Scripts"; +const CONTAINERS_DIR: &str = "Library/Containers"; +const LSREGISTER: &str = "/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister"; +pub(super) const PLUGINKIT: &str = "/usr/bin/pluginkit"; +const CODESIGN: &str = "/usr/bin/codesign"; +const XATTR: &str = "/usr/bin/xattr"; +const FSKIT_EXTENSION_POINT: &str = "com.apple.fskit.fsmodule"; +const FSKIT_APPEX_RELATIVE_PATH: &str = "Contents/Extensions/FSKitExt.appex"; + +static INSTALLER_LOCK: LazyLock> = LazyLock::new(|| Mutex::new(())); + +pub(super) fn install(source: &Path) -> Result<()> { + let _guard = INSTALLER_LOCK.lock().expect("installer mutex poisoned"); + + if !source.is_dir() { + return Err(Error::InvalidSource); + } + + let Some(app_name) = source.file_name() else { + return Err(Error::InvalidSource); + }; + let app_path = app_path(app_name); + + if app_path.exists() { + return Err(Error::AppInstalled); + } + + let parent = app_path.parent().unwrap(); + fs::create_dir_all(parent)?; + + verify_app(source)?; + + // Copy the host app bundle into its final installation path. + run_cmd( + "ditto", + &[source.to_str().unwrap(), app_path.to_str().unwrap()], + )?; + + verify_app(&app_path)?; + + activate_impl(app_name) +} + +pub(super) fn uninstall(app_name: &OsStr) -> Result<()> { + let _guard = INSTALLER_LOCK.lock().expect("installer mutex poisoned"); + + let app_path = app_path(app_name); + + if !app_path.exists() { + return Err(Error::AppNotInstalled); + } + + let appex = appex_path(&app_path)?; + let appex_id = appex_bundle_id(&app_path)?; + let app_id = app_bundle_id(&app_path)?; + + unregister_path(&app_path, &appex); + unregister_registrations(&appex_id); + remove_app_state(&app_id, &appex_id)?; + + fs::remove_dir_all(&app_path)?; + + Ok(()) +} + +pub(super) fn activate(app_name: &OsStr) -> Result<()> { + let _guard = INSTALLER_LOCK.lock().expect("installer mutex poisoned"); + activate_impl(app_name) +} + +fn activate_impl(app_name: &OsStr) -> Result<()> { + let app_path = app_path(app_name); + + if !app_path.exists() { + return Err(Error::AppNotInstalled); + } + + verify_app(&app_path)?; + + let appex = appex_path(&app_path)?; + let appex_id = appex_bundle_id(&app_path)?; + + if is_active(&appex_id, &app_path)? { + return Ok(()); + } + + clear_quarantine(&app_path)?; + + register_app(&app_path)?; + register_ext(&appex)?; + elect_ext(&appex_id); + + if is_active(&appex_id, &app_path)? { + return Ok(()); + } + + Err(Error::ExtensionNotActivated { + appex_id, + reason: failure_reason(&app_path)?, + }) +} + +pub(super) fn app_path(app_name: &OsStr) -> PathBuf { + Path::new(APPLICATIONS_DIR).join(app_name) +} + +pub(super) fn appex_path(app_path: &Path) -> Result { + let appex = app_path.join(FSKIT_APPEX_RELATIVE_PATH); + if !appex.exists() { + return Err(Error::ExtensionNotFound); + } + Ok(appex) +} + +fn appex_bundle_id(app_path: &Path) -> Result { + Ok(Info::new(&appex_path(app_path)?)?.bundle_id()?) +} + +fn app_bundle_id(app_path: &Path) -> Result { + Ok(Info::new(app_path)?.bundle_id()?) +} + +fn register_app(app_path: &Path) -> Result<()> { + // Register the host app with LaunchServices. + run_cmd(LSREGISTER, &["-f", "-R", app_path.to_str().unwrap()]) +} + +fn register_ext(appex_path: &Path) -> Result<()> { + // Register the embedded FSKit extension with PlugInKit. + run_cmd(PLUGINKIT, &["-a", appex_path.to_str().unwrap()]) +} + +fn elect_ext(appex_id: &str) { + // Prefer this FSKit module when the system supports extension election. + let _ = run_cmd( + PLUGINKIT, + &["-e", "use", "-p", FSKIT_EXTENSION_POINT, "-i", appex_id], + ); +} + +fn clear_quarantine(app: &Path) -> Result<()> { + let output = Command::new(XATTR) + .args(["-p", "com.apple.quarantine", app.to_str().unwrap()]) + .output()?; + + if output.status.success() { + // Clear quarantine only when the attribute is actually present. + run_cmd( + XATTR, + &["-dr", "com.apple.quarantine", app.to_str().unwrap()], + )?; + } + + Ok(()) +} + +fn verify_app(app: &Path) -> Result<()> { + // Verify the app bundle signature and nested code signatures. + run_cmd( + CODESIGN, + &[ + "--verify", + "--deep", + "--strict", + "--verbose=2", + app.to_str().unwrap(), + ], + ) +} + +fn failure_reason(app_path: &Path) -> Result { + let appex_id = appex_bundle_id(app_path)?; + let statuses = registration::registrations(&appex_id)?; + let matching: Vec<_> = statuses + .iter() + .filter(|status| status.app_path == app_path) + .collect(); + + if statuses.is_empty() { + return Ok("registration not found after activation commands".to_string()); + } + + if matching.is_empty() { + let other_paths = statuses + .iter() + .map(|status| status.app_path.display().to_string()) + .collect::>() + .join(", "); + return Ok(format!( + "extension is registered only for other app paths: {other_paths}" + )); + } + + if statuses.len() > 1 && !matching.iter().any(|status| status.elected) { + return Ok("matching app registration exists but is not elected".to_string()); + } + + Ok( + "matching app registration exists but the extension is still not considered active" + .to_string(), + ) +} + +fn unregister_registrations(appex_id: &str) { + let Ok(statuses) = registration::registrations(appex_id) else { + return; + }; + + for status in statuses { + let appex = status.app_path.join(FSKIT_APPEX_RELATIVE_PATH); + unregister_path(&status.app_path, &appex); + } +} + +fn unregister_path(app_path: &Path, appex_path: &Path) { + if appex_path.exists() { + // Best-effort unregister the embedded FSKit extension from PlugInKit. + let _ = run_cmd(PLUGINKIT, &["-r", appex_path.to_str().unwrap()]); + } + + // Best-effort unregister the host app from LaunchServices. + let _ = run_cmd(LSREGISTER, &["-u", app_path.to_str().unwrap()]); +} + +fn remove_app_state(app_id: &str, appex_id: &str) -> Result<()> { + let Some(home) = env::var_os("HOME") else { + return Ok(()); + }; + + for path in [ + Path::new(&home).join(APPLICATION_SCRIPTS_DIR).join(app_id), + Path::new(&home) + .join(APPLICATION_SCRIPTS_DIR) + .join(appex_id), + Path::new(&home).join(CONTAINERS_DIR).join(app_id), + Path::new(&home).join(CONTAINERS_DIR).join(appex_id), + ] { + if path.exists() { + fs::remove_dir_all(path)?; + } + } + + Ok(()) +} + +fn is_active(appex_id: &str, app_path: &Path) -> Result { + let statuses = registration::registrations(appex_id)?; + let matching: Vec<_> = statuses + .iter() + .filter(|status| status.app_path == app_path) + .collect(); + + if statuses.len() > 1 { + Ok(matching.iter().any(|status| status.elected)) + } else { + Ok(!matching.is_empty()) + } +} + +fn run_cmd(cmd: &'static str, args: &[&str]) -> Result<()> { + run_cmd_out(cmd, args).map(|_| ()) +} + +pub(super) fn run_cmd_out(cmd: &'static str, args: &[&str]) -> Result { + let output = Command::new(cmd).args(args).output()?; + if output.status.success() { + Ok(output) + } else { + Err(Error::CommandFailed { + command: format!("{cmd} {}", args.join(" ")), + status: describe_failure(&output), + }) + } +} + +pub(super) fn describe_failure(output: &Output) -> String { + let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); + if stderr.is_empty() { + output.status.to_string() + } else { + stderr + } +} + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + Io(#[from] std::io::Error), + + #[error(transparent)] + Info(#[from] super::info::Error), + + #[error("invalid host application source path")] + InvalidSource, + + #[error("host application is not installed")] + AppNotInstalled, + + #[error("host application is already installed")] + AppInstalled, + + #[error("FSKit extension bundle not found in host application")] + ExtensionNotFound, + + #[error("invalid FSKit extension path")] + InvalidExtensionPath, + + #[error("FSKit extension is not active: {appex_id} ({reason})")] + ExtensionNotActivated { appex_id: String, reason: String }, + + #[error("command `{command}` failed: {status}")] + CommandFailed { command: String, status: String }, +} diff --git a/vendor/fskit-rs/src/lib.rs b/vendor/fskit-rs/src/lib.rs new file mode 100644 index 0000000..8d3a908 --- /dev/null +++ b/vendor/fskit-rs/src/lib.rs @@ -0,0 +1,347 @@ +use std::ffi::OsStr; +use std::path::{Path, PathBuf}; + +use async_trait::async_trait; + +pub use crate::pb::check_access::AccessMask; +pub use crate::pb::preallocate_space::PreallocateFlag; +pub use crate::pb::set_xattr::SetXattrPolicy; +pub use crate::pb::supported_capabilities::CaseFormat; +pub use crate::pb::synchronize::SyncFlags; +pub use crate::pb::{ + DirectoryEntries, Item, ItemAttributes, ItemType, OpenMode, PathConfOperations, + ResourceIdentifier, StatFsResult, SupportedCapabilities, TaskOptions, VolumeBehavior, + VolumeIdentifier, Xattrs, directory_entries, +}; +use crate::session::Session; + +mod handler; +mod info; +pub mod installer; +pub mod mounter; +mod registration; +pub mod session; +pub mod socket; + +mod pb { + include!(concat!(env!("OUT_DIR"), "/pb.rs")); +} + +const FSKIT_ID: &str = "network.debox.fskitbridge.fskitext"; +const DEFAULT_MOUNT_POINT: &str = "/tmp/fskitbridge"; + +pub type Result = std::result::Result; + +#[async_trait] +pub trait Filesystem { + /// Get the resource identifier and name. + async fn get_resource_identifier(&mut self) -> Result; + + /// Get the volume identifier and name. + async fn get_volume_identifier(&mut self) -> Result; + + /// Get options that tell FSKit to declare behaviors and selectively inhibit + /// operation protocols. + async fn get_volume_behavior(&mut self) -> Result; + + /// Get properties implemented by volumes that support providing the values of + /// system limits or options. + async fn get_path_conf_operations(&mut self) -> Result; + + /// Get properties that provide the supported capabilities of the volume. + async fn get_volume_capabilities(&mut self) -> Result; + + /// Get properties that provide up-to-date statistics of the volume. + async fn get_volume_statistics(&mut self) -> Result; + + /// Mounts this volume, using the specified options. + async fn mount(&mut self, options: TaskOptions) -> Result<()>; + + /// Unmounts this volume. + async fn unmount(&mut self) -> Result<()>; + + /// Synchronizes the volume with its underlying resource. + async fn synchronize(&mut self, flags: SyncFlags) -> Result<()>; + + /// Fetches attributes for the given item. + async fn get_attributes(&mut self, item_id: u64) -> Result; + + /// Sets the given attributes on an item. + async fn set_attributes( + &mut self, + item_id: u64, + attributes: ItemAttributes, + ) -> Result; + + /// Looks up an item within a directory. + async fn lookup_item(&mut self, name: &OsStr, directory_id: u64) -> Result; + + /// Reclaims an item, releasing any resources allocated for the item. + async fn reclaim_item(&mut self, item_id: u64) -> Result<()>; + + /// Reads a symbolic link. + async fn read_symbolic_link(&mut self, item_id: u64) -> Result>; + + /// Creates a new file or directory item. + async fn create_item( + &mut self, + name: &OsStr, + r#type: ItemType, + directory_id: u64, + attributes: ItemAttributes, + ) -> Result; + + /// Creates a new symbolic link. + async fn create_symbolic_link( + &mut self, + name: &OsStr, + directory_id: u64, + new_attributes: ItemAttributes, + contents: Vec, + ) -> Result; + + /// Creates a new hard link. + async fn create_link( + &mut self, + item_id: u64, + name: &OsStr, + directory_id: u64, + ) -> Result>; + + /// Removes an existing item from a given directory. + async fn remove_item(&mut self, item_id: u64, name: &OsStr, directory_id: u64) -> Result<()>; + + /// Renames an item from one path in the file system to another. + async fn rename_item( + &mut self, + item_id: u64, + source_directory_id: u64, + source_name: &OsStr, + destination_name: &OsStr, + destination_directory_id: u64, + over_item_id: Option, + ) -> Result>; + + /// Enumerates the contents of the given directory. + async fn enumerate_directory( + &mut self, + directory_id: u64, + cookie: u64, + verifier: u64, + ) -> Result; + + /// Activates the volume using the specified options. + async fn activate(&mut self, options: TaskOptions) -> Result; + + /// Tears down a previously initialized volume instance. + async fn deactivate(&mut self) -> Result<()>; + + /// Returns an array that specifies the extended attribute names the given + /// item supports. + async fn get_supported_xattr_names(&mut self, item_id: u64) -> Result; + + /// Gets the specified extended attribute of the given item. + async fn get_xattr(&mut self, name: &OsStr, item_id: u64) -> Result>; + + /// Sets the specified extended attribute data on the given item. + async fn set_xattr( + &mut self, + name: &OsStr, + value: Option>, + item_id: u64, + policy: SetXattrPolicy, + ) -> Result<()>; + + /// Gets the list of extended attributes currently set on the given item. + async fn get_xattrs(&mut self, item_id: u64) -> Result; + + /// Opens a file for access. + async fn open_item(&mut self, item_id: u64, modes: Vec) -> Result<()>; + + /// Closes a file from further access. + async fn close_item(&mut self, item_id: u64, modes: Vec) -> Result<()>; + + /// Reads the contents of the given file item. + async fn read(&mut self, item_id: u64, offset: i64, length: i64) -> Result>; + + /// Writes contents to the given file item. + async fn write(&mut self, contents: Vec, item_id: u64, offset: i64) -> Result; + + /// Checks whether the file system allows access to the given item. + async fn check_access(&mut self, item_id: u64, access: Vec) -> Result; + + /// Sets a new name for the volume. + async fn set_volume_name(&mut self, name: Vec) -> Result>; + + /// Preallocate disk space for the given item. + async fn preallocate_space( + &mut self, + item_id: u64, + offset: i64, + length: i64, + flags: Vec, + ) -> Result; + + /// Notifies the file system that the kernel is no longer making immediate use of + /// the given item. + async fn deactivate_item(&mut self, item_id: u64) -> Result<()>; +} + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("POSIX error: {0}")] + Posix(std::ffi::c_int), +} + +/// Configuration for mounting the file system and connecting between `FSKitExt` and `fskit-rs`. +/// +/// # Parameters +/// * `fskit_id` — Bundle identifier of the FSKit extension used for registration +/// and election. Default: `network.debox.fskitbridge.fskitext`. +/// * `mount_point` — Existing (usually empty) directory to mount onto. Use `/Volumes/` +/// (may require `sudo`) or a user-owned path. Default: `/tmp/fskitbridge`. +/// * `force` — If `true`, preflight **unmounts** anything already mounted at `mount_point` +/// before mounting. Default: `true`. +/// * `skip_registration` — If set, skip PlugInKit / appex `Info.plist` discovery and use the +/// given TCP port and FS type for the listener and for `mount -F -t`. For local testing +/// without a registered FSKit extension. Default: `None`. +#[derive(Debug, Clone)] +pub struct SkipRegistration { + /// Port bound as `127.0.0.1:` (e.g. `35367` for stock FSKitBridge). + pub server_port: u16, + /// Filesystem type passed to `mount -F -t `. + pub fs_type: String, +} + +#[derive(Debug, Clone)] +pub struct MountOptions { + pub fskit_id: String, + pub mount_point: PathBuf, + pub force: bool, + pub skip_registration: Option, +} + +impl Default for MountOptions { + fn default() -> Self { + Self { + fskit_id: FSKIT_ID.into(), + mount_point: PathBuf::from(DEFAULT_MOUNT_POINT), + force: true, + skip_registration: None, + } + } +} + +/// Mounts a user-space file system at `opts.mount_point` and returns a `Session` that +/// keeps the mount alive. Non-blocking: background workers serve kernel requests; +/// dropping `Session` cleanly unmounts. +/// +/// # Parameters +/// * `fs` — Your `Filesystem` impl. Must be `Send + Sync + Clone + 'static`. +/// Prefer keeping a heavy state in `Arc<_>`. +/// * `opts` — Combined mount/connection configuration. +/// +/// # Returns +/// A `Session` handle; while it’s alive, the mount remains active. Dropping it unmounts. +/// +/// # macOS (FSKit) notes +/// * The extension must be **enabled** in System Settings (File System Extensions). +/// * FSKit mounts use `noowners`; you can store/report uid/gid in metadata, +/// but host POSIX enforcement will still be disabled. +pub async fn mount(fs: FS, opts: MountOptions) -> session::Result +where + FS: Filesystem + Send + Sync + Clone + 'static, +{ + Session::new(fs, opts).await +} + +/// Installs the FSKit host application into `/Applications/` +/// and registers its extension. +/// +/// # Behavior +/// - Derives the destination from the source app bundle name. +/// - Returns `AppInstalled` if `/Applications/` already exists. +/// - Verifies the source app bundle signature before the copy step. +/// - Copies the app bundle from `source` to `/Applications/`. +/// - Verifies the installed app bundle signature after the copy step. +/// - Activates the installed host app after the copy step. +/// +/// # Observed macOS behavior +/// - This crate treats `/Applications/` as the supported installation target. +/// - During local experiments, host apps from other locations may also work after the extension +/// is enabled, but that behavior is stateful and not guaranteed. +/// +/// # Commands +/// ```text +/// codesign --verify --deep --strict --verbose=2 +/// ditto /Applications/ +/// codesign --verify --deep --strict --verbose=2 /Applications/ +/// activate() +/// ``` +pub fn install>(source: P) -> installer::Result<()> { + installer::install(source.as_ref()) +} + +/// Activates an already installed FSKit host application from `/Applications/`. +/// +/// # Behavior +/// - Verifies the installed app bundle signature before any registration step. +/// - Clears `com.apple.quarantine` only when it is present on the app. +/// - Registers the host app with LaunchServices. +/// - Registers the embedded `FSKitExt.appex` with PlugInKit. +/// - Requests election for the extension bundle id. +/// - Performs one activation check before registration and one after registration. +/// - Returns success only when the host app is considered active after the registration steps. +/// - On failure, reports whether the extension was never registered, only registered from a +/// different app path, or registered but not elected. +/// +/// # Observed macOS behavior +/// - `activate()` always retries the registration steps when the host app is not already active. +/// - PlugInKit/ExtensionKit state may remain sensitive to prior registrations, app identities, +/// and install paths. +/// +/// # Commands +/// ```text +/// codesign --verify --deep --strict --verbose=2 /Applications/ +/// xattr -dr com.apple.quarantine /Applications/ # only if quarantine is present +/// lsregister -f -R /Applications/ +/// pluginkit -a /Applications//Contents/Extensions/FSKitExt.appex +/// pluginkit -e use -p com.apple.fskit.fsmodule -i +/// ``` +pub fn activate>(app_name: S) -> installer::Result<()> { + installer::activate(app_name.as_ref()) +} + +/// Uninstalls the FSKit host application from `/Applications/`. +/// +/// # Behavior +/// - Resolves the host app as `/Applications/`. +/// - Best-effort unregisters the embedded `FSKitExt.appex` from PlugInKit. +/// - Best-effort unregisters the host app from LaunchServices. +/// - Best-effort unregisters any remaining registrations for the same appex bundle id. +/// - Removes app-specific entries under `~/Library/Application Scripts` and `~/Library/Containers`. +/// - Removes the app bundle from `/Applications/`. +/// +/// # Commands +/// ```text +/// pluginkit -r /Applications//Contents/Extensions/FSKitExt.appex # best effort +/// lsregister -u /Applications/ # best effort +/// pluginkit -m -i --raw # inspect remaining registrations +/// pluginkit -r # best effort +/// lsregister -u # best effort +/// rm -rf ~/Library/Application\ Scripts/ # best effort +/// rm -rf ~/Library/Application\ Scripts/ # best effort +/// rm -rf ~/Library/Containers/ # best effort +/// rm -rf ~/Library/Containers/ # best effort +/// rm -rf /Applications/ +/// ``` +pub fn uninstall>(app_name: S) -> installer::Result<()> { + installer::uninstall(app_name.as_ref()) +} + +#[macro_export] +macro_rules! path { + ($arg:expr) => { + $arg.as_os_str().to_str().unwrap() + }; +} diff --git a/vendor/fskit-rs/src/mounter.rs b/vendor/fskit-rs/src/mounter.rs new file mode 100644 index 0000000..599a4e9 --- /dev/null +++ b/vendor/fskit-rs/src/mounter.rs @@ -0,0 +1,182 @@ +use std::fs::File; +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; +use std::thread; +use std::time::{Duration, Instant}; + +use log::{error, info, warn}; + +use crate::installer::describe_failure; +use crate::{MountOptions, path}; + +pub type Result = std::result::Result; + +#[derive(Debug)] +pub(super) struct Mounter { + path: PathBuf, + device: String, +} + +impl Mounter { + pub(super) fn mount(opts: MountOptions, fs_type: &str) -> Result { + if !opts.mount_point.exists() { + return Err(Error::MountPointMissing); + } + + if opts.force + && let Err(err) = unmount(&opts.mount_point) + { + warn!( + "forced unmount of existing mount at {} failed: {err}", + path!(opts.mount_point) + ); + } + + let image = PathBuf::from(format!("/tmp/{fs_type}.dmg")); + if !image.exists() { + File::create(&image)?; + } + + let device = attach_image(&image)?; + + let args = [ + "-F", + "-t", + fs_type, + device.as_str(), + path!(opts.mount_point), + ]; + let mut process = Command::new("mount") + .args(args) + .stderr(Stdio::piped()) + .spawn()?; + let start = Instant::now(); + loop { + match process.try_wait()? { + Some(status) => { + if status.success() { + break; + } + let stderr = process.wait_with_output()?.stderr; + let out = String::from_utf8_lossy(&stderr); + error!("{out}"); + return if out.contains("is disabled") { + Err(Error::ExtensionDisabled) + } else if out.contains("Resource busy") { + Err(Error::MountPointBusy) + } else if out.contains("Probing resource") || out.contains("Loading resource") { + Err(Error::NeedReboot) + } else { + Err(Error::MountFailed) + }; + } + None => { + if start.elapsed() >= Duration::from_secs(3) { + error!("mount command hung, killing process"); + let _ = process.kill(); + return Err(Error::NeedReboot); + } + thread::sleep(Duration::from_millis(100)); + } + } + } + + info!( + "file system mounted - type: {}, mount point: {} ({})", + fs_type, + path!(opts.mount_point), + device + ); + + Ok(Self { + path: opts.mount_point, + device, + }) + } + + pub(super) fn unmount(&self) -> Result<()> { + unmount(&self.path)?; + detach(&self.device)?; + info!( + "file system unmounted - mount point: {} ({})", + path!(self.path), + self.device + ); + Ok(()) + } +} + +fn attach_image(image: &Path) -> Result { + let args = [ + "attach", + "-imagekey", + "diskimage-class=CRawDiskImage", + "-nomount", + path!(image), + ]; + let output = Command::new("hdiutil").args(args).output()?; + if output.status.success() { + let device = String::from_utf8(output.stdout) + .map_err(|_| Error::InvalidDevice)? + .trim() + .to_string(); + if device.is_empty() { + Err(Error::InvalidDevice) + } else { + Ok(device) + } + } else { + Err(Error::AttachFailed(describe_failure(&output))) + } +} + +fn unmount(path: &PathBuf) -> Result<()> { + let output = Command::new("umount").arg("-f").arg(path).output()?; + if output.status.success() { + Ok(()) + } else { + Err(Error::UnmountFailed(describe_failure(&output))) + } +} + +fn detach(device: &str) -> Result<()> { + let output = Command::new("hdiutil").args(["detach", device]).output()?; + if output.status.success() { + Ok(()) + } else { + Err(Error::DetachFailed(describe_failure(&output))) + } +} + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + Io(#[from] std::io::Error), + + #[error("mount point does not exist")] + MountPointMissing, + + #[error("invalid device identifier returned by hdiutil")] + InvalidDevice, + + #[error("failed to attach disk image: {0}")] + AttachFailed(String), + + #[error("file system extension is disabled")] + ExtensionDisabled, + + #[error("mount point is already in use")] + MountPointBusy, + + #[error("file system extension was updated; reboot the system and try again")] + NeedReboot, + + #[error("mount request failed")] + MountFailed, + + #[error("unmount request failed: {0}")] + UnmountFailed(String), + + #[error("failed to detach disk image: {0}")] + DetachFailed(String), +} diff --git a/vendor/fskit-rs/src/protocol.proto b/vendor/fskit-rs/src/protocol.proto new file mode 100644 index 0000000..a967dae --- /dev/null +++ b/vendor/fskit-rs/src/protocol.proto @@ -0,0 +1,1318 @@ +/* + FSKitExt <-> Backend RPC Protocol + --------------------------------- + File: FSKitExt/protocol.proto + Source (persistent URL): + https://github.com/debox-network/FSKitBridge/blob/main/FSKitExt/protocol.proto + + Purpose: + Language-neutral wire format between FSKitExt (Swift/ExtensionKit) and the + backend (e.g., Rust via fskit-fs). This protocol carries file system + operations (lookup, attributes, read/write, etc). + + License: + SPDX-License-Identifier: MIT OR Apache-2.0 + Copyright (c) 2025 Debox Network + + This file is part of FSKitBridge. See LICENSE-MIT and LICENSE-APACHE at repo root. +*/ +syntax = "proto3"; + +package pb; + +import "google/protobuf/timestamp.proto"; + +// Get the resource identifier and name. +message GetResourceIdentifier {} + +// Get the volume identifier and name. +message GetVolumeIdentifier {} + +// Get options that tell FSKit to declare behaviors and selectively inhibit +// operation protocols. +message GetVolumeBehavior {} + +// Get properties implemented by volumes that support providing the values of +// system limits or options. +message GetPathConfOperations {} + +// Get properties that provide the supported capabilities of the volume. +message GetVolumeCapabilities {} + +// Get properties that provide up-to-date statistics of the volume. +message GetVolumeStatistics {} + +// A class that passes command options to a task, optionally providing +// security-scoped URLs. +message TaskOptions { + // An array of strings that represent command-line options for the task. + // + // This property is equivalent to the `argv` array of C strings passed to + // a command-line tool. + repeated string task_options = 1; +} + +// Mounts this volume, using the specified options. +// +// FSKit calls this method as a signal that some process is trying to mount this volume. +// Your file system receives a call to ``activate(options:)`` prior to receiving +// any mount calls. +message Mount { + // Options to apply to the mount. + // These can include security-scoped file paths. + // There are no defined options currently. + TaskOptions options = 1; +} + +// Unmounts this volume. +// +// Clear and flush all cached state in your implementation of this method. +message Unmount {} + +// Synchronizes the volume with its underlying resource. +// +// After calling this method, FSKit assumes that the volume has sent all pending +// I/O or metadata to its resource. +message Synchronize { + // Behavior flags for use with synchronization calls. + // + // These values are based on flags defined in `mount.h`. + // Since there are system-defined flags that are valid in the kernel but not in FSKit, + // this type defines its members as options rather than use an enumeration. + enum SyncFlags { + NONE = 0; + + // A flag for synchronized I/O with file-integrity completion. + WAIT = 1; + + // A flag for synchronized I/O that starts I/O but doesn't wait for it. + NO_WAIT = 2; + + // A flag for synchronized I/O with data-integrity completion. + D_WAIT = 4; + } + + // Timing flags, as defined in `mount.h`. + // These flags let the file system know whether to run the operation in + // a blocking or nonblocking fashion. + SyncFlags flags = 1; +} + +// Fetches attributes for the given item. +// +// For file systems that don't support hard links, set ``FSItemAttributes/linkCount`` +// to `1` for regular files and symbolic links. +// +// If the item's `bsdFlags` contain the `UF_COMPRESSED` flag, your file system +// returns the uncompressed size of the file. +message GetAttributes { + // The item to get attributes for. + uint64 item_id = 1; +} + +// Sets the given attributes on an item. +// +// Several attributes are considered "read-only", and an attempt to set these +// attributes results in an error with the code `EINVAL`. +// +// A request may set ``FSItem/Attributes/size`` beyond the end of the file. +// If the underlying file system doesn't support sparse files, allocate space to +// fill the new file size. +// Either fill this space with zeroes, or configure it to read as zeroes. +// +// If a request sets the file size below the current end-of-file, truncate the +// file and return any unused space to the file system as free space. +// +// Ignore attempts to set the size of directories or symbolic links; don't +// produce an error. +// +// If the caller attempts to set an attribute not supported by the on-disk file +// system format, don't produce an error. +// The upper layers of the framework will detect this situation. +message SetAttributes { + // A request containing the attributes to set. + ItemAttributes attributes = 1; + + // The item on which to set the attributes. + uint64 item_id = 2; +} + +// Looks up an item within a directory. +// +// If no item matching `name` exists in the directory indicated by `directory`, +// complete the request with an error with a domain of +// +// and a code of `ENOENT`. +// +// > Tip: The ``FSFileName`` sent back to the caller may differ from the `name` +// parameter. +// This flexibility allows your implementation to handle case-insensitive and +// case-sensitive file systems. +// It might also be the case that `name` uses a composed Unicode string, but the +// name maintained by the file system and provided to the caller is uncomposed Unicode. +message LookupItem { + // The name of the item to look up. + bytes name = 1; + + // The directory in which to look up the item. + uint64 directory_id = 2; +} + +// Reclaims an item, releasing any resources allocated for the item. +// +// FSKit guarantees that for every ``FSItem`` returned by the volume, +// a corresponding reclaim operation occurs after the upper layers no longer +// reference that item. +// +// > Note: Block device file systems may assess whether an underlying resource +// terminates before processing reclaim operations. +// On unary file systems, for example, the associated volumes unmount when such +// resources disconnect from the system. +// The unmount triggers a reclaiming of all items. +// Some implementations benefit greatly from short-circuiting in such cases. +// With a terminated resource, all I/O results in an error, making short-circuiting +// the most efficient response. +message ReclaimItem { + // The item to reclaim. + uint64 item_id = 1; +} + +// Reads a symbolic link. +message ReadSymbolicLink { + // The symbolic link to read from. + // FSKit guarantees this item is of type ``FSItem/ItemType/symlink``. + uint64 item_id = 1; +} + +// An enumeration of item types, such as file, directory, or symbolic link. +enum ItemType { + UNKNOWN = 0; + FILE = 1; + DIRECTORY = 2; + SYMLINK = 3; + FIFO = 4; + CHAR_DEVICE = 5; + BLOCK_DEVICE = 6; + SOCKET = 7; +} + +// Creates a new file or directory item. +// +// If an item named `name` already exists in the directory indicated by +// `directory`, complete the request with an error with a domain of +// +// and a code of `EEXIST`. +message CreateItem { + // The new item's name. + bytes name = 1; + + // The new item's type. + // Valid values are ``FSItem/ItemType/file`` or ``FSItem/ItemType/directory``. + ItemType type = 2; + + // The directory in which to create the item. + uint64 directory_id = 3; + + // Attributes to apply to the new item. + ItemAttributes attributes = 4; +} + +// Creates a new symbolic link. +// +// If an item named `name` already exists in the directory indicated by +// `directory`, complete the request with an error with a domain of +// +// and a code of `EEXIST`. +message CreateSymbolicLink { + // The new item's name. + bytes name = 1; + + // The directory in which to create the item. + uint64 directory_id = 2; + + // Attributes to apply to the new item. + ItemAttributes new_attributes = 3; + + // The contents of the new symbolic link. + bytes contents = 4; +} + +// Creates a new hard link. +// +// If creating the link fails, complete the request with an error with a domain +// of +// and the following error codes: +// +// * `EEXIST` if there's already an item named `name` in the directory. +// * `EMLINK` if creating the link would exceed the maximum number of hard links +// supported on `item`. +// * `ENOTSUP` if the file system doesn't support creating hard links to the +// type of file system object that `item` represents. +message CreateLink { + // The existing item to which to link. + uint64 item_id = 1; + + // The name for the new link. + bytes name = 2; + + // The directory in which to create the link. + uint64 directory_id = 3; +} + +// Removes an existing item from a given directory. +// +// Don't actually remove the item object itself in your implementation; +// instead, only remove the given item name from the given directory. +// Remove and deallocate the item in ``reclaimItem(_:)``. +message RemoveItem { + // The item to remove. + uint64 item_id = 1; + + // The name of the item to remove. + bytes name = 2; + + // The directory from which to remove the item. + uint64 directory_id = 3; +} + +// Renames an item from one path in the file system to another. +// +// Implement renaming along the lines of this algorithm: +// +// - If `item` is a file: +// - If the destination file exists: +// - Remove the destination file. +// - If the source and destination directories are the same: +// - Rewrite the name in the existing directory. +// - Else: +// - Write the new entry in the destination directory. +// - Clear the old directory entry. +// - If `item` is a directory: +// - If the destination directory exists: +// - If the destination directory isn't empty: +// - Fail the operation with an error of +// +// and a code of `ENOTEMPTY`. +// - Else: +// - Remove the destination directory. +// - If the source and destination directories are the same: +// - Rewrite the name in the existing directory. +// - Else: +// - If the destination is a child of the source directory: +// - Fail the operation with an error. +// - Else: +// - Write the new entry in the destination directory. +// - Update `"."` and `".."` in the moved directory. +// - Clear the old directory entry. +message RenameItem { + // The file system object being renamed. + uint64 item_id = 1; + + // The directory that currently contains the item to rename. + uint64 source_directory_id = 2; + + // The name of the item within the source directory. + bytes source_name = 3; + + // The new name of the item as it appears in `destinationDirectory`. + bytes destination_name = 4; + + // The directory to contain the renamed object, which may be the same + // as `sourceDirectory`. + uint64 destination_directory_id = 5; + + // The file system object if the destination exists, as discovered in a prior lookup. + // If this parameter is non-`nil`, mark `overItem` as deleted, so the file + // system can free its allocated space on the next call to ``reclaimItem(_:)``. + // After doing so, ensure the operation finishes without errors. + optional uint64 over_item_id = 6; +} + +// Enumerates the contents of the given directory. +// +// This method uses the ``FSDirectoryEntryPacker/packEntry(name:itemType:itemID: +// nextCookie:attributes:)`` method of the `packer` parameter to deliver +// the enumerated items to the caller. +// The general flow of an enumeration implementation follows these steps: +// +// 1. Enumeration starts with a call to `enumerateDirectory` using the initial +// next-cookie and verifier values ``FSDirectoryCookieInitial`` and +// ``FSDirectoryVerifierInitial``, respectively. +// 2. The implementation uses `packer` to pack the initial set of directory entries. +// Packing also sets a `nextCookie` to use on the next call. +// 3. The implementation replies with a new verifier value, a nonzero value that +// reflects the directory's current version. +// 4. On the next call the implementation packs the next set of entries, starting +// with the item indicated by `cookie`. +// If `cookie` doesn't resolve to a valid directory entry, complete the request +// with an error of domain +// +// and code``FSError/Code/invalidDirectoryCookie``. +// +// When packing, make sure to use acceptable directory entry names and +// unambiguous input to all file operations that take names without additional +// normalization, such as`lookupName`. +// +// > Tip: If the `attributes` parameter is `nil`, include at least two entries +// in a directory: `"."` and `".."`, which represent the current and +// parent directories, respectively. +// Both of these items have type ``FSItem/ItemType/directory``. +// For the root directory, `"."` and `".."` have identical contents. +// Don't pack `"."` and `".."` if `attributes` isn't `nil`. +message EnumerateDirectory { + // The item to enumerate. + // FSKit guarantees this item is of type ``FSItem/ItemType/directory``. + uint64 directory_id = 1; + + // A value that indicates the location within the directory from which + // to enumerate. + // Your implementation defines the semantics of the cookie values; they're + // opaque to FSKit. + // The first call to the enumerate method passes ``FSDirectoryCookieInitial`` + // for this parameter. + // Subsequent calls pass whatever cookie value you previously passed to the + // packer's `nextCookie` parameter. + uint64 cookie = 2; + + // A tool to detect whether the directory contents changed since the last call + // to `enumerateDirectory`. + // Your implementation defines the semantics of the verifier values; they're + // opaque to FSKit. + // The first call to the enumerate method passes ``FSDirectoryVerifierInitial`` + // for this parameter. + // Subsequent calls pass whatever cookie value you previously passed to the + // packer's `currentVerifier` parameter. + uint64 verifier = 3; +} + +// Activates the volume using the specified options. +// +// When FSKit calls this method, allocate any in-memory state required to +// represent the file system. +// Also allocate an ``FSItem`` for the root directory of the file system, and +// pass it to the reply block. +// FSKit caches this root item for the lifetime of the volume, and uses it as a +// starting point for all file look-ups. +// +// Volume activation occurs prior to any call to mount the volume. +message Activate { + // Options to apply to the activation. + // These can include security-scoped file paths. + // There are no defined options currently. + TaskOptions options = 1; +} + +// Tears down a previously initialized volume instance. +// +// Set up your implementation to release any resources allocated for +// the volume instance. +// By the time you receive this callback, FSKit has already performed a reclaim +// call to release all other file nodes associated with this file system instance. +// +// Avoid performing any I/O in this method. +// Prior to calling this method, FSKit has already issued a sync call to perform +// any cleanup-related I/O. +// +// FSKit unmounts any mounted volume with a call to ``unmount()`` prior to the +// deactivate callback. +message Deactivate { + // Options that affect the behavior of deactivate methods. + enum DeactivateOption { + // An option to force deactivation. + FORCE = 0; + } + + // Options to apply to the deactivation. + repeated DeactivateOption options = 1; +} + +// Returns an array that specifies the extended attribute names the given +// item supports. +// +// If `item` supports no extended attributes, this method returns `nil`. +// +// Only implement this method if your volume works with "limited" +// extended attributes. +// For purposes of this protocol, "limited" support means the volume doesn't +// support extended attributes generally, but uses these APIs to expose +// specific file system data. +// +// > Note: If a file system implements this method, FSKit assumes limited +// support for extended attributes exists. +// In this mode, FSkit only calls this protocol's methods for the extended +// attribute names this method returns. +message GetSupportedXattrNames { + // The item for which to get information. + uint64 item_id = 1; +} + +// Gets the specified extended attribute of the given item. +message GetXattr { + // The extended attribute name. + bytes name = 1; + + // The item for which to get the extended attribute. + uint64 item_id = 2; +} + +// Sets the specified extended attribute data on the given item. +message SetXattr { + // Flags to specify the policy when setting extended file attributes. + enum SetXattrPolicy { + // Set the value, regardless of previous state. + ALWAYS_SET = 0; + + // Set the value, but fail if the extended attribute already exists. + MUST_CREATE = 1; + + // Set the value, but fail if the extended attribute doesn't already exist. + MUST_REPLACE = 2; + + // Delete the value, failing if the extended attribute doesn't exist. + DELETE = 3; + } + + // The extended attribute name. + bytes name = 1; + + // The extended attribute value to set. + // This can't be `nil`, unless the policy is ``SetXattrPolicy/delete``. + optional bytes value = 2; + + // The item on which to set the extended attribute. + uint64 item_id = 3; + + // The policy to apply when setting the attribute. + // See ``SetXattrPolicy`` for possible values. + SetXattrPolicy policy = 4; +} + +// Gets the list of extended attributes currently set on the given item. +message GetXattrs { + // The item from which to get extended attributes. + uint64 item_id = 1; +} + +// Defined modes for opening a file. +enum OpenMode { + // The read mode. + // + // This mode is equivalent to POSIX `FREAD`. + READ = 0; + + // The write mode. + // + // This mode is equivalent to POSIX `FRWITE`. + WRITE = 1; +} + +// Opens a file for access. +message OpenItem { + // The item to open. + uint64 item_id = 1; + + // The set of mode flags to open the item with. + repeated OpenMode modes = 2; +} + +// Closes a file from further access. +message CloseItem { + // The item to close. + uint64 item_id = 1; + + // The set of mode flags to keep after this close. + repeated OpenMode modes = 2; +} + +// Reads the contents of the given file item. +// +// If the number of bytes requested exceeds the number of bytes available before +// the end of the file, then the call copies only those bytes to `buffer`. +// If `offset` points past the last valid byte of the file, don't reply with an +// error but set `actuallyRead` to `0`. +message Read { + // The item from which to read. + // FSKit guarantees this item will be of type ``FSItem/ItemType/file``. + uint64 item_id = 1; + + // The offset in the file from which to start reading. + int64 offset = 2; + + // The number of bytes to read. + int64 length = 3; +} + +// Writes contents to the given file item. +// +// FSKit expects this routine to allocate space in the file system to extend the +// file as necessary. +// +// If the volume experiences an out-of-space condition, reply with an error of +// domain +// and code `ENOSPC`. +message Write { + // A buffer containing the data to write to the file. + bytes contents = 1; + + // The item to which to write. + // FSKit guarantees this item will be of type ``FSItem/ItemType/file``. + uint64 item_id = 2; + + // The offset in the file from which to start writing. + int64 offset = 3; +} + +// Checks whether the file system allows access to the given item. +message CheckAccess { + // Options of access rights. + enum AccessMask { + // The file system allows reading data. + READ_DATA = 0; + + // The file system allows listing directory contents. + LIST_DIRECTORY = 1; + + // The file system allows writing data. + WRITE_DATA = 2; + + // The file system allows adding files. + ADD_FILE = 3; + + // The file system allows file execution. + EXECUTE = 4; + + // The file system allows searching files. + SEARCH = 5; + + // The file system allows deleting a file. + DELETE = 6; + + // The file system allows appending data to a file. + APPEND_DATA = 7; + + // The file system allows adding subdirectories. + ADD_SUBDIRECTORY = 8; + + // The file system allows deleting subdirectories. + DELETE_CHILD = 9; + + // The file system allows reading file attributes. + READ_ATTRIBUTES = 10; + + // The file system allows writing file attributes. + WRITE_ATTRIBUTES = 11; + + // The file system allows reading extended file attributes. + READ_XATTR = 12; + + // The file system allows writing extended file attributes. + WRITE_XATTR = 13; + + // The file system allows reading a file's security descriptors. + READ_SECURITY = 14; + + // The file system allows writing a file's security descriptors. + WRITE_SECURITY = 15; + + // The file system allows taking ownership of a file. + TAKE_OWNERSHIP = 16; + } + + // The item for which to check access. + uint64 item_id = 1; + + // A mask indicating a set of access types for which to check. + repeated AccessMask access = 2; +} + +// Sets a new name for the volume. +message SetVolumeName { + // The new volume name. + bytes name = 1; +} + +// Preallocate disk space for the given item. +message PreallocateSpace { + // Behavior flags for preallocation operations. + enum PreallocateFlag { + // Allocates contiguous space. + CONTIGUOUS = 0; + + // Allocates all requested space or no space at all. + ALL = 1; + + // Allocates space that isn't freed when deleting the descriptor. + // + // This space remains allocated even after calling `close(2)`. + PERSIST = 2; + + // Allocates space from the physical end of file. + // + // When implementing this behavior, ignore any offset in the preallocate call. + // This flag is currently set for all + // ``FSVolume/PreallocateOperations/preallocateSpace(for:at:length:flags:)`` + // calls. + FROM_EOF = 3; + } + + // The item for which to preallocate space. + uint64 item_id = 1; + + // The offset from which to allocate. + int64 offset = 2; + + // The length of the space in bytes. + int64 length = 3; + + // Flags that affect the preallocation behavior. + repeated PreallocateFlag flags = 4; +} + +// Notifies the file system that the kernel is no longer making immediate use of +// the given item. +// +// This method gives a file system a chance to release resources associated +// with an item. +// However, this method prescribes no specific action; it's acceptable to defer +// all reclamation until ``FSVolume/Operations/reclaimItem(_:)``. +// This method is the equivalent of VFS's `VNOP_INACTIVE`. +// +// FSKit restricts calls to this method based on the current value of +// ``FSVolume/ItemDeactivation/itemDeactivationPolicy``. +message DeactivateItem { + // The item to deactivate. + uint64 item_id = 1; +} + +// Request envelope for communication over the local socket. +message Request { + // Correlation identifier used to match responses to requests. + uint64 id = 1; + + // The concrete operation this request carries. + oneof content { + // Configurations + GetResourceIdentifier get_resource_identifier = 10; + GetVolumeIdentifier get_volume_identifier = 11; + GetVolumeBehavior get_volume_behavior = 12; + GetPathConfOperations get_path_conf_operations = 13; + + // Operations protocol. + // + // Methods that all volumes implement to provide required capabilities. + // + // Conform to this protocol in your subclass of ``FSVolume``. + // To provide additional capabilities, conform to the other `FSVolume` + // operations protocols, like ``FSVolumeOpenCloseOperations`` and + // ``FSVolumeReadWriteOperations``. + // + // > Note: This protocol extends ``FSVolumePathConfOperations``, so your volume + // implementation must also conform to that protocol. + GetVolumeCapabilities get_volume_capabilities = 14; + GetVolumeStatistics get_volume_statistics = 15; + Mount mount = 16; + Unmount unmount = 17; + Synchronize synchronize = 18; + GetAttributes get_attributes = 19; + SetAttributes set_attributes = 20; + LookupItem lookup_item = 21; + ReclaimItem reclaim_item = 22; + ReadSymbolicLink read_symbolic_link = 23; + CreateItem create_item = 24; + CreateSymbolicLink create_symbolic_link = 25; + CreateLink create_link = 26; + RemoveItem remove_item = 27; + RenameItem rename_item = 28; + EnumerateDirectory enumerate_directory = 29; + Activate activate = 30; + Deactivate deactivate = 31; + + // XattrOperations protocol. + // + // Methods and properties implemented by volumes that natively or partially + // support extended attributes. + GetSupportedXattrNames get_supported_xattr_names = 32; + GetXattr get_xattr = 33; + SetXattr set_xattr = 34; + GetXattrs get_xattrs = 35; + + // OpenCloseOperations protocol. + // + // Methods and properties implemented by volumes that want to receive open and + // close calls for each item. + // + // When a file system volume conforms to this protocol, the kernel layer issues + // an open call to indicate desired access, and a close call to indicate + // what access to retain. + // A file is fully closed when the kernel layer issues a close call with no + // retained open nodes. + // When a file system receives the close call, it removes all access to the item. + // When all memory mappings to the item release, the kernel layer issues + // a final close. + // + // If a file system volume doesn't conform to this protocol, the kernel layer + // can skip making such calls to the volume. + OpenItem open_item = 36; + CloseItem close_item = 37; + + // ReadWriteOperations protocol. + // + // Methods implemented for read and write operations that deliver data to and + // from the extension. + // + // Most volumes conform to either this protocol or + // ``FSVolumeKernelOffloadedIOOperations``. + // You can conform to both if you need to provide kernel-offloaded I/O only for + // certain files. + // In that case, files with the ``FSItem/Attribute/inhibitKernelOffloadedIO`` + // attribute set use this protocol, and those without it use + // ``FSVolumeKernelOffloadedIOOperations``. + // A volume that doesn't conform to either protocol can't support + // any I/O operation. + Read read = 38; + Write write = 39; + + // AccessCheckOperations protocol. + // + // Methods and properties implemented by volumes that want to enforce access + // check operations. + CheckAccess check_access = 40; + + // RenameOperations protocol. + // + // Methods and properties implemented by volumes that support renaming + // the volume. + SetVolumeName set_volume_name = 41; + + // ``PreallocateFlags`` protocol. + // + // Methods and properties implemented by volumes that want to offer + // preallocation functions. + // + // A preallocation operation allocates space for a file without writing to it + // yet. + // A file system may use reallocation to avoid performing space allocation while + // in the midst of I/O; this strategy improves performance. + // Also, if the expected I/O pattern is many small writes, + // preallocating contiguous chunks may prevent fragmenting the file system. + // This process can improve performance later. + // + // In a kernel-based file system, you typically preallocate space with the + // `VNOP_ALLOCATE` operation, called from `fcntl(F_PREALLOCATE)`. + PreallocateSpace preallocate_space = 42; + + // ItemDeactivation protocol. + // + // Methods and properties implemented by volumes that support + // deactivating items. + DeactivateItem deactivate_item = 43; + } +} + +// A resource identifier and name. +message ResourceIdentifier { + // A resource name, as found during the probe operation. + // If the file system doesn't support names, or is awaiting naming, + // use an empty string. + optional string name = 1; + + // A container identifier, as found during the probe operation. + // If the file system doesn't support durable identifiers, use a random UUID. + optional string container_id = 2; +} + +// A volume identifier and name. +message VolumeIdentifier { + // An ``FSVolumeIdentifier`` to uniquely identify the volume. + // For a network file system that supports multiple authenticated users, + // disambiguate the users by using qualifying data in the identifier. + optional string id = 1; + + // A name for the volume. + optional string name = 2; +} + +// Per-mount behavior settings for an FSKit volume. +// These options let the host file system extension declare behaviors and +// selectively inhibit operation protocols. +// +// FSKit reads these after the file system replies to the `loadResource` +// message. +// Changing the returned value during the runtime of the volume has no effect. +message VolumeBehavior { + // Options to specify the item deactivation policy. + // + // Callers may want to set a deactivation policy because + // ``FSVolume/ItemDeactivation/deactivateItem(_:)`` + // processing blocks the kernel. + // Setting a deactivation policy allows the file system to take action at a + // definitive point in the item's life cycle. + // These options allow the file system to instruct the FSKit kernel of which + // circumstances require the expense of a round-trip call to the module. + // + // > Note: To avoid performing deactivation calls, use an empty option set (`[]`). + enum ItemDeactivationOption { + // An option to always perform deactivation calls. + // + // Use this option if the file system needs `deactivateItem` calls in + // circumstances beyond those covered + // by ``forRemovedItems`` and ``forPreallocatedItems``. + ALWAYS = 0; + + // An option to process deactivation for open-unlinked items at the moment of + // last close. + FOR_REMOVED_ITEMS = 1; + + // An option to process deactivation for for files with preallocated space. + // + // This option facilitates a sort of trim-on-close behavior. + // It is only meaningful for volumes that conform to ``FSVolume/PreallocateOperations``. + FOR_PREALLOCATED_ITEMS = 2; + } + + // A property that allows the file system to use open-unlink emulation. + // + // _Open-unlink_ functionality refers to a file system's ability to support + // an open file being fully unlinked from the file system namespace. + // If a file system doesn't support this functionality, FSKit can emulate + // it instead; this is called "open-unlink emulation". + // + // Set this property to `true` to allow FSKit to perform open-unlink emulation. + // Otherwise, FSKit doesn't perform open-unlink emulation for this volume. + optional bool enable_open_unlink_emulation = 1; + + // A Boolean value that instructs FSKit not to call ``XattrOperations`` + // protocol's methods. + optional bool xattr_operations_inhibited = 2; + + // A Boolean value that instructs FSKit not to call ``OpenCloseOperations`` + // protocol's methods. + optional bool is_open_close_inhibited = 3; + + // A Boolean value that instructs FSKit not to call ``AccessCheckOperations`` + // protocol's methods. + optional bool is_access_check_inhibited = 4; + + // A Boolean value that instructs FSKit not to call ``RenameOperations`` + // protocol's methods. + optional bool is_volume_rename_inhibited = 5; + + // A Boolean value that instructs FSKit not to call ``PreallocateOperations`` + // protocol's methods. + optional bool is_preallocate_inhibited = 6; + + // A property that tells FSKit to which types of items the deactivation applies, + // if any. + repeated ItemDeactivationOption item_deactivation_options = 7; +} + +// Properties implemented by volumes that support providing the values of system +// limits or options. +// +// This protocol gathers properties related to the `pathconf` and `fpathconf` +// system calls. +// +// For a file, the value of a property applies to just that file; for a +// directory, the value applies to all items in the directory. +// +// Properties that represent limits and have a numeric type use `-1` to +// represent no limit. +message PathConfOperations { + // A property that represents the maximum number of hard links to the object. + int64 maximum_link_count = 1; + + // A property that represents the maximum length of a component of a filename. + int64 maximum_name_length = 2; + + // A Boolean property that indicates whether the volume restricts ownership + // changes based on authorization. + // + // If this value is true, the volume rejects a `chown(2)` from anyone other than + // the superuser. + bool restricts_ownership_changes = 3; + + // A property that indicates whether the volume truncates files longer than its + // maximum supported length. + // + // If this value is `true`, the volume truncates the filename to + // ``maximumNameLength`` if the filename is longer than that. + // If this value is false, the file system responds with the error code + // `ENAMETOOLONG` if the filename is longer than ``maximumNameLength``. + bool truncates_long_names = 4; + + // The maximum extended attribute size in bytes. + // + // Implement at least one of `maximumXattrSize` or ``maximumXattrSizeInBits``. + // FSKit automatically converts from one to another if needed. + // If you implement both, FSKit uses only the `maximumXattrSizeInBits` + // implementation. + optional int64 maximum_xattr_size = 5; + + // The maximum extended attribute size in bits. + // + // Implement at least one of ``maximumXattrSize`` or `maximumXattrSizeInBits`. + // FSKit automatically converts from one to another if needed. + // If you implement both, FSKit uses only the `maximumXattrSizeInBits` + // implementation. + optional int64 maximum_xattr_size_in_bits = 6; + + // The maximum size of a regular file allowed in the volume. + // + // Implement at least one of `maximumFileSize` or ``maximumFileSizeInBits``. + // FSKit automatically converts from one to another if needed. + // If you implement both, FSKit uses only the `maximumFileSizeInBits` + // implementation. + optional uint64 maximum_file_size = 7; + + // The minimum number of bits needed to represent, as a signed integer value, + // the maximum size of a regular file + // allowed in the volume. + // + // The maximum file size is `2^(maximumFileSizeInBits - 1)`. + // + // | Maximum file size (bytes) | Maximum (in hex) | Unsigned bits | Signed bits | + // | -------------------------: | -------------------: | ------------: | ----------: | + // | 65,535 | `0xFFFF` | 16 | 17 | + // | 2,147,483,647 | `0x7FFFFFFF` | 31 | 32 | + // | 4,294,967,295 | `0xFFFFFFFF` | 32 | 33 | + // | 18,446,744,073,709,551,615 | `0xFFFFFFFFFFFFFFFF` | 64 | 65 | + // + // Implement at least one of ``maximumFileSize`` or `maximumFileSizeInBits`. + // FSKit automatically converts from one to another if needed. + // If you implement both, FSKit uses only the `maximumFileSizeInBits` + // implementation. + optional int64 maximum_file_size_in_bits = 8; +} + +// Properties that represent capabilities supported by a volume, such as hard +// and symbolic links, journaling, and large file sizes. +message SupportedCapabilities { + // An enumeration of case-sensitivity support types. + // + // A case-sensitive volume is a volume that treats upper and lower case + // characters in file and directory names as being distinct from each other. + // For example, `FILE.TXT` and `file.TXT` are different names in + // a case-sensitive volume, and the same name in a case-insensitive volume. + enum CaseFormat { + // The volume is case sensitive. + SENSITIVE = 0; + + // The volume isn't case sensitive. + INSENSITIVE = 1; + + // The volume isn't case sensitive, but supports preserving the case of file and + // directory names. + INSENSITIVE_CASE_PRESERVING = 2; + } + + // A Boolean property that indicates whether the volume supports persistent + // object identifiers and can look up file system objects by their IDs. + optional bool supports_persistent_object_ids = 1; + + // A Boolean property that indicates whether the volume supports symbolic links. + optional bool supports_symbolic_links = 2; + + // A Boolean property that indicates whether the volume supports hard links. + optional bool supports_hard_links = 3; + + // A Boolean property that indicates whether the volume supports a journal used + // to speed recovery in case of unplanned restart, such as a power outage or crash. + // + // This property doesn't necessarily mean the volume is actively using + // a journal. + optional bool supports_journal = 4; + + // A Boolean property that indicates whether the volume currently uses a journal + // for speeding recovery after an unplanned shutdown. + optional bool supports_active_journal = 5; + + // A Boolean property that indicates the volume doesn't store reliable times for + // the root directory. + // + // If this value is `true`, the volume doesn't store reliable times for the root + // directory. + optional bool does_not_support_root_times = 6; + + // A Boolean property that indicates whether the volume supports sparse files. + // + // A sparse file is a file that can have "holes" that the file system has never + // written to, and as a result don't consume space on disk. + optional bool supports_sparse_files = 7; + + // A Boolean property that indicates whether the volume supports zero runs + // + // If this value is true, the volume keeps track of allocated but unwritten runs + // of a file so that it can substitute zeroes without actually writing + // zeroes to the media. + optional bool supports_zero_runs = 8; + + // A Boolean property that indicates whether the volume supports fast results + // when fetching file system statistics. + // + // A true value means this volume hints to upper layers to indicate that + // `statfs(2)` is fast enough that its results need not be cached by the caller. + optional bool supports_fast_statfs = 9; + + // A Boolean property that indicates whether the volume supports file sizes + // larger than 4GB, and potentially up to 2TB. + optional bool supports_2tb_files = 10; + + // A Boolean property that indicates whether the volume supports open + // deny modes. + // + // These are modes such as "open for read write, deny write". + optional bool supports_open_deny_modes = 11; + + // A Boolean property that indicates whether the volume supports hidden files. + // + // A `true` value means the volume supports the `UF_HIDDEN` file flag. + optional bool supports_hidden_files = 12; + + // A Boolean property that indicates the volume doesn't support certain volume + // size reports. + // + // A true value means the volume doesn't support determining values for total + // data blocks, available blocks, or free blocks, as in `f_blocks`, `f_bavail`, + // and `f_bfree` in the struct `statFS` returned by `statfs(2)`. + optional bool does_not_support_volume_sizes = 13; + + // A Boolean property that indicates whether the volume supports 64-bit + // object IDs. + optional bool supports_64bit_object_ids = 14; + + // A Boolean property that indicates whether the volume supports document IDs + // for document revisions. + // + // A document ID is an identifier that persists across object ID changes. + optional bool supports_document_id = 15; + + // A Boolean property that indicates the volume doesn't support immutable files. + // + // A `true` value means this volume doesn't support setting + // the `UF_IMMUTABLE` flag. + optional bool does_not_support_immutable_files = 16; + + // A Boolean property that indicates the volume doesn't set file permissions. + // + // If this value is `true`, the volume doesn't support setting file permissions. + optional bool does_not_support_setting_file_permissions = 17; + + // A Boolean property that indicates whether the volume supports multiple + // logical file systems that share space in a single "partition". + optional bool supports_shared_space = 18; + + // A Boolean property that indicates whether the volume supports volume groups. + // + // Volume groups involve multiple logical file systems that the system can mount + // and unmount together, and for which the system can present common file system + // identifier information. + optional bool supports_volume_groups = 19; + + // A value that indicates the volume's support for case sensitivity. + optional CaseFormat case_format = 20; +} + +// Properties used to report a volume's statistics. +// +// The names of this properties match those in the `statfs` structure in +// `statfs(2)`, which reports these values for an FSKit file system. +// All numeric properties default to `0`. +// Override these values, unless a given property has no meaningful value +// to provide. +// +// > Note: Available space, free space, total space, and used space have +// properties to express their values either as a number of blocks or +// a number of bytes. +// Your module may supply both of these values by setting both the relevant +// block or byte property. +// Alternatively, a module may set only one of the two properties. +// When you do this, FSKit calculates the matching value based on ``blockSize``. +// +// For the read-only ``fileSystemTypeName``, set this value with the designated +// initializer. +message StatFSResult { + // A property for the volume's block size, in bytes. + // + // This value defaults to `4096`. + // Zero isn't a valid block size. + int64 block_size = 1; + + // A property for the optimal block size with which to perform I/O. + // + // For best performance, specify an `ioSize` that's an even multiple + // of ``blockSize``. + int64 io_size = 2; + + // A property for the volume's total data block count. + uint64 total_blocks = 3; + + // A property for the number of free blocks available to a non-superuser + // on the volume. + uint64 available_blocks = 4; + + // A property for the number of free blocks in the volume. + uint64 free_blocks = 5; + + // A property for the number of used blocks in the volume. + uint64 used_blocks = 6; + + // A property for the total size, in bytes, of the volume. + uint64 total_bytes = 7; + + // A property for the amount of space available to users, in bytes, + // in the volume. + uint64 available_bytes = 8; + + // A property for the amount of free space, in bytes, in the volume. + uint64 free_bytes = 9; + + // A property for the amount of used space, in bytes, in the volume. + uint64 used_bytes = 10; + + // A property for the total number of file slots in the volume, + uint64 total_files = 11; + + // A property for the total number of free file slots in the volume. + uint64 free_files = 12; +} + +// Attributes of an item, such as size, creation and modification times, +// and user and group identifiers. +message ItemAttributes { + // The user identifier. + optional uint32 uid = 1; + + // The group identifier. + optional uint32 gid = 2; + + // The mode of the item. + // + // The mode is often used for `setuid`, `setgid`, and `sticky` bits. + optional uint32 mode = 3; + + // The item type, such as a regular file, directory, or symbolic link. + optional ItemType type = 4; + + // The number of hard links to the item. + optional uint32 link_count = 5; + + // The item's behavior flags. + // + // See `st_flags` in `stat.h` for flag definitions. + optional uint32 flags = 6; + + // The item's size. + optional uint64 size = 7; + + // The item's allocated size. + optional uint64 alloc_size = 8; + + // The item's file identifier. + // Reserved values: invalid = 0, parentOfRoot = 1, and rootDirectory = 2. + optional uint64 file_id = 9; + + // The identifier of the item's parent. + // Reserved values: invalid = 0, parentOfRoot = 1, and rootDirectory = 2. + optional uint64 parent_id = 10; + + // A Boolean value that indicates whether the item supports a limited set + // of extended attributes. + optional bool supports_limited_xattrs = 11; + + // A Boolean value that indicates whether the file system overrides + // the per-volume settings for kernel offloaded I/O for a specific file. + // + // This property has no meaning if the volume doesn't conform to + // ``FSVolumeKernelOffloadedIOOperations``. + optional bool inhibit_kernel_offloaded_io = 12; + + // The item's last-modified time. + // + // This property represents `mtime`, the last time the item's contents changed. + optional google.protobuf.Timestamp modify_time = 13; + + // The item's added time. + // + // This property represents the time the file system added the item to its + // parent directory. + optional google.protobuf.Timestamp added_time = 14; + + // The item's last-changed time. + // + // This property represents `ctime`, the last time the item's metadata changed. + optional google.protobuf.Timestamp change_time = 15; + + // The item's last-accessed time. + optional google.protobuf.Timestamp access_time = 16; + + // The item's creation time. + optional google.protobuf.Timestamp birth_time = 17; + + // The item's last-backup time. + optional google.protobuf.Timestamp backup_time = 18; +} + +// A distinct object in a file hierarchy, such as a file, directory, symlink, +// socket, and more. +message Item { + // Attributes of an item, such as size, creation and modification times, + // and user and group identifiers. + ItemAttributes attributes = 1; + + // The name of a file, expressed as a data buffer. + bytes name = 2; +} + +// An object used to provide items during a directory enumeration. +message DirectoryEntries { + // An object used to provide item during a directory enumeration. + message Entry { + // A distinct object in a file hierarchy, such as a file, directory, symlink, + // socket, and more. + Item item = 1; + + // A value to indicate the next entry in the directory to enumerate. + uint64 next_cookie = 2; + } + + // List of entries. + repeated Entry entries = 1; + + // A tool to detect whether the directory contents changed since the last call + // to `enumerateDirectory`. + uint64 verifier = 2; +} + +// Extended attributes. +message Xattrs { + // The list of extended attribute names. + repeated bytes names = 1; +} + +// Successful result. +message Success {} + +// Response envelope for communication. +message Response { + // Correlates this response with the original request. + uint64 request_id = 1; + + // The concrete result payload for the handled request. + oneof content { + ResourceIdentifier resource_identifier = 10; + VolumeIdentifier volume_identifier = 11; + VolumeBehavior volume_behavior = 12; + PathConfOperations path_conf_operations = 13; + SupportedCapabilities supported_capabilities = 14; + StatFSResult stat_fs_result = 15; + ItemAttributes item_attributes = 16; + Item item = 17; + DirectoryEntries directory_entries = 18; + Xattrs xattrs = 19; + bytes data = 20; + int64 byte_count = 21; + bool allow = 22; + Success success = 23; + int32 posix_error = 24; + } +} diff --git a/vendor/fskit-rs/src/registration.rs b/vendor/fskit-rs/src/registration.rs new file mode 100644 index 0000000..c9f48cc --- /dev/null +++ b/vendor/fskit-rs/src/registration.rs @@ -0,0 +1,56 @@ +use std::path::PathBuf; + +use log::error; +use regex::Regex; + +use super::installer::{Error, PLUGINKIT, Result, run_cmd_out}; + +#[derive(Debug, Clone)] +pub(super) struct Status { + pub app_path: PathBuf, + pub elected: bool, +} + +pub(super) fn registrations(appex_id: &str) -> Result> { + let output = match run_cmd_out(PLUGINKIT, &["-m", "-i", appex_id, "--raw"]) { + Ok(output) => output, + Err(err) => { + error!("failed to query pluginkit for {appex_id}: {err}"); + return Err(err); + } + }; + let stdout = String::from_utf8_lossy(&output.stdout); + parse_statuses(&stdout) +} + +fn parse_statuses(stdout: &str) -> Result> { + let election_re = Regex::new(r#"^\s*election = (\d+);$"#).unwrap(); + let path_re = Regex::new(r#"^\s*path = "([^"]+)";$"#).unwrap(); + + let mut list = Vec::new(); + let mut elected = false; + + for line in stdout.lines() { + if let Some(captures) = election_re.captures(line) { + elected = &captures[1] == "1"; + } + + if let Some(captures) = path_re.captures(line) { + let appex_path = PathBuf::from(&captures[1]); + let Some(app_path) = appex_path + .parent() + .and_then(|it| it.parent()) + .and_then(|it| it.parent()) + else { + return Err(Error::InvalidExtensionPath); + }; + list.push(Status { + app_path: app_path.to_path_buf(), + elected, + }); + elected = false; + } + } + + Ok(list) +} diff --git a/vendor/fskit-rs/src/session.rs b/vendor/fskit-rs/src/session.rs new file mode 100644 index 0000000..fa29044 --- /dev/null +++ b/vendor/fskit-rs/src/session.rs @@ -0,0 +1,94 @@ +use std::path::Path; + +use log::error; + +use super::handler::Handler; +use super::info::Info; +use super::installer; +use super::mounter::Mounter; +use super::socket::Socket; +use super::{Filesystem, MountOptions, mounter, registration, socket}; + +use self::Error::ExtensionNotRegistered; + +pub type Result = std::result::Result; + +#[derive(Debug)] +pub struct Session { + socket: Socket, + mounter: Mounter, +} + +impl Session { + pub(super) async fn new(fs: FS, opts: MountOptions) -> Result + where + FS: Filesystem + Send + Sync + Clone + 'static, + { + let (server_port, fs_type) = match &opts.skip_registration { + Some(skip) => (skip.server_port, skip.fs_type.clone()), + None => read_config(&opts.fskit_id)?, + }; + + let handler = Handler::new(fs); + + let socket = Socket::start(handler, server_port).await?; + + let mounter = match Mounter::mount(opts, &fs_type) { + Ok(mount) => mount, + Err(err) => { + socket.stop().await; + return Err(Error::Mounter(err)); + } + }; + + Ok(Self { socket, mounter }) + } +} + +impl Drop for Session { + fn drop(&mut self) { + let _ = self.mounter.unmount().inspect_err(|err| error!("{err}")); + + futures::executor::block_on(async { + self.socket.stop().await; + }); + } +} + +fn read_config(appex_id: &str) -> Result<(u16, String)> { + let statuses = registration::registrations(appex_id)?; + + let Some(status) = statuses + .iter() + .find(|status| status.elected) + .or_else(|| statuses.first()) + else { + return Err(ExtensionNotRegistered); + }; + + let appex_path = installer::appex_path(&status.app_path)?; + let info = Info::new(Path::new(&appex_path)).map_err(installer::Error::from)?; + + Ok(( + info.server_port().map_err(installer::Error::from)?, + info.fs_type().map_err(installer::Error::from)?, + )) +} + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + Io(#[from] std::io::Error), + + #[error(transparent)] + Installer(#[from] installer::Error), + + #[error(transparent)] + Socket(#[from] socket::Error), + + #[error(transparent)] + Mounter(#[from] mounter::Error), + + #[error("FSKit extension is not registered")] + ExtensionNotRegistered, +} diff --git a/vendor/fskit-rs/src/socket.rs b/vendor/fskit-rs/src/socket.rs new file mode 100644 index 0000000..2c62bec --- /dev/null +++ b/vendor/fskit-rs/src/socket.rs @@ -0,0 +1,187 @@ +use std::net::Ipv4Addr; + +use bytes::{Buf, BytesMut}; +use log::{debug, error, info, warn}; +use prost::Message; +use tokio::io::{AsyncWriteExt, Interest}; +use tokio::net::{TcpListener, TcpStream}; +use tokio::select; +use tokio::sync::mpsc::{Receiver, Sender}; +use tokio::sync::{broadcast, mpsc}; + +use crate::Filesystem; +use crate::handler::Handler; +use crate::pb::{Request, Response, response}; + +pub type Result = std::result::Result; + +#[derive(Debug)] +pub(super) struct Socket { + stop_tx: Sender<()>, +} + +impl Socket { + pub(super) async fn start(handler: Handler, server_port: u16) -> Result + where + FS: Filesystem + Send + Sync + Clone + 'static, + { + let (start_tx, mut start_rx) = mpsc::channel::(1); + let (stop_tx, stop_rx) = mpsc::channel::<()>(1); + + tokio::spawn(async move { + if spawn_loop(&start_tx, stop_rx, handler, server_port) + .await + .is_err() + { + let _ = start_tx.send(false).await; + } + }); + + if !start_rx.recv().await.unwrap_or(false) { + return Err(Error::StartFailed); + } + + Ok(Self { stop_tx }) + } + + pub(super) async fn stop(&self) { + let _ = self.stop_tx.send(()).await; + } +} + +async fn spawn_loop( + start_tx: &Sender, + mut stop_rx: Receiver<()>, + handler: Handler, + server_port: u16, +) -> Result<()> +where + FS: Filesystem + Send + Sync + Clone + 'static, +{ + let addr = format!("{}:{}", Ipv4Addr::LOCALHOST, server_port); + + let listener = TcpListener::bind(&addr).await?; + info!("listening on {addr}"); + + let _ = start_tx.send(true).await; + + let (shutdown_tx, _) = broadcast::channel::<()>(2); + + loop { + select! { + _ = stop_rx.recv() => { + info!("stop listening"); + let _ = shutdown_tx.send(()); + break; + } + + Ok((stream, peer)) = listener.accept() => { + info!("accepted connection from {peer}"); + let handler = handler.clone(); + let shutdown_rx = shutdown_tx.subscribe(); + tokio::spawn(async move { + if let Err(err) = handle_stream(stream, handler, shutdown_rx).await { + error!("{err}"); + } + }); + } + } + } + + Ok(()) +} + +async fn handle_stream( + mut stream: TcpStream, + mut handler: Handler, + mut shutdown_rx: broadcast::Receiver<()>, +) -> Result<()> +where + FS: Filesystem + Send + Sync + Clone + 'static, +{ + let mut buf = BytesMut::with_capacity(4096); + loop { + select! { + _ = shutdown_rx.recv() => { + let _ = stream.shutdown().await; + info!("connection closed by shutdown: {stream:?}"); + return Ok(()); + } + + r = stream.ready(Interest::READABLE) => { + r?; + match stream.try_read_buf(&mut buf) { + Ok(0) => { + info!("connection closed by peer: {stream:?}"); + return Ok(()); + } + Ok(_) => { + while buf.has_remaining() { + let mut frozen = buf.clone().freeze(); + match Request::decode_length_delimited(&mut frozen) { + Ok(request) => { + debug!("received message: {request:?}"); + buf.advance(buf.len() - frozen.remaining()); + + let content = match request.content { + Some(content) => match handler.handle(content).await { + Ok(content) => Some(content), + Err(err) => { + error!("handler error: {err}"); + None + } + }, + None => { + warn!("received request without content: {}", request.id); + Some(response::Content::PosixError(libc::EINVAL)) + } + }; + + let response = Response { request_id: request.id, content }; + + let mut out = Vec::with_capacity(4096); + response.encode_length_delimited(&mut out).unwrap(); + + stream.ready(Interest::WRITABLE).await?; + if let Err(err) = stream.write_all(&out).await { + error!("write error: {err}"); + return Err(err.into()); + } + } + Err(err) => { + let s = err.to_string(); + if !s.contains("failed to decode length prefix") + && !s.contains("buffer underflow") + { + error!("decode error: {err}"); + return Err(err.into()); + } + break; + } + } + } + } + Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => { + continue; + } + Err(err) => { + error!("read error: {err}"); + return Err(err.into()); + } + } + } + } + } +} + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + Io(#[from] std::io::Error), + + #[error(transparent)] + DecodeError(#[from] prost::DecodeError), + + #[error("socket failed to start")] + StartFailed, +}