diff --git a/.github/workflows/native-prebuilts.yml b/.github/workflows/native-prebuilts.yml index b59a6523..ef344187 100644 --- a/.github/workflows/native-prebuilts.yml +++ b/.github/workflows/native-prebuilts.yml @@ -1,6 +1,7 @@ -name: Build native prebuilts +name: Native and package validation on: + pull_request: push: tags: ['native-*'] workflow_dispatch: @@ -10,11 +11,85 @@ permissions: concurrency: group: native-prebuilts-${{ github.ref }} - cancel-in-progress: false + cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: + verify: + name: Rust, Dart, and lifecycle tests + runs-on: ubuntu-22.04 + timeout-minutes: 60 + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + - name: Install Linux prerequisites + run: | + sudo apt-get update + sudo apt-get install -y build-essential libgtk-3-dev liblzma-dev ninja-build pkg-config perl + - name: Install pinned Rust toolchain + run: bash tool/ci/install_rust.sh --component clippy,rustfmt + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + with: + workspaces: rust + - uses: subosito/flutter-action@1508160852fb97248640997f7cfb38da241df0ba # v2.9.1 + with: + flutter-version: '3.47.2' + channel: stable + - name: Verify Rust + run: | + cargo fmt --manifest-path rust/Cargo.toml --check + cargo test --locked --all-targets --manifest-path rust/Cargo.toml + cargo clippy --locked --all-targets --manifest-path rust/Cargo.toml -- -D warnings + - name: Verify Dart and native lifecycle + run: | + flutter pub get + flutter analyze + flutter test + - name: Test example widgets + working-directory: example + run: flutter test + - name: Cache pinned cargo-audit binary + id: cargo-audit-cache + uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 + with: + path: ~/.cargo/bin/cargo-audit + key: cargo-audit-${{ runner.os }}-${{ runner.arch }}-0.22.2-${{ hashFiles('rust/rust-toolchain.toml') }} + - name: Install pinned cargo-audit + if: steps.cargo-audit-cache.outputs.cache-hit != 'true' + run: cargo install cargo-audit --version 0.22.2 --locked + - name: Audit Rust dependencies + run: | + cargo audit --file rust/Cargo.lock \ + --deny unsound \ + --deny yanked \ + --ignore RUSTSEC-2021-0145 \ + --ignore RUSTSEC-2023-0071 + + windows-tests: + if: github.event_name == 'pull_request' + name: Windows time, IPv6, and file release tests + runs-on: windows-2022 + timeout-minutes: 60 + env: + NATIVE_TARGET: x86_64-pc-windows-msvc + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + - name: Use short Windows build paths + shell: pwsh + run: ./tool/ci/windows_paths.ps1 + - name: Install pinned Rust toolchain + run: bash tool/ci/install_rust.sh --target "$NATIVE_TARGET" + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + with: + workspaces: rust + key: windows-tests + cache-targets: false + - name: Test time limits, IPv6 fallback, and file release + run: bash tool/ci/test_windows.sh + build: - if: github.event.deleted != true + if: github.event_name != 'pull_request' && github.event.deleted != true name: ${{ matrix.target }} runs-on: ${{ matrix.runner }} timeout-minutes: 120 @@ -42,17 +117,11 @@ jobs: run: shell: bash steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 - name: Use short Windows build paths if: runner.os == 'Windows' shell: pwsh - run: | - git config --global core.longpaths true - New-Item -ItemType Directory -Force C:\c, C:\t, C:\tmp | Out-Null - "CARGO_HOME=C:/c" >> $env:GITHUB_ENV - "CARGO_TARGET_DIR=C:/t" >> $env:GITHUB_ENV - "TEMP=C:\tmp" >> $env:GITHUB_ENV - "TMP=C:\tmp" >> $env:GITHUB_ENV - - uses: actions/checkout@v6 + run: ./tool/ci/windows_paths.ps1 - name: Install Linux prerequisites if: runner.os == 'Linux' run: | @@ -64,11 +133,8 @@ jobs: "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" 'ndk;28.2.13676358' echo "ANDROID_NDK_HOME=$ANDROID_HOME/ndk/28.2.13676358" >> "$GITHUB_ENV" - name: Install pinned Rust toolchain - run: | - toolchain=$(sed -n 's/^channel = "\([^"]*\)".*/\1/p' rust/rust-toolchain.toml) - rustup toolchain install "$toolchain" --profile minimal --target "$NATIVE_TARGET" - echo "RUSTUP_TOOLCHAIN=$toolchain" >> "$GITHUB_ENV" - - uses: Swatinem/rust-cache@v2 + run: bash tool/ci/install_rust.sh --target "$NATIVE_TARGET" + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: workspaces: rust key: ${{ matrix.target }} @@ -107,44 +173,110 @@ jobs: esac mkdir -p artifacts cp "$CARGO_TARGET_DIR/$NATIVE_TARGET/release/$library" "artifacts/$NATIVE_TARGET-$library" + - name: Test time limits, IPv6 fallback, and file release + if: runner.os == 'Windows' + run: bash tool/ci/test_windows.sh - name: Smoke test desktop library if: ${{ !contains(matrix.target, 'android') && !contains(matrix.target, '-ios') }} - run: | - python - <<'PY' - import ctypes - from pathlib import Path - library = ctypes.CDLL(str(next(Path('artifacts').iterdir()).resolve())) - library.tor_proxy_stop.argtypes = [ctypes.c_void_p] - library.tor_proxy_stop.restype = None - library.tor_proxy_stop(None) - PY - - uses: actions/upload-artifact@v4 + run: python tool/ci/smoke_native.py artifacts/* + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: prebuilt-${{ matrix.target }} path: artifacts/* if-no-files-found: error + package: + if: github.event_name == 'pull_request' + name: Package example (${{ matrix.platform }}) + runs-on: ${{ matrix.runner }} + timeout-minutes: 120 + strategy: + fail-fast: false + matrix: + include: + - {platform: linux, runner: ubuntu-22.04} + - {platform: android, runner: ubuntu-22.04} + - {platform: ios, runner: macos-15} + - {platform: macos, runner: macos-15} + - {platform: windows, runner: windows-2022} + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + - name: Install Linux prerequisites + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y build-essential clang cmake libgtk-3-dev liblzma-dev ninja-build pkg-config perl + - name: Install Android Java + if: matrix.platform == 'android' + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 + with: + distribution: temurin + java-version: '17' + - name: Install Android NDK + if: matrix.platform == 'android' + run: | + "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" 'ndk;28.2.13676358' + echo "ANDROID_NDK_HOME=$ANDROID_HOME/ndk/28.2.13676358" >> "$GITHUB_ENV" + - name: Install pinned Rust toolchain + run: | + case "${{ matrix.platform }}" in + linux) target=x86_64-unknown-linux-gnu ;; + android) target=aarch64-linux-android ;; + ios) target=aarch64-apple-ios ;; + # flutter build macos is universal, so the hook also builds x86_64. + macos) target=aarch64-apple-darwin,x86_64-apple-darwin ;; + windows) target=x86_64-pc-windows-msvc ;; + esac + bash tool/ci/install_rust.sh --target "$target" + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + with: + workspaces: rust + key: package-${{ matrix.platform }} + - uses: subosito/flutter-action@1508160852fb97248640997f7cfb38da241df0ba # v2.9.1 + with: + flutter-version: '3.47.2' + channel: stable + - name: Build packaged example + working-directory: example + run: | + case "${{ matrix.platform }}" in + linux) flutter build linux --release ;; + android) flutter build apk --release --target-platform android-arm64 ;; + ios) flutter build ios --release --no-codesign ;; + macos) flutter build macos --release ;; + windows) flutter build windows --release ;; + esac + - name: Load packaged macOS library + if: matrix.platform == 'macos' + run: bash tool/ci/check_macos_app.sh + - name: Load packaged Windows library + if: matrix.platform == 'windows' + run: python tool/ci/smoke_native.py example/build/windows/x64/runner/Release/tor_ffi_plugin.dll + release: - needs: build + if: github.event_name != 'pull_request' && github.event.deleted != true + needs: [verify, build] runs-on: ubuntu-22.04 permissions: contents: write steps: - - uses: actions/checkout@v6 - - uses: actions/download-artifact@v4 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: pattern: prebuilt-* path: artifacts merge-multiple: true - - uses: subosito/flutter-action@v2 + - uses: subosito/flutter-action@1508160852fb97248640997f7cfb38da241df0ba # v2.9.1 with: flutter-version: '3.47.2' channel: stable - cache: true - run: flutter pub get - name: Generate manifest run: dart --packages=.dart_tool/package_config.json tool/prebuilt_manifest.dart artifacts - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: native-release path: artifacts/* diff --git a/.gitignore b/.gitignore index faa341d4..cab43c36 100644 --- a/.gitignore +++ b/.gitignore @@ -31,4 +31,9 @@ build/ rust/target/.rustc_info.json rust/target/debug/ +rust/target/release/ rust/target/CACHEDIR.TAG + +# Cargo output from building a vendored crate on its own. +rust/patches/*/target/ +rust/patches/*/Cargo.lock diff --git a/CHANGELOG.md b/CHANGELOG.md index 599c4cf6..c91fa616 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,137 @@ +## 0.2.0 + +* Update the example to socks_socket 2.0.0, await Tor shutdown, and show start + and stop exception text so failures allow a retry. Keep Stop available after + a failed release and clear errors after success. Ignore asynchronous state + updates after the widget is disposed. Await SOCKS request writes so async + write failures reach the example's connection error handler. + The Bitcoin onion test validates its own field and reports a missing port + instead of throwing, and every example connection closes its socket or + HTTP client when the request fails. The buttons follow `Tor.status`, so + Stop stays available while the native handles are held. The sandboxed + macOS release build may open network connections and the local SOCKS + listener, so Tor starts outside debug builds too. + +* Bump arti to 2.7.0 and its supporting crates to 0.47.0; the pinned + toolchain is Rust 1.99.1. +* Vendor a reviewed `saturating-time` 0.5.0 fork under `rust/patches/`, with + regression coverage for coarse platform clocks. Source builds fetch only + from crates.io, and the prebuilt source fingerprint covers the patch but + not Cargo output left by building it on its own. +* Fix `tor_client_bootstrap` and `tor_client_set_dormant` freeing the client + handle Dart still holds. Add `tor_client_free`; `Tor.stop` now releases the + client handle. +* Let the OS allocate the SOCKS port while Rust binds the real IPv4 listener, + removing the probe-then-bind race. The same port is used for optional IPv6 + loopback and returned to Dart while the listeners remain reserved. The SOCKS + accept loop starts only after bootstrap succeeds. +* **Breaking change for C callers:** `tor_start` returns + `struct Tor { client, proxy, port }`. The SOCKS listeners are bound but idle + until `tor_proxy_start(proxy)` is called after a successful bootstrap. + Dart's `Tor.start` does this for you. +* `Tor.stop` resets the wrapper's state, so Tor can be started again. +* **Behavior change:** give bootstrap a configurable two-minute default + deadline. Before this release `Tor.start` waited indefinitely for + bootstrap; pass a longer `bootstrapTimeout` to keep waiting on slow + networks. A `stop` issued during bootstrap cancels it promptly before + releasing the native handles. +* Add `tor_client_cancel_bootstrap`. Cancellation is permanent for the + handle; free it and call `tor_start` again to bootstrap anew. +* `tor_client_bootstrap` returns a `TorBootstrapStatus` code (ok, cancelled, + timed out, failed, null client) instead of a bool, and Dart maps it without + matching error text. C enumerators are prefixed with `TorBootstrapStatus_` + to avoid collisions with application enums; their numeric values and Dart + names are unchanged. The header also supports C++ callers. A passed deadline throws the new `TorBootstrapTimeout` + instead of a plain `Exception`; its `timeout` is the `bootstrapTimeout` + passed to `start`, not the deadline rounded to milliseconds. +* Report Arti's full cause chain on bootstrap failure in the error message + Dart receives, without repeating a cause already included as the exact + suffix of its parent's message. +* A null client handle no longer aborts the process: `tor_client_bootstrap` + returns `NullClient` and `tor_client_set_dormant` does nothing; both set + the last error message. +* Concurrent `Tor.start` calls with the same raw data directory and timeout + share one attempt and complete when Tor is ready, or fail together with the + same error. Other starts queue behind it and either reuse the running + instance or fail with a `StateError` for a different data directory. +* Release the native client and proxy when bootstrap fails during + `Tor.start`, instead of leaving the proxy running. Preserve the bootstrap + error and stack if cleanup also fails; print the secondary cleanup failure + and expose it through `lastCleanupError` and `lastCleanupStackTrace`. +* `Tor.disable` now stops the proxy like `Tor.stop`; before, it only changed + the reported status while traffic kept flowing. **API change:** it returns + a `Future` that can throw on release failure. Await it or attach an + error handler when calling from a synchronous dispose method. +* Free the state and cache directory strings passed to `tor_start`. +* Add `tor_string_free` and use it to release Rust error messages after + Dart copies them. +* Strip NUL bytes from Rust error messages instead of panicking across FFI. +* `Tor.stop` now shuts down the client's runtime, releasing its threads and + its state and cache files, and completes once they are released. Shutdown + waits up to five seconds for blocking storage work; a write still running + after that may keep its file open briefly after the future completes. Before, + every start leaked a client that kept the cache open, so the data directory could + not be deleted on Windows. If the release itself fails, `stop` throws and + the next `start` or `stop` retries it before opening a new client. If proxy + release fails, retain its client too and retry the proxy before freeing it. +* Each client runs on its own runtime with two worker threads, created only + after the SOCKS port is bound. Before, every `tor_start` built a pool with + one thread per CPU core, even when the port turned out to be taken. +* `Tor.start` after an unawaited `Tor.stop` restarts Tor instead of returning + at once and letting the queued stop leave it off. Starts always queue behind + earlier calls; a start with a different data directory than the running + instance fails with a `StateError` and leaves it running. A `stop` issued + while a start is resolving its data directory cancels that start with + `TorStartCancelled` rather than letting it report success just before the + stop turns Tor off. +* `Tor.status` stays `on` while releasing a running instance, including when + release fails and handles remain for retry. Failed startup cleanup stays + `starting`; the next `start` or `stop` retries teardown before creating a new + client. Await a successful `stop` before deleting the data directory. + Status tracks lifecycle transitions and does not detect an unexpected SOCKS + accept-loop exit after startup. `Tor.port` throws as soon as the proxy has stopped, + with the message "Tor proxy is not running" instead of "Tor proxy port is + unexpectedly null"; the type is still `Exception`. +* `tor_proxy_stop` waits for the SOCKS accept loop to exit before returning, + so the listeners and its reference to the client are released, and Dart + runs it on a worker isolate. +* Use the data directories literally; names containing `$` or `~` were + expanded as variables and rejected. +* When the OS assigns the SOCKS port and its IPv6 loopback twin is held by + another process, try up to three more ports, then fail to start with an + error saying the twin stayed occupied. Serving IPv4 alone there would let + clients that resolve `localhost` to `::1` reach the other process. IPv4-only + loopback is used only when IPv6 is unavailable on the host. A caller-requested + non-zero port is not retried, and start fails if its IPv6 twin is held by + another process. Dart always passes zero, so only direct C callers see this. +* Start on Windows hosts with IPv6 disabled. Winsock reports the missing + address family as `WSAEAFNOSUPPORT`, which was not recognized, so + `Tor.start` failed instead of serving IPv4 loopback. +* Reject bootstrap timeouts under one millisecond before starting the native + client. +* `Tor.start` asynchronously creates the data directory and resolves its + physical path inside the queued start before comparing it with the running + instance. Paths through symbolic links or trailing separators reuse that + instance, and on macOS `/var/...` and `/private/var/...` no longer conflict. + When starting from off, status reports `starting` as soon as `start` is + called, before the queued start runs. + A path that cannot be created fails with the `FileSystemException` and restores + status to `off` if no native handles remain. +* Route `Tor.hello` through the injectable native layer so test instances + can substitute the greeting without calling the real library. +* Drop the unused `tor-config` dependency. +* Skip hidden files such as `.DS_Store` in the native source fingerprint and + hash sources as bytes, so stray files no longer fail or invalidate builds. +* Avoid hashing all Rust inputs during source builds; full fingerprints remain + required when selecting prebuilts. +* `rust/build.rs` declares its rerun triggers explicitly and includes + `ANDROID_NDK_HOME` and `CC_x86_64_linux_android`, so an x86_64 Android + build picks up a switched NDK without a clean build. +* Validate packaged macOS framework loading and audit Rust dependencies in CI + without granting write access to the workflow token. +* Keep Cargo's default release stripping on every platform. On macOS it + retains every exported `tor_*` symbol, and the packaged app starts Tor. + ## 0.1.0 * Build the Rust core with Flutter native assets instead of cargokit. diff --git a/README.md b/README.md index 25bcb552..5c767157 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,53 @@ Use `rustup`, not `homebrew`. The toolchain and cross-compilation targets pinne The Rust library is built and bundled by Flutter's native assets support (see `hook/build.dart`), so just `flutter run` or build as usual. Requires Flutter 3.47.2 or later; Android builds need NDK 27 or later. +### Starting and stopping Tor + +`Tor.start` completes after Tor has bootstrapped and the local SOCKS proxy is +ready. Bootstrap has a two-minute deadline by default; when it passes, +`Tor.start` throws `TorBootstrapTimeout`. Before 0.2.0 it waited +indefinitely. Callers can provide a different positive duration when needed: + +```dart +final directory = await getApplicationSupportDirectory(); +await Tor.instance.start( + torDataDirPath: directory.path, + bootstrapTimeout: const Duration(minutes: 3), +); +``` + +Starts and stops run one at a time in the order they were called, so +`stop()` followed by `start()` restarts Tor even when neither is awaited. +Concurrent starts with the same raw data directory and timeout share one +attempt. Other starts queue behind it and either reuse the running instance +or fail with a `StateError` for a different data directory, leaving it running. +The queued start creates and resolves the directory asynchronously before +comparing its physical path with the running instance. `Tor.stop()` and +`Tor.disable()` cancel an in-progress bootstrap, release the native client, and +complete after shutdown finishes. `Tor.status` stays `on` while releasing a +running instance, including when shutdown throws and resources remain for retry. +A failed start whose cleanup fails stays `starting`. The next `start()` or +`stop()` retries teardown before opening another client. Await a successful +`stop()` before deleting the data directory. Status tracks lifecycle +transitions; it does not detect an unexpected SOCKS accept-loop exit after +startup. +Native shutdown waits up to five seconds for blocking storage work; a longer +write may hold its file open briefly after the future completes. + +`disable()` returns the same throwing `Future` as `stop()`. Await it +where possible. A synchronous Flutter `dispose()` must handle its error +explicitly; `unawaited` alone does not catch failures: + +```dart +unawaited(Tor.instance.disable().catchError((Object error, StackTrace stack) { + debugPrint('Tor shutdown failed; retry stop before deleting its data: $error'); +})); +``` + +If startup and its cleanup both fail, the original startup exception and stack +are preserved. The cleanup failure is printed with its stack and remains +available as `lastCleanupError` and `lastCleanupStackTrace` until the next native start attempt. + ### Prebuilt libraries Copy the manifest URL and SHA-256 from a native release into your application's @@ -55,7 +102,9 @@ Prebuilts use dynamic linking without sanitizers and cover: Push a new `native-*` tag (for example `native-0.1.0-1`) to build and publish all targets. Release notes contain the hook settings above with the actual pin. Existing releases are not overwritten. Manual workflow runs on branches only -upload artifacts. To generate the manifest locally, put the target-named +upload artifacts. Pull requests run Rust/Dart tests, Windows time/IPv6/file-release +tests, and example packaging checks; the full cross-build matrix runs for tags +and manual dispatches. To generate the manifest locally, put the target-named libraries in `artifacts/`, run `flutter pub get`, then: ```sh @@ -67,6 +116,23 @@ dart --packages=.dart_tool/package_config.json tool/prebuilt_manifest.dart artif To generate `tor_ffi_plugin.h` C bindings for Rust, `cargo build` in `rust` to produce headers according to `build.rs`. To generate `tor_ffi_plugin_bindings_generated.dart` Dart bindings for C, `dart run ffigen --config ffigen.yaml`. +### Tests + +Run the Rust tests with the pinned toolchain, then the offline Dart tests: + +```sh +(cd rust && cargo test --locked) +flutter test +``` + +The lifecycle regression also verifies stop/restart against the real Tor +network. Enable it explicitly (it requires network access and can take several +minutes): + +```sh +flutter test --dart-define=TOR_NETWORK_TESTS=true +``` + ## Example app `flutter run` in `example` to run the example app diff --git a/example/lib/main.dart b/example/lib/main.dart index c1daf17e..d158fbdf 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -19,22 +19,36 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - return const MaterialApp( - home: Home(), - ); + return const MaterialApp(home: Home()); } } class Home extends StatefulWidget { - const Home({super.key}); + const Home({super.key, this.tor, this.moneroSocketFactory}); + + final Tor? tor; + final Future Function(int proxyPort)? moneroSocketFactory; @override State createState() => _MyAppState(); } class _MyAppState extends State { - // Flag to track if tor has started. - bool torIsRunning = false; + String? torError; + + Tor get _tor => widget.tor ?? Tor.instance; + + // Whether requests can use the proxy. After a failed stop the client is + // still held, so Tor reports on, but its proxy has already shut down and + // `port` throws. + bool get torIsRunning { + if (_tor.status != TorStatus.on) return false; + try { + return _tor.port > 0; + } on Exception { + return false; + } + } // Set the default text for the host input field. final hostController = TextEditingController(text: 'https://icanhazip.com/'); @@ -42,8 +56,8 @@ class _MyAppState extends State { // Set the default text for the onion input field. final onionController = TextEditingController( - text: - 'https://cflarexljc3rw355ysrkrzwapozws6nre6xsy3n4yrj7taye3uiby3ad.onion'); + text: 'https://cflarexljc3rw355ysrkrzwapozws6nre6xsy3n4yrj7taye3uiby3ad.onion', + ); // See https://blog.cloudflare.com/cloudflare-onion-service/ for more options: // cflarexljc3rw355ysrkrzwapozws6nre6xsy3n4yrj7taye3uiby3ad.onion // cflarenuttlfuyn7imozr4atzvfbiw3ezgbdjdldmdx7srterayaozid.onion @@ -57,33 +71,20 @@ class _MyAppState extends State { // cflare2nge4h4yqr3574crrd7k66lil3torzbisz6uciyuzqc2h2ykyd.onion final bitcoinOnionController = TextEditingController( - text: - 'qly7g5n5t3f3h23xvbp44vs6vpmayurno4basuu5rcvrupli7y2jmgid.onion:50001'); + text: + 'qly7g5n5t3f3h23xvbp44vs6vpmayurno4basuu5rcvrupli7y2jmgid.onion:50001', + ); // For more options, see https://bitnodes.io/nodes/addresses/?q=onion and // https://sethforprivacy.com/about/ final moneroOnionController = TextEditingController( - text: - 'ucdouiihzwvb5edg3ezeufcs4yp26gq4x64n6b4kuffb7s7jxynnk7qd.onion:18081/json_rpc'); + text: 'ucdouiihzwvb5edg3ezeufcs4yp26gq4x64n6b4kuffb7s7jxynnk7qd.onion:18081/json_rpc', + ); @override void initState() { super.initState(); - unawaited(init()); - } - - Future init() async { - // Start the Tor daemon. - await Tor.instance.start( - torDataDirPath: (await getApplicationSupportDirectory()).path, - ); - - // Toggle started flag. - setState(() { - torIsRunning = Tor.instance.status == TorStatus.on; // Update flag - }); - - print('Done awaiting; tor should be running'); + unawaited(startTor()); } @override @@ -91,48 +92,79 @@ class _MyAppState extends State { // Clean up the controller when the widget is disposed. hostController.dispose(); onionController.dispose(); + bitcoinOnionController.dispose(); + moneroOnionController.dispose(); super.dispose(); } Future startTor() async { - // Start the Tor daemon. - await Tor.instance.start( - torDataDirPath: (await getApplicationSupportDirectory()).path, - ); + try { + // Start the Tor daemon. + await _tor.start( + torDataDirPath: (await getApplicationSupportDirectory()).path, + ); + + if (!mounted) return; + setState(() => torError = null); + debugPrint('Done awaiting; tor should be running'); + } catch (error) { + debugPrint('Unable to start Tor: $error'); + if (!mounted) return; + setState(() => torError = 'Unable to start Tor: $error'); + } + } - // Toggle started flag. - setState(() { - torIsRunning = Tor.instance.status == TorStatus.on; // Update flag - }); + Future stopTor() async { + try { + await _tor.disable(); + if (!mounted) return; + setState(() => torError = null); + } catch (error) { + debugPrint('Unable to stop Tor: $error'); + if (!mounted) return; + setState(() => torError = 'Unable to stop Tor: $error'); + } + } - print('Done awaiting; tor should be running'); + // Close a socket without letting a failing close() hide the original error. + Future _closeQuietly(SOCKSSocket? socksSocket) async { + if (socksSocket == null) return; + try { + await socksSocket.close(); + } catch (e) { + debugPrint("Error closing socket: $e"); + } } Future testMoneroOnionNodeConnection() async { + SOCKSSocket? socksSocket; try { // Validate the onion address. if (!moneroOnionController.text.contains(".onion")) { - print("Invalid onion address"); + debugPrint("Invalid onion address"); return; } else if (!moneroOnionController.text.contains(":")) { - print("Invalid onion address (needs port)"); + debugPrint("Invalid onion address (needs port)"); return; } final String host = moneroOnionController.text.split(":").first; final int port = int.parse( - moneroOnionController.text.split(":").last.split("/").first); + moneroOnionController.text.split(":").last.split("/").first, + ); final String path = moneroOnionController.text .split(":") .last .split("/") .last; // Extract the path - var socksSocket = await SOCKSSocket.create( - proxyHost: InternetAddress.loopbackIPv4.address, - proxyPort: Tor.instance.port, - sslEnabled: false, - ); + socksSocket = + await (widget.moneroSocketFactory?.call(_tor.port) ?? + SOCKSSocket.create( + proxyHost: InternetAddress.loopbackIPv4.address, + proxyPort: _tor.port, + sslEnabled: false, + )); await socksSocket.connect(); await socksSocket.connectTo(host, port); @@ -143,35 +175,129 @@ class _MyAppState extends State { "method": "get_info", }); - final request = 'POST /$path HTTP/1.1\r\n' + final request = + 'POST /$path HTTP/1.1\r\n' 'Host: $host\r\n' 'Content-Type: application/json\r\n' 'Content-Length: ${body.length}\r\n' '\r\n' '$body'; - socksSocket.write(request); - print("Request sent: $request"); + await socksSocket.write(request); + debugPrint("Request sent: $request"); await for (var response in socksSocket.inputStream) { final result = utf8.decode(response); - print("Response received: $result"); + debugPrint("Response received: $result"); break; } + } catch (e, s) { + debugPrint("Error connecting to Monero onion node: $e\n$s"); + } finally { + // Close the socket, also on failure. + await _closeQuietly(socksSocket); + } + } - await socksSocket.close(); + // Ask an Electrum server for its features through the proxy. + Future _testServerFeatures( + String host, + int port, { + required bool ssl, + required String label, + }) async { + SOCKSSocket? socksSocket; + try { + // Instantiate a socks socket at localhost and on the port selected by the tor service. + socksSocket = await SOCKSSocket.create( + proxyHost: InternetAddress.loopbackIPv4.address, + proxyPort: _tor.port, + sslEnabled: ssl, // For SSL connections. + ); + + // Connect to the socks instantiated above. + await socksSocket.connect(); + + // Connect to the server via socks socket. + await socksSocket.connectTo(host, port); + + // Send a server features command to the connected socket, see method for more specific usage example.. + await socksSocket.sendServerFeaturesCommand(); + + // You should see a server response printed to the console. + // + // Example response: + // `flutter: secure responseData: { + // "id": "0", + // "jsonrpc": "2.0", + // "result": { + // "cashtokens": true, + // "dsproof": true, + // "genesis_hash": "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f", + // "hash_function": "sha256", + // "hosts": { + // "bitcoin.stackwallet.com": { + // "ssl_port": 50002, + // "tcp_port": 50001, + // "ws_port": 50003, + // "wss_port": 50004 + // } + // }, + // "protocol_max": "1.5", + // "protocol_min": "1.4", + // "pruning": null, + // "server_version": "Fulcrum 1.9.1" + // } + // } } catch (e, s) { - print("Error connecting to Monero onion node: $e\n$s"); + debugPrint("Error connecting to $label: $e\n$s"); + } finally { + // Close the socket, also on failure. + await _closeQuietly(socksSocket); + } + } + + // Connect to bitcoin.stackwallet.com on port 50002 via socks socket. + // + // Note that this is an SSL example. + Future testStackWalletSslConnection() => _testServerFeatures( + 'bitcoin.stackwallet.com', + 50002, + ssl: true, + label: 'bitcoin.stackwallet.com', + ); + + Future testBitcoinOnionNodeConnection() async { + // Validate the onion address. + if (!bitcoinOnionController.text.contains(".onion")) { + debugPrint("Invalid onion address"); + return; + } else if (!bitcoinOnionController.text.contains(":")) { + debugPrint("Invalid onion address (needs port)"); + return; + } + + final String domain = bitcoinOnionController.text.split(":").first; + final int? port = int.tryParse(bitcoinOnionController.text.split(":").last); + if (port == null) { + debugPrint("Invalid onion address (invalid port)"); + return; } + + // Connect to the onion node via socks socket. + await _testServerFeatures( + domain, + port, + ssl: !domain.endsWith(".onion"), + label: 'Bitcoin onion node', + ); } @override Widget build(BuildContext context) { const spacerSmall = SizedBox(height: 10); return Scaffold( - appBar: AppBar( - title: const Text('Tor example'), - ), + appBar: AppBar(title: const Text('Tor example')), body: SingleChildScrollView( child: Container( padding: const EdgeInsets.all(10), @@ -198,34 +324,31 @@ class _MyAppState extends State { final time = DateTime.now(); - print("NOW: $time"); + debugPrint("NOW: $time"); await startTor(); - print("Start tor took " - "${DateTime.now().difference(time).inSeconds} " - "seconds"); + debugPrint( + "Start tor took " + "${DateTime.now().difference(time).inSeconds} " + "seconds", + ); - if (mounted) { + if (context.mounted) { Navigator.of(context).pop(); } }, child: const Text("Start tor"), ), TextButton( - onPressed: !torIsRunning - ? null - : () async { - Tor.instance.disable(); - await Tor.instance.stop(); - setState(() { - torIsRunning = false; - }); - }, + // Also while starting, or when a failed release left the + // native handles held, so the stop can be retried. + onPressed: _tor.status == TorStatus.off ? null : stopTor, child: const Text("Stop tor"), ), ], ), + if (torError != null) Text(torError!), Row( children: [ // Host input field. @@ -248,32 +371,40 @@ class _MyAppState extends State { // Create HttpClient object final client = HttpClient(); - - // Assign connection factory. - SocksTCPClient.assignToHttpClient(client, [ - ProxySettings(InternetAddress.loopbackIPv4, - Tor.instance.port, - password: - null), // TODO get from tor's config file. - ]); - - // GET request. - final request = await client - .getUrl(Uri.parse(hostController.text)); - final response = await request.close(); - - // Print response. - var responseString = - await utf8.decodeStream(response); - print(responseString); - // If host input left to default icanhazip.com, a Tor - // exit node IP should be printed to the console. - // - // https://check.torproject.org is also good for - // doublechecking torability. - - // Close client - client.close(); + try { + // Assign connection factory. + SocksTCPClient.assignToHttpClient(client, [ + ProxySettings( + InternetAddress.loopbackIPv4, + _tor.port, + password: null, + ), // TODO get from tor's config file. + ]); + + // GET request. + final request = await client.getUrl( + Uri.parse(hostController.text), + ); + final response = await request.close(); + + // Print response. + var responseString = await utf8.decodeStream( + response, + ); + debugPrint(responseString); + // If host input left to default icanhazip.com, a Tor + // exit node IP should be printed to the console. + // + // https://check.torproject.org is also good for + // doublechecking torability. + } catch (e, s) { + debugPrint( + "Error making proxied request: $e\n$s", + ); + } finally { + // Close client, also on failure. + client.close(); + } } : null, child: const Text("Make proxied request"), @@ -282,57 +413,7 @@ class _MyAppState extends State { ), spacerSmall, TextButton( - onPressed: torIsRunning - ? () async { - // Instantiate a socks socket at localhost and on the port selected by the tor service. - var socksSocket = await SOCKSSocket.create( - proxyHost: InternetAddress.loopbackIPv4.address, - proxyPort: Tor.instance.port, - sslEnabled: true, // For SSL connections. - ); - - // Connect to the socks instantiated above. - await socksSocket.connect(); - - // Connect to bitcoin.stackwallet.com on port 50002 via socks socket. - // - // Note that this is an SSL example. - await socksSocket.connectTo( - 'bitcoin.stackwallet.com', 50002); - - // Send a server features command to the connected socket, see method for more specific usage example.. - await socksSocket.sendServerFeaturesCommand(); - - // You should see a server response printed to the console. - // - // Example response: - // `flutter: secure responseData: { - // "id": "0", - // "jsonrpc": "2.0", - // "result": { - // "cashtokens": true, - // "dsproof": true, - // "genesis_hash": "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f", - // "hash_function": "sha256", - // "hosts": { - // "bitcoin.stackwallet.com": { - // "ssl_port": 50002, - // "tcp_port": 50001, - // "ws_port": 50003, - // "wss_port": 50004 - // } - // }, - // "protocol_max": "1.5", - // "protocol_min": "1.4", - // "pruning": null, - // "server_version": "Fulcrum 1.9.1" - // } - // } - - // Close the socket. - await socksSocket.close(); - } - : null, + onPressed: torIsRunning ? testStackWalletSslConnection : null, child: const Text( "Connect to bitcoin.stackwallet.com:50002 (SSL) via socks socket", ), @@ -353,76 +434,11 @@ class _MyAppState extends State { spacerSmall, TextButton( onPressed: torIsRunning - ? () async { - // Validate the onion address.www - if (!onionController.text.contains(".onion")) { - print("Invalid onion address"); - return; - } else if (!onionController.text.contains(":")) { - print("Invalid onion address (needs port)"); - return; - } - - String domain = - bitcoinOnionController.text.split(":").first; - int port = int.parse( - bitcoinOnionController.text.split(":").last); - - // Instantiate a socks socket at localhost and on the port selected by the tor service. - var socksSocket = await SOCKSSocket.create( - proxyHost: InternetAddress.loopbackIPv4.address, - proxyPort: Tor.instance.port, - sslEnabled: !domain - .endsWith(".onion"), // For SSL connections. - ); - - // Connect to the socks instantiated above. - await socksSocket.connect(); - - // Connect to onion node via socks socket. - // - // Note that this is an SSL example. - await socksSocket.connectTo(domain, port); - - // Send a server features command to the connected socket, see method for more specific usage example.. - await socksSocket.sendServerFeaturesCommand(); - - // You should see a server response printed to the console. - // - // Example response: - // `flutter: secure responseData: { - // "id": "0", - // "jsonrpc": "2.0", - // "result": { - // "cashtokens": true, - // "dsproof": true, - // "genesis_hash": "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f", - // "hash_function": "sha256", - // "hosts": { - // "bitcoin.stackwallet.com": { - // "ssl_port": 50002, - // "tcp_port": 50001, - // "ws_port": 50003, - // "wss_port": 50004 - // } - // }, - // "protocol_max": "1.5", - // "protocol_min": "1.4", - // "pruning": null, - // "server_version": "Fulcrum 1.9.1" - // } - // } - - // Close the socket. - await socksSocket.close(); - } - + ? testBitcoinOnionNodeConnection // A mutex should be added to this example to prevent // multiple connections from being made at once. TODO : null, - child: const Text( - "Test Bitcoin onion node connection", - ), + child: const Text("Test Bitcoin onion node connection"), ), ], ), @@ -440,12 +456,13 @@ class _MyAppState extends State { ), spacerSmall, TextButton( - onPressed: - torIsRunning ? testMoneroOnionNodeConnection : null, + onPressed: torIsRunning + ? testMoneroOnionNodeConnection + : null, child: const Text("Test Monero onion node connection"), ), ], - ) + ), ], ), ), diff --git a/example/macos/Runner.xcodeproj/project.pbxproj b/example/macos/Runner.xcodeproj/project.pbxproj index 800a7e85..ddde0c38 100644 --- a/example/macos/Runner.xcodeproj/project.pbxproj +++ b/example/macos/Runner.xcodeproj/project.pbxproj @@ -549,16 +549,13 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = 4DQKUWSG6C; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); - PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; name = Profile; @@ -677,16 +674,13 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = 4DQKUWSG6C; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); - PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; }; @@ -699,16 +693,13 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = 4DQKUWSG6C; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); - PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; name = Release; diff --git a/example/macos/Runner/Release.entitlements b/example/macos/Runner/Release.entitlements index 852fa1a4..7a2230dc 100644 --- a/example/macos/Runner/Release.entitlements +++ b/example/macos/Runner/Release.entitlements @@ -4,5 +4,9 @@ com.apple.security.app-sandbox + com.apple.security.network.client + + com.apple.security.network.server + diff --git a/example/pubspec.lock b/example/pubspec.lock index 79337fd2..2252ad0d 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -340,10 +340,10 @@ packages: dependency: "direct main" description: name: socks_socket - sha256: "127e919f67de316850d3f1e811ca339c4b85b31e52d1af3838cb5469cb5397f7" + sha256: "09149ed1278584554aeacc60c7aeec5bd80154e6ce1136f00d97485d09d5a338" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "2.0.0" source_span: dependency: transitive description: @@ -406,7 +406,7 @@ packages: path: ".." relative: true source: path - version: "0.1.0" + version: "0.2.0" typed_data: dependency: transitive description: diff --git a/example/pubspec.yaml b/example/pubspec.yaml index b575db77..9becdabf 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -44,7 +44,7 @@ dependencies: cupertino_icons: ^1.0.2 socks5_proxy: 1.0.3+dev.3 path_provider: ^2.1.6 - socks_socket: ^1.1.0 + socks_socket: ^2.0.0 dev_dependencies: flutter_test: diff --git a/example/test/tor_start_test.dart b/example/test/tor_start_test.dart new file mode 100644 index 00000000..dc0bde23 --- /dev/null +++ b/example/test/tor_start_test.dart @@ -0,0 +1,225 @@ +import 'dart:async'; +import 'dart:ffi'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:tor_ffi_plugin_example/main.dart'; +import 'package:socks_socket/socks_socket.dart'; +import 'package:tor_ffi_plugin/tor_ffi_plugin.dart'; + +class _ExampleTorNative extends TorNative { + int freeCalls = 0; + int freeFailures = 0; + Completer? freeGate; + + @override + Future start(int port, String stateDir, String cacheDir) async => + ( + client: Pointer.fromAddress(1), + proxy: Pointer.fromAddress(2), + port: 19050, + ); + + @override + Future bootstrap(Pointer client, Duration timeout) async {} + + @override + void startProxy(Pointer proxy) {} + + @override + Future stopProxy(Pointer proxy) async {} + + @override + Future freeClient(Pointer client) async { + if (client == nullptr) return; + freeCalls++; + final gate = freeGate; + freeGate = null; + await gate?.future; + if (freeFailures > 0) { + freeFailures--; + throw Exception('client release failed'); + } + } +} + +class _FailedWriteSocket implements SOCKSSocket { + bool readStarted = false; + bool closed = false; + + @override + Future connect() async {} + + @override + Future connectTo(String host, int port) async {} + + @override + Future write(Object? object, {bool newline = false}) async { + throw Exception('request write failed'); + } + + @override + Stream> get inputStream { + readStarted = true; + return const Stream.empty(); + } + + @override + Future close() async { + closed = true; + } + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + const channel = MethodChannel('plugins.flutter.io/path_provider'); + final messenger = + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger; + + tearDown(() => messenger.setMockMethodCallHandler(channel, null)); + + Future<(Tor, _ExampleTorNative)> pumpRunningTor( + WidgetTester tester, { + Future Function(int)? moneroSocketFactory, + }) async { + final directory = Directory.systemTemp.createTempSync('tor-example-stop-'); + final native = _ExampleTorNative(); + final tor = Tor.withNative(native); + addTearDown(() => directory.deleteSync(recursive: true)); + messenger.setMockMethodCallHandler(channel, (_) async => directory.path); + await tester.pumpWidget( + MaterialApp( + home: Home(tor: tor, moneroSocketFactory: moneroSocketFactory), + ), + ); + for ( + var attempt = 0; + tor.status != TorStatus.on && attempt < 200; + attempt++ + ) { + await tester.runAsync( + () => Future.delayed(const Duration(milliseconds: 5)), + ); + await tester.pump(); + } + expect(tor.status, TorStatus.on); + await tester.pumpAndSettle(); + return (tor, native); + } + + testWidgets('a failed Monero request write reaches the error handler', ( + tester, + ) async { + final socket = _FailedWriteSocket(); + await pumpRunningTor(tester, moneroSocketFactory: (_) async => socket); + final messages = []; + final originalDebugPrint = debugPrint; + debugPrint = (message, {wrapWidth}) { + if (message != null) messages.add(message); + }; + try { + final button = find.text('Test Monero onion node connection'); + await tester.ensureVisible(button); + await tester.tap(button); + await tester.pumpAndSettle(); + expect(tester.takeException(), isNull); + expect(socket.readStarted, isFalse); + expect(socket.closed, isTrue); + expect( + messages, + contains( + contains( + 'Error connecting to Monero onion node: Exception: request write failed', + ), + ), + ); + } finally { + debugPrint = originalDebugPrint; + } + }); + + testWidgets('a stop failure is shown and can be retried', (tester) async { + final (tor, native) = await pumpRunningTor(tester); + native.freeFailures = 1; + + await tester.tap(find.text('Stop tor')); + await tester.pumpAndSettle(); + expect(tester.takeException(), isNull); + expect(tor.status, TorStatus.on); + expect( + find.text('Unable to stop Tor: Exception: client release failed'), + findsOneWidget, + ); + final requestButton = find.widgetWithText( + TextButton, + 'Test Monero onion node connection', + ); + expect(tester.widget(requestButton).onPressed, isNull); + final stopButton = find.widgetWithText(TextButton, 'Stop tor'); + expect(tester.widget(stopButton).onPressed, isNotNull); + + await tester.tap(stopButton); + await tester.pumpAndSettle(); + expect(tester.takeException(), isNull); + expect(native.freeCalls, 2); + expect(find.textContaining('Unable to stop Tor:'), findsNothing); + expect(tester.widget(stopButton).onPressed, isNull); + }); + + testWidgets('a stop failure after disposal does not update state', ( + tester, + ) async { + final (_, native) = await pumpRunningTor(tester); + final gate = native.freeGate = Completer(); + + await tester.tap(find.text('Stop tor')); + await tester.pump(); + await tester.pumpWidget(const SizedBox()); + gate.completeError(Exception('client release failed after disposal')); + await tester.pumpAndSettle(); + + expect(tester.takeException(), isNull); + }); + + testWidgets('a start failure is shown and the retry dialog closes', ( + tester, + ) async { + final error = PlatformException( + code: 'support-directory-unavailable', + message: 'Application support directory is unavailable', + ); + messenger.setMockMethodCallHandler(channel, (_) async => throw error); + + await tester.pumpWidget(const MyApp()); + await tester.pumpAndSettle(); + expect(tester.takeException(), isNull); + expect(find.text('Unable to start Tor: $error'), findsOneWidget); + + await tester.tap(find.text('Start tor')); + await tester.pumpAndSettle(); + expect(tester.takeException(), isNull); + expect(find.byType(Dialog), findsNothing); + expect(find.text('Unable to start Tor: $error'), findsOneWidget); + }); + + testWidgets('a start failure after disposal does not update state', ( + tester, + ) async { + final directory = Completer(); + messenger.setMockMethodCallHandler(channel, (_) => directory.future); + + await tester.pumpWidget(const MyApp()); + await tester.pumpWidget(const SizedBox()); + directory.completeError( + PlatformException(code: 'support-directory-unavailable'), + ); + await tester.pump(); + + expect(tester.takeException(), isNull); + }); +} diff --git a/ffigen.yaml b/ffigen.yaml index 10f45ae8..2c0ac03a 100644 --- a/ffigen.yaml +++ b/ffigen.yaml @@ -19,3 +19,9 @@ preamble: | comments: style: any length: full + +# Keep Dart names stable while keeping C enumerators out of the global namespace. +enums: + member-rename: + TorBootstrapStatus: + 'TorBootstrapStatus_(.*)': '$1' diff --git a/hook/build.dart b/hook/build.dart index 04c6eee8..19d6fd1c 100644 --- a/hook/build.dart +++ b/hook/build.dart @@ -9,11 +9,7 @@ Future main(List args) async { if (!input.config.buildCodeAssets) return; if (await usePrebuilt(input, output)) return; - output.dependencies.addAll([ - input.packageRoot.resolve('rust/Cargo.toml'), - input.packageRoot.resolve('rust/Cargo.lock'), - input.packageRoot.resolve('rust/rust-toolchain.toml'), - ]); + output.dependencies.addAll(await sourceDependencies(input.packageRoot)); await const RustBuilder( assetName: 'tor_ffi_plugin_bindings_generated.dart', diff --git a/hook/prebuilt.dart b/hook/prebuilt.dart index 460a5c77..b6c321b8 100644 --- a/hook/prebuilt.dart +++ b/hook/prebuilt.dart @@ -29,27 +29,36 @@ String releaseFileName(String target) => '$target-${libraryName(target)}'; Future fileHash(File file) async => (await sha256.bind(file.openRead()).first).toString(); +/// Native source files tracked as a whole. +const _sourceFiles = [ + 'rust/Cargo.toml', + 'rust/Cargo.lock', + 'rust/rust-toolchain.toml', + 'rust/build.rs', + 'hook/build.dart', + 'lib/tor_ffi_plugin_bindings_generated.dart', +]; + +/// Native source trees whose entries are tracked recursively. +const _sourceDirectories = ['rust/src/', 'rust/patches/']; + +/// Native inputs to track for Flutter's hook cache. +/// +/// Directories are included so additions and deletions invalidate the build. +/// Hidden entries are not native inputs and are skipped. +Future> sourceDependencies(Uri root) async => [ + for (final path in [..._sourceFiles, ..._sourceDirectories]) + root.resolve(path), + for (final path in _sourceDirectories) + ...await _sourceEntries(Directory.fromUri(root.resolve(path))), +]..sort((a, b) => a.path.compareTo(b.path)); + // Hash paths and contents so additions, deletions and ABI changes invalidate -// prebuilts. Track directories as well as files for Flutter's hook cache. +// prebuilts. Future<({String hash, List dependencies})> sourceFingerprint( Uri root, ) async { - final dependencies = [ - for (final path in [ - 'rust/Cargo.toml', - 'rust/Cargo.lock', - 'rust/rust-toolchain.toml', - 'rust/build.rs', - 'hook/build.dart', - 'lib/tor_ffi_plugin_bindings_generated.dart', - 'rust/src/', - ]) - root.resolve(path), - await for (final entry in Directory.fromUri( - root.resolve('rust/src/'), - ).list(recursive: true, followLinks: false)) - entry.uri, - ]..sort((a, b) => a.path.compareTo(b.path)); + final dependencies = await sourceDependencies(root); final hashes = StringBuffer(); for (final uri in dependencies) { final type = await FileSystemEntity.type( @@ -60,12 +69,11 @@ Future<({String hash, List dependencies})> sourceFingerprint( throw StateError('Native source symlinks are unsupported: $uri'); } if (type == FileSystemEntityType.directory) continue; - // Git checkouts may use CRLF on Windows; native inputs are text files. - final text = (await File.fromUri( - uri, - ).readAsString()).replaceAll('\r\n', '\n'); + // Git checkouts may use CRLF on Windows. Hash bytes rather than decoded + // text so a file that is not UTF-8 cannot fail the build. + final bytes = _lfLineEndings(await File.fromUri(uri).readAsBytes()); hashes.writeln( - '${uri.path.substring(root.path.length)}:${sha256.convert(utf8.encode(text))}', + '${uri.path.substring(root.path.length)}:${sha256.convert(bytes)}', ); } return ( @@ -74,6 +82,36 @@ Future<({String hash, List dependencies})> sourceFingerprint( ); } +/// Entries under [directory], without descending into skipped directories. +Future> _sourceEntries(Directory directory) async => [ + await for (final entry in directory.list(followLinks: false)) + if (!_isSkipped(entry)) ...[ + entry.uri, + if (entry is Directory) ...await _sourceEntries(entry), + ], +]; + +/// Whether [entry] is hidden, such as .DS_Store, or Cargo output left by +/// building a vendored crate on its own: its `target` directory, or its +/// lockfile, which Cargo ignores for dependencies. +bool _isSkipped(FileSystemEntity entry) { + final name = entry.uri.pathSegments.lastWhere((s) => s.isNotEmpty); + if (name.startsWith('.') || name == 'Cargo.lock') return true; + return entry is Directory && + name == 'target' && + File.fromUri(entry.parent.uri.resolve('Cargo.toml')).existsSync(); +} + +/// [bytes] with each CRLF replaced by LF. +List _lfLineEndings(List bytes) { + if (!bytes.contains(0x0d)) return bytes; + return [ + for (var i = 0; i < bytes.length; i++) + if (bytes[i] != 0x0d || i + 1 == bytes.length || bytes[i + 1] != 0x0a) + bytes[i], + ]; +} + String rustTarget(CodeConfig code) => switch (( code.targetOS, code.targetArchitecture, diff --git a/lib/tor_ffi_plugin.dart b/lib/tor_ffi_plugin.dart index 0c19105c..37972865 100644 --- a/lib/tor_ffi_plugin.dart +++ b/lib/tor_ffi_plugin.dart @@ -6,9 +6,9 @@ import 'dart:async'; import 'dart:ffi'; import 'dart:io'; import 'dart:isolate'; -import 'dart:math'; import 'package:ffi/ffi.dart'; +import 'package:meta/meta.dart'; import 'package:tor_ffi_plugin/tor_ffi_plugin_bindings_generated.dart' as bindings; @@ -18,27 +18,175 @@ class CouldntBootstrapDirectory implements Exception { CouldntBootstrapDirectory({this.rustError}); } -enum TorStatus { - on, - starting, - off; +class TorStartCancelled implements Exception { + const TorStartCancelled(); + + @override + String toString() => 'Tor start was cancelled'; +} + +/// Bootstrap did not finish within the deadline passed to [Tor.start]. +class TorBootstrapTimeout implements Exception { + const TorBootstrapTimeout(this.timeout, this.message); + + /// The deadline that passed. + final Duration timeout; + + /// The native error message. + final String message; + + @override + String toString() => 'TorBootstrapTimeout: $message'; +} + +enum TorStatus { on, starting, off } + +/// Native handles produced by a successful [TorNative.start]. +typedef TorHandles = ({Pointer client, Pointer proxy, int port}); + +/// The native calls [Tor] makes. Tests substitute a fake to drive the +/// lifecycle without starting Tor. +@visibleForTesting +class TorNative { + const TorNative(); + + /// Start Tor with its SOCKS proxy on [port], in a worker isolate. + /// + /// Throws the Rust error if the native start fails. + Future start(int port, String stateDir, String cacheDir) async { + final (client, proxy, boundPort) = await Isolate.run( + () => _startInIsolate(port, stateDir, cacheDir), + ); + return ( + client: Pointer.fromAddress(client), + proxy: Pointer.fromAddress(proxy), + port: boundPort, + ); + } + + static (int, int, int) _startInIsolate( + int port, + String stateDir, + String cacheDir, + ) { + // tor_start() copies both paths, so they are freed once it returns. + final stateDirPtr = stateDir.toNativeUtf8(); + final cacheDirPtr = cacheDir.toNativeUtf8(); + try { + final tor = bindings.tor_start( + port, + stateDirPtr.cast(), + cacheDirPtr.cast(), + ); + + // Throw an exception if the Tor service fails to start. + if (tor.client == nullptr) { + Tor.throwRustException(); + } + + return (tor.client.address, tor.proxy.address, tor.port); + } finally { + malloc.free(stateDirPtr); + malloc.free(cacheDirPtr); + } + } + + /// Bootstrap [client] in a worker isolate. Throws [TorStartCancelled] on + /// cancellation, [TorBootstrapTimeout] when [timeout] elapses, and the Rust + /// error on failure. + Future bootstrap(Pointer client, Duration timeout) async { + final address = client.address; + await Isolate.run(() => _bootstrapInIsolate(address, timeout)); + } + + static void _bootstrapInIsolate(int client, Duration timeout) { + final code = bindings.tor_client_bootstrap( + Pointer.fromAddress(client), + // The native deadline has millisecond resolution. + timeout.inMilliseconds, + ); + switch (bindings.TorBootstrapStatus.fromValue(code)) { + case bindings.TorBootstrapStatus.Ok: + return; + case bindings.TorBootstrapStatus.Cancelled: + Tor._takeRustError(); + throw const TorStartCancelled(); + case bindings.TorBootstrapStatus.TimedOut: + // Report the deadline as the caller passed it, not rounded. + throw TorBootstrapTimeout(timeout, Tor._takeRustError()); + case bindings.TorBootstrapStatus.Failed: + Tor.throwRustException(); + case bindings.TorBootstrapStatus.NullClient: + throw ArgumentError(Tor._takeRustError()); + } + } + + /// Interrupt an in-progress [bootstrap]. + void cancelBootstrap(Pointer client) => + bindings.tor_client_cancel_bootstrap(client); + + void setDormant(Pointer client, bool softMode) => + bindings.tor_client_set_dormant(client, softMode); + + void startProxy(Pointer proxy) { + if (!bindings.tor_proxy_start(proxy)) { + Tor.throwRustException(); + } + } + + /// Stop [proxy] in a worker isolate. The native call waits for the accept + /// loop to exit, so its listeners and client reference are released when + /// this completes. + Future stopProxy(Pointer proxy) async { + if (proxy == nullptr) { + return; + } + + final address = proxy.address; + await Isolate.run(() => _stopProxyInIsolate(address)); + } + + static void _stopProxyInIsolate(int proxy) => + bindings.tor_proxy_stop(Pointer.fromAddress(proxy)); + + /// Release [client] in a worker isolate, since tearing down Tor can block + /// for a few seconds. + Future freeClient(Pointer client) async { + if (client == nullptr) { + return; + } + + final address = client.address; + await Isolate.run(() => _freeInIsolate(address)); + } + + static void _freeInIsolate(int client) => + bindings.tor_client_free(Pointer.fromAddress(client)); + + /// Print the native greeting to verify that the library is linked. + void hello() => bindings.tor_hello(); } class Tor { /// Private constructor for the Tor class. - Tor._(); + Tor._() : _native = const TorNative(); + + /// Create an instance that is independent of [instance] and talks to + /// [native] instead of the Rust library. + @visibleForTesting + Tor.withNative(TorNative native) : _native = native; + + final TorNative _native; /// Singleton instance of the Tor class. static final Tor instance = Tor._(); - /// Status of the tor proxy service + /// The lifecycle state of the Tor service. + /// + /// This does not monitor SOCKS accept-loop liveness after startup. TorStatus get status => _status; TorStatus _status = TorStatus.off; - /// Flag to indicate that a Tor circuit is thought to have been established - /// (true means that Tor has bootstrapped). - bool _bootstrapped = false; - /// Getter for the proxy port. /// /// Throws if Tor is not enabled or if the circuit is not established. @@ -48,7 +196,7 @@ class Tor { /// This is the port that should be used for all requests. int get port { if (_proxyPort == null || _status == TorStatus.off) { - throw Exception("Tor proxy port is unexpectedly null"); + throw Exception('Tor proxy is not running'); } return _proxyPort!; } @@ -64,130 +212,268 @@ class Tor { /// Throws an exception if the Tor service fails to start. /// /// Returns a Future that completes when the Tor service has started. - Future start({required String torDataDirPath}) async { - if (_status != TorStatus.off) { - // already starting or running - return; + /// + /// [start] and [stop] run one at a time, in the order they were called, so + /// a start issued after [stop] restarts Tor once that stop has finished. + /// Concurrent starts with the same raw data directory and timeout share + /// one attempt and see the same success or failure. Other starts queue + /// behind it and reuse the running instance, or fail with a [StateError] + /// for a different data directory, leaving that instance running. + /// + /// [torDataDirPath] is created and resolved asynchronously inside the queued + /// start. Physical paths are compared with the running instance, so symbolic + /// links and trailing separators can reuse it. A path that cannot be created + /// or resolved fails with the file system error. + Future start({ + required String torDataDirPath, + Duration bootstrapTimeout = const Duration(minutes: 2), + }) { + // The native deadline has millisecond resolution. + if (bootstrapTimeout.inMilliseconds <= 0) { + return Future.error( + ArgumentError.value( + bootstrapTimeout, + 'bootstrapTimeout', + 'must be at least one millisecond', + ), + ); + } + + for (final pending in _pendingStarts) { + if (pending.dataDirPath == torDataDirPath && + pending.bootstrapTimeout == bootstrapTimeout) { + return pending.future; + } + } + + // Always queue, even when Tor looks running: a stop may be queued ahead of + // this start, in which case the start must restart Tor rather than return + // early and leave it off. + final operation = _StartOperation(torDataDirPath, bootstrapTimeout); + // _start runs later, so report starting now. A start cancelled before it + // runs is reset to off by the stop that cancelled it. + if (_status == TorStatus.off) { + _status = TorStatus.starting; + } + operation.future = _enqueue(() => _start(operation)).whenComplete(() { + _pendingStarts.remove(operation); + }); + _pendingStarts.add(operation); + return operation.future; + } + + /// Queued starts that later calls can join until a [stop] is requested. + final _pendingStarts = <_StartOperation>[]; + + String? _activeDataDirPath; + + // Retained handles need teardown, even if status still reports on. + bool _releasePending = false; + + /// The last cleanup failure observed while preserving an earlier start + /// failure. This is cleared at the beginning of each start attempt. + Object? get lastCleanupError => _lastCleanupError; + + /// Stack trace associated with [lastCleanupError]. + StackTrace? get lastCleanupStackTrace => _lastCleanupStackTrace; + + Object? _lastCleanupError; + StackTrace? _lastCleanupStackTrace; + + Future _start(_StartOperation operation) async { + if (operation.cancelRequested) { + // A stop was requested after this start was queued and runs next. + throw const TorStartCancelled(); } + final wasRunning = _status == TorStatus.on; + if (!wasRunning) { + _status = TorStatus.starting; + } + final String dataDirPath; try { + final directory = await Directory(operation.dataDirPath).absolute + .create(recursive: true); + dataDirPath = await directory.resolveSymbolicLinks(); + } catch (_) { + if (!wasRunning && !_releasePending) { + _status = TorStatus.off; + } + rethrow; + } + + // A stop queued while the directory was resolving runs next. Returning + // success here would report Tor as running just before that stop turns + // it off. + if (operation.cancelRequested) { + throw const TorStartCancelled(); + } + + if (_status == TorStatus.on && !_releasePending) { + if (_activeDataDirPath == dataDirPath) { + return; + } + throw StateError( + 'Tor is already running with a different data directory.', + ); + } + + _lastCleanupError = null; + _lastCleanupStackTrace = null; + try { + if (_proxyPtr != nullptr || _clientPtr != nullptr) { + await _stop(); + } _status = TorStatus.starting; // Set the state and cache directories. - final stateDir = await Directory('$torDataDirPath/tor_state').create(); - final cacheDir = await Directory('$torDataDirPath/tor_cache').create(); - - // Generate a random port. - final int? newPort = await _getRandomUnusedPort(); + final stateDir = await Directory('$dataDirPath/tor_state').create(); + final cacheDir = await Directory('$dataDirPath/tor_cache').create(); - if (newPort == null) { - throw Exception("Failed to get random unused port!"); + if (operation.cancelRequested) { + throw const TorStartCancelled(); } - // Start the Tor service in an isolate. - final tor = await Isolate.run(() async { - // Start the Tor service. - final tor = bindings.tor_start( - newPort, - stateDir.path.toNativeUtf8() as Pointer, - cacheDir.path.toNativeUtf8() as Pointer); - - // Throw an exception if the Tor service fails to start. - if (tor.client == nullptr) { - throwRustException(); - } + // Let the OS allocate the port while Rust binds the real listeners. This + // avoids a probe-then-bind race and keeps the published port reserved. + final tor = await _native.start(0, stateDir.path, cacheDir.path); - return tor; - }); + // Retain the native handles before bootstrap so stop can cancel it. + _clientPtr = tor.client; + _proxyPtr = tor.proxy; - // Set the client pointer and started flag. - _clientPtr = Pointer.fromAddress(tor.client.address); - _proxyPtr = Pointer.fromAddress(tor.proxy.address); + if (operation.cancelRequested) { + throw const TorStartCancelled(); + } // Bootstrap the Tor service. - _bootstrap(); + await _native.bootstrap(tor.client, operation.bootstrapTimeout); + + if (operation.cancelRequested) { + throw const TorStartCancelled(); + } + + _native.startProxy(tor.proxy); // Set the proxy port and change status. - _proxyPort = newPort; + _proxyPort = tor.port; + _activeDataDirPath = dataDirPath; _status = TorStatus.on; - } catch (_) { - _status = TorStatus.off; - rethrow; + } catch (error, stackTrace) { + // Release anything the native start produced before bootstrap failed; + // the next start would otherwise overwrite these handles. + try { + await _stop(); + } catch (cleanupError, cleanupStackTrace) { + _lastCleanupError = cleanupError; + _lastCleanupStackTrace = cleanupStackTrace; + try { + // Also expose secondary failures in normal application logs. A + // custom zone's print handler must not mask the bootstrap error. + // ignore: avoid_print + print( + 'Tor cleanup failed after start failed: $cleanupError\n$cleanupStackTrace', + ); + } catch (_) { + // Preserve the original start exception even if logging fails. + } + } + Error.throwWithStackTrace(error, stackTrace); } } - /// Bootstrap the Tor service. - /// - /// This will bootstrap the Tor service and establish a Tor circuit. This - /// function should only be called after the Tor service has been started. + /// Prevent traffic flowing through the proxy. /// - /// This function will block until the Tor service has bootstrapped. - /// - /// Throws an exception if the Tor service fails to bootstrap. - /// - /// Returns void. - void _bootstrap() { - // Bootstrap the Tor service. - _bootstrapped = bindings.tor_client_bootstrap(_clientPtr); + /// This is the same as [stop]: the SOCKS proxy is shut down and the native + /// client released. Await the returned future and handle release failures, + /// including when calling from a synchronous dispose method. + Future disable() => stop(); - // Throw an exception if the Tor service fails to bootstrap. - if (!_bootstrapped) { - throwRustException(); + /// Stop the proxy and release the native client handle. + /// + /// Cancels an in-progress [start], which throws [TorStartCancelled], then + /// releases its handles. A running instance stays [TorStatus.on] until its + /// native resources have been released, including when release fails and + /// needs retry. A failed start stays [TorStatus.starting] if cleanup fails. + /// Await a successful [stop] before deleting the data directory. + Future stop() { + for (final pending in _pendingStarts) { + pending.cancelRequested = true; } + _pendingStarts.clear(); + // A bootstrap can only be in progress while starting on a client that no + // _stop has begun releasing. _stop marks the handles before its first + // await, so this never touches a client that is being freed. + if (_status == TorStatus.starting && + _clientPtr != nullptr && + !_releasePending) { + _native.cancelBootstrap(_clientPtr); + } + return _enqueue(_stop); } - /// Prevent traffic flowing through the proxy - void disable() { - _status = TorStatus.off; + Future _stop() async { + // Retry retained handles on the next start or stop. Never shut down the + // client's runtime while its proxy may still be accepting connections. + _releasePending = true; + _proxyPort = null; + _activeDataDirPath = null; + try { + if (_proxyPtr != nullptr) { + await _native.stopProxy(_proxyPtr); + _proxyPtr = nullptr; + } + if (_clientPtr != nullptr) { + await _native.freeClient(_clientPtr); + _clientPtr = nullptr; + } + } finally { + _releasePending = _proxyPtr != nullptr || _clientPtr != nullptr; + if (!_releasePending) { + _status = TorStatus.off; + } + } } - /// Stop the proxy. - stop() async { - bindings.tor_proxy_stop(_proxyPtr); - _proxyPtr = nullptr; + /// Tail of the queue that runs [start] and [stop] one at a time. + Future _lifecycle = Future.value(); + + Future _enqueue(Future Function() operation) { + final result = _lifecycle.then((_) => operation()); + // A failed operation must not stall the ones queued behind it. + _lifecycle = result.catchError((Object _) {}); + return result; } - setClientDormant(bool dormant) async { - if (_clientPtr == nullptr || status == TorStatus.off) { + Future setClientDormant(bool dormant) async { + if (_releasePending || _clientPtr == nullptr || status == TorStatus.off) { throw ClientNotActive(); } - bindings.tor_client_set_dormant(_clientPtr, dormant); + _native.setDormant(_clientPtr, dormant); } Pointer _clientPtr = nullptr; Pointer _proxyPtr = nullptr; - Future _getRandomUnusedPort({List excluded = const []}) async { - var random = Random.secure(); - int potentialPort = 0; - - retry: - while (potentialPort <= 0 || excluded.contains(potentialPort)) { - potentialPort = random.nextInt(65535); - try { - var socket = await ServerSocket.bind("0.0.0.0", potentialPort); - socket.close(); - return potentialPort; - } catch (_) { - continue retry; - } - } - - return null; - } - // Future restart() async { // // TODO: arti seems to recover by itself and there is no client restart fn // // TODO: but follow up with them if restart is truly unnecessary // // if (enabled && started && circuitEstablished) {} // } - static void throwRustException() { - String rustError = - bindings.tor_last_error_message().cast().toDartString(); + static Never throwRustException() { + throw _getRustException(_takeRustError()); + } - throw _getRustException(rustError); + /// The calling thread's last Rust error message, which this clears. + static String _takeRustError() { + final message = bindings.tor_last_error_message(); + try { + return message.cast().toDartString(); + } finally { + bindings.tor_string_free(message); + } } static Exception _getRustException(String rustError) { @@ -198,9 +484,16 @@ class Tor { } } - void hello() { - bindings.tor_hello(); - } + void hello() => _native.hello(); } class ClientNotActive implements Exception {} + +class _StartOperation { + _StartOperation(this.dataDirPath, this.bootstrapTimeout); + + final String dataDirPath; + final Duration bootstrapTimeout; + late final Future future; + bool cancelRequested = false; +} diff --git a/lib/tor_ffi_plugin_bindings_generated.dart b/lib/tor_ffi_plugin_bindings_generated.dart index 6113d4ce..d22a890a 100644 --- a/lib/tor_ffi_plugin_bindings_generated.dart +++ b/lib/tor_ffi_plugin_bindings_generated.dart @@ -13,49 +13,184 @@ library; import 'dart:ffi' as ffi; -@ffi.Native)>() -external bool tor_client_bootstrap(ffi.Pointer client); +/// Bootstrap the client, returning on cancellation or after `timeout_millis`. +/// A null `client` returns `NullClient`. +/// +/// # Safety +/// `client` must be null or a live handle returned by [`tor_start`]. +/// It must not be freed while this call is in progress. +@ffi.Native, ffi.Uint64)>() +external int tor_client_bootstrap( + ffi.Pointer client, + int timeout_millis, +); + +/// Ask an in-progress bootstrap to return promptly. Null is accepted. +/// +/// Cancellation is sticky: once this is called, every later +/// [`tor_client_bootstrap`] on the same handle returns `Cancelled` at once. +/// To bootstrap again, free the handle with [`tor_client_free`] and create a +/// new one with [`tor_start`]. +/// +/// # Safety +/// `client` must be null or a live handle returned by [`tor_start`]. It may be +/// called concurrently with [`tor_client_bootstrap`], but not with or after +/// [`tor_client_free`]. +@ffi.Native)>() +external void tor_client_cancel_bootstrap(ffi.Pointer client); +/// Release the client handle returned by [`tor_start`]. +/// +/// This shuts down the client's background tasks and closes its state and +/// cache files, which can block for a few seconds. Stop the proxy first: it +/// keeps its own reference to the client, but cannot serve connections once +/// the client is freed. +/// +/// # Safety +/// `client` must be null or a live handle returned by [`tor_start`]. +/// No other call may use the handle concurrently with or after this call. +/// It must not be called from within an async runtime. +@ffi.Native)>() +external void tor_client_free(ffi.Pointer client); + +/// Change the client's dormant mode. A null `client` is ignored and sets the +/// message returned by [`tor_last_error_message`]. +/// +/// # Safety +/// `client` must be null or a live handle returned by [`tor_start`]. +/// It must not be freed while this call is in progress. @ffi.Native, ffi.Bool)>() external void tor_client_set_dormant( ffi.Pointer client, bool soft_mode, ); +/// Read the current open-file limit. +/// +/// # Safety +/// This function has no additional safety requirements. @ffi.Native() external int tor_get_nofile_limit(); +/// Print a greeting to verify that the library is linked. +/// +/// # Safety +/// This function has no additional safety requirements. @ffi.Native() external void tor_hello(); +/// Take the current thread's last error as a newly allocated C string. +/// +/// # Safety +/// This function has no preconditions. The returned string is owned by the +/// caller and must be released exactly once with [`tor_string_free`]. @ffi.Native Function()>() external ffi.Pointer tor_last_error_message(); +/// Start accepting SOCKS connections on the listeners reserved by [`tor_start`]. +/// Repeated calls are harmless. +/// +/// # Safety +/// `proxy` must be a live, non-null proxy handle returned by [`tor_start`]. It +/// must not be used concurrently or after [`tor_proxy_stop`]. +@ffi.Native)>() +external bool tor_proxy_start(ffi.Pointer proxy); + +/// Stop the proxy, or release its reserved listeners if it was not started. +/// Null is accepted. +/// +/// This waits for the accept loop to exit, so the listeners and the loop's +/// reference to the client are released by the time it returns. Call it +/// before [`tor_client_free`]. +/// +/// # Safety +/// `proxy` must be null or a live proxy handle returned by [`tor_start`]. +/// A non-null handle must be passed to this function only once. +/// It must not be called from within an async runtime. @ffi.Native)>() external void tor_proxy_stop(ffi.Pointer proxy); +/// Increase the open-file limit, up to the hard limit. +/// +/// # Safety +/// This function has no additional safety requirements. @ffi.Native() external int tor_set_nofile_limit(int limit); +/// Create a Tor client and a localhost SOCKS proxy. +/// +/// # Safety +/// `state_dir` and `cache_dir` must point to valid, NUL-terminated strings +/// for the duration of the call. Each returned non-null handle must be +/// released exactly once with its corresponding cleanup function. @ffi.Native< - Tor Function(ffi.Uint16, ffi.Pointer, ffi.Pointer)>() + Tor Function(ffi.Uint16, ffi.Pointer, ffi.Pointer) +>() external Tor tor_start( int socks_port, ffi.Pointer state_dir, ffi.Pointer cache_dir, ); +/// Release a string returned by [`tor_last_error_message`]. +/// +/// # Safety +/// `message` must be null or a pointer returned by `tor_last_error_message` +/// that has not been released yet. It must not be used after this call. +@ffi.Native)>() +external void tor_string_free(ffi.Pointer message); + final class Tor extends ffi.Struct { external ffi.Pointer client; external ffi.Pointer proxy; + @ffi.Uint16() + external int port; + static ffi.Pointer $allocate( ffi.Allocator $allocator, { required ffi.Pointer client, required ffi.Pointer proxy, - }) => - $allocator() - ..ref.client = client - ..ref.proxy = proxy; + required int port, + }) => $allocator() + ..ref.client = client + ..ref.proxy = proxy + ..ref.port = port; +} + +/// The outcome of [`tor_client_bootstrap`]. +/// +/// Every code other than `Ok` also sets the message returned by +/// [`tor_last_error_message`]. +enum TorBootstrapStatus { + /// The client bootstrapped. + Ok(0), + + /// [`tor_client_cancel_bootstrap`] was called on the handle. + Cancelled(1), + + /// The deadline passed before bootstrap finished. + TimedOut(2), + + /// Arti reported an error. + Failed(3), + + /// The client handle was null. + NullClient(4); + + final int value; + const TorBootstrapStatus(this.value); + + static TorBootstrapStatus fromValue(int value) => switch (value) { + 0 => Ok, + 1 => Cancelled, + 2 => TimedOut, + 3 => Failed, + 4 => NullClient, + _ => throw ArgumentError('Unknown value for TorBootstrapStatus: $value'), + }; } + +typedef TorBootstrapStatus$1 = ffi.Int32; +typedef DartTorBootstrapStatus = int; diff --git a/pubspec.yaml b/pubspec.yaml index 438ec75f..ec548808 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: tor_ffi_plugin description: Tor FFI plugin for Dart and Flutter. Uses Arti (A Rust Tor Implementation) via FFI. -version: 0.1.0 +version: 0.2.0 homepage: https://github.com/cypherstack/tor environment: @@ -14,6 +14,7 @@ dependencies: code_assets: ^1.2.1 crypto: ^3.0.7 hooks: ^2.0.2 + meta: ^1.16.0 native_toolchain_rust: ^1.0.6 dev_dependencies: diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 4664cb43..fbe410ab 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -2,15 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "addr2line" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" -dependencies = [ - "gimli", -] - [[package]] name = "adler" version = "1.0.2" @@ -19,13 +10,13 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aes" -version = "0.8.4" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +checksum = "35f0f96ce78e38c3dc6d8948aa8163d06385be74000f3c7a95bf1eef35d3ea32" dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", + "cipher 0.5.2", + "cpubits", + "cpufeatures 0.3.1", "zeroize", ] @@ -97,12 +88,6 @@ dependencies = [ "libc", ] -[[package]] -name = "anes" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" - [[package]] name = "anstream" version = "0.6.14" @@ -154,9 +139,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.86" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "arrayvec" @@ -166,60 +151,71 @@ checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" [[package]] name = "arti" -version = "1.4.6" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b4487ced256657f3b506a3c9250004ecf74e92f79c3577283c1eb255229573" +checksum = "a567e05cf614b579609d958e41b54602eea6983bccfec46d1e933a45605dd813" dependencies = [ "anyhow", "arti-client", "cfg-if", "clap 4.5.7", + "derive-deftly 1.12.1", "derive_builder_fork_arti", + "derive_more", + "extend", "fs-mistrust", "futures", + "futures-copy", "hickory-proto", "humantime", "humantime-serde", - "itertools 0.14.0", + "itertools", "libc", "notify", "paste", + "pin-project", "postage", - "rlimit", + "rlimit 0.11.0", "safelog", "secmem-proc", "serde", - "thiserror 2.0.14", + "serde_json", + "thiserror 2.0.20", "time", "tokio", "tokio-util", - "toml 0.8.14", + "toml 1.1.6+spec-1.1.0", "tor-async-utils", "tor-basic-utils", "tor-config", "tor-config-path", + "tor-config-shared", "tor-error", + "tor-general-addr", "tor-key-forge", "tor-keymgr", + "tor-log-ratelim", + "tor-proto", "tor-rtcompat", "tor-socksproto", "tracing", "tracing-appender", "tracing-subscriber", "visibility", + "web-time-compat", "winapi", ] [[package]] name = "arti-client" -version = "0.33.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "375c3b0681ca73c8678dc2e879f01964121955dc8e45f3b334ed0f7e7cefec48" +checksum = "43e6531f26fe6f44d71dabdc63b7845bae81547cc738aa942f0e3e482f776388" dependencies = [ "anyhow", "async-trait", "cfg-if", - "derive-deftly 1.2.0", + "derive-deftly 1.12.1", "derive_builder_fork_arti", "derive_more", "educe", @@ -231,10 +227,11 @@ dependencies = [ "libc", "once_cell", "postage", - "rand 0.9.2", + "rand 0.10.2", "safelog", "serde", - "thiserror 2.0.14", + "tempfile", + "thiserror 2.0.20", "time", "tor-async-utils", "tor-basic-utils", @@ -242,6 +239,7 @@ dependencies = [ "tor-circmgr", "tor-config", "tor-config-path", + "tor-dircommon", "tor-dirmgr", "tor-error", "tor-guardmgr", @@ -260,6 +258,7 @@ dependencies = [ "tor-rtcompat", "tracing", "void", + "web-time-compat", ] [[package]] @@ -280,7 +279,7 @@ dependencies = [ "nom", "num-traits", "rusticata-macros", - "thiserror 2.0.14", + "thiserror 2.0.20", ] [[package]] @@ -330,13 +329,13 @@ dependencies = [ [[package]] name = "async-native-tls" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9343dc5acf07e79ff82d0c37899f079db3534d99f189a1837c8e549c99405bec" +checksum = "37dd6b179962fe4048a6f81d4c0d7ed419a21fdf49204b4c6b04971693358e79" dependencies = [ "futures-util", "native-tls", - "thiserror 1.0.61", + "thiserror 2.0.20", "url", ] @@ -400,7 +399,7 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ - "hermit-abi 0.1.19", + "hermit-abi", "libc", "winapi", ] @@ -411,21 +410,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" -[[package]] -name = "backtrace" -version = "0.3.73" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" -dependencies = [ - "addr2line", - "cc", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", -] - [[package]] name = "base16ct" version = "0.2.0" @@ -440,9 +424,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.6.0" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "bincode" @@ -499,10 +483,14 @@ dependencies = [ ] [[package]] -name = "bounded-vec-deque" -version = "0.1.1" +name = "block-buffer" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2225b558afc76c596898f5f1b3fc35cfce0eb1b13635cbd7d1b2a7177dc10ccd" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", + "zeroize", +] [[package]] name = "bstr" @@ -511,7 +499,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" dependencies = [ "memchr", - "regex-automata 0.4.9", + "regex-automata", "serde", ] @@ -529,9 +517,9 @@ checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" [[package]] name = "bytemuck" -version = "1.16.1" +version = "1.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b236fc92302c97ed75b38da1f4917b5cdda4984745740f153a5d3059e48d725e" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" [[package]] name = "byteorder" @@ -541,21 +529,15 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.6.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "caret" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "061dc3258f029feaf9ff02b43c6af5ea67a7dfaed5d2aef36204c812e614ef9c" - -[[package]] -name = "cast" -version = "0.3.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" +checksum = "a3135adcc2ec978dbad556b41b7c71e0be7625d70b79159a8a19b5879a761115" [[package]] name = "cbindgen" @@ -593,6 +575,17 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "rand_core 0.10.1", +] + [[package]] name = "chrono" version = "0.4.38" @@ -607,40 +600,24 @@ dependencies = [ ] [[package]] -name = "ciborium" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" -dependencies = [ - "ciborium-io", - "ciborium-ll", - "serde", -] - -[[package]] -name = "ciborium-io" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" - -[[package]] -name = "ciborium-ll" -version = "0.2.2" +name = "cipher" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "ciborium-io", - "half", + "crypto-common 0.1.6", + "inout 0.1.3", ] [[package]] name = "cipher" -version = "0.4.4" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" dependencies = [ - "crypto-common", - "inout", + "block-buffer 0.12.1", + "crypto-common 0.2.2", + "inout 0.2.2", "zeroize", ] @@ -709,6 +686,12 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + [[package]] name = "coarsetime" version = "0.1.34" @@ -727,12 +710,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" [[package]] -name = "concurrent-queue" -version = "2.5.0" +name = "combine" +version = "4.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" dependencies = [ - "crossbeam-utils", + "bytes", + "memchr", ] [[package]] @@ -743,9 +727,9 @@ checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "convert_case" -version = "0.7.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" dependencies = [ "unicode-segmentation", ] @@ -775,6 +759,12 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +[[package]] +name = "cpubits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" + [[package]] name = "cpufeatures" version = "0.2.12" @@ -785,58 +775,21 @@ dependencies = [ ] [[package]] -name = "crc32fast" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "criterion" -version = "0.5.1" +name = "cpufeatures" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" dependencies = [ - "anes", - "cast", - "ciborium", - "clap 4.5.7", - "criterion-plot", - "is-terminal", - "itertools 0.10.5", - "num-traits", - "once_cell", - "oorandom", - "plotters", - "rayon", - "regex", - "serde", - "serde_derive", - "serde_json", - "tinytemplate", - "walkdir", + "libc", ] [[package]] -name = "criterion-cycles-per-byte" -version = "0.6.1" +name = "crc32fast" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1029452fa751c93f8834962dd74807d69f0a6c7624d5b06625b393aeb6a14fc2" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", - "criterion", -] - -[[package]] -name = "criterion-plot" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" -dependencies = [ - "cast", - "itertools 0.10.5", ] [[package]] @@ -847,28 +800,9 @@ checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" [[package]] name = "crossbeam-channel" -version = "0.5.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-deque" -version = "0.8.6" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1" dependencies = [ "crossbeam-utils", ] @@ -888,12 +822,6 @@ version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" -[[package]] -name = "crunchy" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - [[package]] name = "crypto-bigint" version = "0.5.5" @@ -916,13 +844,31 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + [[package]] name = "ctr" -version = "0.9.2" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +checksum = "baaca1c4b237092596f64d571e9db6ce4109c4ef9742e27590f1709594461f21" dependencies = [ - "cipher", + "cipher 0.5.2", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", ] [[package]] @@ -932,7 +878,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.12", "curve25519-dalek-derive", "digest", "fiat-crypto", @@ -972,6 +918,16 @@ dependencies = [ "darling_macro 0.20.9", ] +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core 0.21.3", + "darling_macro 0.21.3", +] + [[package]] name = "darling_core" version = "0.14.4" @@ -1000,6 +956,19 @@ dependencies = [ "syn 2.0.105", ] +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "syn 2.0.105", +] + [[package]] name = "darling_macro" version = "0.14.4" @@ -1022,6 +991,17 @@ dependencies = [ "syn 2.0.105", ] +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core 0.21.3", + "quote", + "syn 2.0.105", +] + [[package]] name = "data-encoding" version = "2.6.0" @@ -1055,12 +1035,11 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.11" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ - "powerfmt", - "serde", + "serde_core", ] [[package]] @@ -1075,11 +1054,11 @@ dependencies = [ [[package]] name = "derive-deftly" -version = "1.2.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957bb73a3a9c0bbcac67e129b81954661b3cfcb9e28873d8441f91b54852e77a" +checksum = "e64c6955def7f25b8c3becad9e1762ff6abc3ea62704b0883284c771149b45a6" dependencies = [ - "derive-deftly-macros 1.2.0", + "derive-deftly-macros 1.12.1", "heck 0.5.0", ] @@ -1090,32 +1069,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357422a457ccb850dc8f1c1680e0670079560feaad6c2e247e3f345c4fab8a3f" dependencies = [ "heck 0.5.0", - "indexmap 2.2.6", - "itertools 0.12.1", + "indexmap 2.14.2", + "itertools", "proc-macro-crate", "proc-macro2", "quote", "sha3", - "strum 0.26.2", + "strum 0.27.2", "syn 2.0.105", "void", ] [[package]] name = "derive-deftly-macros" -version = "1.2.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ea41269bd490d251b9eca50ccb43117e641cc68b129849757c15ece88fe0574" +checksum = "36e10ac4bd47bf8d24a4852f001df23b7acbbca6e59d0adec0c2e2c4409dfe5b" dependencies = [ "heck 0.5.0", - "indexmap 2.2.6", - "itertools 0.12.1", + "indexmap 2.14.2", + "itertools", "proc-macro-crate", "proc-macro2", "quote", "sha3", - "strum 0.26.2", + "strum 0.28.0", "syn 2.0.105", + "unicode-ident", "void", ] @@ -1152,22 +1132,23 @@ dependencies = [ [[package]] name = "derive_more" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" dependencies = [ "derive_more-impl", ] [[package]] name = "derive_more-impl" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ "convert_case", "proc-macro2", "quote", + "rustc_version", "syn 2.0.105", "unicode-xid", ] @@ -1178,9 +1159,9 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", + "block-buffer 0.10.4", "const-oid", - "crypto-common", + "crypto-common 0.1.6", "subtle", ] @@ -1232,7 +1213,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.2", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1284,9 +1265,9 @@ dependencies = [ [[package]] name = "ed25519-dalek" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" dependencies = [ "curve25519-dalek", "ed25519", @@ -1336,25 +1317,46 @@ dependencies = [ ] [[package]] -name = "enum-as-inner" -version = "0.6.0" +name = "enum-ordinalize" +version = "3.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ffccbb6966c05b32ef8fbac435df276c4ae4d3dc55a8cd0eb9745e6c12f546a" +checksum = "1bf1fa3f06bbff1ea5b1a9c7b14aa992a39657db60a2759457328d7e058f49ee" dependencies = [ - "heck 0.4.1", + "num-bigint", + "num-traits", "proc-macro2", "quote", "syn 2.0.105", ] [[package]] -name = "enum-ordinalize" -version = "3.1.15" +name = "enum_dispatch" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bf1fa3f06bbff1ea5b1a9c7b14aa992a39657db60a2759457328d7e058f49ee" +checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd" dependencies = [ - "num-bigint", - "num-traits", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.105", +] + +[[package]] +name = "enumset" +version = "1.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccc5801fd11762e24d1e420d01d2ac518f2a2ca4329d4fbb6639f2412b6204e0" +dependencies = [ + "enumset_derive", +] + +[[package]] +name = "enumset_derive" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd536557b58c682b217b8fb199afdff47cd3eff260623f19e77074eb073d63a" +dependencies = [ + "darling 0.21.3", "proc-macro2", "quote", "syn 2.0.105", @@ -1373,20 +1375,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] name = "event-listener" -version = "5.3.1" +version = "5.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" dependencies = [ - "concurrent-queue", "parking", "pin-project-lite", ] +[[package]] +name = "extend" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "311a6d2f1f9d60bff73d2c78a0af97ed27f79672f15c238192a5bbb64db56d00" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.105", +] + [[package]] name = "fallible-iterator" version = "0.3.0" @@ -1491,36 +1503,39 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] [[package]] name = "fs-mistrust" -version = "0.10.0" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "198b8f9ab4cff63b5c91e9e64edd4e6b43cd7fe7a52519a03c6c32ea0acfa557" +checksum = "bc7f210768dd30e30895a6024be8472615bd7b6caec6df4f4105fc03e7608bdd" dependencies = [ "derive_builder_fork_arti", "dirs 6.0.0", + "extend", "libc", "pwd-grp", "serde", - "thiserror 2.0.14", + "thiserror 2.0.20", + "void", "walkdir", ] [[package]] -name = "fslock" -version = "0.2.1" +name = "fslock-guard" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04412b8935272e3a9bae6f48c7bfff74c2911f60525404edfdd28e49884c3bfb" +checksum = "8082ded4af6b2e8e75d0ff71848ad7649d1ddff369a718e8e1db918795967bce" dependencies = [ "libc", - "winapi", + "thiserror 2.0.20", + "windows-sys 0.61.2", ] [[package]] @@ -1531,9 +1546,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.30" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" dependencies = [ "futures-channel", "futures-core", @@ -1546,25 +1561,38 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" dependencies = [ "futures-core", "futures-sink", ] +[[package]] +name = "futures-copy" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "853fa8b969b511030e15d9d51db878103b4f1a92d27cb30c63069ae9412f0546" +dependencies = [ + "extend", + "futures", + "libc", + "pin-project", + "windows-sys 0.61.2", +] + [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" [[package]] name = "futures-executor" -version = "0.3.30" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" dependencies = [ "futures-core", "futures-task", @@ -1573,38 +1601,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 3.0.5", ] [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ "futures-channel", "futures-core", @@ -1614,7 +1642,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -1636,8 +1663,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -1647,16 +1676,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", + "js-sys", "libc", - "r-efi", + "r-efi 5.3.0", "wasi 0.14.2+wasi-0.2.4", + "wasm-bindgen", ] [[package]] -name = "gimli" -version = "0.29.0" +name = "getrandom" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasm-bindgen", +] + +[[package]] +name = "getset" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cf442baaabe4213ce7d1239afc26c039180b6456da2cededa316ae2c8a77a77" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.105", +] [[package]] name = "glob" @@ -1681,28 +1731,12 @@ dependencies = [ "subtle", ] -[[package]] -name = "half" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" -dependencies = [ - "cfg-if", - "crunchy", -] - [[package]] name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" - [[package]] name = "hashbrown" version = "0.15.5" @@ -1712,6 +1746,18 @@ dependencies = [ "foldhash", ] +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + [[package]] name = "hashlink" version = "0.10.0" @@ -1742,18 +1788,6 @@ dependencies = [ "libc", ] -[[package]] -name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - -[[package]] -name = "hermit-abi" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" - [[package]] name = "hex" version = "0.4.3" @@ -1762,25 +1796,19 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hickory-proto" -version = "0.25.2" +version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" +checksum = "12b92608f679a6fa515dd1d15c1ff89443026e391200a2c840c7afcba482893d" dependencies = [ - "async-trait", - "cfg-if", "data-encoding", - "enum-as-inner", - "futures-channel", - "futures-io", - "futures-util", "idna", "ipnet", + "jni", "once_cell", - "rand 0.9.2", + "rand 0.10.2", "ring", - "thiserror 2.0.14", + "thiserror 2.0.20", "tinyvec", - "tokio", "tracing", "url", ] @@ -1822,9 +1850,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.9.4" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "httpdate" @@ -1848,6 +1876,16 @@ dependencies = [ "serde", ] +[[package]] +name = "hybrid-array" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3944cf8cf766b40e2a1a333ee5e9b563f854d5fa49d6a8ca2764e97c6eddb214" +dependencies = [ + "typenum", + "zeroize", +] + [[package]] name = "iana-time-zone" version = "0.1.60" @@ -1965,9 +2003,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -1984,6 +2022,16 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "imara-diff" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f01d462f766df78ab820dd06f5eb700233c51f0f4c2e846520eaf4ba6aa5c5c" +dependencies = [ + "hashbrown 0.15.5", + "memchr", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -1997,13 +2045,14 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.6" +version = "2.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" dependencies = [ "equivalent", - "hashbrown 0.14.5", + "hashbrown 0.17.1", "serde", + "serde_core", ] [[package]] @@ -2035,6 +2084,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "inout" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" +dependencies = [ + "hybrid-array", +] + [[package]] name = "inventory" version = "0.3.15" @@ -2043,19 +2101,17 @@ checksum = "f958d3d68f4167080a18141e10381e7634563984a537f2a49a30fd8e53ac5767" [[package]] name = "ipnet" -version = "2.9.0" +version = "2.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" [[package]] -name = "is-terminal" -version = "0.4.16" +name = "iprange" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +checksum = "37209be0ad225457e63814401415e748e2453a5297f9b637338f5fb8afa4ec00" dependencies = [ - "hermit-abi 0.5.2", - "libc", - "windows-sys 0.52.0", + "ipnet", ] [[package]] @@ -2066,36 +2122,67 @@ checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" [[package]] name = "itertools" -version = "0.10.5" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc" dependencies = [ "either", ] [[package]] -name = "itertools" -version = "0.12.1" +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "jni" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" dependencies = [ - "either", + "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror 2.0.20", + "walkdir", + "windows-link 0.2.1", ] [[package]] -name = "itertools" -version = "0.14.0" +name = "jni-macros" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" dependencies = [ - "either", + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.105", ] [[package]] -name = "itoa" -version = "1.0.11" +name = "jni-sys" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.105", +] [[package]] name = "jobserver" @@ -2108,20 +2195,22 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.69" +version = "0.3.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" dependencies = [ + "cfg-if", + "futures-util", "wasm-bindgen", ] [[package]] name = "keccak" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" dependencies = [ - "cpufeatures", + "cpufeatures 0.2.12", ] [[package]] @@ -2155,9 +2244,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.175" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libm" @@ -2233,11 +2322,11 @@ dependencies = [ [[package]] name = "matchers" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" dependencies = [ - "regex-automata 0.1.10", + "regex-automata", ] [[package]] @@ -2248,9 +2337,9 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "memmap2" -version = "0.9.4" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" dependencies = [ "libc", ] @@ -2282,17 +2371,6 @@ dependencies = [ "adler", ] -[[package]] -name = "mio" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" -dependencies = [ - "libc", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.48.0", -] - [[package]] name = "mio" version = "1.0.4" @@ -2332,6 +2410,12 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nonany" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b8866ec53810a9a4b3d434a29801e78c707430a9ae11c2db4b8b62bb9675a0" + [[package]] name = "notify" version = "8.2.0" @@ -2343,7 +2427,7 @@ dependencies = [ "kqueue", "libc", "log", - "mio 1.0.4", + "mio", "notify-types", "walkdir", "windows-sys 0.60.2", @@ -2366,12 +2450,11 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.46.0" +version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "overload", - "winapi", + "windows-sys 0.61.2", ] [[package]] @@ -2386,26 +2469,25 @@ dependencies = [ [[package]] name = "num-bigint-dig" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" dependencies = [ - "byteorder", "lazy_static", "libm", "num-integer", "num-iter", "num-traits", - "rand 0.8.5", + "rand 0.8.8", "smallvec", "zeroize", ] [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-integer" @@ -2437,16 +2519,6 @@ dependencies = [ "libm", ] -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi 0.3.9", - "libc", -] - [[package]] name = "num_enum" version = "0.7.2" @@ -2470,32 +2542,23 @@ dependencies = [ [[package]] name = "objc2-core-foundation" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ "bitflags 2.9.1", ] [[package]] name = "objc2-io-kit" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71c1c64d6120e51cd86033f67176b1cb66780c2efe34dec55176f77befd93c0a" +checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" dependencies = [ "libc", "objc2-core-foundation", ] -[[package]] -name = "object" -version = "0.36.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434" -dependencies = [ - "memchr", -] - [[package]] name = "once_cell" version = "1.21.3" @@ -2508,30 +2571,23 @@ dependencies = [ [[package]] name = "oneshot-fused-workaround" -version = "0.2.3" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2948fd2414b613f9a97f8401270bd5d7638265ab940475cdbcfa28a0273d58" +checksum = "40e2e88d3d699c52530078d715a6b5b10b3010afe570cfdc0240fee1c9d764ba" dependencies = [ "futures", ] -[[package]] -name = "oorandom" -version = "11.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" - [[package]] name = "openssl" -version = "0.10.64" +version = "0.10.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" +checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" dependencies = [ "bitflags 2.9.1", "cfg-if", "foreign-types", "libc", - "once_cell", "openssl-macros", "openssl-sys", ] @@ -2555,18 +2611,18 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "300.3.1+3.3.1" +version = "300.6.1+3.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7259953d42a81bf137fbbd73bd30a8e1914d6dce43c2b90ed575783a22608b91" +checksum = "46eb8fb9fb3b61ce1c0f8a026c4c1a0714d3a9e138e7fbde78753ce2babc3846" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.102" +version = "0.9.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" +checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695" dependencies = [ "cc", "libc", @@ -2599,12 +2655,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - [[package]] name = "p256" version = "0.13.2" @@ -2689,15 +2739,15 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "phf" -version = "0.12.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7" +checksum = "010378780309880b08997fae13be7834dba947d36393bd372f2b1556deb2a2f6" dependencies = [ "phf_macros", "phf_shared", @@ -2706,9 +2756,9 @@ dependencies = [ [[package]] name = "phf_generator" -version = "0.12.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cbb1126afed61dd6368748dae63b1ee7dc480191c6262a3b4ff1e29d86a6c5b" +checksum = "aeb62e0959d5a1bebc965f4d15d9e2b7cea002b6b0f5ba8cde6cc26738467100" dependencies = [ "fastrand", "phf_shared", @@ -2716,9 +2766,9 @@ dependencies = [ [[package]] name = "phf_macros" -version = "0.12.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d713258393a82f091ead52047ca779d37e5766226d009de21696c4e667044368" +checksum = "5fa8d0ca26d424d27630da600c6624696e7dec8bf7b3b492b383c5dc49e5e085" dependencies = [ "phf_generator", "phf_shared", @@ -2729,9 +2779,9 @@ dependencies = [ [[package]] name = "phf_shared" -version = "0.12.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981" +checksum = "c6fd9027e2d9319be6349febd1db4e8d02aa544921200c9b777720ac34a3aa89" dependencies = [ "siphasher", ] @@ -2762,12 +2812,6 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - [[package]] name = "pkcs1" version = "0.7.5" @@ -2796,31 +2840,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] -name = "plotters" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" -dependencies = [ - "num-traits", - "plotters-backend", - "plotters-svg", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "plotters-backend" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" - -[[package]] -name = "plotters-svg" -version = "0.3.7" +name = "polyval" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +checksum = "f0fa31d631f2b2cb2a544d0aa321ce847a94764d701ca2becc411138b93d49cd" dependencies = [ - "plotters-backend", + "cpubits", + "cpufeatures 0.3.1", + "universal-hash", ] [[package]] @@ -2882,7 +2909,7 @@ checksum = "70c501afe3a2e25c9bd219aa56ec1e04cdb3fcdd763055be268778c13fa82c1f" dependencies = [ "autocfg", "equivalent", - "indexmap 2.2.6", + "indexmap 2.14.2", ] [[package]] @@ -2930,6 +2957,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "radium" version = "0.7.0" @@ -2938,9 +2971,9 @@ checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" [[package]] name = "rand" -version = "0.8.5" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" dependencies = [ "rand_chacha 0.3.1", "rand_core 0.6.4", @@ -2948,12 +2981,13 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.2" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.3", + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", ] [[package]] @@ -2968,12 +3002,12 @@ dependencies = [ [[package]] name = "rand_chacha" -version = "0.9.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +checksum = "3e6af7f3e25ded52c41df4e0b1af2d047e45896c2f3281792ed68a1c243daedb" dependencies = [ "ppv-lite86", - "rand_core 0.9.3", + "rand_core 0.10.1", ] [[package]] @@ -2987,51 +3021,35 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.9.3" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" -dependencies = [ - "getrandom 0.3.3", -] +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] name = "rand_jitter" -version = "0.5.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b16df48f071248e67b8fc5e866d9448d45c08ad8b672baaaf796e2f15e606ff0" +checksum = "3fdcd80e68f0a8f9ca5ec7cfd02fd5fbb8fbe6ef4e9b90ea2f48bb929b74f88e" dependencies = [ "libc", - "rand_core 0.9.3", - "winapi", -] - -[[package]] -name = "rayon" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" -dependencies = [ - "either", - "rayon-core", + "rand_core 0.10.1", + "windows-sys 0.59.0", ] [[package]] -name = "rayon-core" -version = "1.13.0" +name = "rangemap" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] +checksum = "a611d15b50743feb4c76b7d03edcb0e64f399c26961e4efe6975bc398be6aa3d" [[package]] name = "rdrand" -version = "0.8.3" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d92195228612ac8eed47adbc2ed0f04e513a4ccb98175b6f2bd04d963b533655" +checksum = "84448986e59427c795b929d8dbe12d176275c7d0887ee48098c35b7206f51bae" dependencies = [ - "rand_core 0.6.4", + "libc", + "rand_core 0.10.1", ] [[package]] @@ -3071,7 +3089,7 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.15", "libredox", - "thiserror 2.0.14", + "thiserror 2.0.20", ] [[package]] @@ -3082,17 +3100,8 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", -] - -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", + "regex-automata", + "regex-syntax", ] [[package]] @@ -3103,26 +3112,33 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.5", + "regex-syntax", ] [[package]] name = "regex-syntax" -version = "0.6.29" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] -name = "regex-syntax" -version = "0.8.5" +name = "reseeding_rng" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "ecfc8e855ab517235426ec1290bfb0745e73fa478b29619eb66a2a81be9d8716" +dependencies = [ + "rand_core 0.10.1", +] [[package]] name = "retry-error" -version = "0.6.5" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce97442758392c7e2a7716e06c514de75f0fe4b5a4b76e14ba1e5edfb7ba3512" +checksum = "90a0dea62e6084ede74beea58ee263fd2d70bdb59260a3bf3ede338dfb76f5cc" +dependencies = [ + "humantime", + "web-time", +] [[package]] name = "rfc6979" @@ -3157,6 +3173,15 @@ dependencies = [ "libc", ] +[[package]] +name = "rlimit" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f35ee2729c56bb610f6dba436bf78135f728b7373bdffae2ec815b2d3eb98cc3" +dependencies = [ + "libc", +] + [[package]] name = "rsa" version = "0.9.6" @@ -3193,12 +3218,6 @@ dependencies = [ "time", ] -[[package]] -name = "rustc-demangle" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" - [[package]] name = "rustc_version" version = "0.4.0" @@ -3240,7 +3259,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.9.4", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] @@ -3267,15 +3286,15 @@ checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "safelog" -version = "0.4.8" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a4e1c994fbc7521a5003e5c1c54304654ea0458881e777f6e2638520c2de8c5" +checksum = "3e091498d7a0a629f344befcf15b1f823a24d5a83d584feeacacc7c764e67c11" dependencies = [ "derive_more", "educe", "either", "fluid-let", - "thiserror 2.0.14", + "thiserror 2.0.20", ] [[package]] @@ -3296,6 +3315,13 @@ dependencies = [ "regex", ] +[[package]] +name = "saturating-time" +version = "0.5.0" +dependencies = [ + "web-time-compat", +] + [[package]] name = "schannel" version = "0.1.23" @@ -3336,8 +3362,8 @@ dependencies = [ "libc", "rustix 1.0.8", "rustix-linux-procfs", - "thiserror 2.0.14", - "windows", + "thiserror 2.0.20", + "windows 0.61.3", ] [[package]] @@ -3371,10 +3397,11 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.219" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ + "serde_core", "serde_derive", ] @@ -3388,15 +3415,24 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 3.0.5", ] [[package]] @@ -3428,6 +3464,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + [[package]] name = "serde_with" version = "3.8.1" @@ -3438,7 +3483,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.2.6", + "indexmap 2.14.2", "serde", "serde_derive", "serde_json", @@ -3465,7 +3510,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.12", "digest", ] @@ -3476,7 +3521,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.12", "digest", ] @@ -3535,6 +3580,22 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "siphasher" version = "1.0.1" @@ -3562,15 +3623,14 @@ dependencies = [ [[package]] name = "slotmap-careful" -version = "0.2.5" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9500059071474a36baac642b6bb99ca1dbac0ce43727abbba02dad83822dadf2" +checksum = "7dae6673c710ab011fc8d2ff604b16201df0a59f943976d911d83bbac2a1854f" dependencies = [ "paste", "serde", "slotmap", - "thiserror 2.0.14", - "void", + "thiserror 2.0.20", ] [[package]] @@ -3581,12 +3641,12 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" -version = "0.5.7" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -3606,20 +3666,20 @@ dependencies = [ ] [[package]] -name = "ssh-cipher" +name = "ssh-cipher-fork-arti" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caac132742f0d33c3af65bfcde7f6aa8f62f0e991d80db99149eb9d44708784f" +checksum = "125c5795103fc93fced42d123c8044180afc55469caa1ab56487c3c5543c898d" dependencies = [ - "cipher", - "ssh-encoding", + "cipher 0.4.4", + "ssh-encoding-fork-arti", ] [[package]] -name = "ssh-encoding" +name = "ssh-encoding-fork-arti" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb9242b9ef4108a78e8cd1a2c98e193ef372437f8c22be363075233321dd4a15" +checksum = "e0cf03c3a7ea88451ff83a129a79451fd9891d44fc76c25e916a11848b81814c" dependencies = [ "base64ct", "pem-rfc7468", @@ -3627,11 +3687,12 @@ dependencies = [ ] [[package]] -name = "ssh-key" -version = "0.6.6" +name = "ssh-key-fork-arti" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca9b366a80cf18bb6406f4cf4d10aebfb46140a8c0c33f666a144c5c76ecbafc" +checksum = "433782176b73ea7907763dc314c4a17438a231864d4aa683ba47c07c1cf0a388" dependencies = [ + "num-bigint-dig", "p256", "p384", "p521", @@ -3640,8 +3701,8 @@ dependencies = [ "sec1", "sha2", "signature", - "ssh-cipher", - "ssh-encoding", + "ssh-cipher-fork-arti", + "ssh-encoding-fork-arti", "subtle", "zeroize", ] @@ -3672,40 +3733,39 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" -version = "0.26.2" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" dependencies = [ - "strum_macros 0.26.4", + "strum_macros 0.27.2", ] [[package]] name = "strum" -version = "0.27.2" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" dependencies = [ - "strum_macros 0.27.2", + "strum_macros 0.28.0", ] [[package]] name = "strum_macros" -version = "0.26.4" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "rustversion", "syn 2.0.105", ] [[package]] name = "strum_macros" -version = "0.27.2" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -3715,9 +3775,9 @@ dependencies = [ [[package]] name = "subtle" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d0208408ba0c3df17ed26eb06992cb1a1268d41b2c0e12e65203fbe3972cee5" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" @@ -3741,6 +3801,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "sync_wrapper" version = "1.0.2" @@ -3760,16 +3831,16 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.36.1" +version = "0.38.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "252800745060e7b9ffb7b2badbd8b31cfa4aa2e61af879d0a3bf2a317c20217d" +checksum = "92ab6a2f8bfe508deb3c6406578252e491d299cbbf3bc0529ecc3313aee4a52f" dependencies = [ "libc", "memchr", "ntapi", "objc2-core-foundation", "objc2-io-kit", - "windows", + "windows 0.62.2", ] [[package]] @@ -3826,11 +3897,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.14" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b0949c3a6c842cbde3f1686d6eea5a010516deb7085f79db747562d4102f41e" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" dependencies = [ - "thiserror-impl 2.0.14", + "thiserror-impl 2.0.20", ] [[package]] @@ -3846,13 +3917,13 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.14" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc5b44b4ab9c2fdd0e0512e6bece8388e214c0749f5862b114cc5b7a25daf227" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 3.0.5", ] [[package]] @@ -3867,30 +3938,29 @@ dependencies = [ [[package]] name = "time" -version = "0.3.36" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" dependencies = [ "deranged", - "itoa", "num-conv", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.2" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.18" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" dependencies = [ "num-conv", "time-core", @@ -3906,16 +3976,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "tinytemplate" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" -dependencies = [ - "serde", - "serde_json", -] - [[package]] name = "tinyvec" version = "1.6.0" @@ -3933,28 +3993,26 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.38.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ - "backtrace", "bytes", "libc", - "mio 0.8.11", - "num_cpus", + "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.3.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", @@ -3991,11 +4049,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335" dependencies = [ "serde", - "serde_spanned", - "toml_datetime", + "serde_spanned 0.6.6", + "toml_datetime 0.6.6", "toml_edit 0.22.14", ] +[[package]] +name = "toml" +version = "1.1.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "920602543f0911ab71da12c50d59701da54c196d1a2bf5cb4b75667f137a406a" +dependencies = [ + "indexmap 2.14.2", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.4", +] + [[package]] name = "toml_datetime" version = "0.6.6" @@ -4005,14 +4078,23 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" dependencies = [ - "indexmap 2.2.6", - "toml_datetime", + "indexmap 2.14.2", + "toml_datetime 0.6.6", "winnow 0.5.40", ] @@ -4022,61 +4104,93 @@ version = "0.22.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.14.2", "serde", - "serde_spanned", - "toml_datetime", + "serde_spanned 0.6.6", + "toml_datetime 0.6.6", "winnow 0.6.13", ] +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow 1.0.4", +] + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + [[package]] name = "tor-async-utils" -version = "0.33.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28240d2b739ecba7514c92c0e42f2b5b81a95b5286366bdb2c2f8ef526a5578c" +checksum = "64e39ddd4c352b8e3166ea0ec49b82a33ad9eeae8bdbbf76e1d2f1e856a6316b" dependencies = [ - "derive-deftly 1.2.0", + "cfg-if", + "derive-deftly 1.12.1", "educe", + "extend", "futures", "oneshot-fused-workaround", "pin-project", "postage", - "thiserror 2.0.14", + "slotmap-careful", + "sync_wrapper", + "thiserror 2.0.20", + "tokio", + "tokio-util", + "tor-basic-utils", + "tor-rtcompat", + "tracing", "void", + "web-time-compat", ] [[package]] name = "tor-basic-utils" -version = "0.33.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55f86a4e4768d337df0e1189eb229c1c27125e86282fabdef89088e1f0be9107" +checksum = "e44c7b006f22966d44a2a0a6c1ee71e472c62182c7f0460077df3294d52f23c4" dependencies = [ + "derive-deftly 1.12.1", "derive_more", + "educe", + "extend", + "getrandom 0.4.3", "hex", - "itertools 0.14.0", + "itertools", "libc", "paste", - "rand 0.9.2", - "rand_chacha 0.9.0", + "rand 0.10.2", + "rand_chacha 0.10.0", "serde", "slab", "smallvec", - "thiserror 2.0.14", + "thiserror 2.0.20", + "tracing", + "weak-table", + "web-time-compat", ] [[package]] name = "tor-bytes" -version = "0.33.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d08b5b2e93fd21a4aaa9a3867962ea82f5dc830522737183abb514bdeae9fc9" +checksum = "ccfed3adc9db61334445e921b95c26c896994db64e031c31e5cc48bd73e57ac7" dependencies = [ "bytes", - "derive-deftly 1.2.0", + "derive-deftly 1.12.1", "digest", "educe", - "getrandom 0.3.3", + "getrandom 0.4.3", "safelog", - "thiserror 2.0.14", + "thiserror 2.0.20", "tor-error", "tor-llcrypto", "zeroize", @@ -4084,21 +4198,22 @@ dependencies = [ [[package]] name = "tor-cell" -version = "0.33.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60677bfa808c00539df7f8276facd59f92e5d0bd22ee8e5bdc1ab6a14632db41" +checksum = "b2d6398fd085306de5af739899d0390674b8646d4ff6cfb69d65f40a5f9d88da" dependencies = [ "amplify", "bitflags 2.9.1", "bytes", "caret", - "derive-deftly 1.2.0", + "derive-deftly 1.12.1", "derive_more", "educe", + "itertools", "paste", - "rand 0.9.2", + "rand 0.10.2", "smallvec", - "thiserror 2.0.14", + "thiserror 2.0.20", "tor-basic-utils", "tor-bytes", "tor-cert", @@ -4114,43 +4229,51 @@ dependencies = [ [[package]] name = "tor-cert" -version = "0.33.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abd81384d03705336b9fb7ecf152e4f61b2e0a0cb1adbd9bbd116b46a010e230" +checksum = "98dec924caf6e9548b4860878373132bf89536eb0d1658666df12dff72b855cd" dependencies = [ "caret", "derive_builder_fork_arti", "derive_more", "digest", - "thiserror 2.0.14", + "thiserror 2.0.20", "tor-bytes", "tor-checkable", + "tor-error", "tor-llcrypto", + "web-time-compat", ] [[package]] name = "tor-chanmgr" -version = "0.33.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74322fa01b09b3839903da1e7f443b2cf8aecd31f0cfd5395253ddad473b4ef3" +checksum = "431ba53bf020ea8681ef0c2fbf0f0f854997fca9afe1b2c081f41f14ec00cf5d" dependencies = [ "async-trait", + "base64ct", "caret", - "derive_builder_fork_arti", + "cfg-if", + "derive-deftly 1.12.1", "derive_more", "educe", "futures", + "httparse", "oneshot-fused-workaround", + "percent-encoding", "postage", - "rand 0.9.2", + "rand 0.10.2", "safelog", "serde", - "thiserror 2.0.14", + "serde_with", + "thiserror 2.0.20", "tor-async-utils", "tor-basic-utils", "tor-cell", "tor-config", "tor-error", + "tor-keymgr", "tor-linkspec", "tor-llcrypto", "tor-memquota", @@ -4160,31 +4283,35 @@ dependencies = [ "tor-socksproto", "tor-units", "tracing", + "url", "void", + "web-time-compat", ] [[package]] name = "tor-checkable" -version = "0.33.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5703d370d4ee4b5c318ac8b944a3b183e2865f6f0104475d36b9e1b8c89f0f38" +checksum = "e58a50a2dbf5d21758c9e2dbaaf085403670b8bf52475c815dd1185698b7af9a" dependencies = [ "humantime", + "itertools", "signature", - "thiserror 2.0.14", + "thiserror 2.0.20", "tor-llcrypto", + "web-time-compat", ] [[package]] name = "tor-circmgr" -version = "0.33.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f8f6d0e84dcf5fa3761e55fe8bb75725e699ee03e3b37bbd06e6421e9961fbd" +checksum = "8b6b55747c690e0c931c5a081640ffdca6d1003c01b57b8102afcbdbaf3ec3fd" dependencies = [ "amplify", "async-trait", - "bounded-vec-deque", "cfg-if", + "derive-deftly 1.12.1", "derive_builder_fork_arti", "derive_more", "downcast-rs", @@ -4192,20 +4319,21 @@ dependencies = [ "educe", "futures", "humantime-serde", - "itertools 0.14.0", + "itertools", "once_cell", "oneshot-fused-workaround", "pin-project", - "rand 0.9.2", + "rand 0.10.2", "retry-error", "safelog", "serde", - "static_assertions", - "thiserror 2.0.14", + "thiserror 2.0.20", "tor-async-utils", "tor-basic-utils", + "tor-cell", "tor-chanmgr", "tor-config", + "tor-dircommon", "tor-error", "tor-guardmgr", "tor-linkspec", @@ -4221,24 +4349,27 @@ dependencies = [ "tracing", "void", "weak-table", + "web-time-compat", ] [[package]] name = "tor-config" -version = "0.33.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3852d0e3a6ca41ab9fed79fa4cb89347bdb24bcd5f6665f506a10b993ac24e8b" +checksum = "b849c69c695b9534cf945c0422b85eaf5760bb6141a73c47b72fd6f95ea4d719" dependencies = [ "amplify", "cfg-if", - "derive-deftly 1.2.0", + "derive-deftly 1.12.1", "derive_builder_fork_arti", "educe", "either", + "extend", "figment", "fs-mistrust", "futures", - "itertools 0.14.0", + "humantime-serde", + "itertools", "notify", "paste", "postage", @@ -4246,9 +4377,9 @@ dependencies = [ "serde", "serde-value", "serde_ignored", - "strum 0.27.2", - "thiserror 2.0.14", - "toml 0.8.14", + "strum 0.28.0", + "thiserror 2.0.20", + "toml 1.1.6+spec-1.1.0", "tor-basic-utils", "tor-error", "tor-rtcompat", @@ -4258,35 +4389,53 @@ dependencies = [ [[package]] name = "tor-config-path" -version = "0.33.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf19ba283027eb8d8c441eec743d2c73c971c4b22c9830aae87163e8fcdc334e" +checksum = "3b7f10bfd0d0fb159a8abc13d2593625bfd9f9a394715605262236d01bc5500f" dependencies = [ "directories", "serde", "shellexpand", - "thiserror 2.0.14", + "thiserror 2.0.20", "tor-error", "tor-general-addr", ] +[[package]] +name = "tor-config-shared" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00e73d2fdf5506c79d878bfd1863585a22c3696e1bac64f5f00ce7e7bdf8ab12" +dependencies = [ + "amplify", + "derive-deftly 1.12.1", + "serde", + "tor-config", + "tor-config-path", +] + [[package]] name = "tor-consdiff" -version = "0.33.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5164757c908a50737ebd483a0e29380e53db0b14103f7996751e28964c6d98a" +checksum = "a6ed097865bfb6d453d9258620cbc952cf7481e9dcfc8901071ee1b8506cea86" dependencies = [ + "derive_more", "digest", "hex", - "thiserror 2.0.14", + "imara-diff", + "static_assertions", + "thiserror 2.0.20", + "tor-error", "tor-llcrypto", + "tor-netdoc", ] [[package]] name = "tor-dirclient" -version = "0.33.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a411a66d9a5f41b2e85e8defb17a3641d5827f168e4b175a28db9b987125843f" +checksum = "f942f4ebaf2ac5275e48d02d77abe1616ce2e8854f032a1b0698ede0566dddfc" dependencies = [ "async-compression", "base64ct", @@ -4296,9 +4445,8 @@ dependencies = [ "http", "httparse", "httpdate", - "itertools 0.14.0", - "memchr", - "thiserror 2.0.14", + "itertools", + "thiserror 2.0.20", "tor-circmgr", "tor-error", "tor-hscrypto", @@ -4308,13 +4456,35 @@ dependencies = [ "tor-proto", "tor-rtcompat", "tracing", + "web-time-compat", +] + +[[package]] +name = "tor-dircommon" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c16065e5166e529492d1be856e1e40316e2532053027c6b7ab5a4a53c14d8c51" +dependencies = [ + "base64ct", + "derive-deftly 1.12.1", + "getset", + "humantime", + "humantime-serde", + "serde", + "tor-basic-utils", + "tor-checkable", + "tor-config", + "tor-linkspec", + "tor-llcrypto", + "tor-netdoc", + "tracing", ] [[package]] name = "tor-dirmgr" -version = "0.33.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c74b75377747b13d338c8ab29c5de44f4485c04c492a15edebed61110e24299" +checksum = "34ea5e77b43bf1ef5fa213a89cb67080f218df6b418c82527e6bb9862a39aef5" dependencies = [ "async-trait", "base64ct", @@ -4324,17 +4494,17 @@ dependencies = [ "educe", "event-listener", "fs-mistrust", - "fslock", + "fslock-guard", "futures", "hex", "humantime", "humantime-serde", - "itertools 0.14.0", + "itertools", "memmap2", "oneshot-fused-workaround", "paste", "postage", - "rand 0.9.2", + "rand 0.10.2", "rusqlite", "safelog", "scopeguard", @@ -4342,8 +4512,8 @@ dependencies = [ "serde_json", "signature", "static_assertions", - "strum 0.27.2", - "thiserror 2.0.14", + "strum 0.28.0", + "thiserror 2.0.20", "time", "tor-async-utils", "tor-basic-utils", @@ -4352,6 +4522,7 @@ dependencies = [ "tor-config", "tor-consdiff", "tor-dirclient", + "tor-dircommon", "tor-error", "tor-guardmgr", "tor-llcrypto", @@ -4362,45 +4533,48 @@ dependencies = [ "tor-protover", "tor-rtcompat", "tracing", + "void", + "web-time-compat", ] [[package]] name = "tor-error" -version = "0.33.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5566edb4beb34d7be7bc4fc2f653a5119a3ccade26a9237912e53d4f4029e83" +checksum = "ca01de513501849b2298e4f16f8760832ef43344b10fe17600daed9a35128b24" dependencies = [ "derive_more", "futures", "paste", "retry-error", "static_assertions", - "strum 0.27.2", - "thiserror 2.0.14", + "strum 0.28.0", + "thiserror 2.0.20", "tracing", "void", + "web-time-compat", ] [[package]] name = "tor-general-addr" -version = "0.33.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc9d4a8c4593a0c590c66bae348590fb49dbf04da264cacc0631d71a92e5d2ac" +checksum = "72965a7044f573bb121467404d423d296592dd3cef87ad7aca4c96cc2b65e75a" dependencies = [ "derive_more", - "thiserror 2.0.14", + "thiserror 2.0.20", "void", ] [[package]] name = "tor-guardmgr" -version = "0.33.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aef0ad8a7ba30210b9c5664e10c423cc19f83d55fafdf1c84097be3a01d0dacd" +checksum = "87694603d6dcee5e0b050f0e7ac8be86348eb78ecb90c36ee8b70bd5822f3eff" dependencies = [ "amplify", "base64ct", - "derive-deftly 1.2.0", + "derive-deftly 1.12.1", "derive_builder_fork_arti", "derive_more", "dyn-clone", @@ -4408,19 +4582,20 @@ dependencies = [ "futures", "humantime", "humantime-serde", - "itertools 0.14.0", + "itertools", "num_enum", "oneshot-fused-workaround", "pin-project", "postage", - "rand 0.9.2", + "rand 0.10.2", "safelog", "serde", - "strum 0.27.2", - "thiserror 2.0.14", + "strum 0.28.0", + "thiserror 2.0.20", "tor-async-utils", "tor-basic-utils", "tor-config", + "tor-dircommon", "tor-error", "tor-linkspec", "tor-llcrypto", @@ -4433,29 +4608,31 @@ dependencies = [ "tor-rtcompat", "tor-units", "tracing", + "web-time-compat", ] [[package]] name = "tor-hsclient" -version = "0.33.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0ed4b8790cf8849d10c7ff13db3ec570c2a4aec68331dfe6e1f05d2987d40d" +checksum = "42addb7f2a7a86e813df223ed4485f2fb61b5736aff040cb5e8f551f83dfade0" dependencies = [ "async-trait", - "derive-deftly 1.2.0", + "cfg-if", + "derive-deftly 1.12.1", "derive_more", "educe", "either", "futures", - "itertools 0.14.0", + "itertools", "oneshot-fused-workaround", "postage", - "rand 0.9.2", + "rand 0.10.2", "retry-error", "safelog", "slotmap-careful", - "strum 0.27.2", - "thiserror 2.0.14", + "strum 0.28.0", + "thiserror 2.0.20", "tor-async-utils", "tor-basic-utils", "tor-bytes", @@ -4477,28 +4654,29 @@ dependencies = [ "tor-protover", "tor-rtcompat", "tracing", + "web-time-compat", ] [[package]] name = "tor-hscrypto" -version = "0.33.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "163e60a7c8069ea8d8a3031be8620047b21cbed7d535fd9f1662daae7803ccd6" +checksum = "6613c5336b71ebd34b8d90c233a7b6b06a0b81899d0545c656b0cac6f24e54a2" dependencies = [ "data-encoding", - "derive-deftly 1.2.0", + "derive-deftly 1.12.1", "derive_more", "digest", "hex", "humantime", - "itertools 0.14.0", + "itertools", "paste", - "rand 0.9.2", + "rand 0.10.2", "safelog", "serde", "signature", "subtle", - "thiserror 2.0.14", + "thiserror 2.0.20", "tor-basic-utils", "tor-bytes", "tor-error", @@ -4507,22 +4685,24 @@ dependencies = [ "tor-memquota", "tor-units", "void", + "web-time-compat", ] [[package]] name = "tor-key-forge" -version = "0.33.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b1ddfa5126b2619a9f27b5c5d1a360acf41f15d99ced37af1f30fc449c65889" +checksum = "5534581a7f347ffcade7ea32ecccdbc035d36468bc8c03b0dce83363b6d41801" dependencies = [ - "derive-deftly 1.2.0", + "derive-deftly 1.12.1", "derive_more", "downcast-rs", "paste", - "rand 0.9.2", + "rand 0.10.2", + "rsa", "signature", - "ssh-key", - "thiserror 2.0.14", + "ssh-key-fork-arti", + "thiserror 2.0.20", "tor-bytes", "tor-cert", "tor-checkable", @@ -4532,14 +4712,15 @@ dependencies = [ [[package]] name = "tor-keymgr" -version = "0.33.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26e8e0222d2ac27bd8bf4848cd46371fdc72d820940eb38f3d8da616176b3d39" +checksum = "e269210a2ba305bf6524990a3c37aab4529b92a01f3d15ef6af23c067c8eb2c4" dependencies = [ "amplify", "arrayvec", "cfg-if", - "derive-deftly 1.2.0", + "data-encoding", + "derive-deftly 1.12.1", "derive_builder_fork_arti", "derive_more", "downcast-rs", @@ -4548,13 +4729,13 @@ dependencies = [ "glob-match", "humantime", "inventory", - "itertools 0.14.0", - "rand 0.9.2", + "itertools", + "rand 0.10.2", "safelog", "serde", "signature", - "ssh-key", - "thiserror 2.0.14", + "ssh-key-fork-arti", + "thiserror 2.0.20", "tor-basic-utils", "tor-bytes", "tor-config", @@ -4567,28 +4748,29 @@ dependencies = [ "tracing", "visibility", "walkdir", + "web-time-compat", "zeroize", ] [[package]] name = "tor-linkspec" -version = "0.33.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e6eab944bf3096b1964cbb0f4a3605c1376a35bdfa9d44512464474ffb8e53c" +checksum = "d287978bd5e1ffbe7ca2eb8daadb4e06e42d084bdad26bea4d4bbe1ff877e10f" dependencies = [ "base64ct", "by_address", "caret", - "derive-deftly 1.2.0", + "derive-deftly 1.12.1", "derive_builder_fork_arti", "derive_more", "hex", - "itertools 0.14.0", + "itertools", "safelog", "serde", "serde_with", - "strum 0.27.2", - "thiserror 2.0.14", + "strum 0.28.0", + "thiserror 2.0.20", "tor-basic-utils", "tor-bytes", "tor-config", @@ -4599,28 +4781,31 @@ dependencies = [ [[package]] name = "tor-llcrypto" -version = "0.33.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b92fa9a99a066f06cd266287f6f89270c010693cce3c4c2fa38c27abfcda5fb" +checksum = "95f921a609ff73736e8e21d11dbee6617cd7568841790f2e3327295627a51b75" dependencies = [ "aes", "base64ct", "ctr", "curve25519-dalek", "der-parser", - "derive-deftly 1.2.0", + "derive-deftly 1.12.1", "derive_more", "digest", "ed25519-dalek", "educe", + "getrandom 0.2.15", "getrandom 0.3.3", + "getrandom 0.4.3", "hex", - "rand 0.9.2", - "rand_chacha 0.9.0", + "rand 0.10.2", + "rand_chacha 0.10.0", + "rand_core 0.10.1", "rand_core 0.6.4", - "rand_core 0.9.3", "rand_jitter", "rdrand", + "reseeding_rng", "rsa", "safelog", "serde", @@ -4629,8 +4814,9 @@ dependencies = [ "sha3", "signature", "subtle", - "thiserror 2.0.14", - "tor-memquota", + "thiserror 2.0.20", + "tor-error", + "tor-memquota-cost", "visibility", "x25519-dalek", "zeroize", @@ -4638,54 +4824,69 @@ dependencies = [ [[package]] name = "tor-log-ratelim" -version = "0.33.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc97bd804ac7aeaf771dc4507c65f8c5fac1d4f8e469551aaa3bf240fce1171" +checksum = "2096a87f0691f0781bf564249c524d62b5338c3954ba1c6ef10ec90df1d97733" dependencies = [ "futures", "humantime", - "thiserror 2.0.14", + "thiserror 2.0.20", "tor-error", "tor-rtcompat", "tracing", "weak-table", + "web-time-compat", ] [[package]] name = "tor-memquota" -version = "0.33.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f015b156dc186601f46b3f89ebd6ace49ef3b142c9a5004559e5d75a6477cc1f" +checksum = "96853fc516264fe2ebefbe40d276b6a3b84e15fc08b74fd7a85e2c5df9f11c37" dependencies = [ "cfg-if", - "derive-deftly 1.2.0", + "derive-deftly 1.12.1", "derive_more", "dyn-clone", "educe", + "extend", "futures", - "itertools 0.14.0", + "itertools", "paste", "pin-project", "serde", "slotmap-careful", "static_assertions", "sysinfo", - "thiserror 2.0.14", + "thiserror 2.0.20", "tor-async-utils", "tor-basic-utils", "tor-config", "tor-error", "tor-log-ratelim", + "tor-memquota-cost", "tor-rtcompat", "tracing", "void", ] +[[package]] +name = "tor-memquota-cost" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5e5712bfe2995a60eddcc32d07e2e38a42120318a8f5411d621bf0a8fa77219" +dependencies = [ + "derive-deftly 1.12.1", + "itertools", + "paste", + "void", +] + [[package]] name = "tor-netdir" -version = "0.33.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7874878d0c579e7b1dea5947581a5f2d4ba350af2a20b1946350ef9ace16ebe1" +checksum = "cd22ffd4d6aa9389ec8e20e9b0d579d61101682c5ec8379115a388e473e83297" dependencies = [ "async-trait", "bitflags 2.9.1", @@ -4694,13 +4895,12 @@ dependencies = [ "futures", "hex", "humantime", - "itertools 0.14.0", + "itertools", "num_enum", - "rand 0.9.2", + "rand 0.10.2", "serde", - "static_assertions", - "strum 0.27.2", - "thiserror 2.0.14", + "strum 0.28.0", + "thiserror 2.0.20", "time", "tor-basic-utils", "tor-error", @@ -4712,34 +4912,44 @@ dependencies = [ "tor-units", "tracing", "typed-index-collections", + "web-time-compat", ] [[package]] name = "tor-netdoc" -version = "0.33.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea853a14cb2011b9f4c45641323e19fbad13010d638dd3d84502e0decf9db0b" +checksum = "78dc8a397a86fd00b17607941b204281dd8a77a6018ca8eedc257bdccb4f3aa7" dependencies = [ "amplify", "base64ct", - "bitflags 2.9.1", - "cipher", + "cipher 0.5.2", + "derive-deftly 1.12.1", "derive_builder_fork_arti", "derive_more", "digest", "educe", + "enumset", + "extend", "hex", + "hostname-validator", "humantime", - "itertools 0.14.0", + "ipnet", + "iprange", + "itertools", "memchr", + "paste", "phf", - "rand 0.9.2", + "rand 0.10.2", + "rangemap", + "saturating-time", "serde", "serde_with", "signature", "smallvec", + "strum 0.28.0", "subtle", - "thiserror 2.0.14", + "thiserror 2.0.20", "time", "tinystr", "tor-basic-utils", @@ -4754,74 +4964,80 @@ dependencies = [ "tor-protover", "tor-units", "void", - "weak-table", + "web-time-compat", "zeroize", ] [[package]] name = "tor-persist" -version = "0.33.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e899d9dd8104fae50c33f15e00a1515bee15683ff0017cfc003315fcd6a195d6" +checksum = "e7a3bc60cbe74bf41cd63e2506a9a16bd52047668771578b12ff2453ec4c7522" dependencies = [ - "derive-deftly 1.2.0", + "derive-deftly 1.12.1", "derive_more", + "extend", "filetime", "fs-mistrust", - "fslock", + "fslock-guard", "futures", - "itertools 0.14.0", + "itertools", "oneshot-fused-workaround", "paste", "sanitize-filename", "serde", "serde_json", - "thiserror 2.0.14", + "thiserror 2.0.20", "time", - "tor-async-utils", "tor-basic-utils", "tor-error", "tracing", "void", + "web-time-compat", ] [[package]] name = "tor-proto" -version = "0.33.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08f8d03310fadbc44e9544777a0bb5a7364564798d2236f00db3c337fb25987" +checksum = "fceb9d4017849440584e541054b6a26517655a3ae48942b6e67476c5376262c3" dependencies = [ + "aes", "amplify", + "async-trait", "asynchronous-codec", "bitvec", "bytes", "caret", "cfg-if", - "cipher", + "cipher 0.5.2", "coarsetime", - "criterion-cycles-per-byte", - "derive-deftly 1.2.0", + "ctr", + "derive-deftly 1.12.1", "derive_builder_fork_arti", "derive_more", "digest", "educe", + "enum_dispatch", "futures", "futures-util", "hkdf", "hmac", - "itertools 0.14.0", + "itertools", + "nonany", "oneshot-fused-workaround", "pin-project", + "polyval", "postage", - "rand 0.9.2", - "rand_core 0.9.3", + "rand 0.10.2", + "rand_core 0.10.1", "safelog", "slotmap-careful", "smallvec", "static_assertions", + "strum 0.28.0", "subtle", - "sync_wrapper", - "thiserror 2.0.14", + "thiserror 2.0.20", "tokio", "tokio-util", "tor-async-utils", @@ -4845,37 +5061,41 @@ dependencies = [ "typenum", "visibility", "void", + "web-time-compat", "zeroize", ] [[package]] name = "tor-protover" -version = "0.33.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fcc72e258205ca0511bdc84801748c59dd5d7accfd080d909afd11f6b8be182" +checksum = "3e69fbc196208a9d6232d2c38e32806f3bb064792fd84ac6ee12b5ba1e482240" dependencies = [ "caret", + "derive-deftly 1.12.1", "paste", "serde_with", - "thiserror 2.0.14", + "thiserror 2.0.20", + "tor-basic-utils", "tor-bytes", ] [[package]] name = "tor-ptmgr" -version = "0.33.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a5f226fd8436d1561ac111d97a7162ba4eed50c5e0791818508b5d1bd511e69" +checksum = "0068103033ae2dcedf88691c4758b1e483dd31d325e4891f5e0d3bb41f24ecbc" dependencies = [ "async-trait", "cfg-if", + "derive-deftly 1.12.1", "derive_builder_fork_arti", "fs-mistrust", "futures", - "itertools 0.14.0", + "itertools", "oneshot-fused-workaround", "serde", - "thiserror 2.0.14", + "thiserror 2.0.20", "tor-async-utils", "tor-basic-utils", "tor-chanmgr", @@ -4883,18 +5103,20 @@ dependencies = [ "tor-config-path", "tor-error", "tor-linkspec", + "tor-proto", "tor-rtcompat", "tor-socksproto", "tracing", + "web-time-compat", ] [[package]] name = "tor-relay-selection" -version = "0.33.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a19816299f125c71bd4ba59be8ea425256aa4494b6a3d39bdb9ccbc385722a70" +checksum = "e420365ef1e89ee93b34b5ab71a6aa840cadcbe04e2ac4b72043846fb2f9e002" dependencies = [ - "rand 0.9.2", + "rand 0.10.2", "serde", "tor-basic-utils", "tor-linkspec", @@ -4904,15 +5126,18 @@ dependencies = [ [[package]] name = "tor-rtcompat" -version = "0.33.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9545fa3b1420df5b165e6bad6537b7202c533311431cabc84ff1908fbca3908" +checksum = "99036f7a4fa9735e0beac7f241592da1a072c94a018e4fa702001700f5922291" dependencies = [ + "amplify", "async-native-tls", "async-trait", "async_executors", "asynchronous-codec", + "cfg-if", "coarsetime", + "derive_builder_fork_arti", "derive_more", "dyn-clone", "educe", @@ -4922,71 +5147,74 @@ dependencies = [ "native-tls", "paste", "pin-project", - "thiserror 2.0.14", + "socket2", + "thiserror 2.0.20", "tokio", "tokio-util", "tor-error", "tor-general-addr", "tracing", "void", + "web-time-compat", + "zeroize", ] [[package]] name = "tor-rtmock" -version = "0.33.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a6ca08427dff9e7a22aa08d8c680c56231ce9b6afa35125e8a9a78f6b8e2890" +checksum = "9140515718465f1ede62fa33e96cc929acf075a3893cb349d1c606976c846a21" dependencies = [ "amplify", "assert_matches", "async-trait", - "derive-deftly 1.2.0", + "derive-deftly 1.12.1", "derive_more", "educe", "futures", "humantime", - "itertools 0.14.0", + "itertools", "oneshot-fused-workaround", "pin-project", "priority-queue", "slotmap-careful", - "strum 0.27.2", - "thiserror 2.0.14", + "strum 0.28.0", + "thiserror 2.0.20", "tor-error", "tor-general-addr", "tor-rtcompat", "tracing", "tracing-test", "void", + "web-time-compat", ] [[package]] name = "tor-socksproto" -version = "0.33.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5b33285d16e5695b6aba7c9656387ab0c662781e2911376887b8b61d1a3d2b2" +checksum = "62d1f5c19f2339b2634556ed35865844276ab889708b482dc351fa18184c5b24" dependencies = [ - "amplify", "caret", - "derive-deftly 1.2.0", + "derive-deftly 1.12.1", "educe", "safelog", "subtle", - "thiserror 2.0.14", + "thiserror 2.0.20", "tor-bytes", "tor-error", ] [[package]] name = "tor-units" -version = "0.33.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40e61ae922f0f0209338d63afd4b1b4ba780313b427a54dda212ca3853845578" +checksum = "aa976f4c84404c9aaf7e8690fe30904a69084d0069ff7b2dfc0c58919d65eebd" dependencies = [ - "derive-deftly 1.2.0", + "derive-deftly 1.12.1", "derive_more", "serde", - "thiserror 2.0.14", + "thiserror 2.0.20", "tor-memquota", ] @@ -5000,20 +5228,21 @@ dependencies = [ "cbindgen", "glob", "lazy_static", + "libc", "log", "lzma-sys", - "rlimit", + "rlimit 0.10.1", + "saturating-time", "security-framework", "tokio", - "tor-config", "tor-rtcompat", ] [[package]] name = "tracing" -version = "0.1.40" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -5034,9 +5263,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.27" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", @@ -5045,9 +5274,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", @@ -5066,14 +5295,14 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.18" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", "once_cell", - "regex", + "regex-automata", "sharded-slab", "smallvec", "thread_local", @@ -5146,6 +5375,16 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "universal-hash" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4987bdc12753382e0bec4a65c50738ffaabc998b9cdd1f952fb5f39b0048a96" +dependencies = [ + "crypto-common 0.2.2", + "ctutils", +] + [[package]] name = "untrusted" version = "0.9.0" @@ -5160,13 +5399,14 @@ checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" [[package]] name = "url" -version = "2.5.4" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] @@ -5252,34 +5492,22 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.92" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" dependencies = [ "cfg-if", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" -dependencies = [ - "bumpalo", - "log", "once_cell", - "proc-macro2", - "quote", - "syn 2.0.105", + "rustversion", + "wasm-bindgen-macro", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.92" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5287,39 +5515,55 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.92" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" dependencies = [ + "bumpalo", "proc-macro2", "quote", - "syn 2.0.105", - "wasm-bindgen-backend", + "syn 3.0.5", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.92" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" +dependencies = [ + "unicode-ident", +] [[package]] name = "weak-table" -version = "0.3.2" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "323f4da9523e9a669e1eaf9c6e763892769b1d38c623913647bfdc1532fe4549" +checksum = "5eba2ec791141b0bf4db542f6b038a8f9310cee33e076cb181245780b09a24ff" +dependencies = [ + "cfg-if", + "hashbrown 0.16.1", +] [[package]] -name = "web-sys" -version = "0.3.69" +name = "web-time" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" dependencies = [ "js-sys", "wasm-bindgen", ] +[[package]] +name = "web-time-compat" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c38a07cb4a2705c91feaedc9013a65481ec1b38d818b885d642b9f06ec6edd" +dependencies = [ + "web-time", +] + [[package]] name = "winapi" version = "0.3.9" @@ -5357,11 +5601,23 @@ version = "0.61.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ - "windows-collections", + "windows-collections 0.2.0", "windows-core 0.61.2", - "windows-future", - "windows-link", - "windows-numerics", + "windows-future 0.2.1", + "windows-link 0.1.3", + "windows-numerics 0.2.0", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections 0.3.2", + "windows-core 0.62.2", + "windows-future 0.3.2", + "windows-numerics 0.3.1", ] [[package]] @@ -5373,6 +5629,15 @@ dependencies = [ "windows-core 0.61.2", ] +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core 0.62.2", +] + [[package]] name = "windows-core" version = "0.52.0" @@ -5390,9 +5655,22 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement", "windows-interface", - "windows-link", - "windows-result", - "windows-strings", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", ] [[package]] @@ -5402,15 +5680,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ "windows-core 0.61.2", - "windows-link", - "windows-threading", + "windows-link 0.1.3", + "windows-threading 0.1.0", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", + "windows-threading 0.2.1", ] [[package]] name = "windows-implement" -version = "0.60.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", @@ -5419,9 +5708,9 @@ dependencies = [ [[package]] name = "windows-interface" -version = "0.59.1" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", @@ -5434,6 +5723,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-numerics" version = "0.2.0" @@ -5441,7 +5736,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ "windows-core 0.61.2", - "windows-link", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", ] [[package]] @@ -5450,7 +5755,16 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", ] [[package]] @@ -5459,7 +5773,16 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-link", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", ] [[package]] @@ -5498,6 +5821,15 @@ dependencies = [ "windows-targets 0.53.3", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -5535,7 +5867,7 @@ version = "0.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" dependencies = [ - "windows-link", + "windows-link 0.1.3", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", @@ -5552,7 +5884,16 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" dependencies = [ - "windows-link", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link 0.2.1", ] [[package]] @@ -5711,6 +6052,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" + [[package]] name = "wit-bindgen-rt" version = "0.39.0" @@ -5803,18 +6150,18 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" dependencies = [ "zeroize_derive", ] [[package]] name = "zeroize_derive" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" dependencies = [ "proc-macro2", "quote", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index b53342ba..9ac2bcbf 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -15,16 +15,18 @@ crate-type = ["cdylib", "staticlib"] [dependencies] lazy_static = "1.4" tokio = { version = "1", features = ["full"] } -arti-client = { version = "0.33.0", features = ["static", "onion-service-client"] } -arti = { version = "1.4.6", features = ["experimental-api", "static"] } -tor-rtcompat = { version = "0.33.0", features = ["static", "native-tls", "tokio"] } -tor-config = "=0.33.0" +arti-client = { version = "0.47.0", features = ["static", "onion-service-client"] } +arti = { version = "2.7.0", features = ["experimental-api", "static"] } +tor-rtcompat = { version = "0.47.0", features = ["static", "native-tls", "tokio"] } log = "0.4.20" lzma-sys = { version = "0.1.20", features = ["static"] } #android_log-sys = "0.3.1" rlimit = "0.10.1" anyhow = "1.0.79" +[target.'cfg(unix)'.dependencies] +libc = "0.2" + [target.'cfg(target_os = "ios")'.dependencies] # Specific version for iOS security-framework = "=2.10.0" @@ -32,3 +34,9 @@ security-framework = "=2.10.0" [build-dependencies] cbindgen = "= 0.24.3" glob = "0.3.1" + +[dev-dependencies] +saturating-time = "0.5.0" + +[patch.crates-io] +saturating-time = { path = "patches/saturating-time" } diff --git a/rust/build.rs b/rust/build.rs index 817bbcf3..761043a9 100644 --- a/rust/build.rs +++ b/rust/build.rs @@ -2,10 +2,10 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -use cbindgen::{Config, Language}; +use cbindgen::{Config, EnumConfig, Language}; +use glob::glob; use std::env; use std::path::PathBuf; -use glob::glob; fn main() { android_on_linux_check(); @@ -18,9 +18,26 @@ fn main() { .join(format!("{}.h", package_name)) .display() .to_string(); + // List files rather than src itself: Cargo copies the directory into the + // dep-info, where the Flutter hook runner takes it for a missing file and + // reruns the hook on every build. Adding a module edits a listed file. + for source in glob("src/**/*.rs").unwrap() { + println!("cargo:rerun-if-changed={}", source.unwrap().display()); + } + println!("cargo:rerun-if-changed=Cargo.toml"); + println!("cargo:rerun-if-env-changed=CARGO_TARGET_DIR"); + // android_on_linux_check reads these to find the NDK's clang runtime. + println!("cargo:rerun-if-env-changed=ANDROID_NDK_HOME"); + println!("cargo:rerun-if-env-changed=CC_x86_64_linux_android"); + println!("cargo:rustc-env=TOR_FFI_PLUGIN_HEADER={}", output_file); let config = Config { language: Language::C, + cpp_compat: true, + enumeration: EnumConfig { + prefix_with_name: true, + ..Default::default() + }, ..Default::default() }; @@ -37,7 +54,6 @@ fn target_dir() -> PathBuf { } } - fn android_on_linux_check() { let target = env::var("TARGET").unwrap(); if target == "x86_64-linux-android" { @@ -48,7 +64,7 @@ fn android_on_linux_check() { }; let ndk_home_result = env::var("ANDROID_NDK_HOME"); - let ndk_home = if let Some(value) = ndk_home_result.ok() { + let ndk_home = if let Ok(value) = ndk_home_result { value } else { println!("ANDROID_NDK_HOME not set. Trying CC_x86_64_linux_android"); diff --git a/rust/patches/saturating-time/Cargo.toml b/rust/patches/saturating-time/Cargo.toml new file mode 100644 index 00000000..f3ecc576 --- /dev/null +++ b/rust/patches/saturating-time/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "saturating-time" +version = "0.5.0" +authors = ["The Tor Project, Inc.", "Clara Engler "] +edition = "2024" +rust-version = "1.92" +description = "A trait for limits and saturations on types inside std::time" +repository = "https://gitlab.torproject.org/tpo/core/arti.git" +license = "MIT OR Apache-2.0" +keywords = ["time", "system-time", "instant", "saturating"] +categories = ["date-and-time", "os", "rust-patterns"] + +[dependencies] +web-time-compat = "0.3.0" + +[features] +full = ["web-time-compat/full"] + +[package.metadata.docs.rs] +all-features = true diff --git a/rust/patches/saturating-time/LICENSE-APACHE b/rust/patches/saturating-time/LICENSE-APACHE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/rust/patches/saturating-time/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 [yyyy] [name of copyright owner] + + 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/rust/patches/saturating-time/LICENSE-MIT b/rust/patches/saturating-time/LICENSE-MIT new file mode 100644 index 00000000..d101fd82 --- /dev/null +++ b/rust/patches/saturating-time/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Clara Engler + +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/rust/patches/saturating-time/README.md b/rust/patches/saturating-time/README.md new file mode 100644 index 00000000..72000cb4 --- /dev/null +++ b/rust/patches/saturating-time/README.md @@ -0,0 +1,106 @@ +# `saturating-time` + +A trait for limits and saturations on types inside [`std::time`]. + +* **Easy**: `saturating-time` only adds a single trait, [`SaturatingTime`] that + is implemented for various types from the standard library. +* **Future-Proof**: In the case that [`SaturatingTime::saturating_add()`] and + [`SaturatingTime::saturating_sub()`] become a part of the standard library, + developers would only have to remove the `use saturating_time::SaturatingTime` + line from their code.[^1] [^2]. +* **Portable**: The algorithm for determining the limits is portable across + operating systems: enjoy this crate from Windows, Darwin, and Linux, across + the BSD systems, up to exotic ones such as Hermit OS and Redox. +* **Secure**: This crate does not make any use of `unsafe` Rust code. + +## Overview + +`saturating-time` is a very minimal crate that only exposes a minimal trait: +[`SaturatingTime`]. + +The trait itself offers the following methods: +* [`SaturatingTime::max_value()`] – Returns the maximum value for this type. +* [`SaturatingTime::min_value()`] – Returns the minimum value for this type. +* [`SaturatingTime::saturating_add()`] – Saturating addition for this type. +* [`SaturatingTime::saturating_sub()`] – Saturating subtraction for this type. +* [`SaturatingTime::saturating_duration_since()`] - Saturating time deltas for this type. + +This trait is sealed, meaning applications may not implement it themselves. +However, this crate implements this trait for two structures: +* [`std::time::Instant`] +* [`std::time::SystemTime`] + +## Example + +Add `saturating-time` to your `Cargo.toml`. + +If you use Rust nightly, you may want to additionally configure the following +in your `.cargo/config.toml`: +```toml +[build] +rustflags = ["--cfg", "saturating_time_nightly"] +``` + +Now, you can use `saturating-time` in your code: +```rust +use std::time::{Duration, SystemTime}; +use saturating_time::SaturatingTime; + +// Get the maximum and minimum. +let max = SystemTime::max_value(); +let min = SystemTime::min_value(); + +assert_eq!(max.saturating_add(Duration::new(1, 0)), max); +assert_eq!(min.saturating_sub(Duration::new(1, 0)), min); +assert!(max.saturating_duration_since(SystemTime::UNIX_EPOCH) >= Duration::ZERO); +``` + +## Standardization Efforts + +The eventual goal is to get this functionality into the Rust standard library. + +### `SystemTime` + +#### `SystemTime::MIN` and `SystemTime::MAX` + +In December 2025, `SystemTime::MIN` and `SystemTime::MAX` got merged +into nightly.[^3] + +This feature is guarded behind `time_systemtime_limits` and a tracking +issue regarding the stabilization of it exists.[^4] + +#### `SystemTime::saturating_add()`, et, al. + +In January 2025, `SystemTime::saturating_add()`, `SystemTime::saturating_sub()`, +and `SystemTime::saturating_duration_since()` got merged into nightly.[^5] + +This feature is guarded behind `time_saturating_systemtime` and a tracking +issue regarding the stabilization of it exists.[^6] + +### `Instant` + +None yet. + +[^1]: This is an effort the maintainers are actively working upon. +[^2]: Assuming the name, signature, and behavior does not change. + Unfortunately, we likely have to change the signature for the parameters + because we currently use `self` whereas the standard library uses `&self`. + It should not be a big problem though, because both `Instant` and + `SystemTime` implement `Copy`. +[^3]: +[^4]: +[^5]: +[^6]: + +## History + +Up until (and including) version 0.4.0, `saturating-time` was maintained as an +independent project hosted on [Codeberg](https://codeberg.org/cve/saturating-time). + +Starting with version 0.5.0, it has been incorporated into the +[Arti](https://arti.torproject.org) and is therefore maintained by +*The Tor Project, Inc.* + +## License and Copyright + +This crate is licensed under `MIT OR Apache-2.0`. diff --git a/rust/patches/saturating-time/VENDORED.md b/rust/patches/saturating-time/VENDORED.md new file mode 100644 index 00000000..7ac1f26b --- /dev/null +++ b/rust/patches/saturating-time/VENDORED.md @@ -0,0 +1 @@ +Vendored from https://github.com/cypherstack/tor.git at rev e4c225da252ac866e9edc642ec218b0c4d2f6e52 (branch deps/saturating-time-0.5), crate saturating-time 0.5.0. diff --git a/rust/patches/saturating-time/src/internal.rs b/rust/patches/saturating-time/src/internal.rs new file mode 100644 index 00000000..aaf5762e --- /dev/null +++ b/rust/patches/saturating-time/src/internal.rs @@ -0,0 +1,292 @@ +//! Internal parts used for sealing. +//! +//! This module primarily consists of the internal [`SaturatingTime`] trait, an +//! unstable abstraction used internally to implement the main logic behind +//! this. +//! +//! Normal users should not be using this. + +use std::{ + cmp, + sync::LazyLock, + time::{Duration, Instant, SystemTime}, +}; + +/// The maximum value of [`SystemTime`] for this platform. +static MAX_SYSTEM_TIME: LazyLock = LazyLock::new(find_max); + +/// The minimum value of [`SystemTime`] for this platform. +static MIN_SYSTEM_TIME: LazyLock = LazyLock::new(find_min); + +/// The maximum value of [`Instant`] for this platform. +static MAX_INSTANT: LazyLock = LazyLock::new(find_max); + +/// The minimum value of [`Instant`] for this platform. +static MIN_INSTANT: LazyLock = LazyLock::new(find_min); + +/// An internal trait implementing the actual magic behind this. +pub trait SaturatingTime: Sized + Copy { + /// Anchor method to obtain an instance of this type. + fn anchor() -> Self; + + /// Returns the maximum value of this type. + fn max_value() -> Self; + + /// Returns the minimum value of this type. + fn min_value() -> Self; + + /// Performs a checked addition on this type. + fn checked_add(&self, duration: Duration) -> Option; + + /// Performs a checked subtraction on this type. + fn checked_sub(&self, duration: Duration) -> Option; + + /// Performs a checked time delta on this type. + fn checked_duration_since(&self, earlier: Self) -> Option; +} + +impl SaturatingTime for SystemTime { + fn anchor() -> Self { + Self::UNIX_EPOCH + } + + fn max_value() -> Self { + *MAX_SYSTEM_TIME + } + + fn min_value() -> Self { + *MIN_SYSTEM_TIME + } + + fn checked_add(&self, duration: Duration) -> Option { + Self::checked_add(self, duration) + } + + fn checked_sub(&self, duration: Duration) -> Option { + Self::checked_sub(self, duration) + } + + fn checked_duration_since(&self, earlier: Self) -> Option { + Self::duration_since(self, earlier).ok() + } +} + +impl SaturatingTime for Instant { + fn anchor() -> Self { + use web_time_compat::InstantExt; + Self::get() + } + + fn max_value() -> Self { + *MAX_INSTANT + } + + fn min_value() -> Self { + *MIN_INSTANT + } + + fn checked_add(&self, duration: Duration) -> Option { + Self::checked_add(self, duration) + } + + fn checked_sub(&self, duration: Duration) -> Option { + Self::checked_sub(self, duration) + } + + /// DO NOT USE! + /// + /// Instead, override the top-level provided implementation with the already + /// existing [`Instant::saturating_duration_since()`]. + fn checked_duration_since(&self, _earlier: Self) -> Option { + unreachable!() + } +} + +/// Finds the value for [`SaturatingTime::max_value()`]. +fn find_max() -> T { + find_limit(T::checked_add) +} + +/// Finds the value for [`SaturatingTime::min_value()`]. +fn find_min() -> T { + find_limit(T::checked_sub) +} + +/// Internal algorithm of [`find_max()`] and [`find_min()`]. +/// +/// It works by performing `f` with a very large [`Duration`] onto +/// [`SaturatingTime::anchor()`] until this call returns [`None`], in which case +/// this [`Duration`] gets halved. This process is repeated until `f` returns +/// [`None`] and the [`Duration`] has reached 1ns. +/// +/// # Algorithm +/// +/// 1. Set `step` to `INITIAL_STEP` and `res` to [`SaturatingTime::anchor()`]. +/// 2. Call `f(&res, step)`. +/// 1. If [`Some`] and different from `res`, set `res` to the returned +/// value and continue. +/// 2. If [`None`] or unchanged, and `step == 1ns`, return `res`. +/// 3. Else, set `step` to `MAX{1ns, step / 2}` and continue. +/// +/// Steps below the platform clock resolution can leave `res` unchanged. +/// Treat those steps as overflow so the search terminates. +fn find_limit(f: F) -> T +where + T: SaturatingTime + PartialEq, + F: Fn(&T, Duration) -> Option, +{ + const INITIAL_STEP: Duration = Duration::new(1_000_000_000_000_000_000, 0); + const ONE_NS: Duration = Duration::new(0, 1); + + // (1) Set step to INITIAL_STEP and res to T::anchor(). + let mut step = INITIAL_STEP; + let mut res = T::anchor(); + + loop { + // (2) Call f(). + let next = f(&res, step); + match next { + Some(st) if st != res => { + // (2.1) Advance only when the clock value changes. + res = st; + } + Some(_) | None => { + if step == ONE_NS { + // (2.2) Stop when no representable step remains. + return res; + } else { + // (2.3) Else, set step to MAX{1ns, step / 2}. + step = cmp::max(ONE_NS, step / 2); + } + } + } + } +} + +#[cfg(test)] +mod tests { + // @@ begin test lint list maintained by maint/add_warning @@ + #![allow(clippy::bool_assert_comparison)] + #![allow(clippy::clone_on_copy)] + #![allow(clippy::dbg_macro)] + #![allow(clippy::mixed_attributes_style)] + #![allow(clippy::print_stderr)] + #![allow(clippy::print_stdout)] + #![allow(clippy::single_char_pattern)] + #![allow(clippy::unwrap_used)] + #![allow(clippy::unchecked_time_subtraction)] + #![allow(clippy::useless_vec)] + #![allow(clippy::needless_pass_by_value)] + #![allow(clippy::string_slice)] // See arti#2571 + //! + + use std::{ + fmt::Debug, + ops::{Add, Sub}, + }; + + use super::*; + + #[test] + fn limit_search_handles_coarse_clock_resolution() { + let calls = std::cell::Cell::new(0); + let minimum = find_limit::(|time, step| { + calls.set(calls.get() + 1); + assert!(calls.get() < 1000, "limit search stopped making progress"); + // Model a 100ns SystemTime resolution on every platform. + let rounded = Duration::new(step.as_secs(), step.subsec_nanos() / 100 * 100); + time.checked_sub(rounded) + }); + + assert!(minimum < SystemTime::UNIX_EPOCH); + assert!(minimum.checked_sub(Duration::from_nanos(100)).is_none()); + } + + /// Checks whether the minimum and maximum values are correct. + fn min_max() + where + T: SaturatingTime + + PartialEq + + Debug + + Add + + Sub, + { + assert_eq!( + T::max_value().checked_add(Duration::ZERO), + Some(T::max_value()) + ); + assert_eq!(T::max_value().checked_add(Duration::new(0, 1)), None); + assert_eq!( + T::max_value().checked_sub(Duration::ZERO), + Some(T::max_value()) + ); + assert_eq!( + T::max_value().checked_sub(Duration::new(0, 1)), + Some(T::max_value() - Duration::new(0, 1)) + ); + + assert_eq!( + T::min_value().checked_sub(Duration::ZERO), + Some(T::min_value()) + ); + assert_eq!(T::min_value().checked_sub(Duration::new(0, 1)), None); + assert_eq!( + T::min_value().checked_add(Duration::ZERO), + Some(T::min_value()) + ); + assert_eq!( + T::min_value().checked_add(Duration::new(0, 1)), + Some(T::min_value() + Duration::new(0, 1)) + ); + } + + /// Verifies [`SystemTime::min_value()`] and [`SystemTime::max_value()`] are + /// correct. + #[test] + fn system_time_min_max() { + min_max::(); + } + + /// Verifies [`Instant::min_value()`] and [`Instant::max_value()`] are + /// correct. + #[test] + fn instant_min_max() { + min_max::(); + } + + /// Verifies [`SystemTime::min_value()`] and [`SystemTime::max_value()`] are + /// correct on Unix systems. + #[cfg(target_family = "unix")] + #[test] + fn system_time_min_max_unix() { + assert_eq!( + SystemTime::max_value(), + SystemTime::UNIX_EPOCH + Duration::new(i64::MAX as u64, 999_999_999) + ); + assert_eq!( + SystemTime::min_value(), + SystemTime::UNIX_EPOCH - Duration::new(i64::MAX as u64 + 1, 0) + ); + } + + /// Verifies that [`Instant::min_value()`] and [`Instant::max_value()`] are + /// correct on Unix systems. + #[test] + #[cfg(target_family = "unix")] + fn instant_min_max_unix() { + // Using format is not nice but I cannot see a better way for now. + assert_eq!( + format!("{:?}", Instant::max_value()), + format!( + "Instant {{ tv_sec: {}, tv_nsec: {} }}", + i64::MAX, + 999_999_999 + ) + ); + + assert_eq!( + format!("{:?}", Instant::min_value()), + format!("Instant {{ tv_sec: {}, tv_nsec: {} }}", i64::MIN, 0) + ); + } +} diff --git a/rust/patches/saturating-time/src/lib.rs b/rust/patches/saturating-time/src/lib.rs new file mode 100644 index 00000000..03b3e239 --- /dev/null +++ b/rust/patches/saturating-time/src/lib.rs @@ -0,0 +1,351 @@ +#![doc = include_str!("../README.md")] +// @@ begin lint list maintained by maint/add_warning @@ +#![allow(renamed_and_removed_lints)] // @@REMOVE_WHEN(ci_arti_stable) +#![allow(unknown_lints)] // @@REMOVE_WHEN(ci_arti_nightly) +#![warn(missing_docs)] +#![warn(noop_method_call)] +#![warn(unreachable_pub)] +#![warn(clippy::all)] +#![deny(clippy::await_holding_lock)] +#![deny(clippy::cargo_common_metadata)] +#![deny(clippy::cast_lossless)] +#![deny(clippy::checked_conversions)] +#![allow(clippy::cognitive_complexity)] // See arti#2556 +#![deny(clippy::debug_assert_with_mut_call)] +#![deny(clippy::exhaustive_enums)] +#![deny(clippy::exhaustive_structs)] +#![deny(clippy::expl_impl_clone_on_copy)] +#![deny(clippy::fallible_impl_from)] +#![deny(clippy::implicit_clone)] +#![deny(clippy::large_stack_arrays)] +#![warn(clippy::manual_ok_or)] +#![deny(clippy::missing_docs_in_private_items)] +#![warn(clippy::needless_borrow)] +#![warn(clippy::needless_pass_by_value)] +#![warn(clippy::option_option)] +#![deny(clippy::print_stderr)] +#![deny(clippy::print_stdout)] +#![warn(clippy::rc_buffer)] +#![deny(clippy::ref_option_ref)] +#![warn(clippy::semicolon_if_nothing_returned)] +#![warn(clippy::trait_duplication_in_bounds)] +#![deny(clippy::unchecked_time_subtraction)] +#![deny(clippy::unnecessary_wraps)] +#![warn(clippy::unseparated_literal_suffix)] +#![deny(clippy::unwrap_used)] +#![deny(clippy::mod_module_files)] +#![allow(clippy::let_unit_value)] // This can reasonably be done for explicitness +#![allow(clippy::uninlined_format_args)] +#![allow(clippy::significant_drop_in_scrutinee)] // arti/-/merge_requests/588/#note_2812945 +#![allow(clippy::result_large_err)] // temporary workaround for arti#587 +#![allow(clippy::needless_raw_string_hashes)] // complained-about code is fine, often best +#![allow(clippy::needless_lifetimes)] // See arti#1765 +#![allow(mismatched_lifetime_syntaxes)] // temporary workaround for arti#2060 +#![allow(clippy::collapsible_if)] // See arti#2342 +#![deny(clippy::unused_async)] +#![deny(clippy::string_slice)] // See arti#2571 +//! +#![allow(unexpected_cfgs)] +#![forbid(unsafe_code)] +#![cfg_attr( + saturating_time_nightly, + feature(time_systemtime_limits, time_saturating_systemtime) +)] + +use std::time::{Duration, Instant, SystemTime}; + +mod internal; + +/// The core trait of this crait, [`SaturatingTime`]. +/// +/// This trait provides methods for performing saturating arithmetic on those +/// types in [`std::time`] that not already come with such a functionality, +/// such as [`SystemTime`] or [`Instant`]. +/// +/// The trait itself is not implementable from the outside, because it is sealed +/// by an internal trait. +/// +/// See the methods or the top-level documentation for concrete code examples. +pub trait SaturatingTime: internal::SaturatingTime { + /// Returns the maximum value for this type on the current platform. + /// + /// This limit is highly platform specific. It differs heavily between + /// Unix, Windows, and other operating systems. + /// + /// The limit itself is calculated dynamically during runtime with a correct + /// algorithm. Afterwards, it gets stored in a lazy static value, meaning + /// that only the first call to it will be slightly more expensive, whereas + /// all latter calls will result in an immediate return of the value. + /// + /// # Examples + /// + /// ``` + /// use std::time::{Duration, SystemTime}; + /// use saturating_time::SaturatingTime; + /// + /// let max = SystemTime::max_value(); + /// + /// // Adding zero to the maximum value will change nothing. + /// assert!(max.checked_add(Duration::ZERO).is_some()); + /// + /// // Adding 1ns to the maximum value will fail. + /// assert!(max.checked_add(Duration::new(0, 1)).is_none()); + /// + /// // Subtracting 1ns from the maximum value will work of course. + /// assert!(max.checked_sub(Duration::new(0, 1)).is_some()); + /// ``` + fn max_value() -> Self { + internal::SaturatingTime::max_value() + } + + /// Returns the minimum value for this type on the current platform. + /// + /// This limit is highly platform specific. It differs heavily between + /// Unix, Windows, and other operating systems. + /// + /// The limit itself is calculated dynamically during runtime with a correct + /// algorithm. Afterwards, it gets stored in a lazy static value, meaning + /// that only the first call to it will be slightly more expensive, whereas + /// all latter calls will result in an immediate return of the value. + /// + /// # Examples + /// + /// ``` + /// use std::time::{Duration, SystemTime}; + /// use saturating_time::SaturatingTime; + /// + /// let min = SystemTime::min_value(); + /// + /// // Subtracting a zero from the minimum value will change nothing. + /// assert!(min.checked_sub(Duration::ZERO).is_some()); + /// + /// // Subtracting 1ns from the minimum value will fail. + /// assert!(min.checked_sub(Duration::new(0, 1)).is_none()); + /// + /// // Adding 1ns to the minimum value will work of course. + /// assert!(min.checked_add(Duration::new(0, 1)).is_some()); + /// ``` + fn min_value() -> Self { + internal::SaturatingTime::min_value() + } + + /// Performs a saturating addition of a [`Duration`]. + /// + /// The resulting value will saturate to [`SaturatingTime::max_value()`] in + /// the case the addition would have caused an overflow of value. + /// + /// # Examples + /// + /// ``` + /// use std::time::{Duration, SystemTime}; + /// use saturating_time::SaturatingTime; + /// + /// let max = SystemTime::max_value(); + /// + /// // Adding zero will change nothing. + /// assert_eq!(max.saturating_add(Duration::ZERO), max); + /// + /// // Adding 1ns would overflow so we saturate to the maximum. + /// assert_eq!(max.saturating_add(Duration::new(0, 1)), max); + /// ``` + fn saturating_add(self, duration: Duration) -> Self { + self.checked_add(duration) + .unwrap_or(SaturatingTime::max_value()) + } + + /// Performs a saturating subtraction of a [`Duration`]. + /// + /// The resulting value will saturate to [`SaturatingTime::min_value()`] in + /// the case the subtraction would have caused an overflow of value. + /// + /// # Examples + /// + /// ``` + /// use std::time::{Duration, SystemTime}; + /// use saturating_time::SaturatingTime; + /// + /// let min = SystemTime::min_value(); + /// + /// // Subtracting zero will change nothing. + /// assert_eq!(min.saturating_sub(Duration::ZERO), min); + /// + /// // Subtracting 1ns would overflow so we saturate to the minimum. + /// assert_eq!(min.saturating_sub(Duration::new(0, 1)), min); + /// ``` + fn saturating_sub(self, duration: Duration) -> Self { + self.checked_sub(duration) + .unwrap_or(SaturatingTime::min_value()) + } + + /// Performs a saturating time difference calculation between two points. + /// + /// The resulting value will saturate to [`Duration::ZERO`] in the case that + /// the `earlier` point in time is actually not earlier, thereby resulting + /// in a negative difference. + /// + /// # Examples + /// + /// ``` + /// use std::time::{Duration, SystemTime}; + /// use saturating_time::SaturatingTime; + /// + /// let epoch = SystemTime::UNIX_EPOCH; + /// let now = SystemTime::now(); + /// let min = SystemTime::min_value(); + /// + /// assert!(now.saturating_duration_since(epoch).as_secs() > 0); + /// assert!(epoch.saturating_duration_since(epoch) == Duration::ZERO); + /// assert!(min.saturating_duration_since(epoch) == Duration::ZERO); + /// ``` + fn saturating_duration_since(&self, earlier: Self) -> Duration { + self.checked_duration_since(earlier) + .unwrap_or(Duration::ZERO) + } +} + +// Use nightly implementation if compiled with the nightly feature. +#[cfg(saturating_time_nightly)] +impl SaturatingTime for SystemTime { + fn max_value() -> Self { + Self::MAX + } + + fn min_value() -> Self { + Self::MIN + } + + fn saturating_add(self, duration: Duration) -> Self { + Self::saturating_add(&self, duration) + } + + fn saturating_sub(self, duration: Duration) -> Self { + Self::saturating_sub(&self, duration) + } + + fn saturating_duration_since(&self, earlier: Self) -> Duration { + Self::saturating_duration_since(&self, earlier) + } +} + +// Otherwise, use the default one. +#[cfg(not(saturating_time_nightly))] +impl SaturatingTime for SystemTime {} + +impl SaturatingTime for Instant { + // Override to use the provided implementation from the standard library. + fn saturating_duration_since(&self, earlier: Self) -> Duration { + Self::saturating_duration_since(self, earlier) + } +} + +#[cfg(test)] +mod tests { + // @@ begin test lint list maintained by maint/add_warning @@ + #![allow(clippy::bool_assert_comparison)] + #![allow(clippy::clone_on_copy)] + #![allow(clippy::dbg_macro)] + #![allow(clippy::mixed_attributes_style)] + #![allow(clippy::print_stderr)] + #![allow(clippy::print_stdout)] + #![allow(clippy::single_char_pattern)] + #![allow(clippy::unwrap_used)] + #![allow(clippy::unchecked_time_subtraction)] + #![allow(clippy::useless_vec)] + #![allow(clippy::needless_pass_by_value)] + #![allow(clippy::string_slice)] // See arti#2571 + //! + + use super::*; + use crate::internal; + use std::{ + fmt::Debug, + ops::{Add, Sub}, + time::{Instant, SystemTime}, + }; + + /// Verifies the maximum and minimum values of [`SaturatingTime`] equal + /// their pedant in [`internal::SaturatingTime`]. + fn min_max() { + assert_eq!( + ::max_value(), + ::max_value() + ); + assert_eq!( + ::min_value(), + ::min_value() + ); + } + + /// Verifies the saturating arithmetic for [`SaturatingTime`]. + fn saturating_add_sub< + T: SaturatingTime + PartialEq + Debug + Add + Sub, + >() { + let max = ::max_value(); + assert_eq!(max.saturating_add(Duration::ZERO), max); + assert_eq!(max.saturating_add(Duration::new(0, 1)), max); + assert_eq!(max.saturating_sub(Duration::ZERO), max); + assert_eq!( + max.saturating_sub(Duration::new(0, 1)), + max - Duration::new(0, 1) + ); + + let min = ::min_value(); + assert_eq!(min.saturating_sub(Duration::ZERO), min); + assert_eq!(min.saturating_sub(Duration::new(0, 1)), min); + assert_eq!(min.saturating_add(Duration::ZERO), min); + assert_eq!( + min.saturating_add(Duration::new(0, 1)), + min + Duration::new(0, 1) + ); + } + + /// Verifies whether the saturating logic behind [`Duration`] types work. + fn saturating_duration() { + // The duration from the same anchor should always be zero. + let anchor = T::anchor(); + assert_eq!(anchor.saturating_duration_since(anchor), Duration::ZERO); + + // Try with a later anchor. + let later_anchor = anchor.checked_add(Duration::from_secs(1)).unwrap(); + assert!(later_anchor.saturating_duration_since(anchor) == Duration::from_secs(1)); + assert_eq!( + anchor.saturating_duration_since(later_anchor), + Duration::ZERO + ); + + // Try with min and max. + let max = ::max_value(); + let min = ::min_value(); + + // This first assertion might not be so portable, maybe remove it if + // this becomes a problem. + assert_eq!(max.saturating_duration_since(min), Duration::MAX); + assert_eq!(min.saturating_duration_since(max), Duration::ZERO); + } + + /// Calls [`min_max()`] using [`SystemTime`]. + #[test] + fn system_time_min_max() { + min_max::(); + } + + /// Calls [`min_max()`] using [`Instant`]. + #[test] + fn instant_min_max() { + min_max::(); + } + + /// Calls [`saturating_add_sub()`] and [`saturating_duration()`] using [`SystemTime`]. + #[test] + fn system_time_saturating() { + saturating_add_sub::(); + saturating_duration::(); + } + + /// Calls [`saturating_add_sub()`] and [`saturating_duration()`] using [`Instant`]. + #[test] + fn instant_saturating() { + saturating_add_sub::(); + saturating_duration::(); + } +} diff --git a/rust/rust-toolchain.toml b/rust/rust-toolchain.toml index edb844f8..edcf233f 100644 --- a/rust/rust-toolchain.toml +++ b/rust/rust-toolchain.toml @@ -1,5 +1,5 @@ [toolchain] -channel = "1.90.0" +channel = "1.99.1" profile = "minimal" targets = [ diff --git a/rust/src/error.rs b/rust/src/error.rs index 7bf5ba0c..b1c1c3fc 100644 --- a/rust/src/error.rs +++ b/rust/src/error.rs @@ -12,6 +12,11 @@ thread_local! { static LAST_ERROR: RefCell>> = RefCell::new(None); } +/// Take the current thread's last error as a newly allocated C string. +/// +/// # Safety +/// This function has no preconditions. The returned string is owned by the +/// caller and must be released exactly once with [`tor_string_free`]. #[no_mangle] pub unsafe extern "C" fn tor_last_error_message() -> *const c_char { let last_error = match crate::error::take_last_error() { @@ -19,10 +24,37 @@ pub unsafe extern "C" fn tor_last_error_message() -> *const c_char { None => return CString::new("").unwrap().into_raw(), }; - let error_message = last_error.to_string(); + // CString::new rejects interior NULs, and a panic here would abort the + // process at the FFI boundary. + let mut error_message = last_error.to_string().replace('\0', ""); + let mut cause = last_error.source(); + while let Some(error) = cause { + let message = error.to_string().replace('\0', ""); + // Some Display implementations already include their source. Only + // omit an exact, colon-delimited suffix, never a substring match. + if error_message != message && !error_message.ends_with(&format!(": {message}")) { + error_message.push_str(": "); + error_message.push_str(&message); + } + cause = error.source(); + } CString::new(error_message).unwrap().into_raw() } +/// Release a string returned by [`tor_last_error_message`]. +/// +/// # Safety +/// `message` must be null or a pointer returned by `tor_last_error_message` +/// that has not been released yet. It must not be used after this call. +#[no_mangle] +pub unsafe extern "C" fn tor_string_free(message: *mut c_char) { + if message.is_null() { + return; + } + + drop(CString::from_raw(message)); +} + macro_rules! unwrap_or_return { ($a:expr,$b:expr) => { match $a { @@ -58,3 +90,89 @@ pub fn update_last_error(err: E) { pub fn take_last_error() -> Option> { LAST_ERROR.with(|prev| prev.borrow_mut().take()) } + +#[cfg(test)] +mod tests { + use super::*; + use std::ffi::CStr; + use std::{fmt, io}; + + #[derive(Debug)] + struct BootstrapError(io::Error); + + impl fmt::Display for BootstrapError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "bootstrap failed") + } + } + + impl Error for BootstrapError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + Some(&self.0) + } + } + + fn take_message() -> String { + unsafe { + let message = tor_last_error_message(); + let text = CStr::from_ptr(message).to_string_lossy().into_owned(); + tor_string_free(message.cast_mut()); + text + } + } + + #[test] + fn error_message_includes_causes_in_order() { + update_last_error(BootstrapError(io::Error::other("storage unavailable"))); + assert_eq!(take_message(), "bootstrap failed: storage unavailable"); + + update_last_error(io::Error::other("standalone error")); + assert_eq!(take_message(), "standalone error"); + } + #[derive(Debug)] + struct Context { + message: &'static str, + source: Box, + display_source: bool, + } + + impl fmt::Display for Context { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.message)?; + if self.display_source { + write!(f, ": {}", self.source)?; + } + Ok(()) + } + } + + impl Error for Context { + fn source(&self) -> Option<&(dyn Error + 'static)> { + Some(self.source.as_ref()) + } + } + + #[test] + fn error_message_does_not_repeat_embedded_causes() { + for outer_embeds in [false, true] { + update_last_error(Context { + message: "top", + source: Box::new(Context { + message: "cause", + source: Box::new(io::Error::other("root")), + display_source: true, + }), + display_source: outer_embeds, + }); + assert_eq!(take_message(), "top: cause: root"); + } + + // Shared words are not evidence that a cause is already embedded. + update_last_error(Context { + message: "root failed during bootstrap", + source: Box::new(io::Error::other("root failed")), + display_source: false, + }); + assert_eq!(take_message(), "root failed during bootstrap: root failed"); + } +} diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 3af430ce..80f1a38b 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -3,19 +3,23 @@ // SPDX-License-Identifier: GPL-3.0-or-later use crate::error::update_last_error; -use arti::socks; +use arti::proxy::{run_proxy_with_listeners, ListenProtocols}; use arti_client::config::CfgPath; use arti_client::{DormantMode, TorClient, TorClientConfig}; use lazy_static::lazy_static; use std::ffi::{c_char, c_void, CStr}; +use std::future::Future; +use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr}; +use std::sync::Arc; +use std::time::{Duration, Instant}; use std::{io, ptr}; use tokio::runtime::{Builder, Runtime}; +use tokio::sync::watch; use tokio::task::JoinHandle; -use tor_config::Listen; use tor_rtcompat::tokio::TokioNativeTlsRuntime; -use tor_rtcompat::ToplevelBlockOn; +use tor_rtcompat::{NetStreamListener, NetStreamProvider, TcpListenOptions, ToplevelBlockOn}; -pub use crate::error::tor_last_error_message; +pub use crate::error::{tor_last_error_message, tor_string_free}; #[cfg(not(target_os = "windows"))] pub use crate::util::tor_get_nofile_limit; #[cfg(not(target_os = "windows"))] @@ -29,12 +33,118 @@ lazy_static! { static ref RUNTIME: io::Result = Builder::new_multi_thread().enable_all().build(); } +/// How long freeing a client waits for its background tasks to finish. +const CLIENT_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5); + +/// Worker threads on each client's runtime. Tor traffic is I/O bound and +/// storage runs on the blocking pool, so tokio's default of one thread per +/// core would sit idle. +const CLIENT_WORKER_THREADS: usize = 2; + +/// How many more OS-assigned ports to try when another process holds the +/// IPv6 twin of the assigned port, before failing. +const IPV6_BIND_RETRIES: u32 = 3; + #[repr(C)] pub struct Tor { client: *mut c_void, proxy: *mut c_void, + port: u16, +} + +/// The tokio runtime a single client and its background tasks run on. +/// +/// Arti's background tasks hold the client's runtime, so a runtime the client +/// owns is never dropped and the tasks keep the directory cache and state +/// files open after the client is freed. Each client instead runs on a +/// runtime owned here and shut down when this is dropped. +struct ClientRuntime(Option); + +impl ClientRuntime { + fn new() -> io::Result { + Ok(Self(Some( + Builder::new_multi_thread() + .worker_threads(CLIENT_WORKER_THREADS) + .enable_all() + .build()?, + ))) + } + + /// An arti runtime that spawns onto this runtime without owning it. + fn arti(&self) -> io::Result { + let _guard = self.0.as_ref().expect("runtime is live").enter(); + TokioNativeTlsRuntime::current() + } +} + +impl Drop for ClientRuntime { + fn drop(&mut self) { + if let Some(runtime) = self.0.take() { + let started = Instant::now(); + runtime.shutdown_timeout(CLIENT_SHUTDOWN_TIMEOUT); + if started.elapsed() >= CLIENT_SHUTDOWN_TIMEOUT { + // Blocking tasks cannot be interrupted, so a slow storage + // write may still hold its file open after this returns. + log::warn!( + "Tor client runtime did not shut down within {:?}; files may remain open", + CLIENT_SHUTDOWN_TIMEOUT + ); + } + } + } +} + +/// What a client handle passed across FFI points to. +struct ClientHandle { + // Fields drop in order, so the client is released before its runtime + // shuts down. + client: Arc>, + cancel_bootstrap: watch::Sender, + _runtime: ClientRuntime, +} + +/// A reserved SOCKS listener that becomes active only after bootstrap. +enum ProxyHandle { + /// Bound but idle until `start`. + Reserved { + client: Arc>, + listeners: Vec, + }, + /// The accept loop is running. + Running(JoinHandle>), +} + +impl ProxyHandle { + fn new(client: Arc>, listeners: Vec) -> Self { + Self::Reserved { client, listeners } + } + + fn start(&mut self, rt: &Runtime) { + if let Self::Reserved { client, listeners } = self { + let task = start_proxy(rt, Arc::clone(client), std::mem::take(listeners)); + *self = Self::Running(task); + } + } +} + +fn into_client_handle( + client: Arc>, + runtime: ClientRuntime, +) -> *mut c_void { + let (cancel_bootstrap, _) = watch::channel(false); + Box::into_raw(Box::new(ClientHandle { + client, + cancel_bootstrap, + _runtime: runtime, + })) as *mut c_void } +/// Create a Tor client and a localhost SOCKS proxy. +/// +/// # Safety +/// `state_dir` and `cache_dir` must point to valid, NUL-terminated strings +/// for the duration of the call. Each returned non-null handle must be +/// released exactly once with its corresponding cleanup function. #[no_mangle] pub unsafe extern "C" fn tor_start( socks_port: u16, @@ -44,18 +154,28 @@ pub unsafe extern "C" fn tor_start( let err_ret = Tor { client: ptr::null_mut(), proxy: ptr::null_mut(), + port: 0, }; let state_dir = unwrap_or_return!(CStr::from_ptr(state_dir).to_str(), err_ret); let cache_dir = unwrap_or_return!(CStr::from_ptr(cache_dir).to_str(), err_ret); - let runtime = unwrap_or_return!(TokioNativeTlsRuntime::create(), err_ret); + // Reserve the port before creating the client's threads, initializing + // storage or bootstrapping. On any later failure these listeners are + // dropped and the port is released. + let rt = unwrap_or_return!(proxy_runtime(), err_ret); + let (listeners, socks_port) = unwrap_or_return!(bind_localhost(rt, socks_port), err_ret); + + let client_runtime = unwrap_or_return!(ClientRuntime::new(), err_ret); + let runtime = unwrap_or_return!(client_runtime.arti(), err_ret); let mut cfg_builder = TorClientConfig::builder(); cfg_builder .storage() - .state_dir(CfgPath::new(state_dir.to_owned())) - .cache_dir(CfgPath::new(cache_dir.to_owned())); + // The paths come from the caller as-is; `CfgPath::new` would expand + // `$VAR` and `~` and reject directories whose names contain them. + .state_dir(CfgPath::new_literal(state_dir)) + .cache_dir(CfgPath::new_literal(cache_dir)); cfg_builder.address_filter().allow_onion_addrs(true); let cfg = unwrap_or_return!(cfg_builder.build(), err_ret); @@ -64,37 +184,184 @@ pub unsafe extern "C" fn tor_start( runtime.block_on(async { TorClient::with_runtime(runtime.clone()) .config(cfg) - .create_bootstrapped() - .await + .create_unbootstrapped() }), err_ret ); - let proxy_handle_box = Box::new(start_proxy(socks_port, client.clone())); - let client_box = Box::new(client.clone()); + let proxy_handle_box = Box::new(ProxyHandle::new(Arc::clone(&client), listeners)); Tor { - client: Box::into_raw(client_box) as *mut c_void, + client: into_client_handle(client, client_runtime), proxy: Box::into_raw(proxy_handle_box) as *mut c_void, + port: socks_port, } } +/// Borrow the client handle produced by [`tor_start`] without taking ownership. +/// +/// Ownership stays with the caller until [`tor_client_free`] is called, so the +/// handle can be passed to any number of FFI calls in between. Null yields +/// `None` so callers can report it instead of aborting the process. +unsafe fn client_handle_ref<'a>(client: *mut c_void) -> Option<&'a ClientHandle> { + (client as *const ClientHandle).as_ref() +} + +fn null_client_error() -> io::Error { + io::Error::new(io::ErrorKind::InvalidInput, "client handle is null") +} + +/// The outcome of [`tor_client_bootstrap`]. +/// +/// Every code other than `Ok` also sets the message returned by +/// [`tor_last_error_message`]. +#[repr(i32)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TorBootstrapStatus { + /// The client bootstrapped. + Ok = 0, + /// [`tor_client_cancel_bootstrap`] was called on the handle. + Cancelled = 1, + /// The deadline passed before bootstrap finished. + TimedOut = 2, + /// Arti reported an error. + Failed = 3, + /// The client handle was null. + NullClient = 4, +} + +/// Bootstrap the client, returning on cancellation or after `timeout_millis`. +/// A null `client` returns `NullClient`. +/// +/// # Safety +/// `client` must be null or a live handle returned by [`tor_start`]. +/// It must not be freed while this call is in progress. #[no_mangle] -pub unsafe extern "C" fn tor_client_bootstrap(client: *mut c_void) -> bool { - let client = { - assert!(!client.is_null()); - Box::from_raw(client as *mut TorClient) +pub unsafe extern "C" fn tor_client_bootstrap( + client: *mut c_void, + timeout_millis: u64, +) -> TorBootstrapStatus { + let Some(handle) = client_handle_ref(client) else { + update_last_error(null_client_error()); + return TorBootstrapStatus::NullClient; }; + let client = &handle.client; + let cancelled = handle.cancel_bootstrap.subscribe(); - unwrap_or_return!(client.runtime().block_on(client.bootstrap()), false); - true + let result = client.runtime().block_on(controlled_bootstrap( + client.bootstrap(), + cancelled, + Duration::from_millis(timeout_millis), + )); + + match result { + Ok(()) => TorBootstrapStatus::Ok, + Err(error) => { + let status = error.status(); + update_last_error(error); + status + } + } +} + +/// Why [`controlled_bootstrap`] returned without bootstrapping. +#[derive(Debug)] +enum BootstrapError { + Cancelled, + TimedOut(Duration), + Failed(io::Error), +} + +impl BootstrapError { + fn status(&self) -> TorBootstrapStatus { + match self { + BootstrapError::Cancelled => TorBootstrapStatus::Cancelled, + BootstrapError::TimedOut(_) => TorBootstrapStatus::TimedOut, + BootstrapError::Failed(_) => TorBootstrapStatus::Failed, + } + } +} + +impl std::fmt::Display for BootstrapError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + BootstrapError::Cancelled => write!(f, "Tor bootstrap cancelled"), + BootstrapError::TimedOut(timeout) => write!( + f, + "Tor bootstrap timed out after {} ms", + timeout.as_millis() + ), + BootstrapError::Failed(error) => error.fmt(f), + } + } +} + +impl std::error::Error for BootstrapError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + BootstrapError::Failed(error) => error.source(), + _ => None, + } + } +} + +async fn controlled_bootstrap( + bootstrap: F, + mut cancelled: watch::Receiver, + timeout: Duration, +) -> Result<(), BootstrapError> +where + F: Future>, + E: std::error::Error + Send + Sync + 'static, +{ + if *cancelled.borrow() { + return Err(BootstrapError::Cancelled); + } + + tokio::select! { + biased; + result = cancelled.changed() => { + result.map_err(|_| { + BootstrapError::Failed(io::Error::other("Tor bootstrap cancellation channel closed")) + })?; + Err(BootstrapError::Cancelled) + } + // Keep the error itself, not its message, so its source chain reaches + // update_last_error. + result = bootstrap => result.map_err(|error| BootstrapError::Failed(io::Error::other(error))), + _ = tokio::time::sleep(timeout) => Err(BootstrapError::TimedOut(timeout)), + } } +/// Ask an in-progress bootstrap to return promptly. Null is accepted. +/// +/// Cancellation is sticky: once this is called, every later +/// [`tor_client_bootstrap`] on the same handle returns `Cancelled` at once. +/// To bootstrap again, free the handle with [`tor_client_free`] and create a +/// new one with [`tor_start`]. +/// +/// # Safety +/// `client` must be null or a live handle returned by [`tor_start`]. It may be +/// called concurrently with [`tor_client_bootstrap`], but not with or after +/// [`tor_client_free`]. +#[no_mangle] +pub unsafe extern "C" fn tor_client_cancel_bootstrap(client: *mut c_void) { + if let Some(handle) = client_handle_ref(client) { + handle.cancel_bootstrap.send_replace(true); + } +} + +/// Change the client's dormant mode. A null `client` is ignored and sets the +/// message returned by [`tor_last_error_message`]. +/// +/// # Safety +/// `client` must be null or a live handle returned by [`tor_start`]. +/// It must not be freed while this call is in progress. #[no_mangle] pub unsafe extern "C" fn tor_client_set_dormant(client: *mut c_void, soft_mode: bool) { - let client = { - assert!(!client.is_null()); - Box::from_raw(client as *mut TorClient) + let Some(handle) = client_handle_ref(client) else { + update_last_error(null_client_error()); + return; }; let dormant_mode = if soft_mode { @@ -102,37 +369,669 @@ pub unsafe extern "C" fn tor_client_set_dormant(client: *mut c_void, soft_mode: } else { DormantMode::Normal }; - client.set_dormant(dormant_mode); - Box::leak(client); + handle.client.set_dormant(dormant_mode); +} + +/// Release the client handle returned by [`tor_start`]. +/// +/// This shuts down the client's background tasks and closes its state and +/// cache files, which can block for a few seconds. Stop the proxy first: it +/// keeps its own reference to the client, but cannot serve connections once +/// the client is freed. +/// +/// # Safety +/// `client` must be null or a live handle returned by [`tor_start`]. +/// No other call may use the handle concurrently with or after this call. +/// It must not be called from within an async runtime. +#[no_mangle] +pub unsafe extern "C" fn tor_client_free(client: *mut c_void) { + if client.is_null() { + return; + } + + drop(Box::from_raw(client as *mut ClientHandle)); +} + +/// Start accepting SOCKS connections on the listeners reserved by [`tor_start`]. +/// Repeated calls are harmless. +/// +/// # Safety +/// `proxy` must be a live, non-null proxy handle returned by [`tor_start`]. It +/// must not be used concurrently or after [`tor_proxy_stop`]. +#[no_mangle] +pub unsafe extern "C" fn tor_proxy_start(proxy: *mut c_void) -> bool { + if proxy.is_null() { + update_last_error(io::Error::new( + io::ErrorKind::InvalidInput, + "proxy handle is null", + )); + return false; + } + let rt = unwrap_or_return!(proxy_runtime(), false); + let proxy = &mut *(proxy as *mut ProxyHandle); + proxy.start(rt); + true } +/// Stop the proxy, or release its reserved listeners if it was not started. +/// Null is accepted. +/// +/// This waits for the accept loop to exit, so the listeners and the loop's +/// reference to the client are released by the time it returns. Call it +/// before [`tor_client_free`]. +/// +/// # Safety +/// `proxy` must be null or a live proxy handle returned by [`tor_start`]. +/// A non-null handle must be passed to this function only once. +/// It must not be called from within an async runtime. #[no_mangle] pub unsafe extern "C" fn tor_proxy_stop(proxy: *mut c_void) { if proxy.is_null() { return; } - let proxy = Box::from_raw(proxy as *mut JoinHandle>); + if let ProxyHandle::Running(task) = *Box::from_raw(proxy as *mut ProxyHandle) { + task.abort(); + // Aborting only requests cancellation; the task's future, which owns + // the listeners and an `Arc` of the client, is dropped by a runtime + // worker. Join it so teardown is complete when this returns. + if let Ok(rt) = proxy_runtime() { + let _ = rt.block_on(task); + } + } +} + +type Listener = ::Listener; - proxy.abort(); +/// The runtime that drives the SOCKS accept loop. +fn proxy_runtime() -> io::Result<&'static Runtime> { + RUNTIME + .as_ref() + .map_err(|e| io::Error::new(e.kind(), e.to_string())) } fn start_proxy( - port: u16, - client: TorClient, + rt: &Runtime, + client: Arc>, + listeners: Vec, ) -> JoinHandle> { - println!("Starting proxy!"); - let rt = RUNTIME.as_ref().unwrap(); - rt.spawn(socks::run_socks_proxy( - client.runtime().clone(), - client.clone(), - Listen::new_localhost(port), + rt.spawn(run_proxy_with_listeners( + client, + listeners, + ListenProtocols::SocksOnly, None, )) } +/// Bind `port` on every localhost address family that is available. +/// +/// IPv4 is authoritative: the IPv4 listener's port is the published port, and +/// IPv6 loopback is added on the same port. IPv4 alone is served only when +/// IPv6 is unavailable on the host. When the OS picks the port and another +/// process already holds its IPv6 twin, a fresh port is tried a few times +/// before failing, since a client resolving `localhost` to `::1` would reach +/// that process. +/// +/// This blocks on `rt` so the listeners are registered with the reactor that +/// later runs the accept loop, and so the caller sees a bind failure before +/// the port is published. +fn bind_localhost(rt: &Runtime, requested_port: u16) -> io::Result<(Vec, u16)> { + rt.block_on(async { + // Arti binds through the runtime that is current, which is `rt`. + let runtime = TokioNativeTlsRuntime::current()?; + let options = TcpListenOptions::default(); + let mut retries_left = if requested_port == 0 { + IPV6_BIND_RETRIES + } else { + 0 + }; + loop { + let ipv4 = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), requested_port); + let ipv4_listener = runtime + .listen(&ipv4, &options) + .await + .map_err(|e| io::Error::new(e.kind(), format!("Can't listen on {ipv4}: {e}")))?; + let port = ipv4_listener.local_addr()?.port(); + + let ipv6 = SocketAddr::new(Ipv6Addr::LOCALHOST.into(), port); + let error = match runtime.listen(&ipv6, &options).await { + Ok(listener) => return Ok((vec![ipv4_listener, listener], port)), + Err(error) => error, + }; + match ipv6_bind_fallback(requested_port, retries_left, &error) { + Ipv6Fallback::Ipv4Only => return Ok((vec![ipv4_listener], port)), + Ipv6Fallback::Retry => { + retries_left -= 1; + drop(ipv4_listener); + } + Ipv6Fallback::Fail => return Err(ipv6_bind_error(requested_port, ipv6, error)), + } + } + }) +} + +/// The error reported when IPv6 loopback could not be bound on `ipv6`. +fn ipv6_bind_error(requested_port: u16, ipv6: SocketAddr, error: io::Error) -> io::Error { + let reason = if requested_port == 0 && ipv6_twin_is_occupied(&error) { + format!( + "the IPv6 twin of the assigned port was occupied after {} attempts ({error})", + IPV6_BIND_RETRIES + 1 + ) + } else { + error.to_string() + }; + io::Error::new(error.kind(), format!("Can't listen on {ipv6}: {reason}")) +} + +/// What to do when IPv6 loopback could not be bound on the IPv4 port. +#[derive(Debug, PartialEq, Eq)] +enum Ipv6Fallback { + /// Serve IPv4 loopback only. + Ipv4Only, + /// Release the IPv4 listener and ask the OS for another port. + Retry, + /// Report the error. This includes an OS-assigned port whose IPv6 twin + /// stayed occupied after every retry. + Fail, +} + +fn ipv6_bind_fallback(requested_port: u16, retries_left: u32, error: &io::Error) -> Ipv6Fallback { + if ipv6_is_unavailable(error) { + return Ipv6Fallback::Ipv4Only; + } + // Only an OS-assigned port can be swapped for another one. A port the + // caller asked for is reported as occupied, as it is for IPv4. Serving + // IPv4 alone would leave `[::1]` on the published port to another process. + if requested_port == 0 && ipv6_twin_is_occupied(error) && retries_left > 0 { + return Ipv6Fallback::Retry; + } + Ipv6Fallback::Fail +} + +/// Whether another process holds the IPv6 loopback port. +fn ipv6_twin_is_occupied(error: &io::Error) -> bool { + if error.kind() == io::ErrorKind::AddrInUse { + return true; + } + + // A process holding `[::]` with SO_EXCLUSIVEADDRUSE makes binding `[::1]` + // on that port fail with WSAEACCES (10013), not WSAEADDRINUSE. A port in + // an excluded range (Hyper-V, WinNAT) fails the same way. + #[cfg(windows)] + if error.raw_os_error() == Some(10013) { + return true; + } + + false +} + +/// Whether IPv6 loopback is unavailable on this host. IPv4 remains usable in +/// this case, so the SOCKS proxy should still start. +fn ipv6_is_unavailable(error: &io::Error) -> bool { + if matches!( + error.kind(), + io::ErrorKind::AddrNotAvailable | io::ErrorKind::Unsupported + ) { + return true; + } + + #[cfg(unix)] + if error.raw_os_error() == Some(libc::EAFNOSUPPORT) { + return true; + } + + // std maps only WSAEADDRNOTAVAIL to a typed kind on Windows. A disabled + // IPv6 stack fails socket creation with WSAEAFNOSUPPORT (10047) or + // WSAEPROTONOSUPPORT (10043), which arrive uncategorized. + #[cfg(windows)] + if matches!(error.raw_os_error(), Some(10047 | 10043)) { + return true; + } + + false +} + // Due to its simple signature this dummy function is the one added (unused) to iOS swift codebase to force Xcode to link the lib +/// Print a greeting to verify that the library is linked. +/// +/// # Safety +/// This function has no additional safety requirements. #[no_mangle] pub unsafe extern "C" fn tor_hello() { println!("HELLO THERE"); -} \ No newline at end of file +} + +#[cfg(test)] +mod tests { + use super::*; + use arti_client::config::TorClientConfigBuilder; + use std::ffi::CString; + use std::net::{Ipv4Addr, TcpListener}; + use std::path::{Path, PathBuf}; + use std::sync::atomic::{AtomicUsize, Ordering}; + + fn scratch_dir(label: &str) -> PathBuf { + static COUNTER: AtomicUsize = AtomicUsize::new(0); + let n = COUNTER.fetch_add(1, Ordering::Relaxed); + let dir = std::env::temp_dir().join(format!( + "tor_ffi_plugin-test-{}-{n}-{label}", + std::process::id() + )); + // Process ids are reused, so a directory from an earlier run (such as + // a deliberately corrupted cache) may already exist under this name. + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + dir + } + + /// Build a client under `root` the same way `tor_start` does, minus + /// bootstrapping. + fn unbootstrapped_client( + root: &Path, + ) -> (Arc>, ClientRuntime) { + let runtime = ClientRuntime::new().unwrap(); + let cfg = TorClientConfigBuilder::from_directories(root.join("state"), root.join("cache")) + .build() + .unwrap(); + let client = TorClient::with_runtime(runtime.arti().unwrap()) + .config(cfg) + .create_unbootstrapped() + .unwrap(); + (client, runtime) + } + + /// An unbootstrapped client as the opaque pointer Dart would receive, with + /// the directory to remove once the handle is freed. + fn unbootstrapped_client_handle() -> (*mut c_void, PathBuf) { + let root = scratch_dir("client"); + let (client, runtime) = unbootstrapped_client(&root); + (into_client_handle(client, runtime), root) + } + + #[test] + fn freeing_the_client_closes_its_files() { + let root = scratch_dir("free"); + let (client, runtime) = unbootstrapped_client(&root); + // Leave a bootstrap running, as Tor.stop() does with a live client, so + // background tasks are using the directory cache. The result does not + // matter, so this works offline too. + let bootstrapping = Arc::clone(&client); + runtime + .0 + .as_ref() + .unwrap() + .spawn(async move { drop(bootstrapping.bootstrap().await) }); + std::thread::sleep(Duration::from_secs(1)); + unsafe { tor_client_free(into_client_handle(client, runtime)) }; + + // Windows refuses to delete files that are still open. Before the fix + // the client's background tasks kept the directory cache open forever. + std::fs::remove_dir_all(&root).unwrap(); + } + + #[test] + fn client_handle_survives_repeated_calls_until_freed() { + let (handle, root) = unbootstrapped_client_handle(); + + // Before the fix every call through the handle reconstructed and + // dropped the owning Box, so the second call here was a use after + // free and the explicit free a double free. + unsafe { + tor_client_set_dormant(handle, true); + tor_client_set_dormant(handle, false); + tor_client_free(handle); + } + std::fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn cancelled_bootstrap_returns_without_network_and_preserves_the_handle() { + let (handle, root) = unbootstrapped_client_handle(); + + unsafe { + tor_client_cancel_bootstrap(handle); + assert_eq!( + tor_client_bootstrap(handle, 60_000), + TorBootstrapStatus::Cancelled + ); + tor_client_set_dormant(handle, true); + tor_client_set_dormant(handle, false); + tor_client_free(handle); + } + let error = crate::error::take_last_error().unwrap().to_string(); + assert!(error.contains("cancelled"), "{error}"); + std::fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn controlled_bootstrap_honors_its_deadline() { + let (_cancel, cancelled) = tokio::sync::watch::channel(false); + let result = proxy_runtime().unwrap().block_on(controlled_bootstrap( + std::future::pending::>(), + cancelled, + Duration::ZERO, + )); + + let error = result.unwrap_err(); + assert_eq!(error.status(), TorBootstrapStatus::TimedOut); + assert!(error.to_string().contains("timed out"), "{error}"); + } + + #[test] + fn controlled_bootstrap_keeps_the_error_chain() { + #[derive(Debug)] + struct Inner; + impl std::fmt::Display for Inner { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("no consensus") + } + } + impl std::error::Error for Inner {} + + #[derive(Debug)] + struct Outer(Inner); + impl std::fmt::Display for Outer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("bootstrap failed") + } + } + impl std::error::Error for Outer { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + Some(&self.0) + } + } + + let (_cancel, cancelled) = tokio::sync::watch::channel(false); + let error = proxy_runtime() + .unwrap() + .block_on(controlled_bootstrap( + async { Err::<(), _>(Outer(Inner)) }, + cancelled, + Duration::from_secs(60), + )) + .unwrap_err(); + + assert_eq!(error.status(), TorBootstrapStatus::Failed); + assert_eq!(error.to_string(), "bootstrap failed"); + let source = std::error::Error::source(&error).expect("source chain was dropped"); + assert_eq!(source.to_string(), "no consensus"); + } + + #[test] + fn bootstrap_status_codes_are_stable() { + let (handle, root) = unbootstrapped_client_handle(); + + unsafe { + // A zero deadline expires before an offline bootstrap can finish. + assert_eq!(tor_client_bootstrap(handle, 0) as i32, 2); + let error = crate::error::take_last_error().unwrap().to_string(); + assert!(error.contains("timed out"), "{error}"); + + tor_client_cancel_bootstrap(handle); + assert_eq!(tor_client_bootstrap(handle, 60_000) as i32, 1); + let error = crate::error::take_last_error().unwrap().to_string(); + assert!(error.contains("cancelled"), "{error}"); + + tor_client_free(handle); + } + std::fs::remove_dir_all(root).unwrap(); + assert_eq!(TorBootstrapStatus::Ok as i32, 0); + assert_eq!(TorBootstrapStatus::Failed as i32, 3); + assert_eq!(TorBootstrapStatus::NullClient as i32, 4); + } + + #[test] + fn error_message_is_released_by_tor_string_free() { + update_last_error(io::Error::other("boom")); + + unsafe { + let message = tor_last_error_message(); + assert_eq!(CStr::from_ptr(message).to_str().unwrap(), "boom"); + tor_string_free(message as *mut c_char); + tor_string_free(ptr::null_mut()); + } + } + + #[test] + fn error_message_drops_interior_nul() { + update_last_error(io::Error::other("bo\0om")); + + unsafe { + let message = tor_last_error_message(); + assert_eq!(CStr::from_ptr(message).to_str().unwrap(), "boom"); + tor_string_free(message as *mut c_char); + } + } + + #[test] + fn client_free_ignores_null() { + unsafe { tor_client_free(ptr::null_mut()) }; + } + + #[test] + fn client_bootstrap_reports_null() { + let status = unsafe { tor_client_bootstrap(ptr::null_mut(), 1_000) }; + + assert_eq!(status, TorBootstrapStatus::NullClient); + let error = crate::error::take_last_error().unwrap().to_string(); + assert!(error.contains("null"), "{error}"); + } + + #[test] + fn client_set_dormant_ignores_null() { + unsafe { tor_client_set_dormant(ptr::null_mut(), true) }; + + let error = crate::error::take_last_error().unwrap().to_string(); + assert!(error.contains("null"), "{error}"); + } + + #[test] + fn bind_localhost_reports_an_occupied_port() { + let occupied = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).unwrap(); + let port = occupied.local_addr().unwrap().port(); + + let err = match bind_localhost(proxy_runtime().unwrap(), port) { + Ok(_) => panic!("bound port {port} while it was occupied"), + Err(err) => err, + }; + + assert!(err.to_string().contains("Can't listen on"), "{err}"); + } + + #[test] + fn tor_start_rejects_an_occupied_port_before_opening_storage() { + let occupied = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).unwrap(); + let root = scratch_dir("busy-port"); + // This cannot be a storage directory. A late bind would fail on storage + // initialization instead of reporting the occupied port. + let state = root.join("state-file"); + std::fs::write(&state, b"not a directory").unwrap(); + let state = CString::new(state.to_str().unwrap()).unwrap(); + let cache = CString::new(root.join("cache").to_str().unwrap()).unwrap(); + + let result = unsafe { + tor_start( + occupied.local_addr().unwrap().port(), + state.as_ptr(), + cache.as_ptr(), + ) + }; + assert!(result.client.is_null()); + assert!(result.proxy.is_null()); + let error = crate::error::take_last_error().unwrap().to_string(); + std::fs::remove_dir_all(root).unwrap(); + assert!(error.contains("Can't listen on"), "{error}"); + } + + #[test] + fn unstarted_proxy_holds_its_port_and_can_be_released() { + let root = scratch_dir("reserved-proxy"); + let state = CString::new(root.join("state").to_str().unwrap()).unwrap(); + let cache = CString::new(root.join("cache").to_str().unwrap()).unwrap(); + + let result = unsafe { tor_start(0, state.as_ptr(), cache.as_ptr()) }; + assert!(!result.client.is_null()); + assert!(!result.proxy.is_null()); + assert_ne!(result.port, 0); + assert!(TcpListener::bind((Ipv4Addr::LOCALHOST, result.port)).is_err()); + + unsafe { + tor_proxy_stop(result.proxy); + tor_client_free(result.client); + } + TcpListener::bind((Ipv4Addr::LOCALHOST, result.port)).unwrap(); + std::fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn bind_localhost_holds_the_port_until_dropped() { + let rt = proxy_runtime().unwrap(); + + // A requested port must be honored and held. Another test may take the + // freed port first, so retry with a fresh one. + let (listeners, port) = (0..8) + .find_map(|_| { + let port = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)) + .unwrap() + .local_addr() + .unwrap() + .port(); + match bind_localhost(rt, port) { + Ok((listeners, bound_port)) => { + assert_eq!(bound_port, port); + Some((listeners, port)) + } + Err(e) if e.kind() == io::ErrorKind::AddrInUse => None, + Err(e) => panic!("bind_localhost failed: {e}"), + } + }) + .expect("no free port after 8 attempts"); + + assert!( + TcpListener::bind((Ipv4Addr::LOCALHOST, port)).is_err(), + "port {port} was published without being held" + ); + drop(listeners); + TcpListener::bind((Ipv4Addr::LOCALHOST, port)).unwrap(); + } + + #[test] + fn stopping_the_proxy_releases_its_listeners_and_client_reference() { + let root = scratch_dir("proxy-stop"); + let (client, runtime) = unbootstrapped_client(&root); + let rt = proxy_runtime().unwrap(); + let (listeners, port) = bind_localhost(rt, 0).unwrap(); + let mut proxy = Box::new(ProxyHandle::new(Arc::clone(&client), listeners)); + proxy.start(rt); + // Let the accept loop start polling its listeners. + std::thread::sleep(Duration::from_millis(100)); + assert_eq!(Arc::strong_count(&client), 2); + + unsafe { tor_proxy_stop(Box::into_raw(proxy) as *mut c_void) }; + + // Before the fix the aborted task still owned the listeners and the + // client until a runtime worker got around to dropping it. + assert_eq!(Arc::strong_count(&client), 1); + TcpListener::bind((Ipv4Addr::LOCALHOST, port)).unwrap(); + drop(client); + drop(runtime); + std::fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn tor_start_uses_the_data_directories_literally() { + let root = scratch_dir("literal-paths"); + // `CfgPath::new` treated this as a variable reference and failed. + let state_path = root.join("$state"); + let cache_path = root.join("~cache"); + let state = CString::new(state_path.to_str().unwrap()).unwrap(); + let cache = CString::new(cache_path.to_str().unwrap()).unwrap(); + + let result = unsafe { tor_start(0, state.as_ptr(), cache.as_ptr()) }; + let error = crate::error::take_last_error().map(|e| e.to_string()); + assert!(!result.client.is_null(), "{error:?}"); + unsafe { + tor_proxy_stop(result.proxy); + tor_client_free(result.client); + } + + assert!(state_path.is_dir(), "state dir was not created literally"); + assert!(cache_path.is_dir(), "cache dir was not created literally"); + std::fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn ipv6_bind_fallback_retries_only_os_assigned_ports() { + let in_use = io::Error::from(io::ErrorKind::AddrInUse); + let unavailable = io::Error::from(io::ErrorKind::AddrNotAvailable); + let denied = io::Error::from(io::ErrorKind::PermissionDenied); + + assert_eq!(ipv6_bind_fallback(0, 2, &in_use), Ipv6Fallback::Retry); + // Out of retries, the IPv6 twin still belongs to another process. + assert_eq!(ipv6_bind_fallback(0, 0, &in_use), Ipv6Fallback::Fail); + assert_eq!(ipv6_bind_fallback(9050, 0, &in_use), Ipv6Fallback::Fail); + assert_eq!( + ipv6_bind_fallback(9050, 0, &unavailable), + Ipv6Fallback::Ipv4Only + ); + assert_eq!(ipv6_bind_fallback(0, 2, &denied), Ipv6Fallback::Fail); + + #[cfg(windows)] + { + // WSAEACCES: the IPv6 twin is held with SO_EXCLUSIVEADDRUSE. + let exclusive = io::Error::from_raw_os_error(10013); + assert_eq!(ipv6_bind_fallback(0, 2, &exclusive), Ipv6Fallback::Retry); + assert_eq!(ipv6_bind_fallback(0, 0, &exclusive), Ipv6Fallback::Fail); + assert_eq!(ipv6_bind_fallback(9050, 2, &exclusive), Ipv6Fallback::Fail); + + let no_ipv6 = io::Error::from_raw_os_error(10047); + assert_eq!(ipv6_bind_fallback(0, 0, &no_ipv6), Ipv6Fallback::Ipv4Only); + assert_eq!( + ipv6_bind_fallback(9050, 0, &no_ipv6), + Ipv6Fallback::Ipv4Only + ); + } + } + + #[test] + fn ipv6_bind_error_names_the_exhausted_retries() { + let ipv6 = SocketAddr::new(Ipv6Addr::LOCALHOST.into(), 41000); + let in_use = || io::Error::from(io::ErrorKind::AddrInUse); + + let exhausted = ipv6_bind_error(0, ipv6, in_use()); + assert_eq!(exhausted.kind(), io::ErrorKind::AddrInUse); + let message = exhausted.to_string(); + assert!( + message.contains("IPv6 twin of the assigned port was occupied after 4 attempts"), + "{message}" + ); + + let requested = ipv6_bind_error(41000, ipv6, in_use()).to_string(); + assert!( + requested.starts_with("Can't listen on [::1]:41000"), + "{requested}" + ); + assert!(!requested.contains("attempts"), "{requested}"); + } + + #[test] + #[cfg(windows)] + fn windows_reports_a_disabled_ipv6_stack_as_unavailable() { + // WSAEAFNOSUPPORT and WSAEPROTONOSUPPORT have no typed ErrorKind. + assert!(ipv6_is_unavailable(&io::Error::from_raw_os_error(10047))); + assert!(ipv6_is_unavailable(&io::Error::from_raw_os_error(10043))); + } + + #[test] + fn bind_localhost_allocates_and_reports_one_port() { + let (listeners, port) = bind_localhost(proxy_runtime().unwrap(), 0).unwrap(); + + assert_ne!(port, 0); + assert!( + TcpListener::bind((Ipv4Addr::LOCALHOST, port)).is_err(), + "reported port {port} was not held by the proxy listener" + ); + drop(listeners); + TcpListener::bind((Ipv4Addr::LOCALHOST, port)).unwrap(); + } +} diff --git a/rust/src/util.rs b/rust/src/util.rs index 76f1dd7c..770ae5c1 100644 --- a/rust/src/util.rs +++ b/rust/src/util.rs @@ -1,8 +1,13 @@ // SPDX-FileCopyrightText: 2024 Foundation Devices Inc. // // SPDX-License-Identifier: GPL-3.0-or-later +#[cfg(not(target_os = "windows"))] use crate::error::update_last_error; +/// Read the current open-file limit. +/// +/// # Safety +/// This function has no additional safety requirements. #[no_mangle] #[cfg(not(target_os = "windows"))] pub unsafe extern "C" fn tor_get_nofile_limit() -> u64 { @@ -10,8 +15,12 @@ pub unsafe extern "C" fn tor_get_nofile_limit() -> u64 { nofile_limit.0 } +/// Increase the open-file limit, up to the hard limit. +/// +/// # Safety +/// This function has no additional safety requirements. #[no_mangle] #[cfg(not(target_os = "windows"))] pub unsafe extern "C" fn tor_set_nofile_limit(limit: u64) -> u64 { unwrap_or_return!(rlimit::increase_nofile_limit(limit), 0) -} \ No newline at end of file +} diff --git a/rust/target/tor_ffi_plugin.h b/rust/target/tor_ffi_plugin.h index 052f1375..00ae3e28 100644 --- a/rust/target/tor_ffi_plugin.h +++ b/rust/target/tor_ffi_plugin.h @@ -3,23 +3,179 @@ #include #include +/** + * The outcome of [`tor_client_bootstrap`]. + * + * Every code other than `Ok` also sets the message returned by + * [`tor_last_error_message`]. + */ +enum TorBootstrapStatus +#ifdef __cplusplus + : int32_t +#endif // __cplusplus + { + /** + * The client bootstrapped. + */ + TorBootstrapStatus_Ok = 0, + /** + * [`tor_client_cancel_bootstrap`] was called on the handle. + */ + TorBootstrapStatus_Cancelled = 1, + /** + * The deadline passed before bootstrap finished. + */ + TorBootstrapStatus_TimedOut = 2, + /** + * Arti reported an error. + */ + TorBootstrapStatus_Failed = 3, + /** + * The client handle was null. + */ + TorBootstrapStatus_NullClient = 4, +}; +#ifndef __cplusplus +typedef int32_t TorBootstrapStatus; +#endif // __cplusplus + typedef struct Tor { void *client; void *proxy; + uint16_t port; } Tor; +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +/** + * Create a Tor client and a localhost SOCKS proxy. + * + * # Safety + * `state_dir` and `cache_dir` must point to valid, NUL-terminated strings + * for the duration of the call. Each returned non-null handle must be + * released exactly once with its corresponding cleanup function. + */ struct Tor tor_start(uint16_t socks_port, const char *state_dir, const char *cache_dir); -bool tor_client_bootstrap(void *client); +/** + * Bootstrap the client, returning on cancellation or after `timeout_millis`. + * A null `client` returns `NullClient`. + * + * # Safety + * `client` must be null or a live handle returned by [`tor_start`]. + * It must not be freed while this call is in progress. + */ +TorBootstrapStatus tor_client_bootstrap(void *client, uint64_t timeout_millis); +/** + * Ask an in-progress bootstrap to return promptly. Null is accepted. + * + * Cancellation is sticky: once this is called, every later + * [`tor_client_bootstrap`] on the same handle returns `Cancelled` at once. + * To bootstrap again, free the handle with [`tor_client_free`] and create a + * new one with [`tor_start`]. + * + * # Safety + * `client` must be null or a live handle returned by [`tor_start`]. It may be + * called concurrently with [`tor_client_bootstrap`], but not with or after + * [`tor_client_free`]. + */ +void tor_client_cancel_bootstrap(void *client); + +/** + * Change the client's dormant mode. A null `client` is ignored and sets the + * message returned by [`tor_last_error_message`]. + * + * # Safety + * `client` must be null or a live handle returned by [`tor_start`]. + * It must not be freed while this call is in progress. + */ void tor_client_set_dormant(void *client, bool soft_mode); +/** + * Release the client handle returned by [`tor_start`]. + * + * This shuts down the client's background tasks and closes its state and + * cache files, which can block for a few seconds. Stop the proxy first: it + * keeps its own reference to the client, but cannot serve connections once + * the client is freed. + * + * # Safety + * `client` must be null or a live handle returned by [`tor_start`]. + * No other call may use the handle concurrently with or after this call. + * It must not be called from within an async runtime. + */ +void tor_client_free(void *client); + +/** + * Start accepting SOCKS connections on the listeners reserved by [`tor_start`]. + * Repeated calls are harmless. + * + * # Safety + * `proxy` must be a live, non-null proxy handle returned by [`tor_start`]. It + * must not be used concurrently or after [`tor_proxy_stop`]. + */ +bool tor_proxy_start(void *proxy); + +/** + * Stop the proxy, or release its reserved listeners if it was not started. + * Null is accepted. + * + * This waits for the accept loop to exit, so the listeners and the loop's + * reference to the client are released by the time it returns. Call it + * before [`tor_client_free`]. + * + * # Safety + * `proxy` must be null or a live proxy handle returned by [`tor_start`]. + * A non-null handle must be passed to this function only once. + * It must not be called from within an async runtime. + */ void tor_proxy_stop(void *proxy); +/** + * Print a greeting to verify that the library is linked. + * + * # Safety + * This function has no additional safety requirements. + */ void tor_hello(void); +/** + * Take the current thread's last error as a newly allocated C string. + * + * # Safety + * This function has no preconditions. The returned string is owned by the + * caller and must be released exactly once with [`tor_string_free`]. + */ const char *tor_last_error_message(void); +/** + * Release a string returned by [`tor_last_error_message`]. + * + * # Safety + * `message` must be null or a pointer returned by `tor_last_error_message` + * that has not been released yet. It must not be used after this call. + */ +void tor_string_free(char *message); + +/** + * Read the current open-file limit. + * + * # Safety + * This function has no additional safety requirements. + */ uint64_t tor_get_nofile_limit(void); +/** + * Increase the open-file limit, up to the hard limit. + * + * # Safety + * This function has no additional safety requirements. + */ uint64_t tor_set_nofile_limit(uint64_t limit); + +#ifdef __cplusplus +} // extern "C" +#endif // __cplusplus diff --git a/rust/tests/header.rs b/rust/tests/header.rs new file mode 100644 index 00000000..991700e6 --- /dev/null +++ b/rust/tests/header.rs @@ -0,0 +1,52 @@ +//! Check that the public C ABI can coexist with application enum names. + +#[cfg(unix)] +#[test] +fn header_enum_names_do_not_collide() { + use std::io::Write; + use std::process::{Command, Stdio}; + + let header = std::path::Path::new(env!("TOR_FFI_PLUGIN_HEADER")); + let source = format!( + "enum ApplicationResult {{ Ok, Cancelled, TimedOut, Failed, NullClient }};\n\ + #include \"{}\"\n\ + static_assert(sizeof(TorBootstrapStatus) == sizeof(int32_t), \"ABI size\");\n\ + static_assert(TorBootstrapStatus_Ok == 0, \"ABI value\");\n\ + static_assert(TorBootstrapStatus_Cancelled == 1, \"ABI value\");\n\ + static_assert(TorBootstrapStatus_TimedOut == 2, \"ABI value\");\n\ + static_assert(TorBootstrapStatus_Failed == 3, \"ABI value\");\n\ + static_assert(TorBootstrapStatus_NullClient == 4, \"ABI value\");\n", + header.display() + ); + for (compiler, language, standard) in [("cc", "c", "c11"), ("c++", "c++", "c++11")] { + let source = if language == "c" { + source.replace("static_assert", "_Static_assert") + } else { + source.clone() + }; + let mut child = Command::new(compiler) + .args([ + "-x", + language, + &format!("-std={standard}"), + "-fsyntax-only", + "-", + ]) + .stdin(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("install a C and C++ compiler to check the public header"); + child + .stdin + .take() + .unwrap() + .write_all(source.as_bytes()) + .unwrap(); + let output = child.wait_with_output().unwrap(); + assert!( + output.status.success(), + "{language} header compilation failed:\n{}", + String::from_utf8_lossy(&output.stderr) + ); + } +} diff --git a/rust/tests/saturating_time.rs b/rust/tests/saturating_time.rs new file mode 100644 index 00000000..9b3fe3ed --- /dev/null +++ b/rust/tests/saturating_time.rs @@ -0,0 +1,32 @@ +//! tor-netdoc reads `SystemTime::min_value()` while parsing every consensus. +//! Upstream saturating-time 0.5.0 never finishes computing it on Windows, +//! where `SystemTime` has 100ns resolution, so bootstrap hung forever. + +use saturating_time::SaturatingTime; +use std::sync::mpsc; +use std::thread; +use std::time::{Duration, Instant, SystemTime}; + +#[test] +fn time_limits_are_found_on_this_platform() { + let (tx, rx) = mpsc::channel(); + thread::spawn(move || { + let limits = ( + SystemTime::min_value(), + SystemTime::max_value(), + Instant::min_value(), + Instant::max_value(), + ); + tx.send(limits).unwrap(); + }); + + let (min_time, max_time, min_instant, max_instant) = rx + .recv_timeout(Duration::from_secs(10)) + .expect("saturating-time limits did not resolve"); + + assert!(min_time < SystemTime::UNIX_EPOCH); + assert!(max_time > SystemTime::now()); + assert!(min_time.checked_sub(Duration::from_secs(1)).is_none()); + assert!(max_time.checked_add(Duration::from_secs(1)).is_none()); + assert!(min_instant < max_instant); +} diff --git a/test/source_fingerprint_test.dart b/test/source_fingerprint_test.dart new file mode 100644 index 00000000..e25de4f5 --- /dev/null +++ b/test/source_fingerprint_test.dart @@ -0,0 +1,83 @@ +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; + +import '../hook/prebuilt.dart'; + +/// A minimal package tree containing every tracked native input. +Future sourceTree() async { + final root = await Directory.systemTemp.createTemp('tor-sources-'); + addTearDown(() => root.delete(recursive: true)); + for (final path in [ + 'rust/Cargo.toml', + 'rust/Cargo.lock', + 'rust/rust-toolchain.toml', + 'rust/build.rs', + 'hook/build.dart', + 'lib/tor_ffi_plugin_bindings_generated.dart', + 'rust/src/lib.rs', + 'rust/patches/saturating-time/Cargo.toml', + 'rust/patches/saturating-time/src/lib.rs', + ]) { + final file = File.fromUri(root.uri.resolve(path)); + await file.parent.create(recursive: true); + await file.writeAsString(path); + } + return root; +} + +void main() { + test('source dependencies are reusable without calculating hashes', () async { + final root = await sourceTree(); + + final dependencies = await sourceDependencies(root.uri); + final fingerprint = await sourceFingerprint(root.uri); + + expect(dependencies, fingerprint.dependencies); + expect(dependencies, contains(root.uri.resolve('rust/src/lib.rs'))); + expect(fingerprint.hash, hasLength(64)); + }); + + test('a change to a vendored patch changes the fingerprint', () async { + final root = await sourceTree(); + const patched = 'rust/patches/saturating-time/src/lib.rs'; + final before = await sourceFingerprint(root.uri); + + expect(before.dependencies, contains(root.uri.resolve(patched))); + await File.fromUri(root.uri.resolve(patched)).writeAsString('changed'); + final after = await sourceFingerprint(root.uri); + + expect(after.hash, isNot(before.hash)); + }); + + test('CRLF line endings do not change the fingerprint', () async { + final root = await sourceTree(); + final file = File.fromUri(root.uri.resolve('rust/src/lib.rs')); + await file.writeAsBytes('a\nb\r\nc\r'.codeUnits); + final lf = await sourceFingerprint(root.uri); + await file.writeAsBytes('a\r\nb\r\nc\r'.codeUnits); + final crlf = await sourceFingerprint(root.uri); + + expect(crlf.hash, lf.hash); + }); + + test('cargo output beside a vendored patch is not an input', () async { + final root = await sourceTree(); + final before = await sourceFingerprint(root.uri); + + // What `cargo test` leaves behind when run inside the vendored crate. + const crate = 'rust/patches/saturating-time/'; + final target = Directory.fromUri(root.uri.resolve('${crate}target/debug/')); + await target.create(recursive: true); + await File.fromUri(target.uri.resolve('libsaturating_time.rlib')) + .writeAsString('built'); + await File.fromUri(root.uri.resolve('${crate}Cargo.lock')) + .writeAsString('# lock'); + final after = await sourceFingerprint(root.uri); + + expect(after.hash, before.hash); + expect(after.dependencies, isNot(contains(target.uri))); + // The package's own lockfile is still an input. + expect(after.dependencies, contains(root.uri.resolve('rust/Cargo.lock'))); + }); +} diff --git a/test/tor_lifecycle_ordering_test.dart b/test/tor_lifecycle_ordering_test.dart new file mode 100644 index 00000000..7e9dd9b4 --- /dev/null +++ b/test/tor_lifecycle_ordering_test.dart @@ -0,0 +1,835 @@ +import 'dart:async'; +import 'dart:ffi'; +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:tor_ffi_plugin/tor_ffi_plugin.dart'; + +/// Stands in for the Rust library. Client creation is immediate, while each +/// network bootstrap stays pending until the test releases or cancels it. +class FakeTorNative implements TorNative { + final pendingBootstraps = >[]; + final startDirectories = []; + final bootstrapTimeouts = []; + final liveClients = {}; + final liveProxies = {}; + final createdProxies = {}; + int maxLiveClients = 0; + int cancelCalls = 0; + int helloCalls = 0; + int freeSpawnFailures = 0; + int stopProxySpawnFailures = 0; + Object? freeError; + Completer? startGate; + Completer? freeGate; + + /// Native teardown calls in the order they completed. + final teardownOrder = []; + int _nextAddress = 0x1000; + + Future waitForBootstraps(int count) async { + while (pendingBootstraps.length < count) { + await Future.delayed(const Duration(milliseconds: 5)); + } + } + + @override + Future start(int port, String stateDir, String cacheDir) async { + startDirectories.add(stateDir); + await startGate?.future; + final client = _nextAddress += 0x10; + final proxy = _nextAddress += 0x10; + liveClients.add(client); + createdProxies.add(proxy); + if (liveClients.length > maxLiveClients) { + maxLiveClients = liveClients.length; + } + return ( + client: Pointer.fromAddress(client), + proxy: Pointer.fromAddress(proxy), + port: 19050, + ); + } + + @override + Future bootstrap(Pointer client, Duration timeout) async { + bootstrapTimeouts.add(timeout); + final gate = Completer(); + pendingBootstraps.add(gate); + await gate.future; + } + + @override + void cancelBootstrap(Pointer client) { + cancelCalls++; + for (final bootstrap in pendingBootstraps) { + if (!bootstrap.isCompleted) { + bootstrap.completeError(Exception('bootstrap cancelled')); + } + } + } + + @override + void hello() => helloCalls++; + + @override + void setDormant(Pointer client, bool softMode) {} + + @override + void startProxy(Pointer proxy) { + if (createdProxies.contains(proxy.address)) { + liveProxies.add(proxy.address); + } + } + + @override + Future stopProxy(Pointer proxy) async { + if (proxy == nullptr) { + return; + } + if (stopProxySpawnFailures > 0) { + stopProxySpawnFailures--; + throw Exception('stopProxy isolate failed to spawn'); + } + // Complete later, like the native stop does once it joins the accept loop. + await Future.delayed(Duration.zero); + liveProxies.remove(proxy.address); + createdProxies.remove(proxy.address); + teardownOrder.add('stopProxy'); + } + + @override + Future freeClient(Pointer client) async { + if (client == nullptr) { + return; + } + if (freeSpawnFailures > 0) { + freeSpawnFailures--; + throw Exception('freeClient isolate failed to spawn'); + } + // Complete later, like the native free does, so tests catch callers that + // do not wait for it. + await Future.delayed(Duration.zero); + await freeGate?.future; + if (freeError case final error?) { + throw error; + } + liveClients.remove(client.address); + teardownOrder.add('freeClient'); + } +} + +/// Start [tor] and complete its bootstrap so it reports on. +Future startAndBootstrap( + Tor tor, + FakeTorNative native, + String dir, +) async { + final started = tor.start(torDataDirPath: dir); + final index = native.pendingBootstraps.length; + await native.waitForBootstraps(index + 1); + native.pendingBootstraps[index].complete(); + await started; + expect(tor.status, TorStatus.on); +} + +/// A skip reason when this machine cannot create symlinks, otherwise false. +/// +/// Windows allows them only with Developer Mode or elevation. +final Object symlinkSkip = () { + final probe = Directory.systemTemp.createTempSync('tor-symlink-'); + final sep = Platform.pathSeparator; + try { + final target = Directory('${probe.path}${sep}target')..createSync(); + Link('${probe.path}${sep}link').createSync(target.path); + return false; + } on FileSystemException { + return 'creating symlinks needs Developer Mode or elevation'; + } finally { + probe.deleteSync(recursive: true); + } +}(); + +void main() { + late Directory dataDir; + late FakeTorNative native; + late Tor tor; + + setUp(() async { + dataDir = await Directory.systemTemp.createTemp('tor-ordering-'); + native = FakeTorNative(); + tor = Tor.withNative(native); + }); + + tearDown(() => dataDir.delete(recursive: true)); + + test('hello uses the injected native layer', () { + tor.hello(); + expect(native.helloCalls, 1); + expect(native.liveClients, isEmpty); + }); + + test('stop cancels a pending bootstrap and tears down its handles', () async { + final started = tor.start(torDataDirPath: dataDir.path); + final startFailure = expectLater(started, throwsException); + await native.waitForBootstraps(1); + + final stopped = tor.stop(); + await startFailure; + await stopped; + + expect(native.cancelCalls, 1); + expect(tor.status, TorStatus.off); + expect(native.liveClients, isEmpty); + expect(native.liveProxies, isEmpty); + }); + + test('concurrent starts wait for the same attempt', () async { + final first = tor.start(torDataDirPath: dataDir.path); + final second = tor.start(torDataDirPath: dataDir.path); + await native.waitForBootstraps(1); + + var secondDone = false; + unawaited(second.then((_) => secondDone = true)); + await Future.delayed(const Duration(milliseconds: 20)); + expect(secondDone, isFalse); + + native.pendingBootstraps[0].complete(); + await Future.wait([first, second]); + + expect(tor.status, TorStatus.on); + expect(native.pendingBootstraps, hasLength(1)); + await tor.stop(); + }); + + test('the proxy is not active until bootstrap succeeds', () async { + final started = tor.start(torDataDirPath: dataDir.path); + await native.waitForBootstraps(1); + + expect(native.liveProxies, isEmpty); + native.pendingBootstraps[0].complete(); + await started; + expect(native.liveProxies, hasLength(1)); + + await tor.stop(); + }); + + test('concurrent starts both see a failed attempt', () async { + final first = tor.start(torDataDirPath: dataDir.path); + final second = tor.start(torDataDirPath: dataDir.path); + await native.waitForBootstraps(1); + + native.pendingBootstraps[0].completeError(Exception('bootstrap failed')); + + await expectLater(first, throwsException); + await expectLater(second, throwsException); + expect(tor.status, TorStatus.off); + expect(native.pendingBootstraps, hasLength(1)); + }, timeout: const Timeout(Duration(seconds: 10))); + + test( + 'a bootstrap timeout releases handles and reaches every caller', + () async { + const timeout = Duration(seconds: 3); + final first = tor.start( + torDataDirPath: dataDir.path, + bootstrapTimeout: timeout, + ); + final second = tor.start( + torDataDirPath: dataDir.path, + bootstrapTimeout: timeout, + ); + await native.waitForBootstraps(1); + + native.pendingBootstraps[0].completeError( + const TorBootstrapTimeout( + timeout, + 'Tor bootstrap timed out after 3000 ms', + ), + ); + + final isTimeout = isA().having( + (error) => error.timeout, + 'timeout', + timeout, + ); + await expectLater(first, throwsA(isTimeout)); + await expectLater(second, throwsA(isTimeout)); + expect(tor.status, TorStatus.off); + expect(native.liveClients, isEmpty); + expect(native.liveProxies, isEmpty); + expect(native.pendingBootstraps, hasLength(1)); + }, + ); + + test('a failed bootstrap releases the native handles', () async { + final started = tor.start(torDataDirPath: dataDir.path); + await native.waitForBootstraps(1); + native.pendingBootstraps[0].completeError(Exception('bootstrap failed')); + + await expectLater(started, throwsException); + expect(tor.status, TorStatus.off); + expect(native.liveClients, isEmpty); + expect(native.liveProxies, isEmpty); + }); + + test('disable stops the proxy', () async { + final started = tor.start(torDataDirPath: dataDir.path); + await native.waitForBootstraps(1); + native.pendingBootstraps[0].complete(); + await started; + + await tor.disable(); + expect(tor.status, TorStatus.off); + expect(native.liveClients, isEmpty); + expect(native.liveProxies, isEmpty); + + // Callers that follow disable() with stop() keep working. + await tor.stop(); + }); + + test('restart during a pending start never overlaps instances', () async { + final first = tor.start(torDataDirPath: dataDir.path); + final firstFailure = expectLater(first, throwsException); + await native.waitForBootstraps(1); + + final stopped = tor.stop(); + final second = tor.start(torDataDirPath: dataDir.path); + await firstFailure; + await stopped; + + await native.waitForBootstraps(2); + native.pendingBootstraps[1].complete(); + await second; + + expect(tor.status, TorStatus.on); + expect(native.liveClients, hasLength(1)); + expect(native.maxLiveClients, 1); + + await tor.stop(); + expect(native.liveClients, isEmpty); + expect(native.liveProxies, isEmpty); + }); + + test('stop then start on a running instance restarts Tor', () async { + await startAndBootstrap(tor, native, dataDir.path); + final firstClient = native.liveClients.single; + + // Neither call is awaited before the next, as a toggle handler does. + final stopped = tor.stop(); + final restarted = tor.start(torDataDirPath: dataDir.path); + await stopped; + // The queued restart begins as soon as the stop completes. + expect(tor.status, isNot(TorStatus.on)); + + await native.waitForBootstraps(2); + native.pendingBootstraps[1].complete(); + await restarted; + + // Before the fix start() saw the stale "on" status, returned at once, and + // the queued stop then left Tor off while the caller thought it was on. + expect(tor.status, TorStatus.on); + expect(tor.port, 19050); + expect(native.liveClients.single, isNot(firstClient)); + expect(native.maxLiveClients, 1); + await tor.stop(); + }); + + test('a start queued before stop is cancelled by it', () async { + await startAndBootstrap(tor, native, dataDir.path); + + final queued = tor.start(torDataDirPath: dataDir.path); + final stopped = tor.stop(); + + await expectLater(queued, throwsA(isA())); + await stopped; + expect(tor.status, TorStatus.off); + expect(native.liveClients, isEmpty); + // Nothing was bootstrapping, so there was nothing to cancel natively. + expect(native.cancelCalls, 0); + }); + + test( + 'a stop during a reusing start cancels it instead of reporting on', + () async { + await startAndBootstrap(tor, native, dataDir.path); + + final reused = tor.start(torDataDirPath: dataDir.path); + // The queued start is now resolving the data directory, which cannot + // finish until the event loop advances beyond this microtask. + await Future.value(); + final stopped = tor.stop(); + + // Before the fix the start saw the running instance and returned success, + // and the queued stop then turned Tor off right after. + await expectLater(reused, throwsA(isA())); + await stopped; + expect(tor.status, TorStatus.off); + expect(native.liveClients, isEmpty); + expect(native.cancelCalls, 0); + }, + ); + + test('stop during failed-start cleanup leaves the client alone', () async { + final gate = native.freeGate = Completer(); + final started = tor.start(torDataDirPath: dataDir.path); + final failure = expectLater(started, throwsException); + await native.waitForBootstraps(1); + native.pendingBootstraps[0].completeError(Exception('bootstrap failed')); + while (native.createdProxies.isNotEmpty) { + await Future.delayed(const Duration(milliseconds: 5)); + } + + // The proxy is gone and the client is being freed; status is still + // starting and the handle is still set. + expect(tor.status, TorStatus.starting); + expect(native.liveClients, hasLength(1)); + final stopped = tor.stop(); + // Cancelling a bootstrap on a client that is being freed would race the + // free, so stop must not touch the handle. + expect(native.cancelCalls, 0); + + gate.complete(); + await failure; + await stopped; + expect(tor.status, TorStatus.off); + expect(native.liveClients, isEmpty); + }); + + test('a start cancelled by an immediate stop leaves Tor off', () async { + final started = tor.start(torDataDirPath: dataDir.path); + expect(tor.status, TorStatus.starting); + final stopped = tor.stop(); + + await expectLater(started, throwsA(isA())); + await stopped; + expect(tor.status, TorStatus.off); + expect(native.startDirectories, isEmpty); + }); + + test('a running instance rejects a different data directory', () async { + await startAndBootstrap(tor, native, dataDir.path); + final other = await Directory.systemTemp.createTemp('tor-ordering-other-'); + addTearDown(() => other.delete(recursive: true)); + + await expectLater( + tor.start(torDataDirPath: other.path), + throwsA(isA()), + ); + + // The rejection must not tear down the running instance. + expect(tor.status, TorStatus.on); + expect(native.liveClients, hasLength(1)); + expect(native.startDirectories, hasLength(1)); + await tor.stop(); + }); + + test('status stays on until the native client is released', () async { + await startAndBootstrap(tor, native, dataDir.path); + final gate = native.freeGate = Completer(); + + final stopped = tor.stop(); + while (native.liveProxies.isNotEmpty) { + await Future.delayed(const Duration(milliseconds: 5)); + } + + // The proxy is gone but the client, and so its files, is still live. + expect(native.liveClients, hasLength(1)); + expect(tor.status, TorStatus.on); + expect( + () => tor.port, + throwsA( + isA().having( + (e) => e.toString(), + 'message', + 'Exception: Tor proxy is not running', + ), + ), + ); + await expectLater( + tor.setClientDormant(true), + throwsA(isA()), + ); + + gate.complete(); + await stopped; + expect(tor.status, TorStatus.off); + expect(native.liveClients, isEmpty); + }); + + test('a failed free is retried by the next stop', () async { + await startAndBootstrap(tor, native, dataDir.path); + native.freeSpawnFailures = 1; + + await expectLater(tor.stop(), throwsException); + expect(tor.status, TorStatus.on); + expect(native.liveClients, hasLength(1)); + await tor.stop(); + expect(tor.status, TorStatus.off); + expect(native.liveClients, isEmpty); + }); + + test('a failed free is released before the next start', () async { + await startAndBootstrap(tor, native, dataDir.path); + native.freeSpawnFailures = 1; + + await expectLater(tor.stop(), throwsException); + expect(tor.status, TorStatus.on); + expect(native.liveClients, hasLength(1)); + await startAndBootstrap(tor, native, dataDir.path); + expect(native.maxLiveClients, 1); + expect(native.startDirectories, hasLength(2)); + await tor.stop(); + }); + + test('a failed proxy stop is retried', () async { + await startAndBootstrap(tor, native, dataDir.path); + native.stopProxySpawnFailures = 1; + + await expectLater(tor.stop(), throwsException); + expect(tor.status, TorStatus.on); + expect(native.liveProxies, hasLength(1)); + expect(native.liveClients, hasLength(1)); + expect(native.teardownOrder, isEmpty); + await tor.stop(); + expect(native.liveProxies, isEmpty); + expect(native.liveClients, isEmpty); + expect(native.teardownOrder, ['stopProxy', 'freeClient']); + expect(tor.status, TorStatus.off); + }); + + test('a failed proxy stop is released before the next start', () async { + await startAndBootstrap(tor, native, dataDir.path); + native.stopProxySpawnFailures = 1; + await expectLater(tor.stop(), throwsException); + expect(native.liveClients, hasLength(1)); + await startAndBootstrap(tor, native, dataDir.path); + expect(native.maxLiveClients, 1); + expect(native.startDirectories, hasLength(2)); + expect(native.teardownOrder, ['stopProxy', 'freeClient']); + await tor.stop(); + }); + + test('failed startup cleanup never reports on and is retried', () async { + native.freeSpawnFailures = 1; + final started = tor.start(torDataDirPath: dataDir.path); + final failed = expectLater(started, throwsException); + await native.waitForBootstraps(1); + native.pendingBootstraps[0].completeError(Exception('bootstrap failed')); + await failed; + expect(tor.status, TorStatus.starting); + expect(native.liveClients, hasLength(1)); + await startAndBootstrap(tor, native, dataDir.path); + expect(native.maxLiveClients, 1); + expect(native.startDirectories, hasLength(2)); + await tor.stop(); + }); + + test('stop releases the proxy before the client', () async { + await startAndBootstrap(tor, native, dataDir.path); + + await tor.stop(); + + expect(native.teardownOrder, ['stopProxy', 'freeClient']); + }); + + test( + 'a sub-millisecond timeout is rejected before the native start', + () async { + await expectLater( + tor.start( + torDataDirPath: dataDir.path, + bootstrapTimeout: const Duration(microseconds: 500), + ), + throwsA(isA()), + ); + + expect(native.startDirectories, isEmpty); + expect(tor.status, TorStatus.off); + }, + ); + + test('trailing separators do not change the data directory', () async { + final slashed = '${dataDir.path}${Platform.pathSeparator}'; + final first = tor.start(torDataDirPath: slashed); + final second = tor.start(torDataDirPath: dataDir.path); + await native.waitForBootstraps(1); + + native.pendingBootstraps[0].complete(); + await Future.wait([first, second]); + + expect(native.startDirectories, hasLength(1)); + await tor.start(torDataDirPath: slashed); + expect(tor.status, TorStatus.on); + await tor.stop(); + }); + + test('a symlink and its target share one start', () async { + final sep = Platform.pathSeparator; + final real = Directory('${dataDir.path}${sep}real')..createSync(); + final link = Link('${dataDir.path}${sep}link')..createSync(real.path); + + final first = tor.start(torDataDirPath: link.path); + final second = tor.start(torDataDirPath: real.path); + await native.waitForBootstraps(1); + native.pendingBootstraps[0].complete(); + await Future.wait([first, second]); + + expect(native.startDirectories, hasLength(1)); + await tor.stop(); + }, skip: symlinkSkip); + + test( + 'a parent segment after a symlink follows the link', + () async { + final sep = Platform.pathSeparator; + final target = Directory('${dataDir.path}${sep}a${sep}target') + ..createSync(recursive: true); + Link('${dataDir.path}${sep}link').createSync(target.path); + + // Lexically this is dataDir/sibling; the file system resolves it to + // dataDir/a/sibling. + await startAndBootstrap( + tor, + native, + '${dataDir.path}${sep}link$sep..${sep}sibling', + ); + + final physical = Directory('${dataDir.path}${sep}a${sep}sibling') + .resolveSymbolicLinksSync(); + expect(native.startDirectories[0], '$physical/tor_state'); + expect(Directory('${dataDir.path}${sep}sibling').existsSync(), isFalse); + await tor.stop(); + }, + skip: Platform.isWindows ? 'Windows resolves .. before links' : symlinkSkip, + ); + + test('on Windows a parent segment after a symlink is lexical', () async { + final sep = Platform.pathSeparator; + final target = Directory('${dataDir.path}${sep}a${sep}target') + ..createSync(recursive: true); + Link('${dataDir.path}${sep}link').createSync(target.path); + + // Win32 normalizes the path before the file system sees the link. + await startAndBootstrap( + tor, + native, + '${dataDir.path}${sep}link$sep..${sep}sibling', + ); + + final physical = Directory('${dataDir.path}${sep}sibling') + .resolveSymbolicLinksSync(); + expect(native.startDirectories[0], '$physical/tor_state'); + expect( + Directory('${dataDir.path}${sep}a${sep}sibling').existsSync(), + isFalse, + ); + await tor.stop(); + }, skip: Platform.isWindows ? symlinkSkip : 'Win32 path semantics'); + + test('an unusable data directory fails while it is resolved', () async { + final sep = Platform.pathSeparator; + final file = File('${dataDir.path}${sep}file')..createSync(); + + // The error names the data directory itself, not a subdirectory that a + // queued start failed to create. + await expectLater( + tor.start(torDataDirPath: file.path), + throwsA( + isA().having((e) => e.path, 'path', file.path), + ), + ); + + expect(tor.status, TorStatus.off); + expect(native.startDirectories, isEmpty); + // The failure is not left behind as a pending start. + await startAndBootstrap(tor, native, dataDir.path); + await tor.stop(); + }); + + test('a pending start rejects a different data directory', () async { + final first = tor.start(torDataDirPath: dataDir.path); + await native.waitForBootstraps(1); + final other = await Directory.systemTemp.createTemp('tor-ordering-other-'); + addTearDown(() => other.delete(recursive: true)); + + final second = tor.start(torDataDirPath: other.path); + final secondFailure = expectLater(second, throwsA(isA())); + native.pendingBootstraps[0].complete(); + await first; + await secondFailure; + + expect(tor.status, TorStatus.on); + expect(native.startDirectories, hasLength(1)); + await tor.stop(); + }); + + test( + 'directory preparation reports starting and preserves running status', + () async { + final started = tor.start(torDataDirPath: dataDir.path); + expect(tor.status, TorStatus.starting); + // The queued start has begun, but filesystem I/O cannot finish until the + // event loop advances beyond this microtask. + await Future.value(); + expect(tor.status, TorStatus.starting); + expect(native.startDirectories, isEmpty); + await native.waitForBootstraps(1); + native.pendingBootstraps[0].complete(); + await started; + + final reused = tor.start(torDataDirPath: dataDir.path); + await Future.value(); + expect(tor.status, TorStatus.on); + await reused; + expect(native.startDirectories, hasLength(1)); + await tor.stop(); + }, + ); + + test('start defers data directory creation to the queue', () async { + final fresh = Directory('${dataDir.path}/fresh'); + final started = tor.start(torDataDirPath: fresh.path); + expect(fresh.existsSync(), isFalse); + await native.waitForBootstraps(1); + native.pendingBootstraps[0].complete(); + await started; + await tor.stop(); + }); + + test( + 'a start with a different timeout reuses the running instance', + () async { + final first = tor.start(torDataDirPath: dataDir.path); + final second = tor.start( + torDataDirPath: dataDir.path, + bootstrapTimeout: const Duration(seconds: 17), + ); + final both = Future.wait([first, second]); + await native.waitForBootstraps(1); + native.pendingBootstraps[0].complete(); + await both; + + expect(native.bootstrapTimeouts, hasLength(1)); + expect(tor.status, TorStatus.on); + await tor.stop(); + }, + ); + + test('stop cancels every queued start', () async { + final gate = native.startGate = Completer(); + final first = tor.start(torDataDirPath: dataDir.path); + final firstFailure = expectLater(first, throwsA(isA())); + while (native.startDirectories.isEmpty) { + await Future.delayed(const Duration(milliseconds: 5)); + } + final second = tor.start( + torDataDirPath: '${dataDir.path}${Platform.pathSeparator}', + ); + final secondFailure = expectLater( + second.timeout(const Duration(seconds: 2)), + throwsA(isA()), + ); + final stopped = tor.stop(); + gate.complete(); + await firstFailure; + await secondFailure; + await stopped; + + expect(tor.status, TorStatus.off); + expect(native.liveClients, isEmpty); + expect(native.startDirectories, hasLength(1)); + }); + + test('cleanup failure does not replace the bootstrap failure', () async { + native.freeError = Exception('cleanup failed'); + final started = tor.start(torDataDirPath: dataDir.path); + await native.waitForBootstraps(1); + native.pendingBootstraps[0].completeError(Exception('bootstrap failed')); + + await expectLater( + started, + throwsA( + isA().having( + (error) => error.toString(), + 'message', + contains('bootstrap failed'), + ), + ), + ); + expect(tor.status, TorStatus.starting); + expect(native.freeError, same(tor.lastCleanupError)); + expect(tor.lastCleanupStackTrace, isNotNull); + }); + + for (final loggingThrows in [false, true]) { + test( + 'cleanup failure is reported without masking the typed start error (logging throws: $loggingThrows)', + () async { + final messages = []; + const primary = TorBootstrapTimeout( + Duration(seconds: 1), + 'deadline passed', + ); + native.freeSpawnFailures = 1; + final started = runZoned( + () => tor.start(torDataDirPath: dataDir.path), + zoneSpecification: ZoneSpecification( + print: (self, parent, zone, line) { + messages.add(line); + if (loggingThrows) throw StateError('log sink failed'); + }, + ), + ); + final failure = expectLater(started, throwsA(same(primary))); + await native.waitForBootstraps(1); + native.pendingBootstraps[0].completeError(primary); + await failure; + expect(messages, hasLength(1)); + expect(messages.single, contains('freeClient isolate failed to spawn')); + expect(tor.lastCleanupError, isNotNull); + expect(tor.lastCleanupStackTrace, isNotNull); + await tor.stop(); + expect(tor.status, TorStatus.off); + }, + ); + } + + test( + 'stop before client creation prevents bootstrap from starting', + () async { + final gate = native.startGate = Completer(); + final started = tor.start(torDataDirPath: dataDir.path); + final startFailure = expectLater( + started, + throwsA(isA()), + ); + while (native.startDirectories.isEmpty) { + await Future.delayed(const Duration(milliseconds: 5)); + } + + final stopped = tor.stop(); + gate.complete(); + await startFailure; + await stopped; + + expect(native.pendingBootstraps, isEmpty); + expect(native.liveClients, isEmpty); + expect(native.liveProxies, isEmpty); + }, + ); + + test('forwards a finite bootstrap timeout', () async { + const timeout = Duration(seconds: 17); + final started = tor.start( + torDataDirPath: dataDir.path, + bootstrapTimeout: timeout, + ); + await native.waitForBootstraps(1); + + expect(native.bootstrapTimeouts, [timeout]); + native.pendingBootstraps[0].complete(); + await started; + await tor.stop(); + }); +} diff --git a/test/tor_lifecycle_test.dart b/test/tor_lifecycle_test.dart new file mode 100644 index 00000000..b57f1a7c --- /dev/null +++ b/test/tor_lifecycle_test.dart @@ -0,0 +1,144 @@ +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:tor_ffi_plugin/tor_ffi_plugin.dart'; + +Future expectSocksHandshake(int port) async { + final socket = await Socket.connect( + InternetAddress.loopbackIPv4, + port, + timeout: const Duration(seconds: 10), + ); + try { + socket.add([5, 1, 0]); + final response = await socket + .expand((chunk) => chunk) + .take(2) + .toList() + .timeout(const Duration(seconds: 10)); + expect(response, [5, 0]); + } finally { + socket.destroy(); + } +} + +void main() { + test('stop is idempotent while off', () async { + final tor = Tor.instance; + await tor.stop(); + await tor.stop(); + expect(tor.status, TorStatus.off); + expect(() => tor.port, throwsException); + await expectLater( + tor.setClientDormant(true), + throwsA(isA()), + ); + }); + + test('native start reports an occupied port', () async { + final occupied = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0); + addTearDown(occupied.close); + final dir = await Directory.systemTemp.createTemp('tor-busy-port-'); + addTearDown(() => dir.delete(recursive: true)); + + // Fails before storage or network are touched, and runs the error string + // and path string cleanup against the real library. + await expectLater( + const TorNative().start( + occupied.port, + '${dir.path}/state', + '${dir.path}/cache', + ), + throwsA( + isA().having( + (e) => e.toString(), + 'message', + contains("Can't listen on"), + ), + ), + ); + }); + + group('native bootstrap status', () { + const native = TorNative(); + Directory? dir; + TorHandles? handles; + + setUp(() async { + dir = null; + handles = null; + dir = await Directory.systemTemp.createTemp('tor-bootstrap-status-'); + // Creating the client does not touch the network. + handles = await native.start( + 0, + '${dir!.path}/state', + '${dir!.path}/cache', + ); + }); + + tearDown(() async { + final createdHandles = handles; + if (createdHandles != null) { + await native.stopProxy(createdHandles.proxy); + await native.freeClient(createdHandles.client); + } + final createdDir = dir; + if (createdDir != null) { + await createdDir.delete(recursive: true); + } + }); + + test('a passed deadline throws TorBootstrapTimeout', () async { + // Not a whole number of milliseconds: the exception must carry the + // duration as passed, not the rounded native deadline. + const timeout = Duration(microseconds: 1500); + await expectLater( + native.bootstrap(handles!.client, timeout), + throwsA( + isA() + .having((e) => e.timeout, 'timeout', timeout) + .having((e) => e.message, 'message', contains('timed out')), + ), + ); + }); + + test('a cancelled bootstrap throws TorStartCancelled', () async { + native.cancelBootstrap(handles!.client); + await expectLater( + native.bootstrap(handles!.client, const Duration(minutes: 1)), + throwsA(isA()), + ); + }); + }); + + test( + 'stop resets state and allows a fresh start', + () async { + final directory = await Directory.systemTemp.createTemp('tor-lifecycle-'); + final tor = Tor.instance; + try { + for (var attempt = 0; attempt < 2; attempt++) { + await tor.start(torDataDirPath: directory.path); + expect(tor.status, TorStatus.on); + await expectSocksHandshake(tor.port); + await tor.setClientDormant(true); + await tor.setClientDormant(false); + await expectSocksHandshake(tor.port); + await tor.stop(); + expect(tor.status, TorStatus.off); + expect(() => tor.port, throwsException); + await expectLater( + tor.setClientDormant(true), + throwsA(isA()), + ); + await tor.stop(); + } + } finally { + await tor.stop(); + await directory.delete(recursive: true); + } + }, + skip: !const bool.fromEnvironment('TOR_NETWORK_TESTS'), + timeout: const Timeout(Duration(minutes: 5)), + ); +} diff --git a/tool/ci/check_macos_app.sh b/tool/ci/check_macos_app.sh new file mode 100644 index 00000000..b6374da4 --- /dev/null +++ b/tool/ci/check_macos_app.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# Check that the packaged macOS example's library exports and loads. +set -euo pipefail +cd "$(dirname "$0")/../.." +library=$(find -L example/build/macos/Build/Products/Release \ + -type f \ + -path '*/Contents/Frameworks/tor_ffi_plugin.framework/tor_ffi_plugin' \ + -print -quit) +test -n "$library" +nm -gj "$library" | grep -Fx _tor_proxy_stop +python3 tool/ci/smoke_native.py "$library" diff --git a/tool/ci/install_rust.sh b/tool/ci/install_rust.sh new file mode 100644 index 00000000..22710520 --- /dev/null +++ b/tool/ci/install_rust.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# Install the repository toolchain, with caller-selected components/targets. +set -euo pipefail +cd "$(dirname "$0")/../.." +toolchain=$(sed -n 's/^channel = "\([^"]*\)".*/\1/p' rust/rust-toolchain.toml) +test -n "$toolchain" +rustup toolchain install "$toolchain" --profile minimal "$@" +echo "RUSTUP_TOOLCHAIN=$toolchain" >> "$GITHUB_ENV" diff --git a/tool/ci/smoke_native.py b/tool/ci/smoke_native.py new file mode 100644 index 00000000..ec7cd6ea --- /dev/null +++ b/tool/ci/smoke_native.py @@ -0,0 +1,11 @@ +"""Load an exact artifact and check its null-safe proxy-stop entry point.""" + +import ctypes +import sys +from pathlib import Path + +library = ctypes.CDLL(str(Path(sys.argv[1]).resolve(strict=True))) +library.tor_proxy_stop.argtypes = [ctypes.c_void_p] +library.tor_proxy_stop.restype = None +library.tor_proxy_stop(None) +print(f"Loaded native library: {sys.argv[1]}") diff --git a/tool/ci/test_windows.sh b/tool/ci/test_windows.sh new file mode 100644 index 00000000..edd9dd6d --- /dev/null +++ b/tool/ci/test_windows.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# Run the Windows-only time, IPv6, and file release tests for $NATIVE_TARGET. +set -euo pipefail +cd "$(dirname "$0")/../.." +cargo test --locked --manifest-path rust/Cargo.toml --target "$NATIVE_TARGET" --test saturating_time +cargo test --locked --manifest-path rust/Cargo.toml --target "$NATIVE_TARGET" --lib -- ipv6 closes_its_files diff --git a/tool/ci/windows_paths.ps1 b/tool/ci/windows_paths.ps1 new file mode 100644 index 00000000..0f746b68 --- /dev/null +++ b/tool/ci/windows_paths.ps1 @@ -0,0 +1,8 @@ +# Point cargo, target, and temp directories at short paths on Windows runners. +$ErrorActionPreference = 'Stop' +git config --global core.longpaths true +New-Item -ItemType Directory -Force C:\c, C:\t, C:\tmp | Out-Null +"CARGO_HOME=C:/c" >> $env:GITHUB_ENV +"CARGO_TARGET_DIR=C:/t" >> $env:GITHUB_ENV +"TEMP=C:\tmp" >> $env:GITHUB_ENV +"TMP=C:\tmp" >> $env:GITHUB_ENV