commit 360ecbdad0705368cc17b240fa100a8ebfd47e28 Author: Jason Tudisco Date: Thu Mar 12 10:32:04 2026 -0600 Initial commit: CAN Service + examples (can-sync v1, canfs, filemanager, paste) CAN Service: content-addressable storage with HTTP API, SQLite metadata, file-based blob storage, thumbnail generation, and integrity verification. can-sync v1: P2P sync sidecar using iroh-docs for encrypted peer-to-peer replication with library/filter-based selective sync. Fully builds but being superseded by v2 (simplified full-mirror approach). Co-Authored-By: Claude Opus 4.6 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..affcc8d --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Build artifacts +/target/ +examples/*/target/ + +# Data files (runtime-generated) +can_data/ +*.db +*.db-shm +*.db-wal +can_sync_data/ +examples/can-sync/can_sync_data/ + +# IDE / Editor +.vscode/ +.idea/ +*.swp +*.swo + +# OS files +.DS_Store +Thumbs.db + +# Claude local settings +.claude/ diff --git a/API.md b/API.md new file mode 100644 index 0000000..fbcfc53 --- /dev/null +++ b/API.md @@ -0,0 +1,483 @@ +# CAN Service API Reference + +**Base URL:** `http://localhost:3210` +**API Prefix:** `/api/v1/can/0` +**Content-Type:** `application/json` (all responses are JSON) + +> The `0` in the path is the `can_id`. MVP supports only container `0`. + +--- + +## Quick Start + +```bash +# Start the server +cargo run + +# Store a file +curl -X POST http://localhost:3210/api/v1/can/0/ingest \ + -F "file=@photo.jpg" \ + -F "tags=vacation,beach" \ + -F "description=Summer trip" + +# Store data (no file needed) +curl -X POST http://localhost:3210/api/v1/can/0/ingest/data \ + -H "Content-Type: application/json" \ + -d '{"data": {"sensor": "temp", "value": 22.5}, "tags": "iot,sensor"}' + +# List everything +curl http://localhost:3210/api/v1/can/0/list + +# Search by tag +curl "http://localhost:3210/api/v1/can/0/search?tags=vacation" +``` + +--- + +## Response Envelope + +All responses use a standard wrapper: + +```json +// Success +{ + "status": "success", + "data": { ... } +} + +// Error +{ + "status": "error", + "error": "Human-readable error message" +} +``` + +**HTTP Status Codes:** +| Code | Meaning | +|------|---------| +| 200 | Success | +| 400 | Bad request (missing/invalid parameters) | +| 404 | Asset not found | +| 500 | Internal error / corrupted asset | + +--- + +## Endpoints + +### 1. Ingest File (Multipart) + +Upload a binary file with optional metadata. + +``` +POST /api/v1/can/0/ingest +Content-Type: multipart/form-data +``` + +**Form Fields:** + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `file` | Binary | **Yes** | The file to store | +| `mime_type` | String | No | Override MIME type (auto-detected from filename if omitted) | +| `human_file_name` | String | No | Logical filename (e.g. `report.pdf`) | +| `human_readable_path` | String | No | Logical folder path (e.g. `/docs/reports/`) | +| `application` | String | No | Application that produced this file | +| `user` | String | No | User or agent identity | +| `tags` | String | No | Comma-separated tags (e.g. `finance,Q4,report`) | +| `description` | String | No | Human-readable description | + +**Response:** + +```json +{ + "status": "success", + "data": { + "timestamp": 1773014400123, + "hash": "a3b2c4d5e6f7...", + "filename": "1773014400123_a3b2c4d5e6f7_finance_Q4.pdf" + } +} +``` + +**Example:** + +```bash +curl -X POST http://localhost:3210/api/v1/can/0/ingest \ + -F "file=@quarterly_report.pdf" \ + -F "application=WebUI" \ + -F "user=jason" \ + -F "tags=finance,Q4,report" \ + -F "description=Q4 2025 financial report" \ + -F "human_file_name=quarterly_report.pdf" \ + -F "human_readable_path=/finance/reports/" +``` + +--- + +### 2. Ingest Data (JSON) + +Store any JSON value directly -- no multipart needed. Designed for agents and programmatic use. + +``` +POST /api/v1/can/0/ingest/data +Content-Type: application/json +``` + +**JSON Body:** + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `data` | Any JSON | **Yes** | -- | The payload to store. Object, array, string, number, boolean, or null. | +| `mime_type` | String | No | `application/json` | Override MIME type (e.g. `text/plain` to store as `.txt`) | +| `human_file_name` | String | No | | Logical filename | +| `human_readable_path` | String | No | | Logical folder path | +| `application` | String | No | | Application/agent that produced this data | +| `user` | String | No | | User or agent identity | +| `tags` | String | No | | Comma-separated tags | +| `description` | String | No | | Human-readable description | + +The `data` field is serialized to pretty-printed JSON and stored as a `.json` file (or `.txt` etc. if you override `mime_type`). + +**Response:** Same as file ingest. + +**Examples:** + +```bash +# Minimal -- just dump an object +curl -X POST http://localhost:3210/api/v1/can/0/ingest/data \ + -H "Content-Type: application/json" \ + -d '{"data": {"key": "value"}}' + +# Agent saving structured output +curl -X POST http://localhost:3210/api/v1/can/0/ingest/data \ + -H "Content-Type: application/json" \ + -d '{ + "data": { + "agent_id": "planner-v2", + "session": "abc-123", + "steps": ["research", "outline", "draft"] + }, + "application": "AgentOrchestrator", + "user": "planner_agent", + "tags": "agent,plan,session", + "description": "Planning output for session abc-123", + "human_file_name": "plan_output.json", + "human_readable_path": "/agents/planner/" + }' + +# Store a plain string +curl -X POST http://localhost:3210/api/v1/can/0/ingest/data \ + -H "Content-Type: application/json" \ + -d '{"data": "Log: task completed at 14:30", "tags": "log"}' + +# Store an array +curl -X POST http://localhost:3210/api/v1/can/0/ingest/data \ + -H "Content-Type: application/json" \ + -d '{"data": [1, 2, 3, "four"], "tags": "test"}' + +# Store as plain text instead of JSON +curl -X POST http://localhost:3210/api/v1/can/0/ingest/data \ + -H "Content-Type: application/json" \ + -d '{"data": "plain text content", "mime_type": "text/plain"}' +``` + +--- + +### 3. Retrieve Asset + +Download the physical file by its hash. + +``` +GET /api/v1/can/0/asset/{hash} +``` + +**Path Parameters:** + +| Param | Type | Description | +|-------|------|-------------| +| `hash` | String | The SHA-256 hash returned from ingest | + +**Response:** Raw file bytes with headers: +- `Content-Type` set to the asset's MIME type +- `Content-Disposition: attachment; filename=""` + +Returns `500` if the asset is flagged as corrupted. + +**Example:** + +```bash +curl -o output.pdf http://localhost:3210/api/v1/can/0/asset/a3b2c4d5e6f7... +``` + +--- + +### 4. Get Asset Metadata + +Retrieve all metadata for an asset without downloading the file. + +``` +GET /api/v1/can/0/asset/{hash}/meta +``` + +**Response:** + +```json +{ + "status": "success", + "data": { + "hash": "a3b2c4d5e6f7...", + "mime_type": "application/pdf", + "application": "WebUI", + "user": "jason", + "tags": ["finance", "Q4", "report"], + "description": "Q4 2025 financial report", + "human_filename": "quarterly_report.pdf", + "human_path": "/finance/reports/", + "timestamp": 1773014400123, + "is_trashed": false, + "is_corrupted": false + } +} +``` + +--- + +### 5. Update Metadata + +Modify tags and/or description for an existing asset. The physical file is unchanged. + +``` +PATCH /api/v1/can/0/asset/{hash} +Content-Type: application/json +``` + +**JSON Body:** + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `tags` | String[] | No | New tag list (replaces all existing tags) | +| `description` | String | No | New description | + +Both fields are optional. Only provided fields are updated. + +**Example:** + +```bash +curl -X PATCH http://localhost:3210/api/v1/can/0/asset/a3b2c4d5e6f7... \ + -H "Content-Type: application/json" \ + -d '{ + "tags": ["finance", "Q4", "report", "reviewed"], + "description": "Q4 2025 report - reviewed by CFO" + }' +``` + +**Response:** + +```json +{ + "status": "success", + "data": "updated" +} +``` + +--- + +### 6. List Assets + +Paginated listing of all assets with optional filtering. + +``` +GET /api/v1/can/0/list +``` + +**Query Parameters:** + +| Param | Type | Default | Description | +|-------|------|---------|-------------| +| `limit` | Integer | `50` | Page size | +| `offset` | Integer | `0` | Starting position | +| `offset_time` | Integer | -- | Epoch ms cursor. Lists items strictly before/after this timestamp (based on `order`). Faster than offset for large datasets. | +| `order` | String | `desc` | Sort direction: `asc` or `desc` (by timestamp) | +| `application` | String | -- | Filter to assets from this application | +| `include_trashed` | Boolean | `false` | Include soft-deleted assets | +| `include_corrupted` | Boolean | `false` | Include corrupted assets | + +**Response:** + +```json +{ + "status": "success", + "data": { + "items": [ + { + "hash": "a3b2...", + "mime_type": "application/pdf", + "application": "WebUI", + "user": "jason", + "tags": ["finance"], + "description": "...", + "human_filename": "report.pdf", + "human_path": "/docs/", + "timestamp": 1773014400123, + "is_trashed": false, + "is_corrupted": false + } + ], + "pagination": { + "limit": 50, + "offset": 0, + "total": 142 + } + } +} +``` + +**Examples:** + +```bash +# First page, 10 items +curl "http://localhost:3210/api/v1/can/0/list?limit=10" + +# Second page +curl "http://localhost:3210/api/v1/can/0/list?limit=10&offset=10" + +# Only assets from a specific app, oldest first +curl "http://localhost:3210/api/v1/can/0/list?application=IoTAgent&order=asc" + +# Include trashed assets +curl "http://localhost:3210/api/v1/can/0/list?include_trashed=true" +``` + +--- + +### 7. Search Assets + +Search with multiple filters. All filters are AND-combined. + +``` +GET /api/v1/can/0/search +``` + +**Query Parameters:** + +| Param | Type | Default | Description | +|-------|------|---------|-------------| +| `hash` | String | -- | Exact hash or prefix match (e.g. `a3b2` matches `a3b2c4d5...`) | +| `start_time` | Integer | -- | Epoch ms lower bound (inclusive) | +| `end_time` | Integer | -- | Epoch ms upper bound (inclusive) | +| `tags` | String | -- | Comma-separated. AND logic: asset must have **all** specified tags. | +| `mime_type` | String | -- | Exact MIME type match (e.g. `image/jpeg`) | +| `user` | String | -- | Exact user/agent identity match | +| `application` | String | -- | Exact application match | +| `limit` | Integer | `50` | Page size | +| `offset` | Integer | `0` | Starting position | +| `order` | String | `desc` | Sort direction: `asc` or `desc` | +| `include_trashed` | Boolean | `false` | Include soft-deleted assets | +| `include_corrupted` | Boolean | `false` | Include corrupted assets | + +**Response:** Same structure as List (items + pagination). + +**Examples:** + +```bash +# Find by hash prefix +curl "http://localhost:3210/api/v1/can/0/search?hash=a3b2" + +# All JPEG images from last 24 hours +curl "http://localhost:3210/api/v1/can/0/search?mime_type=image/jpeg&start_time=1773014400000" + +# Assets tagged with BOTH "sensor" AND "temperature" +curl "http://localhost:3210/api/v1/can/0/search?tags=sensor,temperature" + +# Everything from a specific agent +curl "http://localhost:3210/api/v1/can/0/search?application=PlannerAgent&user=agent_v2" + +# Combine filters +curl "http://localhost:3210/api/v1/can/0/search?tags=report&application=WebUI&order=asc&limit=5" +``` + +--- + +### 8. Get Thumbnail + +Generate a resized thumbnail for image assets. Non-image assets return a placeholder SVG. + +``` +GET /api/v1/can/0/asset/{hash}/thumb/{max_width}/{max_height} +``` + +**Path Parameters:** + +| Param | Type | Description | +|-------|------|-------------| +| `hash` | String | Asset hash | +| `max_width` | Integer | Maximum width in pixels | +| `max_height` | Integer | Maximum height in pixels | + +Aspect ratio is always preserved. The image fits within the `max_width x max_height` box. + +**Response:** +- **Image assets:** JPEG bytes (`Content-Type: image/jpeg`). Cached in `.thumbs/` if enabled. +- **Non-image assets:** SVG placeholder icon (`Content-Type: image/svg+xml`). + +**Example:** + +```bash +# 200x200 thumbnail +curl -o thumb.jpg http://localhost:3210/api/v1/can/0/asset/a3b2.../thumb/200/200 +``` + +--- + +## Configuration + +`config.yaml` at the project root (or pass a custom path as the first CLI argument): + +```yaml +storage_root: "/var/lib/can_data" # Where files are stored +admin_token: "super_secret_rebuild" # Bearer token for admin operations +enable_thumbnail_cache: true # Cache thumbnails in .thumbs/ +rebuild_error_threshold: 50 # Error tolerance before hard rebuild +verify_interval_hours: 12 # Hours between integrity scrubs +``` + +--- + +## Concepts + +### Hash + +Every asset gets a unique SHA-256 hash computed as `SHA256(timestamp_be_bytes + file_content)`. This hash is the primary identifier used in all API calls. Because the timestamp is mixed in, even identical file content produces different hashes at different times. + +### Physical Filename + +Files are stored as: `{timestamp}_{hash}_{tags}.{extension}` + +For example: `1773014400123_a3b2c4d5e6f7_finance_Q4.pdf` + +This naming allows offline integrity verification -- you can recompute the hash from the timestamp and file contents and compare it to the filename. + +### Tags + +Tags are comma-separated strings. On ingest and in search, pass them as a single string: `"finance,Q4,report"`. In metadata responses, they come back as an array: `["finance", "Q4", "report"]`. + +When searching, tag matching uses AND logic: `?tags=finance,Q4` finds only assets that have **both** tags. + +When patching, tags are **replaced** entirely (not merged). + +### Integrity Verification + +A background verifier runs automatically: +1. **On startup:** Scrubs all assets against their hashes. +2. **On file change:** Watches the storage directory for modifications. +3. **Periodically:** Every `verify_interval_hours`. + +Corrupted assets are flagged (`is_corrupted: true`) and excluded from standard list/search results unless `include_corrupted=true` is passed. Retrieving a corrupted asset via GET returns a 500 error. + +### OS File Attributes + +Critical metadata is written to the host OS as file attributes for disaster recovery: +- **Linux/macOS:** Extended attributes (`xattr`) under the `user.can.*` namespace +- **Windows:** NTFS Alternate Data Streams (`:can.*`) + +This means the SQLite database can be rebuilt from scratch by scanning the storage directory. diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..81e5a5b --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2630 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "axum" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "multer", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "can-service" +version = "0.1.0" +dependencies = [ + "anyhow", + "axum", + "chrono", + "hex", + "image", + "mime", + "mime_guess", + "notify", + "reqwest", + "rusqlite", + "serde", + "serde_json", + "serde_yaml", + "sha2", + "tempfile", + "thiserror", + "tokio", + "tokio-test", + "tokio-util", + "tower-http", + "tracing", + "tracing-subscriber", + "xattr", +] + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "filetime" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +dependencies = [ + "cfg-if", + "libc", + "libredox", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "gif" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5df2ba84018d80c213569363bdcd0c64e6933c67fe4c1d60ecf822971a3c35e" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "image" +version = "0.25.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "gif", + "image-webp", + "moxcms", + "num-traits", + "png", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "inotify" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd168d97690d0b8c412d6b6c10360277f4d7ee495c5d0d5d5fe0854923255cc" +dependencies = [ + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "kqueue" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + +[[package]] +name = "libredox" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" +dependencies = [ + "bitflags 2.11.0", + "libc", + "plain", + "redox_syscall 0.7.3", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "moxcms" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "multer" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http", + "httparse", + "memchr", + "mime", + "spin", + "version_check", +] + +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "notify" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c533b4c39709f9ba5005d8002048266593c1cfaf3c5f0739d5b8ab0c6c504009" +dependencies = [ + "bitflags 2.11.0", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "notify-types", + "walkdir", + "windows-sys 0.52.0", +] + +[[package]] +name = "notify-types" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585d3cb5e12e01aed9e8a1f70d5c6b5e86fe2a6e48fc8cd0b3e0b8df6f6eb174" +dependencies = [ + "instant", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "openssl" +version = "0.10.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +dependencies = [ + "bitflags 2.11.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.11.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pxfm" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d" + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "redox_syscall" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "mime_guess", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rusqlite" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" +dependencies = [ + "bitflags 2.11.0", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.11.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-test" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6d24790a10a7af737693a3e8f1d03faef7e6ca0cc99aae5066f533766de545" +dependencies = [ + "futures-core", + "tokio", + "tokio-stream", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags 2.11.0", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.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", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.0", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-jpeg" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "410e9ecef634c709e3831c2cfdb8d9c32164fae1c67496d5b68fff728eec37fe" +dependencies = [ + "zune-core", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..261423f --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,52 @@ +[package] +name = "can-service" +version = "0.1.0" +edition = "2021" +description = "Containerized Asset Network - a self-healing local storage daemon" + +[dependencies] +# Web framework +axum = { version = "0.8", features = ["multipart"] } +tokio = { version = "1", features = ["full"] } +tower-http = { version = "0.6", features = ["cors", "trace"] } +tokio-util = { version = "0.7", features = ["io"] } + +# Database +rusqlite = { version = "0.32", features = ["bundled"] } + +# Serialization +serde = { version = "1", features = ["derive"] } +serde_json = "1" +serde_yaml = "0.9" + +# Hashing +sha2 = "0.10" +hex = "0.4" + +# Image processing +image = { version = "0.25", default-features = false, features = ["jpeg", "png", "gif", "webp"] } + +# File system watching +notify = "7" + +# MIME type detection +mime_guess = "2" +mime = "0.3" + +# Logging +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } + +# Utilities +chrono = { version = "0.4", features = ["serde"] } +anyhow = "1" +thiserror = "2" + +# OS attributes (unix only, windows uses custom ADS) +[target.'cfg(unix)'.dependencies] +xattr = "1" + +[dev-dependencies] +tempfile = "3" +reqwest = { version = "0.12", features = ["multipart", "json"] } +tokio-test = "0.4" diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..ac70e1b --- /dev/null +++ b/config.yaml @@ -0,0 +1,5 @@ +storage_root: "./can_data" +admin_token: "super_secret_rebuild" +enable_thumbnail_cache: true +rebuild_error_threshold: 50 +verify_interval_hours: 12 diff --git a/examples/can-sync/Cargo.lock b/examples/can-sync/Cargo.lock new file mode 100644 index 0000000..c36ce67 --- /dev/null +++ b/examples/can-sync/Cargo.lock @@ -0,0 +1,5575 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "asn1-rs" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 2.0.18", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-compat" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ba85bc55464dcbf728b56d97e119d673f4cf9062be330a9a26f3acf504a590" +dependencies = [ + "futures-core", + "futures-io", + "once_cell", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "async_io_stream" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d7b9decdf35d8908a7e3ef02f64c5e9b1695e230154c0e8de3969142d9b94c" +dependencies = [ + "futures", + "pharos", + "rustc_version", +] + +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "attohttpc" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e2cdb6d5ed835199484bb92bb8b3edd526effe995c61732580439c1a67e2e9" +dependencies = [ + "base64", + "http", + "log", + "url", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "aws-lc-rs" +version = "1.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94bffc006df10ac2a68c83692d734a465f8ee6c5b384d8545a636f81d858f4bf" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4321e568ed89bb5a7d291a7f37997c2c0df89809d7b6d12062c81ddb54aa782e" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "axum" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "backon" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" +dependencies = [ + "fastrand", + "gloo-timers", + "tokio", +] + +[[package]] +name = "bao-tree" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06384416b1825e6e04fde63262fda2dc408f5b64c02d04e0d8b70ae72c17a52b" +dependencies = [ + "blake3", + "bytes", + "futures-lite", + "genawaiter", + "iroh-io", + "positioned-io", + "range-collections", + "self_cell", + "serde", + "smallvec", + "tokio", +] + +[[package]] +name = "base32" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "022dfe9eb35f19ebbcb51e0b40a5ab759f46ad60cadf7297e0bd085afb50e076" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "binary-merge" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597bb81c80a54b6a4381b23faba8d7774b144c94cbd1d6fe3f1329bd776554ab" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "blake3" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96eb4cdd6cf1b31d671e9efe75c5d1ec614776856cefbe109ca373554a6d514f" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +dependencies = [ + "serde", +] + +[[package]] +name = "can-sync" +version = "0.1.0" +dependencies = [ + "anyhow", + "axum", + "bytes", + "chrono", + "futures-lite", + "hex", + "iroh", + "iroh-blobs", + "iroh-docs", + "iroh-gossip", + "open", + "postcard", + "reqwest 0.12.28", + "rusqlite", + "serde", + "serde_json", + "serde_yaml", + "sha2 0.10.9", + "tokio", + "tokio-util", + "tower-http", + "tracing", + "tracing-subscriber", + "uuid", +] + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "cmake" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +dependencies = [ + "cc", +] + +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.18", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cordyceps" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "688d7fbb8092b8de775ef2536f36c8c31f2bc4006ece2e8d8ad2d17d00ce0a2a" +dependencies = [ + "loom", + "tracing", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "curve25519-dalek" +version = "5.0.0-pre.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f9200d1d13637f15a6acb71e758f64624048d85b31a5fdbfd8eca1e2687d0b7" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest 0.11.0-rc.10", + "fiat-crypto", + "rand_core", + "rustc_version", + "serde", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "data-encoding" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" + +[[package]] +name = "der" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fd89660b2dc699704064e59e9dba0147b903e85319429e131620d022be411b" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn 2.0.117", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", + "unicode-xid", +] + +[[package]] +name = "diatomic-waker" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab03c107fafeb3ee9f5925686dbb7a73bc76e3932abb0d2b365cb64b169cf04c" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.7", +] + +[[package]] +name = "digest" +version = "0.11.0-rc.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afa94b64bfc6549e6e4b5a3216f22593224174083da7a90db47e951c4fb31725" +dependencies = [ + "block-buffer 0.11.0", + "const-oid", + "crypto-common 0.2.1", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dlopen2" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b4f5f101177ff01b8ec4ecc81eead416a8aa42819a2869311b3420fa114ffa" +dependencies = [ + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "ed25519" +version = "3.0.0-rc.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6e914c7c52decb085cea910552e24c63ac019e3ab8bf001ff736da9a9d9d890" +dependencies = [ + "pkcs8", + "serde", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "3.0.0-pre.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad207ed88a133091f83224265eac21109930db09bedcad05d5252f2af2de20a1" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core", + "serde", + "sha2 0.11.0-rc.2", + "signature", + "subtle", + "zeroize", +] + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "enum-as-inner" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "enum-assoc" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed8956bd5c1f0415200516e78ff07ec9e16415ade83c056c230d7b7ea0d55b7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fastbloom" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7f34442dbe69c60fe8eaf58a8cafff81a1f278816d8ab4db255b3bef4ac3c4" +dependencies = [ + "getrandom 0.3.4", + "libm", + "rand", + "siphasher", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fiat-crypto" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-buffered" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4421cb78ee172b6b06080093479d3c50f058e7c81b7d577bbb8d118d551d4cd5" +dependencies = [ + "cordyceps", + "diatomic-waker", + "futures-core", + "pin-project-lite", + "spin 0.10.0", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-concurrency" +version = "7.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175cd8cca9e1d45b87f18ffa75088f2099e3c4fe5e2f83e42de112560bea8ea6" +dependencies = [ + "fixedbitset", + "futures-core", + "futures-lite", + "pin-project", + "smallvec", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "genawaiter" +version = "0.99.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c86bd0361bcbde39b13475e6e36cb24c329964aa2611be285289d1e4b751c1a0" +dependencies = [ + "futures-core", + "genawaiter-macro", + "genawaiter-proc-macro", + "proc-macro-hack", +] + +[[package]] +name = "genawaiter-macro" +version = "0.99.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b32dfe1fdfc0bbde1f22a5da25355514b5e450c33a6af6770884c8750aedfbc" + +[[package]] +name = "genawaiter-proc-macro" +version = "0.99.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784f84eebc366e15251c4a8c3acee82a6a6f427949776ecb88377362a9621738" +dependencies = [ + "proc-macro-error", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "generator" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f04ae4152da20c76fe800fa48659201d5cf627c5149ca0b707b69d7eef6cf9" +dependencies = [ + "cc", + "cfg-if", + "libc", + "log", + "rustversion", + "windows-link", + "windows-result", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", + "wasm-bindgen", +] + +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version", + "serde", + "spin 0.9.8", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hickory-proto" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" +dependencies = [ + "async-trait", + "bytes", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "h2", + "http", + "idna", + "ipnet", + "once_cell", + "rand", + "ring", + "rustls", + "thiserror 2.0.18", + "tinyvec", + "tokio", + "tokio-rustls", + "tracing", + "url", +] + +[[package]] +name = "hickory-resolver" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" +dependencies = [ + "cfg-if", + "futures-util", + "hickory-proto", + "ipconfig", + "moka", + "once_cell", + "parking_lot", + "rand", + "resolv-conf", + "rustls", + "smallvec", + "thiserror 2.0.18", + "tokio", + "tokio-rustls", + "tracing", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hybrid-array" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8655f91cd07f2b9d0c24137bd650fe69617773435ee5ec83022377777ce65ef1" +dependencies = [ + "typenum", +] + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.3", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "identity-hash" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfdd7caa900436d8f13b2346fe10257e0c05c1f1f9e351f4f5d57c03bd5f45da" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "igd-next" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516893339c97f6011282d5825ac94fc1c7aad5cad26bdc2d0cee068c0bf97f97" +dependencies = [ + "async-trait", + "attohttpc", + "bytes", + "futures", + "http", + "http-body-util", + "hyper", + "hyper-util", + "log", + "rand", + "tokio", + "url", + "xmltree", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "inplace-vec-builder" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf64c2edc8226891a71f127587a2861b132d2b942310843814d5001d99a1d307" +dependencies = [ + "smallvec", +] + +[[package]] +name = "ipconfig" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" +dependencies = [ + "socket2 0.5.10", + "widestring", + "windows-sys 0.48.0", + "winreg", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "iroh" +version = "0.96.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5236da4d5681f317ec393c8fe2b7e3d360d31c6bb40383991d0b7429ca5ad117" +dependencies = [ + "backon", + "bytes", + "cfg_aliases", + "data-encoding", + "derive_more", + "ed25519-dalek", + "futures-util", + "getrandom 0.3.4", + "hickory-resolver", + "http", + "igd-next", + "iroh-base 0.96.1", + "iroh-metrics", + "iroh-quinn", + "iroh-quinn-proto", + "iroh-quinn-udp", + "iroh-relay", + "n0-error", + "n0-future", + "n0-watcher", + "netdev", + "netwatch", + "papaya", + "pin-project", + "pkarr", + "pkcs8", + "portmapper", + "rand", + "reqwest 0.12.28", + "rustc-hash", + "rustls", + "rustls-pki-types", + "rustls-webpki", + "serde", + "smallvec", + "strum 0.27.2", + "sync_wrapper", + "time", + "tokio", + "tokio-stream", + "tokio-util", + "tracing", + "url", + "wasm-bindgen-futures", + "webpki-roots", +] + +[[package]] +name = "iroh-base" +version = "0.95.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a8c5fb1cc65589f0d7ab44269a76f615a8c4458356952c9b0ef1c93ea45ff8" +dependencies = [ + "curve25519-dalek", + "data-encoding", + "derive_more", + "ed25519-dalek", + "n0-error", + "rand_core", + "serde", + "url", + "zeroize", + "zeroize_derive", +] + +[[package]] +name = "iroh-base" +version = "0.96.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20c99d836a1c99e037e98d1bf3ef209c3a4df97555a00ce9510eb78eccdf5567" +dependencies = [ + "curve25519-dalek", + "data-encoding", + "derive_more", + "digest 0.11.0-rc.10", + "ed25519-dalek", + "n0-error", + "rand_core", + "serde", + "sha2 0.11.0-rc.2", + "url", + "zeroize", + "zeroize_derive", +] + +[[package]] +name = "iroh-blobs" +version = "0.98.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f253ea06293e51e166a88a3faa019b67e187d12bd7c6a04369a0ec86f53272" +dependencies = [ + "arrayvec", + "bao-tree", + "bytes", + "cfg_aliases", + "chrono", + "data-encoding", + "derive_more", + "futures-lite", + "genawaiter", + "hex", + "iroh", + "iroh-base 0.96.1", + "iroh-io", + "iroh-metrics", + "iroh-quinn", + "iroh-tickets 0.3.0", + "irpc", + "n0-error", + "n0-future", + "nested_enum_utils", + "postcard", + "rand", + "range-collections", + "redb", + "ref-cast", + "reflink-copy", + "self_cell", + "serde", + "smallvec", + "tokio", + "tracing", +] + +[[package]] +name = "iroh-docs" +version = "0.96.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42292da17d6be0b73c5897f1ff395ad7c6f858b107ff76a7605867fbdd6c2e72" +dependencies = [ + "anyhow", + "async-channel", + "blake3", + "bytes", + "derive_more", + "ed25519-dalek", + "futures-buffered", + "futures-lite", + "futures-util", + "hex", + "iroh", + "iroh-blobs", + "iroh-gossip", + "iroh-metrics", + "iroh-quinn", + "iroh-tickets 0.2.0", + "irpc", + "n0-error", + "n0-future", + "num_enum", + "postcard", + "rand", + "redb", + "self_cell", + "serde", + "serde-error", + "strum 0.26.3", + "tempfile", + "thiserror 2.0.18", + "tokio", + "tokio-stream", + "tokio-util", + "tracing", +] + +[[package]] +name = "iroh-gossip" +version = "0.96.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d04f83254c847ac61a9b2215b95a36d598d87af033ca12a546cd1c6a2e06dab" +dependencies = [ + "blake3", + "bytes", + "data-encoding", + "derive_more", + "ed25519-dalek", + "futures-concurrency", + "futures-lite", + "futures-util", + "hex", + "indexmap", + "iroh", + "iroh-base 0.96.1", + "iroh-metrics", + "irpc", + "n0-error", + "n0-future", + "postcard", + "rand", + "serde", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "iroh-io" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a5feb781017b983ff1b155cd1faf8174da2acafd807aa482876da2d7e6577a" +dependencies = [ + "bytes", + "futures-lite", + "pin-project", + "smallvec", + "tokio", +] + +[[package]] +name = "iroh-metrics" +version = "0.38.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "761b45ba046134b11eb3e432fa501616b45c4bf3a30c21717578bc07aa6461dd" +dependencies = [ + "iroh-metrics-derive", + "itoa", + "n0-error", + "portable-atomic", + "postcard", + "ryu", + "serde", + "tracing", +] + +[[package]] +name = "iroh-metrics-derive" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab063c2bfd6c3d5a33a913d4fdb5252f140db29ec67c704f20f3da7e8f92dbf" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "iroh-quinn" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "034ed21f34c657a123d39525d948c885aacba59508805e4dd67d71f022e7151b" +dependencies = [ + "bytes", + "cfg_aliases", + "iroh-quinn-proto", + "iroh-quinn-udp", + "pin-project-lite", + "rustc-hash", + "rustls", + "socket2 0.6.3", + "thiserror 2.0.18", + "tokio", + "tokio-stream", + "tracing", + "web-time", +] + +[[package]] +name = "iroh-quinn-proto" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de99ad8adc878ee0e68509ad256152ce23b8bbe45f5539d04e179630aca40a9" +dependencies = [ + "bytes", + "derive_more", + "enum-assoc", + "fastbloom", + "getrandom 0.3.4", + "identity-hash", + "lru-slab", + "rand", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "slab", + "sorted-index-buffer", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "iroh-quinn-udp" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f981dadd5a072a9e0efcd24bdcc388e570073f7e51b33505ceb1ef4668c80c86" +dependencies = [ + "cfg_aliases", + "libc", + "socket2 0.6.3", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "iroh-relay" +version = "0.96.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd2b63e654b9dec799a73372cdc79b529ca6c7248c0c8de7da78a02e3a46f03c" +dependencies = [ + "blake3", + "bytes", + "cfg_aliases", + "data-encoding", + "derive_more", + "getrandom 0.3.4", + "hickory-resolver", + "http", + "http-body-util", + "hyper", + "hyper-util", + "iroh-base 0.96.1", + "iroh-metrics", + "iroh-quinn", + "iroh-quinn-proto", + "lru", + "n0-error", + "n0-future", + "num_enum", + "pin-project", + "pkarr", + "postcard", + "rand", + "reqwest 0.12.28", + "rustls", + "rustls-pki-types", + "serde", + "serde_bytes", + "strum 0.27.2", + "tokio", + "tokio-rustls", + "tokio-util", + "tokio-websockets", + "tracing", + "url", + "vergen-gitcl", + "webpki-roots", + "ws_stream_wasm", + "z32", +] + +[[package]] +name = "iroh-tickets" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a322053cacddeca222f0999ce3cf6aa45c64ae5ad8c8911eac9b66008ffbaa5" +dependencies = [ + "data-encoding", + "derive_more", + "iroh-base 0.95.1", + "n0-error", + "postcard", + "serde", +] + +[[package]] +name = "iroh-tickets" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cd580bf680db919cbbce6886a47314acb0e9b4f7b639acebcea5e9f485d183" +dependencies = [ + "data-encoding", + "derive_more", + "iroh-base 0.96.1", + "n0-error", + "postcard", + "serde", +] + +[[package]] +name = "irpc" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bbc84aaeab13a6d7502bae4f40f2517b643924842e0230ea0bf807477cc208" +dependencies = [ + "futures-buffered", + "futures-util", + "iroh-quinn", + "irpc-derive", + "n0-error", + "n0-future", + "postcard", + "rcgen", + "rustls", + "serde", + "smallvec", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "irpc-derive" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58148196d2230183c9679431ac99b57e172000326d664e8456fa2cd27af6505a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "loom" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "lru" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "mac-addr" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3d25b0e0b648a86960ac23b7ad4abb9717601dec6f66c165f5b037f3f03065f" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "moka" +version = "0.12.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85f8024e1c8e71c778968af91d43700ce1d11b219d127d79fb2934153b82b42b" +dependencies = [ + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "equivalent", + "parking_lot", + "portable-atomic", + "smallvec", + "tagptr", + "uuid", +] + +[[package]] +name = "n0-error" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af4782b4baf92d686d161c15460c83d16ebcfd215918763903e9619842665cae" +dependencies = [ + "anyhow", + "n0-error-macros", + "spez", +] + +[[package]] +name = "n0-error-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03755949235714b2b307e5ae89dd8c1c2531fb127d9b8b7b4adf9c876cd3ed18" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "n0-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2ab99dfb861450e68853d34ae665243a88b8c493d01ba957321a1e9b2312bbe" +dependencies = [ + "cfg_aliases", + "derive_more", + "futures-buffered", + "futures-lite", + "futures-util", + "js-sys", + "pin-project", + "send_wrapper", + "tokio", + "tokio-util", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-time", +] + +[[package]] +name = "n0-watcher" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38795f7932e6e9d1c6e989270ef5b3ff24ebb910e2c9d4bed2d28d8bae3007dc" +dependencies = [ + "derive_more", + "n0-error", + "n0-future", +] + +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nested_enum_utils" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d5475271bdd36a4a2769eac1ef88df0f99428ea43e52dfd8b0ee5cb674695f" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "netdev" +version = "0.40.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b0a0096d9613ee878dba89bbe595f079d373e3f1960d882e4f2f78ff9c30a0a" +dependencies = [ + "block2", + "dispatch2", + "dlopen2", + "ipnet", + "libc", + "mac-addr", + "netlink-packet-core", + "netlink-packet-route 0.29.0", + "netlink-sys", + "objc2-core-foundation", + "objc2-system-configuration", + "once_cell", + "plist", + "windows-sys 0.59.0", +] + +[[package]] +name = "netlink-packet-core" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3463cbb78394cb0141e2c926b93fc2197e473394b761986eca3b9da2c63ae0f4" +dependencies = [ + "paste", +] + +[[package]] +name = "netlink-packet-route" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ce3636fa715e988114552619582b530481fd5ef176a1e5c1bf024077c2c9445" +dependencies = [ + "bitflags", + "libc", + "log", + "netlink-packet-core", +] + +[[package]] +name = "netlink-packet-route" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9854ea6ad14e3f4698a7f03b65bce0833dd2d81d594a0e4a984170537146b6" +dependencies = [ + "bitflags", + "libc", + "log", + "netlink-packet-core", +] + +[[package]] +name = "netlink-proto" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b65d130ee111430e47eed7896ea43ca693c387f097dd97376bffafbf25812128" +dependencies = [ + "bytes", + "futures", + "log", + "netlink-packet-core", + "netlink-sys", + "thiserror 2.0.18", +] + +[[package]] +name = "netlink-sys" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6c30ed10fa69cc491d491b85cc971f6bdeb8e7367b7cde2ee6cc878d583fae" +dependencies = [ + "bytes", + "futures-util", + "libc", + "log", + "tokio", +] + +[[package]] +name = "netwatch" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "454b8c0759b2097581f25ed5180b4a1d14c324fde6d0734932a288e044d06232" +dependencies = [ + "atomic-waker", + "bytes", + "cfg_aliases", + "derive_more", + "iroh-quinn-udp", + "js-sys", + "libc", + "n0-error", + "n0-future", + "n0-watcher", + "netdev", + "netlink-packet-core", + "netlink-packet-route 0.28.0", + "netlink-proto", + "netlink-sys", + "objc2-core-foundation", + "objc2-system-configuration", + "pin-project-lite", + "serde", + "socket2 0.6.3", + "time", + "tokio", + "tokio-util", + "tracing", + "web-sys", + "windows", + "windows-result", + "wmi", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "ntimestamp" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c50f94c405726d3e0095e89e72f75ce7f6587b94a8bd8dc8054b73f65c0fd68c" +dependencies = [ + "base32", + "document-features", + "getrandom 0.2.17", + "httpdate", + "js-sys", + "once_cell", + "serde", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags", + "block2", + "dispatch2", + "libc", + "objc2", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-security" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" +dependencies = [ + "bitflags", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-system-configuration" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7216bd11cbda54ccabcab84d523dc93b858ec75ecfb3a7d89513fa22464da396" +dependencies = [ + "bitflags", + "dispatch2", + "libc", + "objc2", + "objc2-core-foundation", + "objc2-security", +] + +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +dependencies = [ + "critical-section", + "portable-atomic", +] + +[[package]] +name = "open" +version = "5.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43bb73a7fa3799b198970490a51174027ba0d4ec504b03cd08caf513d40024bc" +dependencies = [ + "is-wsl", + "libc", + "pathdiff", +] + +[[package]] +name = "openssl" +version = "0.10.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.112" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "papaya" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f92dd0b07c53a0a0c764db2ace8c541dc47320dad97c2200c2a637ab9dd2328f" +dependencies = [ + "equivalent", + "seize", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64", + "serde_core", +] + +[[package]] +name = "pem-rfc7468" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pharos" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9567389417feee6ce15dd6527a8a1ecac205ef62c2932bcf3d9f6fc5b78b414" +dependencies = [ + "futures", + "rustc_version", +] + +[[package]] +name = "pin-project" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkarr" +version = "5.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f950360d31be432c0c9467fba5024a94f55128e7f32bc9d32db140369f24c77" +dependencies = [ + "async-compat", + "base32", + "bytes", + "cfg_aliases", + "document-features", + "dyn-clone", + "ed25519-dalek", + "futures-buffered", + "futures-lite", + "getrandom 0.4.2", + "log", + "lru", + "ntimestamp", + "reqwest 0.13.2", + "self_cell", + "serde", + "sha1_smol", + "simple-dns", + "thiserror 2.0.18", + "tokio", + "tracing", + "url", + "wasm-bindgen-futures", +] + +[[package]] +name = "pkcs8" +version = "0.11.0-rc.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12922b6296c06eb741b02d7b5161e3aaa22864af38dfa025a1a3ba3f68c84577" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plist" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" +dependencies = [ + "base64", + "indexmap", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +dependencies = [ + "serde", +] + +[[package]] +name = "portmapper" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d2a8825353ace3285138da3378b1e21860d60351942f7aa3b99b13b41f80318" +dependencies = [ + "base64", + "bytes", + "derive_more", + "futures-lite", + "futures-util", + "hyper-util", + "igd-next", + "iroh-metrics", + "libc", + "n0-error", + "netwatch", + "num_enum", + "rand", + "serde", + "smallvec", + "socket2 0.6.3", + "time", + "tokio", + "tokio-util", + "tower-layer", + "tracing", + "url", +] + +[[package]] +name = "positioned-io" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4ec4b80060f033312b99b6874025d9503d2af87aef2dd4c516e253fbfcdada7" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "heapless", + "postcard-derive", + "serde", +] + +[[package]] +name = "postcard-derive" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0232bd009a197ceec9cc881ba46f727fcd8060a2d8d6a9dde7a69030a6fe2bb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro-error" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18f33027081eba0a6d8aba6d1b1c3a3be58cbb12106341c2d5759fcd9b5277e7" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a5b4b77fdb63c1eca72173d68d24501c54ab1269409f6b672c85deb18af69de" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "syn-mid", + "version_check", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +dependencies = [ + "memchr", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2 0.6.3", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.6.3", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +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 = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "range-collections" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "861706ea9c4aded7584c5cd1d241cec2ea7f5f50999f236c22b65409a1f1a0d0" +dependencies = [ + "binary-merge", + "inplace-vec-builder", + "ref-cast", + "serde", + "smallvec", +] + +[[package]] +name = "rcgen" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10b99e0098aa4082912d4c649628623db6aba77335e4f4569ff5083a6448b32e" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "x509-parser", + "yasna", +] + +[[package]] +name = "redb" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eca1e9d98d5a7e9002d0013e18d5a9b000aee942eb134883a82f06ebffb6c01" +dependencies = [ + "libc", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "reflink-copy" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13362233b147e57674c37b802d216b7c5e3dcccbed8967c84f0d8d223868ae27" +dependencies = [ + "cfg-if", + "libc", + "rustix", + "windows", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "mime_guess", + "native-tls", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "reqwest" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +dependencies = [ + "base64", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "resolv-conf" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rusqlite" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "seize" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b55fb86dfd3a2f5f76ea78310a88f96c4ea21a3031f8d212443d56123fd0521" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "self_cell" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "send_wrapper" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-error" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "342110fb7a5d801060c885da03bf91bfa7c7ca936deafcc64bb6706375605d47" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0-rc.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1e3878ab0f98e35b2df35fe53201d088299b41a6bb63e3e34dada2ac4abd924" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.11.0-rc.10", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "3.0.0-rc.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f1880df446116126965eeec169136b2e0251dba37c6223bcc819569550edea3" + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "simple-dns" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee851d0e5e7af3721faea1843e8015e820a234f81fda3dea9247e15bac9a86a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "sorted-index-buffer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea06cc588e43c632923a55450401b8f25e628131571d4e1baea1bdfdb2b5ed06" + +[[package]] +name = "spez" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c87e960f4dca2788eeb86bbdde8dd246be8948790b7618d656e68f9b720a86e8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spin" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" + +[[package]] +name = "spki" +version = "0.8.0-rc.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8baeff88f34ed0691978ec34440140e1572b68c7dd4a495fd14a3dc1944daa80" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros 0.26.4", +] + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros 0.27.2", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.117", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn-mid" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea305d57546cc8cd04feb14b62ec84bf17f50e3f7b12560d7bfa9265f39d9ed" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "js-sys", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.6.3", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "futures-util", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-websockets" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1b6348ebfaaecd771cecb69e832961d277f59845d4220a584701f72728152b7" +dependencies = [ + "base64", + "bytes", + "futures-core", + "futures-sink", + "getrandom 0.3.4", + "http", + "httparse", + "rand", + "ring", + "rustls-pki-types", + "simdutf8", + "tokio", + "tokio-rustls", + "tokio-util", +] + +[[package]] +name = "toml_datetime" +version = "1.0.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.4+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.9+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +dependencies = [ + "winnow", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "vergen" +version = "9.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b849a1f6d8639e8de261e81ee0fc881e3e3620db1af9f2e0da015d4382ceaf75" +dependencies = [ + "anyhow", + "derive_builder", + "rustversion", + "vergen-lib 9.1.0", +] + +[[package]] +name = "vergen-gitcl" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9dfc1de6eb2e08a4ddf152f1b179529638bedc0ea95e6d667c014506377aefe" +dependencies = [ + "anyhow", + "derive_builder", + "rustversion", + "time", + "vergen", + "vergen-lib 0.1.6", +] + +[[package]] +name = "vergen-lib" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b07e6010c0f3e59fcb164e0163834597da68d1f864e2b8ca49f74de01e9c166" +dependencies = [ + "anyhow", + "derive_builder", + "rustversion", +] + +[[package]] +name = "vergen-lib" +version = "9.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b34a29ba7e9c59e62f229ae1932fb1b8fb8a6fdcc99215a641913f5f5a59a569" +dependencies = [ + "anyhow", + "derive_builder", + "rustversion", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core", +] + +[[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", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[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.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "wmi" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "003e65f4934cf9449b9ce913ad822cd054a5af669d24f93db101fdb02856bb23" +dependencies = [ + "chrono", + "futures", + "log", + "serde", + "thiserror 2.0.18", + "windows", + "windows-core", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "ws_stream_wasm" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c173014acad22e83f16403ee360115b38846fe754e735c5d9d3803fe70c6abc" +dependencies = [ + "async_io_stream", + "futures", + "js-sys", + "log", + "pharos", + "rustc_version", + "send_wrapper", + "thiserror 2.0.18", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "x509-parser" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "ring", + "rusticata-macros", + "thiserror 2.0.18", + "time", +] + +[[package]] +name = "xml-rs" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" + +[[package]] +name = "xmltree" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7d8a75eaf6557bb84a65ace8609883db44a29951042ada9b393151532e41fcb" +dependencies = [ + "xml-rs", +] + +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "z32" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2164e798d9e3d84ee2c91139ace54638059a3b23e361f5c11781c2c6459bde0f" + +[[package]] +name = "zerocopy" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/examples/can-sync/Cargo.toml b/examples/can-sync/Cargo.toml new file mode 100644 index 0000000..06660aa --- /dev/null +++ b/examples/can-sync/Cargo.toml @@ -0,0 +1,44 @@ +[package] +name = "can-sync" +version = "0.1.0" +edition = "2021" +description = "P2P sync service for CAN content-addressable storage" + +[[bin]] +name = "can-sync" +path = "src/main.rs" + +[dependencies] +# P2P networking +iroh = "0.96" +iroh-blobs = "0.98" +iroh-docs = "0.96" +iroh-gossip = "0.96" + +# HTTP server + client +axum = "0.8" +tokio = { version = "1", features = ["full"] } +reqwest = { version = "0.12", features = ["json", "multipart"] } +tower-http = { version = "0.6", features = ["cors"] } + +# Serialization +serde = { version = "1", features = ["derive"] } +serde_json = "1" +serde_yaml = "0.9" +postcard = { version = "1", features = ["alloc"] } + +# Storage +rusqlite = { version = "0.32", features = ["bundled"] } + +# Utilities +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +anyhow = "1" +open = "5" +sha2 = "0.10" +hex = "0.4" +uuid = { version = "1", features = ["v4"] } +chrono = { version = "0.4", features = ["serde"] } +bytes = "1" +futures-lite = "2" +tokio-util = { version = "0.7", features = ["io"] } diff --git a/examples/can-sync/README.md b/examples/can-sync/README.md new file mode 100644 index 0000000..6c11545 --- /dev/null +++ b/examples/can-sync/README.md @@ -0,0 +1,263 @@ +# CAN Sync + +P2P file synchronization service that runs on top of [CAN Service](../../). Uses [iroh](https://iroh.computer/) for encrypted peer-to-peer networking with NAT traversal. + +``` +┌─────────────┐ HTTP API ┌─────────────┐ iroh (QUIC) ┌─────────────┐ +│ CAN Service │◄───────────►│ CAN Sync │◄─────────────►│ CAN Sync │ +│ (port 3210)│ │ (port 3213)│ │ (remote) │ +│ storage + │ │ P2P node + │ │ │ +│ SQLite │ │ libraries │ │ │ +└─────────────┘ └─────────────┘ └─────────────┘ +``` + +CAN Sync communicates with CAN Service **only** via its public HTTP API — zero changes to CAN Service required. + +## Quick Start + +1. **Start CAN Service** (default port 3210): + ```bash + cd ../.. + cargo run + ``` + +2. **Edit config** (optional — defaults work out of the box): + ```bash + cp config.yaml my-config.yaml + # edit my-config.yaml if needed + ``` + +3. **Start CAN Sync**: + ```bash + cargo run + # or with a custom config: + cargo run -- my-config.yaml + ``` + + CAN Sync starts on `http://127.0.0.1:3213` and connects to CAN Service at `http://127.0.0.1:3210/api/v1/can/0`. + +## Configuration + +`config.yaml`: + +```yaml +# URL of the local CAN Service API +can_service_url: "http://127.0.0.1:3210/api/v1/can/0" + +# Address for the CAN Sync HTTP API +listen_addr: "127.0.0.1:3213" + +# Directory for persistent data (peer key, sync state DB) +data_dir: "./can_sync_data" + +# Custom relay server URL (null = iroh's public relay) +relay_url: null + +# Seconds between fast polls for new assets +poll_interval_secs: 5 + +# Seconds between full scans of all assets +full_scan_interval_secs: 300 +``` + +## Concepts + +### Libraries + +A **library** is a shared collection of CAN assets that syncs between peers. Each library has a **filter** that determines which assets belong to it. + +Filter options (combined with AND logic): +- `application` — match assets with this application tag (e.g. `"paste"`) +- `tags` — match assets with any of these tags (e.g. `["photos", "backup"]`) +- `user` — match assets from this user identity +- `mime_prefix` — match assets whose MIME type starts with this (e.g. `"image/"`) +- `hashes` — manual list of specific asset hashes to include + +### Sync Flow + +**Outbound** (local → remote): +1. Announcer polls CAN Service for new/changed assets +2. Assets matching a library's filter get announced to the library's iroh document +3. iroh replicates the entry to all subscribed peers +4. Remote peer's fetcher downloads the blob and ingests it into their local CAN Service + +**Inbound** (remote → local): +1. iroh document receives new entry from remote peer +2. Fetcher downloads the blob via iroh's encrypted QUIC transport +3. Fetcher verifies the CAN hash (SHA-256) independently +4. Fetcher ingests the file into local CAN Service with all metadata preserved + +## API + +All endpoints return JSON with `{ "status": "success", "data": ... }` or `{ "status": "error", "error": "..." }`. + +### Status & Peers + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/status` | Node status, CAN service health, library count | +| GET | `/peers` | Connected peers list | + +### Libraries + +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/libraries` | Create a library | +| GET | `/libraries` | List all libraries | +| GET | `/libraries/{id}` | Get library details | +| DELETE | `/libraries/{id}` | Remove a library | + +### Sharing + +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/libraries/{id}/invite` | Generate a share ticket | +| POST | `/join` | Join a library from a ticket | + +### Examples + +**Create a library** that syncs all assets with `application=paste`: +```bash +curl -X POST http://127.0.0.1:3213/libraries \ + -H "Content-Type: application/json" \ + -d '{"name": "my-pastes", "filter": {"application": "paste"}}' +``` + +**Create a library** that syncs all images: +```bash +curl -X POST http://127.0.0.1:3213/libraries \ + -H "Content-Type: application/json" \ + -d '{"name": "images", "filter": {"mime_prefix": "image/"}}' +``` + +**Generate an invite ticket** to share with another machine: +```bash +curl -X POST http://127.0.0.1:3213/libraries/{id}/invite +``` + +**Join a library** on another machine using the ticket: +```bash +curl -X POST http://127.0.0.1:3213/join \ + -H "Content-Type: application/json" \ + -d '{"ticket": "eyJsaWJyYXJ5X25hbWUiOi..."}' +``` + +**List all libraries**: +```bash +curl http://127.0.0.1:3213/libraries +``` + +**Check status**: +```bash +curl http://127.0.0.1:3213/status +``` + +## Two-Machine Setup + +### Machine A (the host) + +**1. Start CAN Service** (default port 3210): +```bash +cd /path/to/CanService +cargo run +``` + +**2. Start CAN Sync** with default config (port 3213): +```bash +cd examples/can-sync +cargo run +``` + +**3. Create a library** (e.g. sync all images): +```bash +curl -X POST http://127.0.0.1:3213/libraries \ + -H "Content-Type: application/json" \ + -d '{"name": "shared-images", "filter": {"mime_prefix": "image/"}}' +``` +Save the `id` from the response (e.g. `"id": "a1b2c3d4-..."`). + +**4. Generate an invite ticket:** +```bash +curl -X POST http://127.0.0.1:3213/libraries/a1b2c3d4-.../invite +``` +Copy the `ticket` string from the response — this is what Machine B needs. + +### Machine B (the joiner) + +**1. Start CAN Service** on a different port: +```bash +cd /path/to/CanService +CAN_PORT=3220 cargo run +``` + +**2. Create a config file** for CAN Sync pointing at Machine B's CAN Service: +```yaml +# machine-b-config.yaml +can_service_url: "http://127.0.0.1:3220/api/v1/can/0" +listen_addr: "127.0.0.1:3223" +data_dir: "./can_sync_data_b" +``` + +**3. Start CAN Sync** with that config: +```bash +cd examples/can-sync +cargo run -- machine-b-config.yaml +``` + +**4. Join the library** using Machine A's ticket: +```bash +curl -X POST http://127.0.0.1:3223/join \ + -H "Content-Type: application/json" \ + -d '{"ticket": "eyJsaWJyYXJ5X25hbWUiOi..."}' +``` + +### Verify it works + +**Ingest a file on Machine A:** +```bash +curl -X POST http://127.0.0.1:3210/api/v1/can/0/ingest \ + -F "file=@photo.jpg" \ + -F "mime_type=image/jpeg" +``` + +**Check Machine B** — the file should appear within a few seconds: +```bash +curl http://127.0.0.1:3220/api/v1/can/0/list?limit=5 +``` + +The same image (with matching hash and metadata) will be in Machine B's CAN Service, synced over iroh's encrypted P2P connection. + +## Architecture + +``` +src/ +├── main.rs — entry point: config, iroh node, announcer, fetcher, HTTP server +├── config.rs — YAML config loading +├── can_client.rs — HTTP client for CAN Service API (list, search, ingest, meta, etc.) +├── node.rs — iroh endpoint + blobs + docs + gossip + router +├── library.rs — library/filter definitions + SQLite state tracking +├── manifest.rs — AssetSyncEntry serialized into iroh document entries +├── announcer.rs — polls CAN Service, announces matching assets to libraries +├── fetcher.rs — receives remote entries, downloads blobs, ingests into CAN Service +└── routes.rs — Axum HTTP API handlers +``` + +## Security + +- **Transport**: All peer-to-peer traffic is encrypted with QUIC + TLS 1.3 (mandatory in iroh) +- **Identity**: Each node has an Ed25519 keypair generated on first run +- **Access control**: Library access via cryptographic capability tickets — only peers with a valid ticket can read/write +- **NAT traversal**: iroh's built-in relay servers and hole-punching +- **Hash verification**: Downloaded files are independently verified against CAN's SHA-256 hash before ingestion + +## Current Status + +The service compiles and runs with the following fully implemented: +- iroh P2P node startup with all protocol handlers (blobs, docs, gossip) +- CAN Service HTTP client with full API coverage +- Library management with SQLite persistence +- Announcer polling loop (fast + full scan) with real iroh-docs writes +- Fetcher with iroh document event subscription for real-time sync +- Fetcher blob download via iroh and CAN hash verification before ingestion +- Real DocTicket-based invite/join with cryptographic capability tokens +- HTTP API for library CRUD, invite, and join diff --git a/examples/can-sync/config.yaml b/examples/can-sync/config.yaml new file mode 100644 index 0000000..c5b804c --- /dev/null +++ b/examples/can-sync/config.yaml @@ -0,0 +1,7 @@ +# CAN Sync configuration +can_service_url: "http://127.0.0.1:3210/api/v1/can/0" +listen_addr: "127.0.0.1:3213" +data_dir: "./can_sync_data" +relay_url: null +poll_interval_secs: 5 +full_scan_interval_secs: 300 diff --git a/examples/can-sync/src/announcer.rs b/examples/can-sync/src/announcer.rs new file mode 100644 index 0000000..58b9b68 --- /dev/null +++ b/examples/can-sync/src/announcer.rs @@ -0,0 +1,234 @@ +use std::sync::Arc; +use std::time::Duration; + +use anyhow::Result; +use tracing::{debug, error, info, warn}; + +use crate::can_client::CanClient; +use crate::library::SyncState; +use crate::manifest::AssetSyncEntry; +use crate::node::SyncNode; + +/// The announcer periodically polls CAN service for new or changed assets +/// and writes matching entries into iroh library documents. +pub struct Announcer { + can: CanClient, + state: Arc, + node: Arc, + poll_interval: Duration, + full_scan_interval: Duration, +} + +impl Announcer { + pub fn new( + can: CanClient, + state: Arc, + node: Arc, + poll_interval_secs: u64, + full_scan_interval_secs: u64, + ) -> Self { + Self { + can, + state, + node, + poll_interval: Duration::from_secs(poll_interval_secs), + full_scan_interval: Duration::from_secs(full_scan_interval_secs), + } + } + + /// Run the announcer loop — fast polls + periodic full scans + pub async fn run(self) { + let mut fast_tick = tokio::time::interval(self.poll_interval); + let mut full_tick = tokio::time::interval(self.full_scan_interval); + // Skip the first immediate tick for full scan (let fast poll get first data) + full_tick.tick().await; + + info!( + "Announcer started (fast poll: {}s, full scan: {}s)", + self.poll_interval.as_secs(), + self.full_scan_interval.as_secs(), + ); + + loop { + tokio::select! { + _ = fast_tick.tick() => { + if let Err(e) = self.fast_poll().await { + warn!("Fast poll error: {:#}", e); + } + } + _ = full_tick.tick() => { + if let Err(e) = self.full_scan().await { + warn!("Full scan error: {:#}", e); + } + } + } + } + } + + /// Fast poll: check for recently ingested assets + async fn fast_poll(&self) -> Result<()> { + let last_ts = self + .state + .get_state("last_seen_timestamp")? + .and_then(|s| s.parse::().ok()) + .unwrap_or(0); + + // Get recent assets ordered newest first + let resp = self.can.list(50, 0, "desc", Some(last_ts)).await?; + + if resp.items.is_empty() { + return Ok(()); + } + + debug!("Fast poll found {} new assets since ts={}", resp.items.len(), last_ts); + + // Track the newest timestamp we see + let mut max_ts = last_ts; + for asset in &resp.items { + if asset.timestamp > max_ts { + max_ts = asset.timestamp; + } + } + + // Process assets against libraries + let libraries = self.state.list_libraries()?; + + for asset in &resp.items { + for lib in &libraries { + if lib.filter.matches(asset) { + self.announce_asset(lib, asset).await?; + } + } + } + + // Update last seen timestamp + self.state.set_state("last_seen_timestamp", &max_ts.to_string())?; + + Ok(()) + } + + /// Full scan: paginate through all assets, checking for metadata changes + async fn full_scan(&self) -> Result<()> { + info!("Starting full scan..."); + + let libraries = self.state.list_libraries()?; + + if libraries.is_empty() { + debug!("No libraries configured, skipping full scan"); + return Ok(()); + } + + let page_size = 100; + let mut offset = 0; + let mut total_scanned = 0; + let mut total_announced = 0; + + loop { + let resp = self.can.list_all(page_size, offset, true).await?; + let count = resp.items.len(); + total_scanned += count; + + for asset in &resp.items { + for lib in &libraries { + if lib.filter.matches(asset) { + let was_new = self.announce_asset(lib, asset).await?; + if was_new { + total_announced += 1; + } + } + } + } + + if (count as i64) < page_size { + break; + } + offset += page_size; + } + + info!( + "Full scan complete: scanned {}, announced {} new/updated", + total_scanned, total_announced + ); + Ok(()) + } + + /// Announce a single asset to a library's iroh document. + /// Returns true if the asset was newly announced or updated. + async fn announce_asset( + &self, + lib: &crate::library::Library, + asset: &crate::can_client::AssetMeta, + ) -> Result { + let doc_id = match &lib.doc_id { + Some(id) => id.clone(), + None => { + debug!("Library '{}' has no doc_id yet, skipping", lib.name); + return Ok(false); + } + }; + + // Check if already announced at current version + if self.state.is_announced(&lib.id, &asset.hash)? { + // Already announced — skip unless metadata changed + // (full scan handles re-announcement on metadata change) + return Ok(false); + } + + // Download file content from CAN service and add as iroh blob + let iroh_blob_hash = match self.can.get_asset(&asset.hash).await { + Ok(content) => { + // Add to iroh blob store so remote peers can download it + match self.node.blobs.add_bytes(content).await { + Ok(tag_info) => Some(tag_info.hash.to_string()), + Err(e) => { + warn!( + "Failed to add blob for asset {}: {:#}", + &asset.hash[..12], + e + ); + None + } + } + } + Err(e) => { + warn!( + "Failed to download asset {} from CAN service: {:#}", + &asset.hash[..12], + e + ); + None + } + }; + + // Create sync entry with the iroh blob hash + let mut entry = AssetSyncEntry::from_asset_meta(asset, &self.node.peer_id()); + entry.iroh_blob_hash = iroh_blob_hash; + let entry_bytes = entry.to_bytes(); + + // Write to iroh document (CRDT — concurrent writes merge automatically) + if let Err(e) = self + .node + .write_to_doc(&doc_id, asset.hash.as_bytes(), &entry_bytes) + .await + { + error!( + "Failed to write asset {} to doc {}: {:#}", + &asset.hash[..12], + &doc_id[..12], + e + ); + return Ok(false); + } + + // Mark as announced in local state + self.state.mark_announced(&lib.id, &asset.hash, entry.version)?; + + debug!( + "Announced asset {} to library '{}' (doc {})", + &asset.hash[..12], + lib.name, + &doc_id[..12] + ); + Ok(true) + } +} diff --git a/examples/can-sync/src/can_client.rs b/examples/can-sync/src/can_client.rs new file mode 100644 index 0000000..12d1af6 --- /dev/null +++ b/examples/can-sync/src/can_client.rs @@ -0,0 +1,291 @@ +use anyhow::{Context, Result}; +use bytes::Bytes; +use reqwest::multipart; +use serde::{Deserialize, Serialize}; + +/// HTTP client for CAN service API +#[derive(Debug, Clone)] +pub struct CanClient { + client: reqwest::Client, + base_url: String, +} + +// ── API response types (mirror CAN service) ── + +#[derive(Debug, Deserialize)] +pub struct ApiResponse { + pub status: String, + pub data: T, +} + +#[derive(Debug, Deserialize)] +pub struct ErrorResponse { + pub status: String, + pub error: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AssetMeta { + pub hash: String, + pub mime_type: String, + pub application: Option, + pub user: Option, + pub tags: Vec, + pub description: Option, + pub human_filename: Option, + pub human_path: Option, + pub timestamp: i64, + pub is_trashed: bool, + #[serde(default)] + pub is_corrupted: bool, + pub size: i64, +} + +#[derive(Debug, Deserialize)] +pub struct ListResponse { + pub items: Vec, + pub pagination: Pagination, +} + +#[derive(Debug, Deserialize)] +pub struct Pagination { + pub limit: i64, + pub offset: i64, + pub total: i64, +} + +#[derive(Debug, Deserialize)] +pub struct IngestResult { + pub timestamp: i64, + pub hash: String, + pub filename: String, +} + +// ── Search parameters ── + +#[derive(Debug, Default, Serialize)] +pub struct SearchParams { + #[serde(skip_serializing_if = "Option::is_none")] + pub hash: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub start_time: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub end_time: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub tags: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub mime_type: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub user: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub application: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub limit: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub offset: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub order: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub include_trashed: Option, +} + +// ── Ingest metadata ── + +#[derive(Debug, Default)] +pub struct IngestMeta { + pub mime_type: Option, + pub human_file_name: Option, + pub human_readable_path: Option, + pub application: Option, + pub user: Option, + pub tags: Option, + pub description: Option, +} + +// ── Client implementation ── + +impl CanClient { + pub fn new(base_url: &str) -> Self { + Self { + client: reqwest::Client::new(), + base_url: base_url.trim_end_matches('/').to_string(), + } + } + + /// List assets with pagination and ordering + pub async fn list( + &self, + limit: i64, + offset: i64, + order: &str, + offset_time: Option, + ) -> Result { + let mut url = format!("{}/list?limit={}&offset={}&order={}", self.base_url, limit, offset, order); + if let Some(ts) = offset_time { + url.push_str(&format!("&offset_time={}", ts)); + } + let resp = self.client.get(&url).send().await.context("list request failed")?; + let status = resp.status(); + if !status.is_success() { + let text = resp.text().await.unwrap_or_default(); + anyhow::bail!("CAN list failed ({}): {}", status, text); + } + let api: ApiResponse = resp.json().await.context("parse list response")?; + Ok(api.data) + } + + /// List all assets (paginated, including trashed for full sync) + pub async fn list_all( + &self, + limit: i64, + offset: i64, + include_trashed: bool, + ) -> Result { + let mut url = format!("{}/list?limit={}&offset={}&order=asc", self.base_url, limit, offset); + if include_trashed { + url.push_str("&include_trashed=true"); + } + let resp = self.client.get(&url).send().await.context("list_all request failed")?; + let status = resp.status(); + if !status.is_success() { + let text = resp.text().await.unwrap_or_default(); + anyhow::bail!("CAN list_all failed ({}): {}", status, text); + } + let api: ApiResponse = resp.json().await.context("parse list_all response")?; + Ok(api.data) + } + + /// Search assets by filters + pub async fn search(&self, params: &SearchParams) -> Result { + let resp = self + .client + .get(&format!("{}/search", self.base_url)) + .query(params) + .send() + .await + .context("search request failed")?; + let status = resp.status(); + if !status.is_success() { + let text = resp.text().await.unwrap_or_default(); + anyhow::bail!("CAN search failed ({}): {}", status, text); + } + let api: ApiResponse = resp.json().await.context("parse search response")?; + Ok(api.data) + } + + /// Download asset content by hash + pub async fn get_asset(&self, hash: &str) -> Result { + let resp = self + .client + .get(&format!("{}/asset/{}", self.base_url, hash)) + .send() + .await + .context("get_asset request failed")?; + let status = resp.status(); + if !status.is_success() { + let text = resp.text().await.unwrap_or_default(); + anyhow::bail!("CAN get_asset failed ({}): {}", status, text); + } + resp.bytes().await.context("read asset bytes") + } + + /// Get asset metadata by hash + pub async fn get_meta(&self, hash: &str) -> Result { + let resp = self + .client + .get(&format!("{}/asset/{}/meta", self.base_url, hash)) + .send() + .await + .context("get_meta request failed")?; + let status = resp.status(); + if !status.is_success() { + let text = resp.text().await.unwrap_or_default(); + anyhow::bail!("CAN get_meta failed ({}): {}", status, text); + } + let api: ApiResponse = resp.json().await.context("parse meta response")?; + Ok(api.data) + } + + /// Ingest a file into CAN service via multipart upload + pub async fn ingest(&self, content: Bytes, meta: IngestMeta) -> Result { + let file_part = multipart::Part::bytes(content.to_vec()) + .file_name(meta.human_file_name.clone().unwrap_or_else(|| "file".to_string())) + .mime_str(meta.mime_type.as_deref().unwrap_or("application/octet-stream"))?; + + let mut form = multipart::Form::new().part("file", file_part); + + if let Some(ref v) = meta.mime_type { + form = form.text("mime_type", v.clone()); + } + if let Some(ref v) = meta.human_file_name { + form = form.text("human_file_name", v.clone()); + } + if let Some(ref v) = meta.human_readable_path { + form = form.text("human_readable_path", v.clone()); + } + if let Some(ref v) = meta.application { + form = form.text("application", v.clone()); + } + if let Some(ref v) = meta.user { + form = form.text("user", v.clone()); + } + if let Some(ref v) = meta.tags { + form = form.text("tags", v.clone()); + } + if let Some(ref v) = meta.description { + form = form.text("description", v.clone()); + } + + let resp = self + .client + .post(&format!("{}/ingest", self.base_url)) + .multipart(form) + .send() + .await + .context("ingest request failed")?; + let status = resp.status(); + if !status.is_success() { + let text = resp.text().await.unwrap_or_default(); + anyhow::bail!("CAN ingest failed ({}): {}", status, text); + } + let api: ApiResponse = resp.json().await.context("parse ingest response")?; + Ok(api.data) + } + + /// Update asset metadata (tags, description) + pub async fn update_meta( + &self, + hash: &str, + tags: Option>, + description: Option, + ) -> Result<()> { + #[derive(Serialize)] + struct MetadataUpdate { + #[serde(skip_serializing_if = "Option::is_none")] + tags: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + description: Option, + } + let resp = self + .client + .patch(&format!("{}/asset/{}", self.base_url, hash)) + .json(&MetadataUpdate { tags, description }) + .send() + .await + .context("update_meta request failed")?; + let status = resp.status(); + if !status.is_success() { + let text = resp.text().await.unwrap_or_default(); + anyhow::bail!("CAN update_meta failed ({}): {}", status, text); + } + Ok(()) + } + + /// Check if CAN service is reachable + pub async fn health_check(&self) -> Result { + match self.list(1, 0, "desc", None).await { + Ok(_) => Ok(true), + Err(_) => Ok(false), + } + } +} diff --git a/examples/can-sync/src/config.rs b/examples/can-sync/src/config.rs new file mode 100644 index 0000000..d889d1b --- /dev/null +++ b/examples/can-sync/src/config.rs @@ -0,0 +1,78 @@ +use anyhow::{Context, Result}; +use serde::{Deserialize, Serialize}; +use std::path::{Path, PathBuf}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SyncConfig { + /// Base URL for the CAN service API (e.g. "http://127.0.0.1:3210/api/v1/can/0") + pub can_service_url: String, + + /// Address for the CAN Sync HTTP API (e.g. "127.0.0.1:3213") + pub listen_addr: String, + + /// Directory for persistent data (peer key, sync state DB) + pub data_dir: String, + + /// Optional custom relay URL; null uses iroh's public relay + pub relay_url: Option, + + /// Seconds between fast polls for new assets + #[serde(default = "default_poll_interval")] + pub poll_interval_secs: u64, + + /// Seconds between full scans of all assets + #[serde(default = "default_full_scan_interval")] + pub full_scan_interval_secs: u64, +} + +fn default_poll_interval() -> u64 { + 5 +} + +fn default_full_scan_interval() -> u64 { + 300 +} + +impl SyncConfig { + /// Load config from a YAML file, falling back to defaults if not found + pub fn load(path: &Path) -> Result { + if path.exists() { + let contents = + std::fs::read_to_string(path).context("Failed to read config file")?; + let config: SyncConfig = + serde_yaml::from_str(&contents).context("Failed to parse config YAML")?; + Ok(config) + } else { + tracing::warn!("Config file not found at {}, using defaults", path.display()); + Ok(Self::default()) + } + } + + /// Resolved data directory path + pub fn data_path(&self) -> PathBuf { + PathBuf::from(&self.data_dir) + } + + /// Path to the peer keypair file + pub fn peer_key_path(&self) -> PathBuf { + self.data_path().join("peer_key") + } + + /// Path to the sync state SQLite database + pub fn db_path(&self) -> PathBuf { + self.data_path().join("can_sync.db") + } +} + +impl Default for SyncConfig { + fn default() -> Self { + Self { + can_service_url: "http://127.0.0.1:3210/api/v1/can/0".to_string(), + listen_addr: "127.0.0.1:3213".to_string(), + data_dir: "./can_sync_data".to_string(), + relay_url: None, + poll_interval_secs: default_poll_interval(), + full_scan_interval_secs: default_full_scan_interval(), + } + } +} diff --git a/examples/can-sync/src/fetcher.rs b/examples/can-sync/src/fetcher.rs new file mode 100644 index 0000000..84f1dc1 --- /dev/null +++ b/examples/can-sync/src/fetcher.rs @@ -0,0 +1,352 @@ +use std::sync::Arc; + +use anyhow::Result; +use futures_lite::StreamExt; +use sha2::{Digest, Sha256}; +use tokio::io::AsyncReadExt; +use tracing::{debug, error, info, warn}; + +use crate::can_client::{CanClient, IngestMeta}; +use crate::library::SyncState; +use crate::manifest::AssetSyncEntry; +use crate::node::SyncNode; + +/// The fetcher receives remote asset entries from iroh documents +/// and ingests them into the local CAN service. +pub struct Fetcher { + can: CanClient, + state: Arc, + node: Arc, +} + +impl Fetcher { + pub fn new(can: CanClient, state: Arc, node: Arc) -> Self { + Self { can, state, node } + } + + /// Run the fetcher — subscribes to library document events for real-time sync, + /// falls back to periodic polling for documents without active subscriptions + pub async fn run(self) { + info!("Fetcher started — watching for remote asset entries"); + + // Run two loops concurrently: + // 1. Subscription watcher — subscribes to active library docs + // 2. Periodic checker — catches anything missed + let poll_interval = tokio::time::interval(std::time::Duration::from_secs(10)); + let sub_interval = tokio::time::interval(std::time::Duration::from_secs(5)); + + tokio::pin!(poll_interval); + tokio::pin!(sub_interval); + + loop { + tokio::select! { + _ = poll_interval.tick() => { + if let Err(e) = self.check_for_new_entries().await { + warn!("Fetcher poll error: {:#}", e); + } + } + _ = sub_interval.tick() => { + // Try to subscribe to any library docs that we haven't subscribed to yet + if let Err(e) = self.subscribe_to_libraries().await { + debug!("Fetcher subscription check: {:#}", e); + } + } + } + } + } + + /// Subscribe to document events for all libraries that have doc_ids + async fn subscribe_to_libraries(&self) -> Result<()> { + let libraries = self.state.list_libraries()?; + + for lib in &libraries { + if let Some(ref doc_id_hex) = lib.doc_id { + // Open the doc and subscribe to events + let doc = match self.node.open_doc(doc_id_hex).await { + Ok(d) => d, + Err(_) => continue, + }; + + let mut events = match doc.subscribe().await { + Ok(e) => e, + Err(_) => continue, + }; + + // Spawn a task to process events from this doc + let can = self.can.clone(); + let node_peer_id = self.node.peer_id(); + let node = self.node.clone(); + let lib_name = lib.name.clone(); + + tokio::spawn(async move { + while let Some(event) = events.next().await { + match event { + Ok(iroh_docs::engine::LiveEvent::InsertRemote { + entry, + content_status, + .. + }) => { + let key = entry.key().to_vec(); + let can_hash = String::from_utf8_lossy(&key).to_string(); + + if content_status == iroh_docs::ContentStatus::Complete { + // The entry value (our AssetSyncEntry) is available + // Read the entry content from the blob store + let content_hash = entry.content_hash(); + let mut reader = node.blobs.reader(content_hash); + let mut buf = Vec::new(); + if reader.read_to_end(&mut buf).await.is_ok() { + if let Ok(sync_entry) = AssetSyncEntry::from_bytes(&buf) { + if sync_entry.last_modified_by == node_peer_id { + continue; // Skip our own entries + } + info!( + "Received remote entry for {} in library '{}'", + &can_hash[..can_hash.len().min(12)], + lib_name + ); + if let Err(e) = process_remote_entry( + &can, + &node, + &node_peer_id, + &can_hash, + sync_entry, + ) + .await + { + error!( + "Error processing remote entry {}: {:#}", + &can_hash[..can_hash.len().min(12)], + e + ); + } + } + } + } + } + Ok(iroh_docs::engine::LiveEvent::NeighborUp(peer)) => { + info!("Peer connected: {}", peer.fmt_short()); + } + Ok(iroh_docs::engine::LiveEvent::NeighborDown(peer)) => { + info!("Peer disconnected: {}", peer.fmt_short()); + } + Ok(_) => {} // Ignore other events + Err(e) => { + warn!("Document event error: {:#}", e); + break; + } + } + } + }); + + // Only subscribe to one doc per tick to avoid overwhelming + return Ok(()); + } + } + Ok(()) + } + + /// Check all library documents for entries we don't have locally (polling fallback) + async fn check_for_new_entries(&self) -> Result<()> { + let libraries = self.state.list_libraries()?; + + for lib in &libraries { + if let Some(ref doc_id_hex) = lib.doc_id { + let doc = match self.node.open_doc(doc_id_hex).await { + Ok(d) => d, + Err(_) => continue, + }; + + // Query all entries (latest per key) + let query = iroh_docs::store::Query::single_latest_per_key().build(); + let entries = match doc.get_many(query).await { + Ok(e) => e, + Err(_) => continue, + }; + tokio::pin!(entries); + + while let Some(Ok(entry)) = entries.next().await { + let key = entry.key().to_vec(); + let can_hash = String::from_utf8_lossy(&key).to_string(); + + // Read the entry value (AssetSyncEntry) + let content_hash = entry.content_hash(); + let mut reader = self.node.blobs.reader(content_hash); + let mut buf = Vec::new(); + if reader.read_to_end(&mut buf).await.is_err() { + continue; + } + + let sync_entry = match AssetSyncEntry::from_bytes(&buf) { + Ok(e) => e, + Err(_) => continue, + }; + + // Skip our own entries + if sync_entry.last_modified_by == self.node.peer_id() { + continue; + } + + // Check if already processed + if self.state.is_announced(&lib.id, &can_hash).unwrap_or(false) { + continue; + } + + info!( + "Polling found remote entry for {} in library '{}'", + &can_hash[..can_hash.len().min(12)], + lib.name + ); + + if let Err(e) = process_remote_entry( + &self.can, + &self.node, + &self.node.peer_id(), + &can_hash, + sync_entry, + ) + .await + { + error!( + "Error processing remote entry {}: {:#}", + &can_hash[..can_hash.len().min(12)], + e + ); + } + + // Mark as processed + let _ = self.state.mark_announced(&lib.id, &can_hash, 1); + } + } + } + Ok(()) + } +} + +/// Process a remote asset entry — download blob and ingest into CAN service +async fn process_remote_entry( + can: &CanClient, + node: &SyncNode, + local_peer_id: &str, + can_hash: &str, + entry: AssetSyncEntry, +) -> Result<()> { + // Skip if this is our own entry + if entry.last_modified_by == local_peer_id { + return Ok(()); + } + + // Check if already in local CAN service + match can.get_meta(can_hash).await { + Ok(existing) => { + // Asset exists — check if metadata needs updating + if entry.tags != existing.tags + || entry.description != existing.description + || entry.is_trashed != existing.is_trashed + { + info!("Updating metadata for {} from remote peer", &can_hash[..12]); + can.update_meta( + can_hash, + Some(entry.tags.clone()), + entry.description.clone(), + ) + .await?; + } + return Ok(()); + } + Err(_) => { + // Asset not found locally — need to fetch and ingest + } + } + + info!( + "Fetching remote asset {} ({}B) from peer {}", + &can_hash[..12], + entry.size, + &entry.last_modified_by[..entry.last_modified_by.len().min(12)] + ); + + // Download blob via iroh + let content = download_blob(node, &entry).await?; + + if content.is_empty() { + warn!("Downloaded empty blob for {} — skipping", &can_hash[..12]); + return Ok(()); + } + + // Verify CAN hash: SHA256(timestamp_bytes + content) + if !verify_can_hash(can_hash, entry.timestamp, &content) { + error!( + "CAN hash verification failed for {} — rejecting", + &can_hash[..12] + ); + return Ok(()); + } + + // Ingest into local CAN service + let meta = IngestMeta { + mime_type: Some(entry.mime_type.clone()), + human_file_name: entry.human_filename.clone(), + human_readable_path: entry.human_path.clone(), + application: entry.application.clone(), + user: entry.user.clone(), + tags: if entry.tags.is_empty() { + None + } else { + Some(entry.tags.join(",")) + }, + description: entry.description.clone(), + }; + + match can.ingest(content.into(), meta).await { + Ok(result) => { + info!( + "Ingested remote asset: hash={}, filename={}", + &result.hash[..12], + result.filename + ); + } + Err(e) => { + error!("Failed to ingest remote asset {}: {:#}", &can_hash[..12], e); + } + } + + Ok(()) +} + +/// Download a blob via iroh using the blob hash from the sync entry +async fn download_blob(node: &SyncNode, entry: &AssetSyncEntry) -> Result> { + let blob_hash_str = match &entry.iroh_blob_hash { + Some(h) => h, + None => { + warn!("No iroh blob hash in sync entry — cannot download"); + return Ok(Vec::new()); + } + }; + + // Parse the BLAKE3 hash + let blob_hash: iroh_blobs::Hash = blob_hash_str + .parse() + .map_err(|_| anyhow::anyhow!("Invalid iroh blob hash: {}", &blob_hash_str[..12]))?; + + // Read from the local blob store (iroh-docs should have synced it) + let mut reader = node.blobs.reader(blob_hash); + let mut buf = Vec::with_capacity(entry.size as usize); + reader.read_to_end(&mut buf).await?; + + debug!( + "Downloaded blob {} ({} bytes)", + &blob_hash_str[..12], + buf.len() + ); + Ok(buf) +} + +/// Verify CAN hash: SHA256(timestamp_string + content) matches expected hash +fn verify_can_hash(expected_hash: &str, timestamp: i64, content: &[u8]) -> bool { + let mut hasher = Sha256::new(); + hasher.update(timestamp.to_string().as_bytes()); + hasher.update(content); + let computed = hex::encode(hasher.finalize()); + computed == expected_hash +} diff --git a/examples/can-sync/src/library.rs b/examples/can-sync/src/library.rs new file mode 100644 index 0000000..1c0f985 --- /dev/null +++ b/examples/can-sync/src/library.rs @@ -0,0 +1,288 @@ +use anyhow::{Context, Result}; +use rusqlite::Connection; +use serde::{Deserialize, Serialize}; + +use crate::can_client::AssetMeta; + +/// Filter criteria that determines which CAN assets belong to a library +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LibraryFilter { + /// Match assets with this application tag + #[serde(skip_serializing_if = "Option::is_none")] + pub application: Option, + /// Match assets with any of these tags + #[serde(skip_serializing_if = "Option::is_none")] + pub tags: Option>, + /// Match assets from this user + #[serde(skip_serializing_if = "Option::is_none")] + pub user: Option, + /// Match assets with MIME type prefix (e.g. "image/") + #[serde(skip_serializing_if = "Option::is_none")] + pub mime_prefix: Option, + /// Manual list of specific hashes to include + #[serde(skip_serializing_if = "Option::is_none")] + pub hashes: Option>, +} + +impl LibraryFilter { + /// Check if an asset matches this filter + pub fn matches(&self, asset: &AssetMeta) -> bool { + // If hashes list is set, only match those exact hashes + if let Some(ref hashes) = self.hashes { + return hashes.contains(&asset.hash); + } + + // All set criteria must match (AND logic) + if let Some(ref app) = self.application { + if asset.application.as_deref() != Some(app.as_str()) { + return false; + } + } + + if let Some(ref required_tags) = self.tags { + // Asset must have at least one of the required tags + if !required_tags.iter().any(|t| asset.tags.contains(t)) { + return false; + } + } + + if let Some(ref user) = self.user { + if asset.user.as_deref() != Some(user.as_str()) { + return false; + } + } + + if let Some(ref prefix) = self.mime_prefix { + if !asset.mime_type.starts_with(prefix.as_str()) { + return false; + } + } + + true + } +} + +/// A library definition stored locally +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Library { + /// Unique library ID (UUID) + pub id: String, + /// Human-readable name + pub name: String, + /// Filter criteria + pub filter: LibraryFilter, + /// iroh document ID (namespace) — set after creation + pub doc_id: Option, + /// Whether this library was created locally or joined from remote + pub is_local: bool, + /// Creation timestamp + pub created_at: i64, +} + +/// Tracks which assets have been announced to which libraries. +/// Uses std::sync::Mutex because rusqlite::Connection is !Send, +/// so tokio::sync::RwLock won't work across .await points. +pub struct SyncState { + db: std::sync::Mutex, +} + +impl SyncState { + /// Open or create the sync state database + pub fn open(path: &std::path::Path) -> Result { + let db = Connection::open(path).context("open sync state DB")?; + db.execute_batch( + " + CREATE TABLE IF NOT EXISTS libraries ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + filter_json TEXT NOT NULL, + doc_id TEXT, + is_local INTEGER NOT NULL DEFAULT 1, + created_at INTEGER NOT NULL + ); + + CREATE TABLE IF NOT EXISTS announced_assets ( + library_id TEXT NOT NULL, + hash TEXT NOT NULL, + version INTEGER NOT NULL DEFAULT 1, + announced_at INTEGER NOT NULL, + PRIMARY KEY (library_id, hash), + FOREIGN KEY (library_id) REFERENCES libraries(id) ON DELETE CASCADE + ); + + CREATE TABLE IF NOT EXISTS sync_state ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL + ); + ", + ) + .context("init sync state tables")?; + Ok(Self { + db: std::sync::Mutex::new(db), + }) + } + + fn lock_db(&self) -> std::sync::MutexGuard<'_, Connection> { + self.db.lock().expect("sync state DB lock poisoned") + } + + // ── Library CRUD ── + + pub fn save_library(&self, lib: &Library) -> Result<()> { + let db = self.lock_db(); + let filter_json = serde_json::to_string(&lib.filter)?; + db.execute( + "INSERT OR REPLACE INTO libraries (id, name, filter_json, doc_id, is_local, created_at) + VALUES (?1, ?2, ?3, ?4, ?5, ?6)", + rusqlite::params![ + lib.id, + lib.name, + filter_json, + lib.doc_id, + lib.is_local as i32, + lib.created_at, + ], + )?; + Ok(()) + } + + pub fn list_libraries(&self) -> Result> { + let db = self.lock_db(); + let mut stmt = + db.prepare("SELECT id, name, filter_json, doc_id, is_local, created_at FROM libraries")?; + let libs = stmt + .query_map([], |row| { + let filter_json: String = row.get(2)?; + Ok(Library { + id: row.get(0)?, + name: row.get(1)?, + filter: serde_json::from_str(&filter_json).unwrap_or(LibraryFilter { + application: None, + tags: None, + user: None, + mime_prefix: None, + hashes: None, + }), + doc_id: row.get(3)?, + is_local: row.get::<_, i32>(4)? != 0, + created_at: row.get(5)?, + }) + })? + .collect::, _>>()?; + Ok(libs) + } + + pub fn get_library(&self, id: &str) -> Result> { + let db = self.lock_db(); + let mut stmt = db.prepare( + "SELECT id, name, filter_json, doc_id, is_local, created_at FROM libraries WHERE id = ?1", + )?; + let mut rows = stmt.query_map([id], |row| { + let filter_json: String = row.get(2)?; + Ok(Library { + id: row.get(0)?, + name: row.get(1)?, + filter: serde_json::from_str(&filter_json).unwrap_or(LibraryFilter { + application: None, + tags: None, + user: None, + mime_prefix: None, + hashes: None, + }), + doc_id: row.get(3)?, + is_local: row.get::<_, i32>(4)? != 0, + created_at: row.get(5)?, + }) + })?; + match rows.next() { + Some(Ok(lib)) => Ok(Some(lib)), + Some(Err(e)) => Err(e.into()), + None => Ok(None), + } + } + + pub fn delete_library(&self, id: &str) -> Result<()> { + let db = self.lock_db(); + db.execute("DELETE FROM announced_assets WHERE library_id = ?1", [id])?; + db.execute("DELETE FROM libraries WHERE id = ?1", [id])?; + Ok(()) + } + + pub fn update_library_doc_id(&self, id: &str, doc_id: &str) -> Result<()> { + let db = self.lock_db(); + db.execute( + "UPDATE libraries SET doc_id = ?1 WHERE id = ?2", + [doc_id, id], + )?; + Ok(()) + } + + // ── Asset announcement tracking ── + + pub fn is_announced(&self, library_id: &str, hash: &str) -> Result { + let db = self.lock_db(); + let count: i64 = db.query_row( + "SELECT COUNT(*) FROM announced_assets WHERE library_id = ?1 AND hash = ?2", + [library_id, hash], + |row| row.get(0), + )?; + Ok(count > 0) + } + + pub fn get_announced_version(&self, library_id: &str, hash: &str) -> Result> { + let db = self.lock_db(); + let mut stmt = db.prepare( + "SELECT version FROM announced_assets WHERE library_id = ?1 AND hash = ?2", + )?; + let mut rows = stmt.query_map(rusqlite::params![library_id, hash], |row| { + row.get::<_, i64>(0) + })?; + match rows.next() { + Some(Ok(v)) => Ok(Some(v as u64)), + Some(Err(e)) => Err(e.into()), + None => Ok(None), + } + } + + pub fn mark_announced(&self, library_id: &str, hash: &str, version: u64) -> Result<()> { + let db = self.lock_db(); + let now = chrono::Utc::now().timestamp_millis(); + db.execute( + "INSERT OR REPLACE INTO announced_assets (library_id, hash, version, announced_at) + VALUES (?1, ?2, ?3, ?4)", + rusqlite::params![library_id, hash, version as i64, now], + )?; + Ok(()) + } + + pub fn remove_announced(&self, library_id: &str, hash: &str) -> Result<()> { + let db = self.lock_db(); + db.execute( + "DELETE FROM announced_assets WHERE library_id = ?1 AND hash = ?2", + [library_id, hash], + )?; + Ok(()) + } + + // ── General state ── + + pub fn get_state(&self, key: &str) -> Result> { + let db = self.lock_db(); + let mut stmt = db.prepare("SELECT value FROM sync_state WHERE key = ?1")?; + let mut rows = stmt.query_map([key], |row| row.get::<_, String>(0))?; + match rows.next() { + Some(Ok(v)) => Ok(Some(v)), + Some(Err(e)) => Err(e.into()), + None => Ok(None), + } + } + + pub fn set_state(&self, key: &str, value: &str) -> Result<()> { + let db = self.lock_db(); + db.execute( + "INSERT OR REPLACE INTO sync_state (key, value) VALUES (?1, ?2)", + [key, value], + )?; + Ok(()) + } +} diff --git a/examples/can-sync/src/main.rs b/examples/can-sync/src/main.rs new file mode 100644 index 0000000..4dc31ad --- /dev/null +++ b/examples/can-sync/src/main.rs @@ -0,0 +1,121 @@ +#![allow(dead_code)] + +mod announcer; +mod can_client; +mod config; +mod fetcher; +mod library; +mod manifest; +mod node; +mod routes; + +use std::path::PathBuf; +use std::sync::Arc; + +use anyhow::{Context, Result}; +use tracing::info; + +use crate::announcer::Announcer; +use crate::can_client::CanClient; +use crate::config::SyncConfig; +use crate::fetcher::Fetcher; +use crate::library::SyncState; +use crate::node::SyncNode; +use crate::routes::AppState; + +#[tokio::main] +async fn main() -> Result<()> { + // Initialize tracing + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| "can_sync=info,iroh=warn".parse().unwrap()), + ) + .init(); + + // Load config + let config_path = std::env::args() + .nth(1) + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from("config.yaml")); + + let config = SyncConfig::load(&config_path)?; + info!("CAN Sync starting..."); + info!(" CAN service: {}", config.can_service_url); + info!(" Listen addr: {}", config.listen_addr); + info!(" Data dir: {}", config.data_dir); + + // Ensure data directory exists + std::fs::create_dir_all(config.data_path()) + .context("Failed to create data directory")?; + + // Initialize CAN service client + let can = CanClient::new(&config.can_service_url); + + // Check CAN service health + match can.health_check().await { + Ok(true) => info!("CAN service is reachable"), + Ok(false) | Err(_) => { + tracing::warn!( + "CAN service at {} is not reachable — will retry on each poll", + config.can_service_url + ); + } + } + + // Open sync state database + let state = SyncState::open(&config.db_path())?; + let state = Arc::new(state); + info!("Sync state DB opened at {}", config.db_path().display()); + + // Start iroh P2P node + let node = SyncNode::spawn(&config).await?; + let node = Arc::new(node); + info!("iroh node ID: {}", node.peer_id()); + + // Build shared app state + let app_state = Arc::new(AppState { + node: node.clone(), + state: state.clone(), + can: can.clone(), + }); + + // Start the announcer (polls CAN service for new assets) + let announcer = Announcer::new( + can.clone(), + state.clone(), + node.clone(), + config.poll_interval_secs, + config.full_scan_interval_secs, + ); + tokio::spawn(async move { + announcer.run().await; + }); + + // Start the fetcher (receives remote assets and ingests them) + let fetcher = Fetcher::new(can.clone(), state.clone(), node.clone()); + tokio::spawn(async move { + fetcher.run().await; + }); + + // Build HTTP router + let router = routes::build_router(app_state); + + // Start HTTP server + let listener = tokio::net::TcpListener::bind(&config.listen_addr) + .await + .context("Failed to bind HTTP listener")?; + info!("CAN Sync API listening on http://{}", config.listen_addr); + + // Open browser to status page + let status_url = format!("http://{}/status", config.listen_addr); + if open::that(&status_url).is_err() { + info!("Open {} in your browser to check status", status_url); + } + + axum::serve(listener, router) + .await + .context("HTTP server error")?; + + Ok(()) +} diff --git a/examples/can-sync/src/manifest.rs b/examples/can-sync/src/manifest.rs new file mode 100644 index 0000000..b8210ed --- /dev/null +++ b/examples/can-sync/src/manifest.rs @@ -0,0 +1,75 @@ +use serde::{Deserialize, Serialize}; + +use crate::can_client::AssetMeta; + +/// Entry stored in iroh documents for each synced asset. +/// Key = CAN hash, Value = serialized AssetSyncEntry +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AssetSyncEntry { + /// CAN timestamp (milliseconds since epoch) + pub timestamp: i64, + /// MIME type + pub mime_type: String, + /// Application tag + pub application: Option, + /// User identity + pub user: Option, + /// Tags list + pub tags: Vec, + /// Description + pub description: Option, + /// Original human-readable filename + pub human_filename: Option, + /// Original human-readable path + pub human_path: Option, + /// File size in bytes + pub size: i64, + /// Whether the asset is trashed + pub is_trashed: bool, + /// iroh blob hash (BLAKE3) for downloading via iroh + pub iroh_blob_hash: Option, + /// Version counter for conflict resolution (higher wins) + pub version: u64, + /// Peer ID that last modified this entry + pub last_modified_by: String, +} + +impl AssetSyncEntry { + /// Create from CAN service asset metadata + pub fn from_asset_meta(meta: &AssetMeta, peer_id: &str) -> Self { + Self { + timestamp: meta.timestamp, + mime_type: meta.mime_type.clone(), + application: meta.application.clone(), + user: meta.user.clone(), + tags: meta.tags.clone(), + description: meta.description.clone(), + human_filename: meta.human_filename.clone(), + human_path: meta.human_path.clone(), + size: meta.size, + is_trashed: meta.is_trashed, + iroh_blob_hash: None, + version: 1, + last_modified_by: peer_id.to_string(), + } + } + + /// Serialize to bytes for storage in iroh document + pub fn to_bytes(&self) -> Vec { + postcard::to_allocvec(self).expect("serialize AssetSyncEntry") + } + + /// Deserialize from bytes + pub fn from_bytes(bytes: &[u8]) -> anyhow::Result { + Ok(postcard::from_bytes(bytes)?) + } + + /// Check if metadata differs from a CAN asset (indicates update needed) + pub fn metadata_differs(&self, meta: &AssetMeta) -> bool { + self.tags != meta.tags + || self.description != meta.description + || self.is_trashed != meta.is_trashed + || self.human_filename != meta.human_filename + || self.human_path != meta.human_path + } +} diff --git a/examples/can-sync/src/node.rs b/examples/can-sync/src/node.rs new file mode 100644 index 0000000..4a32d61 --- /dev/null +++ b/examples/can-sync/src/node.rs @@ -0,0 +1,150 @@ +use anyhow::{Context, Result}; +use iroh::protocol::Router; +use iroh::Endpoint; +use iroh_blobs::store::mem::MemStore; +use iroh_blobs::{BlobsProtocol, ALPN as BLOBS_ALPN}; +use iroh_docs::api::protocol::{AddrInfoOptions, ShareMode}; +use iroh_docs::protocol::Docs; +use iroh_docs::{AuthorId, DocTicket, NamespaceId, ALPN as DOCS_ALPN}; +use iroh_gossip::net::Gossip; +use iroh_gossip::ALPN as GOSSIP_ALPN; +use tokio::sync::OnceCell; + +use crate::config::SyncConfig; + +/// Holds all iroh subsystems for the P2P node +pub struct SyncNode { + pub endpoint: Endpoint, + pub blobs: BlobsProtocol, + pub docs: Docs, + pub gossip: Gossip, + pub router: Router, + /// Cached default author ID (created once on startup) + author_id: OnceCell, +} + +impl SyncNode { + /// Start the iroh node with all protocol handlers + pub async fn spawn(_config: &SyncConfig) -> Result { + // Build endpoint (Ed25519 keypair auto-generated and cached) + let endpoint = Endpoint::bind() + .await + .map_err(|e| anyhow::anyhow!("Failed to bind iroh endpoint: {}", e))?; + + tracing::info!( + "iroh node started — EndpointID: {}", + endpoint.id() + ); + + // Gossip for peer communication + let gossip = Gossip::builder().spawn(endpoint.clone()); + + // Blob store (in-memory — blobs are transient, CAN service is authoritative) + let mem_store = MemStore::default(); + let blobs_store: &iroh_blobs::api::Store = &mem_store; + let blobs = BlobsProtocol::new(blobs_store, None); + + // Document sync (CRDT-replicated key-value store) + let docs = Docs::memory() + .spawn(endpoint.clone(), blobs_store.clone(), gossip.clone()) + .await + .context("Failed to spawn iroh-docs")?; + + // Router accepts incoming connections and dispatches to handlers + let router = Router::builder(endpoint.clone()) + .accept(BLOBS_ALPN, blobs.clone()) + .accept(GOSSIP_ALPN, gossip.clone()) + .accept(DOCS_ALPN, docs.clone()) + .spawn(); + + Ok(Self { + endpoint, + blobs, + docs, + gossip, + router, + author_id: OnceCell::new(), + }) + } + + /// Get this node's peer ID as a string + pub fn peer_id(&self) -> String { + self.endpoint.id().to_string() + } + + /// Get the node's endpoint address info for sharing + pub fn endpoint_addr(&self) -> iroh::EndpointAddr { + self.endpoint.addr() + } + + /// Get or create the default author for writing to documents + pub async fn author(&self) -> Result { + self.author_id + .get_or_try_init(|| async { + self.docs.author_default().await + }) + .await + .copied() + } + + /// Create a new iroh document and return its NamespaceId as a hex string + pub async fn create_doc(&self) -> Result { + let doc = self.docs.create().await?; + let ns_id = doc.id(); + Ok(hex::encode(ns_id.to_bytes())) + } + + /// Open an existing document by its hex-encoded namespace ID + pub async fn open_doc(&self, doc_id_hex: &str) -> Result { + let ns_id = parse_namespace_id(doc_id_hex)?; + self.docs + .open(ns_id) + .await? + .ok_or_else(|| anyhow::anyhow!("Document {} not found", &doc_id_hex[..12])) + } + + /// Write a key-value entry to a document + pub async fn write_to_doc( + &self, + doc_id_hex: &str, + key: &[u8], + value: &[u8], + ) -> Result<()> { + let doc = self.open_doc(doc_id_hex).await?; + let author = self.author().await?; + doc.set_bytes(author, key.to_vec(), value.to_vec()).await?; + Ok(()) + } + + /// Generate a share ticket (DocTicket) for a document + pub async fn share_doc(&self, doc_id_hex: &str) -> Result { + let doc = self.open_doc(doc_id_hex).await?; + let ticket = doc + .share(ShareMode::Write, AddrInfoOptions::RelayAndAddresses) + .await?; + Ok(ticket) + } + + /// Import a document from a DocTicket, returns the namespace ID as hex + pub async fn import_doc(&self, ticket: DocTicket) -> Result { + let doc = self.docs.import(ticket).await?; + let ns_id = doc.id(); + Ok(hex::encode(ns_id.to_bytes())) + } + + /// Graceful shutdown + pub async fn shutdown(self) -> Result<()> { + tracing::info!("Shutting down iroh node..."); + self.router.shutdown().await?; + Ok(()) + } +} + +/// Parse a hex-encoded NamespaceId +pub fn parse_namespace_id(hex_str: &str) -> Result { + let bytes: [u8; 32] = hex::decode(hex_str) + .context("Invalid hex in doc_id")? + .try_into() + .map_err(|_| anyhow::anyhow!("doc_id must be 32 bytes (64 hex chars)"))?; + Ok(NamespaceId::from(bytes)) +} diff --git a/examples/can-sync/src/routes.rs b/examples/can-sync/src/routes.rs new file mode 100644 index 0000000..3175b16 --- /dev/null +++ b/examples/can-sync/src/routes.rs @@ -0,0 +1,430 @@ +use std::sync::Arc; + +use axum::{ + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, + routing::{get, post}, + Json, Router, +}; +use serde::{Deserialize, Serialize}; + +use crate::can_client::CanClient; +use crate::library::{Library, LibraryFilter, SyncState}; +use crate::node::SyncNode; + +/// Shared application state for route handlers +pub struct AppState { + pub node: Arc, + pub state: Arc, + pub can: CanClient, +} + +// ── Request/Response types ── + +#[derive(Serialize)] +struct StatusResponse { + peer_id: String, + can_service_healthy: bool, + library_count: usize, +} + +#[derive(Serialize)] +struct PeerInfo { + peer_id: String, +} + +#[derive(Deserialize)] +pub struct CreateLibraryRequest { + pub name: String, + pub filter: LibraryFilter, +} + +#[derive(Serialize)] +struct LibraryResponse { + id: String, + name: String, + filter: LibraryFilter, + doc_id: Option, + is_local: bool, + created_at: i64, +} + +#[derive(Serialize)] +struct InviteResponse { + ticket: String, +} + +#[derive(Deserialize)] +pub struct JoinRequest { + pub ticket: String, +} + +#[derive(Serialize)] +struct JoinResponse { + library_id: String, + message: String, +} + +#[derive(Serialize)] +struct ApiResp { + status: String, + data: T, +} + +#[derive(Serialize)] +struct ApiErr { + status: String, + error: String, +} + +fn ok_json(data: T) -> Json> { + Json(ApiResp { + status: "success".to_string(), + data, + }) +} + +fn err_resp(status: StatusCode, msg: &str) -> (StatusCode, Json) { + ( + status, + Json(ApiErr { + status: "error".to_string(), + error: msg.to_string(), + }), + ) +} + +// ── Routes ── + +pub fn build_router(app_state: Arc) -> Router { + Router::new() + .route("/status", get(get_status)) + .route("/peers", get(get_peers)) + .route("/libraries", post(create_library).get(list_libraries)) + .route( + "/libraries/{id}", + get(get_library).delete(delete_library), + ) + .route("/libraries/{id}/invite", post(create_invite)) + .route("/join", post(join_library)) + .with_state(app_state) +} + +// ── Handlers ── + +async fn get_status(State(app): State>) -> impl IntoResponse { + let can_healthy = app.can.health_check().await.unwrap_or(false); + let lib_count = app.state.list_libraries().unwrap_or_default().len(); + + ok_json(StatusResponse { + peer_id: app.node.peer_id(), + can_service_healthy: can_healthy, + library_count: lib_count, + }) + .into_response() +} + +async fn get_peers(State(app): State>) -> impl IntoResponse { + let peers: Vec = vec![PeerInfo { + peer_id: app.node.peer_id(), + }]; + ok_json(peers).into_response() +} + +async fn create_library( + State(app): State>, + Json(req): Json, +) -> impl IntoResponse { + // Create an iroh document for this library + let doc_id = match app.node.create_doc().await { + Ok(id) => Some(id), + Err(e) => { + tracing::warn!("Failed to create iroh document for library: {:#}", e); + None + } + }; + + let lib = Library { + id: uuid::Uuid::new_v4().to_string(), + name: req.name, + filter: req.filter, + doc_id, + is_local: true, + created_at: chrono::Utc::now().timestamp_millis(), + }; + + if let Err(e) = app.state.save_library(&lib) { + return err_resp( + StatusCode::INTERNAL_SERVER_ERROR, + &format!("save failed: {}", e), + ) + .into_response(); + } + + tracing::info!( + "Created library '{}' (id={}, doc_id={:?})", + lib.name, + &lib.id[..8], + lib.doc_id.as_deref().map(|d| &d[..12.min(d.len())]) + ); + + ok_json(LibraryResponse { + id: lib.id, + name: lib.name, + filter: lib.filter, + doc_id: lib.doc_id, + is_local: lib.is_local, + created_at: lib.created_at, + }) + .into_response() +} + +async fn list_libraries(State(app): State>) -> impl IntoResponse { + match app.state.list_libraries() { + Ok(libs) => { + let responses: Vec = libs + .into_iter() + .map(|lib| LibraryResponse { + id: lib.id, + name: lib.name, + filter: lib.filter, + doc_id: lib.doc_id, + is_local: lib.is_local, + created_at: lib.created_at, + }) + .collect(); + ok_json(responses).into_response() + } + Err(e) => { + err_resp(StatusCode::INTERNAL_SERVER_ERROR, &format!("{}", e)).into_response() + } + } +} + +async fn get_library( + State(app): State>, + Path(id): Path, +) -> impl IntoResponse { + match app.state.get_library(&id) { + Ok(Some(lib)) => ok_json(LibraryResponse { + id: lib.id, + name: lib.name, + filter: lib.filter, + doc_id: lib.doc_id, + is_local: lib.is_local, + created_at: lib.created_at, + }) + .into_response(), + Ok(None) => err_resp(StatusCode::NOT_FOUND, "Library not found").into_response(), + Err(e) => { + err_resp(StatusCode::INTERNAL_SERVER_ERROR, &format!("{}", e)).into_response() + } + } +} + +async fn delete_library( + State(app): State>, + Path(id): Path, +) -> impl IntoResponse { + match app.state.delete_library(&id) { + Ok(()) => ok_json("deleted").into_response(), + Err(e) => { + err_resp(StatusCode::INTERNAL_SERVER_ERROR, &format!("{}", e)).into_response() + } + } +} + +async fn create_invite( + State(app): State>, + Path(id): Path, +) -> impl IntoResponse { + match app.state.get_library(&id) { + Ok(Some(lib)) => { + let doc_id = match &lib.doc_id { + Some(d) => d, + None => { + return err_resp( + StatusCode::BAD_REQUEST, + "Library has no iroh document — cannot create invite", + ) + .into_response() + } + }; + + // Generate a real DocTicket via iroh + match app.node.share_doc(doc_id).await { + Ok(ticket) => { + // DocTicket implements Display via iroh's Ticket trait (base32 serialization) + let ticket_str = ticket.to_string(); + + // Wrap with library metadata so the joiner knows the name and filter + let invite_data = serde_json::json!({ + "ticket": ticket_str, + "library_name": lib.name, + "filter": lib.filter, + }); + let invite_b64 = base64_encode( + &serde_json::to_vec(&invite_data).unwrap(), + ); + + ok_json(InviteResponse { ticket: invite_b64 }).into_response() + } + Err(e) => err_resp( + StatusCode::INTERNAL_SERVER_ERROR, + &format!("Failed to create invite: {}", e), + ) + .into_response(), + } + } + Ok(None) => err_resp(StatusCode::NOT_FOUND, "Library not found").into_response(), + Err(e) => { + err_resp(StatusCode::INTERNAL_SERVER_ERROR, &format!("{}", e)).into_response() + } + } +} + +async fn join_library( + State(app): State>, + Json(req): Json, +) -> impl IntoResponse { + // Decode our envelope + let ticket_bytes = match base64_decode(&req.ticket) { + Ok(b) => b, + Err(_) => { + return err_resp(StatusCode::BAD_REQUEST, "Invalid ticket encoding").into_response() + } + }; + + let ticket_data: serde_json::Value = match serde_json::from_slice(&ticket_bytes) { + Ok(v) => v, + Err(_) => { + return err_resp(StatusCode::BAD_REQUEST, "Invalid ticket data").into_response() + } + }; + + // Extract the real DocTicket string + let ticket_str = match ticket_data["ticket"].as_str() { + Some(s) => s, + None => { + return err_resp(StatusCode::BAD_REQUEST, "Missing 'ticket' field in invite") + .into_response() + } + }; + + // Parse DocTicket from the serialized string + let doc_ticket: iroh_docs::DocTicket = match ticket_str.parse() { + Ok(t) => t, + Err(e) => { + return err_resp( + StatusCode::BAD_REQUEST, + &format!("Invalid DocTicket: {}", e), + ) + .into_response() + } + }; + + // Import the document via iroh (starts sync with remote peers) + let doc_id_hex = match app.node.import_doc(doc_ticket).await { + Ok(id) => id, + Err(e) => { + return err_resp( + StatusCode::INTERNAL_SERVER_ERROR, + &format!("Failed to join document: {}", e), + ) + .into_response() + } + }; + + let name = ticket_data["library_name"] + .as_str() + .unwrap_or("remote library") + .to_string(); + + let filter: LibraryFilter = serde_json::from_value(ticket_data["filter"].clone()) + .unwrap_or(LibraryFilter { + application: None, + tags: None, + user: None, + mime_prefix: None, + hashes: None, + }); + + let lib = Library { + id: uuid::Uuid::new_v4().to_string(), + name: name.clone(), + filter, + doc_id: Some(doc_id_hex), + is_local: false, + created_at: chrono::Utc::now().timestamp_millis(), + }; + + if let Err(e) = app.state.save_library(&lib) { + return err_resp( + StatusCode::INTERNAL_SERVER_ERROR, + &format!("save failed: {}", e), + ) + .into_response(); + } + + tracing::info!( + "Joined library '{}' (id={}, doc_id={:?})", + name, + &lib.id[..8], + lib.doc_id.as_deref().map(|d| &d[..12.min(d.len())]) + ); + + ok_json(JoinResponse { + library_id: lib.id, + message: "Joined library successfully".to_string(), + }) + .into_response() +} + +// ── Base64 helpers ── + +fn base64_encode(data: &[u8]) -> String { + const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + let mut result = Vec::new(); + for chunk in data.chunks(3) { + let b0 = chunk[0] as u32; + let b1 = if chunk.len() > 1 { chunk[1] as u32 } else { 0 }; + let b2 = if chunk.len() > 2 { chunk[2] as u32 } else { 0 }; + let triple = (b0 << 16) | (b1 << 8) | b2; + result.push(CHARS[((triple >> 18) & 0x3F) as usize]); + result.push(CHARS[((triple >> 12) & 0x3F) as usize]); + if chunk.len() > 1 { + result.push(CHARS[((triple >> 6) & 0x3F) as usize]); + } else { + result.push(b'='); + } + if chunk.len() > 2 { + result.push(CHARS[(triple & 0x3F) as usize]); + } else { + result.push(b'='); + } + } + String::from_utf8(result).unwrap() +} + +fn base64_decode(input: &str) -> Result, &'static str> { + const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + let input = input.trim_end_matches('='); + let bytes: Vec = input + .bytes() + .filter_map(|b| CHARS.iter().position(|&c| c == b).map(|p| p as u8)) + .collect(); + let mut buf = Vec::new(); + for chunk in bytes.chunks(4) { + if chunk.len() >= 2 { + buf.push((chunk[0] << 2) | (chunk[1] >> 4)); + } + if chunk.len() >= 3 { + buf.push((chunk[1] << 4) | (chunk[2] >> 2)); + } + if chunk.len() >= 4 { + buf.push((chunk[2] << 6) | chunk[3]); + } + } + Ok(buf) +} diff --git a/examples/canfs/Cargo.lock b/examples/canfs/Cargo.lock new file mode 100644 index 0000000..cc2e000 --- /dev/null +++ b/examples/canfs/Cargo.lock @@ -0,0 +1,2377 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "canfs" +version = "0.1.0" +dependencies = [ + "anyhow", + "chrono", + "clap", + "ctrlc", + "parking_lot", + "reqwest", + "serde", + "serde_json", + "tracing", + "tracing-subscriber", + "widestring", + "winfsp", +] + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link 0.2.1", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "ctrlc" +version = "3.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162" +dependencies = [ + "dispatch2", + "nix", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link 0.2.1", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nix" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "openssl" +version = "0.10.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "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]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "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]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "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]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[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.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winfsp" +version = "0.12.4+winfsp-2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51325fc20d218567a903cd5c382ad7d5a765f6860043f9c7d9ee1e907c5b75f6" +dependencies = [ + "bytemuck", + "parking_lot", + "paste", + "static_assertions", + "thiserror", + "widestring", + "windows", + "winfsp-sys", +] + +[[package]] +name = "winfsp-sys" +version = "0.12.1+winfsp-2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d42acc30c105f8d33507f556398237c738b681271c775a3b94eefd29d2b8c77e" +dependencies = [ + "bindgen", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/examples/canfs/Cargo.toml b/examples/canfs/Cargo.toml new file mode 100644 index 0000000..e227045 --- /dev/null +++ b/examples/canfs/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "canfs" +version = "0.1.0" +edition = "2021" +publish = false +description = "Mount CAN service assets as a virtual Windows filesystem via WinFSP" + +[[bin]] +name = "canfs" +path = "src/main.rs" + +[dependencies] +winfsp = "0.12" +widestring = "1" +reqwest = { version = "0.12", features = ["json", "blocking"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +chrono = "0.4" +parking_lot = "0.12" +clap = { version = "4", features = ["derive"] } +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +anyhow = "1" +ctrlc = "3" + +[build-dependencies] +winfsp = { version = "0.12", features = ["delayload"] } diff --git a/examples/canfs/build.rs b/examples/canfs/build.rs new file mode 100644 index 0000000..1b41e00 --- /dev/null +++ b/examples/canfs/build.rs @@ -0,0 +1,3 @@ +fn main() { + winfsp::build::winfsp_link_delayload(); +} diff --git a/examples/canfs/run.bat b/examples/canfs/run.bat new file mode 100644 index 0000000..322bb8e --- /dev/null +++ b/examples/canfs/run.bat @@ -0,0 +1,4 @@ +@echo off +set PATH=C:\Program Files (x86)\WinFsp\bin;%PATH% +cd /d "%~dp0" +cargo run -- --mount J: diff --git a/examples/canfs/src/api.rs b/examples/canfs/src/api.rs new file mode 100644 index 0000000..87b45d3 --- /dev/null +++ b/examples/canfs/src/api.rs @@ -0,0 +1,104 @@ +use anyhow::{Context, Result}; +use serde::Deserialize; + +/// Mirrors the server's AssetMeta response type. +#[derive(Debug, Clone, Deserialize)] +#[allow(dead_code)] +pub struct AssetMeta { + pub hash: String, + pub mime_type: String, + pub application: Option, + pub user: Option, + pub tags: Vec, + pub description: Option, + pub human_filename: Option, + pub human_path: Option, + pub timestamp: i64, + pub is_trashed: bool, + pub is_corrupted: bool, + #[serde(default)] + pub size: i64, +} + +#[derive(Debug, Deserialize)] +struct ApiResponse { + #[allow(dead_code)] + status: String, + data: T, +} + +#[derive(Debug, Deserialize)] +struct ListData { + items: Vec, + pagination: Pagination, +} + +#[derive(Debug, Deserialize)] +struct Pagination { + #[allow(dead_code)] + limit: i64, + #[allow(dead_code)] + offset: i64, + total: i64, +} + +/// Blocking HTTP client for the CAN service API. +pub struct CanClient { + client: reqwest::blocking::Client, + base_url: String, +} + +impl CanClient { + pub fn new(base_url: &str) -> Self { + Self { + client: reqwest::blocking::Client::new(), + base_url: base_url.trim_end_matches('/').to_string(), + } + } + + /// Fetch all non-trashed assets by paginating through the list endpoint. + pub fn list_all(&self) -> Result> { + let mut all = Vec::new(); + let page_size = 500; + let mut offset = 0i64; + + loop { + let url = format!( + "{}/list?limit={}&offset={}&order=desc", + self.base_url, page_size, offset + ); + let resp: ApiResponse = self + .client + .get(&url) + .send() + .context("failed to reach CAN service")? + .json() + .context("failed to parse list response")?; + + let count = resp.data.items.len() as i64; + all.extend(resp.data.items); + + if all.len() as i64 >= resp.data.pagination.total || count < page_size { + break; + } + offset += count; + } + + // Filter out trashed and corrupted + all.retain(|a| !a.is_trashed && !a.is_corrupted); + Ok(all) + } + + /// Download the raw bytes of an asset by hash. + pub fn fetch_bytes(&self, hash: &str) -> Result> { + let url = format!("{}/asset/{}", self.base_url, hash); + let bytes = self + .client + .get(&url) + .send() + .context("failed to fetch asset")? + .bytes() + .context("failed to read asset body")?; + Ok(bytes.to_vec()) + } +} diff --git a/examples/canfs/src/fs.rs b/examples/canfs/src/fs.rs new file mode 100644 index 0000000..3853d78 --- /dev/null +++ b/examples/canfs/src/fs.rs @@ -0,0 +1,316 @@ +use std::os::raw::c_void; +use std::sync::Arc; + +use parking_lot::{Mutex, RwLock}; +use tracing::{debug, warn}; +use widestring::U16CStr; + +use winfsp::filesystem::{ + DirBuffer, DirInfo, DirMarker, FileInfo, FileSecurity, FileSystemContext, OpenFileInfo, + VolumeInfo, WideNameInfo, +}; +use winfsp::FspError; + +use crate::api::{AssetMeta, CanClient}; +use crate::tree::{NodeId, NodeKind, VirtualTree}; +use crate::util; + +// NTSTATUS constants (raw i32 values to avoid windows crate version conflicts) +const STATUS_OBJECT_NAME_NOT_FOUND: i32 = 0xC0000034_u32 as i32; +const STATUS_NOT_A_DIRECTORY: i32 = 0xC0000103_u32 as i32; +const STATUS_UNEXPECTED_NETWORK_ERROR: i32 = 0xC00000C4_u32 as i32; +const STATUS_INVALID_DEVICE_REQUEST: i32 = 0xC0000010_u32 as i32; + +// File attribute constants +const FILE_ATTRIBUTE_DIRECTORY: u32 = 0x10; +const FILE_ATTRIBUTE_READONLY: u32 = 0x01; +const FILE_ATTRIBUTE_ARCHIVE: u32 = 0x20; + +fn ntstatus(code: i32) -> FspError { + FspError::NTSTATUS(code) +} + +/// Shared cache state: asset list + virtual tree. +pub struct CacheState { + pub assets: Vec, + pub tree: VirtualTree, +} + +/// The WinFSP filesystem context for CAN service. +pub struct CanFs { + pub cache: Arc>, + pub client: Arc, +} + +/// Per-open-handle context. +pub struct CanFileContext { + node_id: NodeId, + /// Lazily fetched file bytes. + content: Mutex>>, + /// Directory enumeration buffer. + dir_buffer: DirBuffer, +} + +impl FileSystemContext for CanFs { + type FileContext = CanFileContext; + + fn get_security_by_name( + &self, + file_name: &U16CStr, + _security_descriptor: Option<&mut [c_void]>, + _resolve_reparse_points: impl FnOnce(&U16CStr) -> Option, + ) -> winfsp::Result { + let path = util::normalize_path(file_name); + debug!("get_security_by_name: {}", path); + + let cache = self.cache.read(); + let node_id = cache + .tree + .lookup(&path) + .ok_or(ntstatus(STATUS_OBJECT_NAME_NOT_FOUND))?; + let node = cache.tree.get(node_id); + + let attributes = if node.is_directory() { + FILE_ATTRIBUTE_DIRECTORY + } else { + FILE_ATTRIBUTE_READONLY | FILE_ATTRIBUTE_ARCHIVE + }; + + Ok(FileSecurity { + reparse: false, + sz_security_descriptor: 0, + attributes, + }) + } + + fn open( + &self, + file_name: &U16CStr, + _create_options: u32, + _granted_access: u32, + file_info: &mut OpenFileInfo, + ) -> winfsp::Result { + let path = util::normalize_path(file_name); + debug!("open: {}", path); + + let cache = self.cache.read(); + let node_id = cache + .tree + .lookup(&path) + .ok_or(ntstatus(STATUS_OBJECT_NAME_NOT_FOUND))?; + let node = cache.tree.get(node_id); + + let fi = file_info.as_mut(); + if node.is_directory() { + fi.file_attributes = FILE_ATTRIBUTE_DIRECTORY; + fi.file_size = 0; + fi.allocation_size = 0; + } else { + fi.file_attributes = FILE_ATTRIBUTE_READONLY | FILE_ATTRIBUTE_ARCHIVE; + if let NodeKind::File { asset_index } = &node.kind { + let sz = cache.assets[*asset_index].size as u64; + fi.file_size = sz; + fi.allocation_size = sz; + } else { + fi.file_size = 0; + fi.allocation_size = 0; + } + } + + if let NodeKind::File { asset_index } = &node.kind { + let ts = util::epoch_ms_to_filetime(cache.assets[*asset_index].timestamp); + fi.creation_time = ts; + fi.last_access_time = ts; + fi.last_write_time = ts; + fi.change_time = ts; + } else { + fi.creation_time = 0; + fi.last_access_time = 0; + fi.last_write_time = 0; + fi.change_time = 0; + } + + fi.index_number = 0; + fi.hard_links = 0; + fi.ea_size = 0; + fi.reparse_tag = 0; + + Ok(CanFileContext { + node_id, + content: Mutex::new(None), + dir_buffer: DirBuffer::new(), + }) + } + + fn close(&self, _context: Self::FileContext) {} + + fn get_file_info( + &self, + context: &Self::FileContext, + file_info: &mut FileInfo, + ) -> winfsp::Result<()> { + let cache = self.cache.read(); + let node = cache.tree.get(context.node_id); + + if node.is_directory() { + file_info.file_attributes = FILE_ATTRIBUTE_DIRECTORY; + file_info.file_size = 0; + file_info.allocation_size = 0; + file_info.creation_time = 0; + file_info.last_access_time = 0; + file_info.last_write_time = 0; + file_info.change_time = 0; + } else { + file_info.file_attributes = FILE_ATTRIBUTE_READONLY | FILE_ATTRIBUTE_ARCHIVE; + + // Use actual downloaded size if available, otherwise metadata size + let content = context.content.lock(); + if let Some(ref bytes) = *content { + let sz = bytes.len() as u64; + file_info.file_size = sz; + file_info.allocation_size = sz; + } else if let NodeKind::File { asset_index } = &node.kind { + let sz = cache.assets[*asset_index].size as u64; + file_info.file_size = sz; + file_info.allocation_size = sz; + } else { + file_info.file_size = 0; + file_info.allocation_size = 0; + } + + if let NodeKind::File { asset_index } = &node.kind { + let ts = util::epoch_ms_to_filetime(cache.assets[*asset_index].timestamp); + file_info.creation_time = ts; + file_info.last_access_time = ts; + file_info.last_write_time = ts; + file_info.change_time = ts; + } + } + + file_info.index_number = 0; + file_info.hard_links = 0; + file_info.ea_size = 0; + file_info.reparse_tag = 0; + Ok(()) + } + + fn read( + &self, + context: &Self::FileContext, + buffer: &mut [u8], + offset: u64, + ) -> winfsp::Result { + let mut content = context.content.lock(); + + if content.is_none() { + let cache = self.cache.read(); + let node = cache.tree.get(context.node_id); + if let NodeKind::File { asset_index } = &node.kind { + let hash = &cache.assets[*asset_index].hash; + debug!("fetching bytes for {}", hash); + match self.client.fetch_bytes(hash) { + Ok(bytes) => { + *content = Some(bytes); + } + Err(e) => { + warn!("failed to fetch asset: {}", e); + return Err(ntstatus(STATUS_UNEXPECTED_NETWORK_ERROR)); + } + } + } else { + return Err(ntstatus(STATUS_INVALID_DEVICE_REQUEST)); + } + } + + let bytes = content.as_ref().unwrap(); + let offset = offset as usize; + if offset >= bytes.len() { + return Ok(0); + } + let end = (offset + buffer.len()).min(bytes.len()); + let count = end - offset; + buffer[..count].copy_from_slice(&bytes[offset..end]); + Ok(count as u32) + } + + fn read_directory( + &self, + context: &Self::FileContext, + _pattern: Option<&U16CStr>, + marker: DirMarker, + buffer: &mut [u8], + ) -> winfsp::Result { + let cache = self.cache.read(); + let node = cache.tree.get(context.node_id); + + if !node.is_directory() { + return Err(ntstatus(STATUS_NOT_A_DIRECTORY)); + } + + if let Ok(dir_buffer_lock) = context.dir_buffer.acquire(marker.is_none(), None) { + // "." entry + { + let mut di: DirInfo = DirInfo::new(); + let _ = di.set_name(std::ffi::OsStr::new(".")); + di.file_info_mut().file_attributes = FILE_ATTRIBUTE_DIRECTORY; + let _ = dir_buffer_lock.write(&mut di); + } + // ".." entry + { + let mut di: DirInfo = DirInfo::new(); + let _ = di.set_name(std::ffi::OsStr::new("..")); + di.file_info_mut().file_attributes = FILE_ATTRIBUTE_DIRECTORY; + let _ = dir_buffer_lock.write(&mut di); + } + + for &child_id in &node.children { + let child = cache.tree.get(child_id); + let mut di: DirInfo = DirInfo::new(); + + if di.set_name(std::ffi::OsStr::new(&child.name)).is_err() { + continue; + } + + let fi = di.file_info_mut(); + if child.is_directory() { + fi.file_attributes = FILE_ATTRIBUTE_DIRECTORY; + fi.file_size = 0; + fi.allocation_size = 0; + } else { + fi.file_attributes = FILE_ATTRIBUTE_READONLY | FILE_ATTRIBUTE_ARCHIVE; + if let NodeKind::File { asset_index } = &child.kind { + let sz = cache.assets[*asset_index].size as u64; + fi.file_size = sz; + fi.allocation_size = sz; + } else { + fi.file_size = 0; + fi.allocation_size = 0; + } + } + + if let NodeKind::File { asset_index } = &child.kind { + let ts = util::epoch_ms_to_filetime(cache.assets[*asset_index].timestamp); + fi.creation_time = ts; + fi.last_access_time = ts; + fi.last_write_time = ts; + fi.change_time = ts; + } + + fi.index_number = 0; + fi.hard_links = 0; + fi.ea_size = 0; + fi.reparse_tag = 0; + + let _ = dir_buffer_lock.write(&mut di); + } + } + + Ok(context.dir_buffer.read(marker, buffer)) + } + + fn get_volume_info(&self, out_volume_info: &mut VolumeInfo) -> winfsp::Result<()> { + out_volume_info.total_size = 1024 * 1024 * 1024; // 1 GB + out_volume_info.free_size = 0; + Ok(()) + } +} diff --git a/examples/canfs/src/main.rs b/examples/canfs/src/main.rs new file mode 100644 index 0000000..b552a2a --- /dev/null +++ b/examples/canfs/src/main.rs @@ -0,0 +1,139 @@ +mod api; +mod fs; +mod tree; +mod util; + +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::time::Duration; + +use clap::Parser; +use parking_lot::RwLock; +use tracing::{error, info}; +use winfsp::host::{FileSystemHost, FileSystemParams, VolumeParams}; +use winfsp::winfsp_init_or_die; + +use crate::api::CanClient; +use crate::fs::{CacheState, CanFs}; +use crate::tree::VirtualTree; + +#[derive(Parser)] +#[command(name = "canfs", about = "Mount CAN service assets as a virtual drive")] +struct Args { + /// Mount point: a drive letter like "X:" or a directory path. + #[arg(short, long, default_value = "X:")] + mount: String, + + /// CAN service base URL. + #[arg(long, default_value = "http://127.0.0.1:3210/api/v1/can/0")] + can_url: String, + + /// Cache refresh interval in seconds. + #[arg(long, default_value = "60")] + refresh_secs: u64, +} + +fn main() { + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")), + ) + .init(); + + let _init = winfsp_init_or_die(); + let args = Args::parse(); + + info!("connecting to CAN service at {}", args.can_url); + let client = Arc::new(CanClient::new(&args.can_url)); + + let assets = match client.list_all() { + Ok(a) => a, + Err(e) => { + error!("initial fetch failed — is the CAN service running? {}", e); + std::process::exit(1); + } + }; + info!("loaded {} assets", assets.len()); + + let tree = VirtualTree::build(&assets); + let cache = Arc::new(RwLock::new(CacheState { assets, tree })); + + // Background refresh thread + { + let cache = Arc::clone(&cache); + let client = Arc::clone(&client); + let interval = Duration::from_secs(args.refresh_secs); + std::thread::spawn(move || loop { + std::thread::sleep(interval); + match client.list_all() { + Ok(assets) => { + let tree = VirtualTree::build(&assets); + let count = assets.len(); + *cache.write() = CacheState { assets, tree }; + info!("cache refreshed: {} assets", count); + } + Err(e) => { + error!("cache refresh failed: {}", e); + } + } + }); + } + + let canfs = CanFs { + cache: Arc::clone(&cache), + client, + }; + + let mut volume_params = VolumeParams::new(); + volume_params + .filesystem_name("CanFS") + .sector_size(512) + .sectors_per_allocation_unit(1) + .file_info_timeout(1000) + .case_sensitive_search(false) + .case_preserved_names(true) + .read_only_volume(true) + .unicode_on_disk(true) + .persistent_acls(false); + + let params = FileSystemParams::default_params(volume_params); + + let mut host = match FileSystemHost::new_with_options(params, canfs) { + Ok(h) => h, + Err(e) => { + error!("failed to create filesystem host: {:?}", e); + std::process::exit(1); + } + }; + + info!("mounting on {}", args.mount); + if let Err(e) = host.mount(std::ffi::OsStr::new(&args.mount)) { + error!("failed to mount: {:?}", e); + std::process::exit(1); + } + if let Err(e) = host.start() { + error!("failed to start: {:?}", e); + std::process::exit(1); + } + + info!("CanFS mounted on {} — press Ctrl+C to unmount", args.mount); + + // Wait for Ctrl+C + let running = Arc::new(AtomicBool::new(true)); + { + let running = Arc::clone(&running); + ctrlc::set_handler(move || { + info!("shutting down..."); + running.store(false, Ordering::SeqCst); + }) + .expect("failed to set Ctrl+C handler"); + } + + while running.load(Ordering::SeqCst) { + std::thread::sleep(Duration::from_millis(100)); + } + + host.stop(); + info!("unmounted"); +} diff --git a/examples/canfs/src/tree.rs b/examples/canfs/src/tree.rs new file mode 100644 index 0000000..d8698f6 --- /dev/null +++ b/examples/canfs/src/tree.rs @@ -0,0 +1,259 @@ +use std::collections::{HashMap, HashSet}; + +use crate::api::AssetMeta; +use crate::util; +use chrono::DateTime; + +/// Unique identifier for a node in the virtual tree. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct NodeId(pub usize); + +/// A node is either a directory or a file reference. +#[derive(Debug, Clone)] +pub enum NodeKind { + Directory, + /// Points to an index in the flat asset list. + File { asset_index: usize }, +} + +/// A node in the virtual directory tree. +#[derive(Debug, Clone)] +pub struct VNode { + pub name: String, + pub kind: NodeKind, + pub children: Vec, + #[allow(dead_code)] + pub parent: Option, +} + +impl VNode { + pub fn is_directory(&self) -> bool { + matches!(self.kind, NodeKind::Directory) + } +} + +/// The complete virtual directory tree built from a list of assets. +pub struct VirtualTree { + nodes: Vec, + /// Normalized path -> NodeId lookup. + path_index: HashMap, +} + +impl VirtualTree { + /// Build the virtual tree from a flat list of assets. + pub fn build(assets: &[AssetMeta]) -> Self { + let mut tree = TreeBuilder::new(); + + // Create top-level directories + let root = tree.root(); + let can_dir = tree.add_dir("CAN", root); + let app_dir = tree.add_dir("APPLICATION", root); + let dates_dir = tree.add_dir("DATES", root); + let tags_dir = tree.add_dir("TAGS", root); + + for (i, asset) in assets.iter().enumerate() { + let ext = util::mime_to_ext(&asset.mime_type); + let hash8 = &asset.hash[..asset.hash.len().min(8)]; + + // 1) CAN/ — always: {timestamp}_{hash8}.ext + let can_name = format!("{}_{}.{}", asset.timestamp, hash8, ext); + let can_name = util::sanitize_filename(&can_name); + tree.add_file(&can_name, can_dir, i); + + // Display name for other folders: human_filename if available, else hash8.ext + let display_name = if let Some(ref hf) = asset.human_filename { + util::sanitize_filename(hf) + } else { + format!("{}.{}", hash8, ext) + }; + + // 2) APPLICATION/{app}/ — if application is set + if let Some(ref app) = asset.application { + let app_name = util::sanitize_filename(app); + let app_sub = tree.ensure_dir(&app_name, app_dir); + tree.add_file(&display_name, app_sub, i); + } + + // 3) DATES/{year}/{month:02}/ + if let Some(dt) = DateTime::from_timestamp_millis(asset.timestamp) { + let year_str = dt.format("%Y").to_string(); + let month_str = dt.format("%m").to_string(); + let year_dir = tree.ensure_dir(&year_str, dates_dir); + let month_dir = tree.ensure_dir(&month_str, year_dir); + tree.add_file(&display_name, month_dir, i); + } + + // 4) TAGS/{tag}/ + for tag in &asset.tags { + let tag_name = util::sanitize_filename(tag); + if tag_name.is_empty() { + continue; + } + let tag_sub = tree.ensure_dir(&tag_name, tags_dir); + tree.add_file(&display_name, tag_sub, i); + } + } + + // Sort children and build path index + tree.finalize() + } + + /// Look up a node by its normalized path (e.g., `\can\file.txt`). + pub fn lookup(&self, path: &str) -> Option { + let normalized = path.to_lowercase().replace('/', "\\"); + let normalized = if normalized.len() > 1 && normalized.ends_with('\\') { + &normalized[..normalized.len() - 1] + } else { + &normalized + }; + self.path_index.get(normalized).copied() + } + + /// Get a node by ID. + pub fn get(&self, id: NodeId) -> &VNode { + &self.nodes[id.0] + } + + /// Get the root node ID. + #[allow(dead_code)] + pub fn root(&self) -> NodeId { + NodeId(0) + } +} + +/// Builder helper for constructing the virtual tree. +struct TreeBuilder { + nodes: Vec, + /// Track names used per directory to resolve collisions. + dir_names: HashMap>, + /// Cache for ensure_dir: (parent_id, name) -> NodeId + dir_cache: HashMap<(usize, String), NodeId>, +} + +impl TreeBuilder { + fn new() -> Self { + let root = VNode { + name: String::new(), + kind: NodeKind::Directory, + children: Vec::new(), + parent: None, + }; + let mut dir_names = HashMap::new(); + dir_names.insert(0, HashSet::new()); + Self { + nodes: vec![root], + dir_names, + dir_cache: HashMap::new(), + } + } + + fn root(&self) -> NodeId { + NodeId(0) + } + + /// Add a directory as a child of `parent`. Returns its NodeId. + fn add_dir(&mut self, name: &str, parent: NodeId) -> NodeId { + let id = NodeId(self.nodes.len()); + self.nodes.push(VNode { + name: name.to_string(), + kind: NodeKind::Directory, + children: Vec::new(), + parent: Some(parent), + }); + self.nodes[parent.0].children.push(id); + self.dir_names.insert(id.0, HashSet::new()); + self.dir_names + .entry(parent.0) + .or_default() + .insert(name.to_lowercase()); + self.dir_cache + .insert((parent.0, name.to_lowercase()), id); + id + } + + /// Get or create a subdirectory by name under `parent`. + fn ensure_dir(&mut self, name: &str, parent: NodeId) -> NodeId { + let key = (parent.0, name.to_lowercase()); + if let Some(&id) = self.dir_cache.get(&key) { + return id; + } + self.add_dir(name, parent) + } + + /// Add a file node as a child of `parent`, deduplicating names. + fn add_file(&mut self, name: &str, parent: NodeId, asset_index: usize) { + let used = self.dir_names.entry(parent.0).or_default(); + let lower = name.to_lowercase(); + + let final_name = if !used.contains(&lower) { + used.insert(lower); + name.to_string() + } else { + // Deduplicate: try _2, _3, etc. + let (stem, ext) = if let Some(dot_pos) = name.rfind('.') { + (&name[..dot_pos], &name[dot_pos..]) + } else { + (name, "") + }; + let mut n = 2; + loop { + let candidate = format!("{}_{}{}", stem, n, ext); + let cand_lower = candidate.to_lowercase(); + if !used.contains(&cand_lower) { + used.insert(cand_lower); + break candidate; + } + n += 1; + } + }; + + let id = NodeId(self.nodes.len()); + self.nodes.push(VNode { + name: final_name, + kind: NodeKind::File { asset_index }, + children: Vec::new(), + parent: Some(parent), + }); + self.nodes[parent.0].children.push(id); + } + + /// Sort children and build the path index. + fn finalize(mut self) -> VirtualTree { + // Sort children by name (case-insensitive) + let names: Vec = self.nodes.iter().map(|n| n.name.to_lowercase()).collect(); + for node in &mut self.nodes { + node.children.sort_by(|a, b| names[a.0].cmp(&names[b.0])); + } + + // Build path index by walking the tree + let mut path_index = HashMap::new(); + path_index.insert("\\".to_string(), NodeId(0)); + + fn walk( + nodes: &[VNode], + id: NodeId, + prefix: &str, + index: &mut HashMap, + ) { + for &child_id in &nodes[id.0].children { + let child = &nodes[child_id.0]; + let path = if prefix == "\\" { + format!("\\{}", child.name.to_lowercase()) + } else { + format!("{}\\{}", prefix, child.name.to_lowercase()) + }; + index.insert(path.clone(), child_id); + if child.is_directory() { + walk(nodes, child_id, &path, index); + } + } + } + + walk(&self.nodes, NodeId(0), "\\", &mut path_index); + + VirtualTree { + nodes: self.nodes, + path_index, + } + } +} diff --git a/examples/canfs/src/util.rs b/examples/canfs/src/util.rs new file mode 100644 index 0000000..ac30a90 --- /dev/null +++ b/examples/canfs/src/util.rs @@ -0,0 +1,76 @@ +use widestring::U16CStr; + +/// Convert a MIME type string to a file extension (without dot). +pub fn mime_to_ext(mime: &str) -> &'static str { + // Common overrides for types where mime_guess gives odd results + match mime { + "text/plain" => "txt", + "text/html" => "html", + "text/css" => "css", + "text/javascript" | "application/javascript" => "js", + "application/json" => "json", + "application/pdf" => "pdf", + "application/zip" => "zip", + "application/gzip" => "gz", + "image/jpeg" => "jpg", + "image/png" => "png", + "image/gif" => "gif", + "image/webp" => "webp", + "image/svg+xml" => "svg", + "audio/mpeg" => "mp3", + "audio/ogg" => "ogg", + "video/mp4" => "mp4", + "video/webm" => "webm", + "application/octet-stream" => "bin", + other => { + // Try to extract from subtype: "image/tiff" -> "tiff" + if let Some((_main, sub)) = other.split_once('/') { + let sub = sub.split('+').next().unwrap_or(sub); + let sub = sub.split('.').next_back().unwrap_or(sub); + // Leak a static string for the extension - acceptable for a small set + // In practice we'll hit the match arms above for common types + Box::leak(sub.to_string().into_boxed_str()) + } else { + "bin" + } + } + } +} + +/// Convert Unix epoch milliseconds to Windows FILETIME (100ns intervals since 1601-01-01). +pub fn epoch_ms_to_filetime(ts_ms: i64) -> u64 { + // Windows epoch is 1601-01-01, Unix epoch is 1970-01-01 + // Difference: 11644473600 seconds + const EPOCH_DIFF_SECS: i64 = 11_644_473_600; + const TICKS_PER_SEC: i64 = 10_000_000; + const TICKS_PER_MS: i64 = 10_000; + + let secs = ts_ms / 1000; + let ms_remainder = ts_ms % 1000; + + ((secs + EPOCH_DIFF_SECS) * TICKS_PER_SEC + ms_remainder * TICKS_PER_MS) as u64 +} + +/// Normalize a WinFSP U16CStr path to a lowercase String with backslash separators. +/// Strips trailing backslash (except for root "\"). +pub fn normalize_path(path: &U16CStr) -> String { + let s = path.to_string_lossy(); + let s = s.to_lowercase(); + let s = s.replace('/', "\\"); + if s.len() > 1 && s.ends_with('\\') { + s[..s.len() - 1].to_string() + } else { + s + } +} + +/// Sanitize a string for use as a filename: replace invalid chars with underscore. +pub fn sanitize_filename(s: &str) -> String { + s.chars() + .map(|c| match c { + '<' | '>' | ':' | '"' | '/' | '\\' | '|' | '?' | '*' => '_', + c if c.is_control() => '_', + _ => c, + }) + .collect() +} diff --git a/examples/filemanager/Cargo.lock b/examples/filemanager/Cargo.lock new file mode 100644 index 0000000..ca8b7de --- /dev/null +++ b/examples/filemanager/Cargo.lock @@ -0,0 +1,1955 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "axum" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "filemanager" +version = "0.1.0" +dependencies = [ + "axum", + "open", + "reqwest", + "serde", + "serde_json", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "open" +version = "5.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43bb73a7fa3799b198970490a51174027ba0d4ec504b03cd08caf513d40024bc" +dependencies = [ + "is-wsl", + "libc", + "pathdiff", +] + +[[package]] +name = "openssl" +version = "0.10.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.112" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/examples/filemanager/Cargo.toml b/examples/filemanager/Cargo.toml new file mode 100644 index 0000000..9ad9c6a --- /dev/null +++ b/examples/filemanager/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "filemanager" +version = "0.1.0" +edition = "2021" +publish = false +description = "Web-based file manager for CAN service assets" + +[[bin]] +name = "filemanager" +path = "src/main.rs" + +[dependencies] +axum = "0.8" +tokio = { version = "1", features = ["full"] } +reqwest = { version = "0.12", features = ["json"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +open = "5" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } diff --git a/examples/filemanager/src/html.rs b/examples/filemanager/src/html.rs new file mode 100644 index 0000000..154ee69 --- /dev/null +++ b/examples/filemanager/src/html.rs @@ -0,0 +1,992 @@ +pub const INDEX_HTML: &str = r##" + + + + +CAN File Manager + + + + + +
+ + +
+ + + +
+
+ + +
+ + + + + + + + + + + +
+ + +
+ +
+ +
+
+
+ + +
Loading...
+ + + + + + +"##; diff --git a/examples/filemanager/src/main.rs b/examples/filemanager/src/main.rs new file mode 100644 index 0000000..cbdb6b6 --- /dev/null +++ b/examples/filemanager/src/main.rs @@ -0,0 +1,161 @@ +mod html; + +use axum::extract::{Path, Query, State}; +use axum::http::{HeaderMap, HeaderValue, StatusCode}; +use axum::response::{Html, IntoResponse, Response}; +use axum::routing::get; +use axum::Router; +use std::collections::HashMap; + +const CAN_API: &str = "http://127.0.0.1:3210/api/v1/can/0"; + +#[derive(Clone)] +struct AppState { + client: reqwest::Client, +} + +#[tokio::main] +async fn main() { + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| "filemanager=info".into()), + ) + .init(); + + let state = AppState { + client: reqwest::Client::new(), + }; + + let app = Router::new() + .route("/", get(serve_index)) + .route("/fm/list", get(proxy_list)) + .route("/fm/search", get(proxy_search)) + .route("/fm/asset/{hash}", get(proxy_asset)) + .route("/fm/asset/{hash}/meta", get(proxy_meta)) + .route("/fm/thumb/{hash}", get(proxy_thumb)) + .with_state(state); + + let addr = "127.0.0.1:3212"; + let url = format!("http://{}", addr); + tracing::info!("File Manager listening on {}", url); + + // Best-effort browser open + let _ = open::that(&url); + + let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); + axum::serve(listener, app).await.unwrap(); +} + +async fn serve_index() -> Html<&'static str> { + Html(html::INDEX_HTML) +} + +/// Forward a reqwest response to the axum caller, preserving status + content-type. +async fn forward(resp: Result) -> Response { + match resp { + Ok(r) => { + let status = StatusCode::from_u16(r.status().as_u16()).unwrap_or(StatusCode::OK); + let ct = r + .headers() + .get("content-type") + .and_then(|v| v.to_str().ok()) + .unwrap_or("application/octet-stream") + .to_string(); + let body = r.bytes().await.unwrap_or_default(); + let mut headers = HeaderMap::new(); + headers.insert("content-type", HeaderValue::from_str(&ct).unwrap()); + (status, headers, body).into_response() + } + Err(e) => { + tracing::warn!("CAN service error: {}", e); + (StatusCode::BAD_GATEWAY, "CAN service unavailable").into_response() + } + } +} + +/// Build a query string from a HashMap of params. +fn build_qs(params: &HashMap) -> String { + if params.is_empty() { + return String::new(); + } + let qs: Vec = params + .iter() + .map(|(k, v)| format!("{}={}", urlencoding(k), urlencoding(v))) + .collect(); + format!("?{}", qs.join("&")) +} + +fn urlencoding(s: &str) -> String { + s.chars() + .map(|c| match c { + 'A'..='Z' | 'a'..='z' | '0'..='9' | '-' | '_' | '.' | '~' => c.to_string(), + _ => format!("%{:02X}", c as u8), + }) + .collect() +} + +/// Proxy list: pass all query params through to CAN /list. +async fn proxy_list( + State(state): State, + Query(params): Query>, +) -> Response { + let url = format!("{}/list{}", CAN_API, build_qs(¶ms)); + forward(state.client.get(&url).send().await).await +} + +/// Proxy search: pass all query params through to CAN /search. +async fn proxy_search( + State(state): State, + Query(params): Query>, +) -> Response { + let url = format!("{}/search{}", CAN_API, build_qs(¶ms)); + forward(state.client.get(&url).send().await).await +} + +/// Proxy asset download. +async fn proxy_asset( + State(state): State, + Path(hash): Path, +) -> Response { + let url = format!("{}/asset/{}", CAN_API, hash); + let resp = state.client.get(&url).send().await; + // Drop Content-Disposition so images render inline + match resp { + Ok(r) => { + let status = StatusCode::from_u16(r.status().as_u16()).unwrap_or(StatusCode::OK); + let ct = r + .headers() + .get("content-type") + .and_then(|v| v.to_str().ok()) + .unwrap_or("application/octet-stream") + .to_string(); + let body = r.bytes().await.unwrap_or_default(); + let mut headers = HeaderMap::new(); + headers.insert("content-type", HeaderValue::from_str(&ct).unwrap()); + (status, headers, body).into_response() + } + Err(e) => { + tracing::warn!("CAN service error: {}", e); + (StatusCode::BAD_GATEWAY, "CAN service unavailable").into_response() + } + } +} + +/// Proxy asset metadata. +async fn proxy_meta( + State(state): State, + Path(hash): Path, +) -> Response { + let url = format!("{}/asset/{}/meta", CAN_API, hash); + forward(state.client.get(&url).send().await).await +} + +/// Proxy thumbnail (200x200). +async fn proxy_thumb( + State(state): State, + Path(hash): Path, +) -> Response { + let url = format!("{}/asset/{}/thumb/200/200", CAN_API, hash); + forward(state.client.get(&url).send().await).await +} diff --git a/examples/paste/Cargo.lock b/examples/paste/Cargo.lock new file mode 100644 index 0000000..4834dad --- /dev/null +++ b/examples/paste/Cargo.lock @@ -0,0 +1,2003 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "axum" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "multer", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "multer" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http", + "httparse", + "memchr", + "mime", + "spin", + "version_check", +] + +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "open" +version = "5.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43bb73a7fa3799b198970490a51174027ba0d4ec504b03cd08caf513d40024bc" +dependencies = [ + "is-wsl", + "libc", + "pathdiff", +] + +[[package]] +name = "openssl" +version = "0.10.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "0.1.0" +dependencies = [ + "axum", + "open", + "reqwest", + "serde", + "serde_json", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "mime_guess", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/examples/paste/Cargo.toml b/examples/paste/Cargo.toml new file mode 100644 index 0000000..d019dbd --- /dev/null +++ b/examples/paste/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "paste" +version = "0.1.0" +edition = "2021" +publish = false +description = "Clipboard log UI — example app for CanService" + +[[bin]] +name = "paste" +path = "src/main.rs" + +[dependencies] +axum = { version = "0.8", features = ["multipart"] } +tokio = { version = "1", features = ["full"] } +reqwest = { version = "0.12", features = ["multipart", "json"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +open = "5" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } diff --git a/examples/paste/src/html.rs b/examples/paste/src/html.rs new file mode 100644 index 0000000..c641bd1 --- /dev/null +++ b/examples/paste/src/html.rs @@ -0,0 +1,382 @@ +pub const INDEX_HTML: &str = r##" + + + + +paste + + + + +
+

paste

+

type text + enter, or paste an image — use #hashtags to add tags

+
+ +
+
+
+ + + +
+
+
+
+
+ + + + +"##; diff --git a/examples/paste/src/main.rs b/examples/paste/src/main.rs new file mode 100644 index 0000000..0c54e9b --- /dev/null +++ b/examples/paste/src/main.rs @@ -0,0 +1,263 @@ +mod html; + +use axum::extract::{DefaultBodyLimit, Multipart, Path, State}; +use axum::http::{header, StatusCode}; +use axum::response::{Html, IntoResponse, Response}; +use axum::routing::{get, post}; +use axum::{Json, Router}; +use serde::Deserialize; +use std::net::SocketAddr; + +const CAN_API: &str = "http://127.0.0.1:3210/api/v1/can/0"; + +#[derive(Clone)] +struct AppState { + client: reqwest::Client, +} + +#[derive(Deserialize)] +struct PasteTextRequest { + text: String, +} + +// ── Helpers ────────────────────────────────────────────────────────────── + +/// Extract #hashtags from text, returning the comma-separated tag string. +/// e.g. "some #chicken and #food" -> "chicken,food" +fn extract_tags(text: &str) -> String { + text.split_whitespace() + .filter(|w| w.starts_with('#') && w.len() > 1) + .map(|w| w[1..].trim_end_matches(|c: char| !c.is_alphanumeric() && c != '_' && c != '-')) + .filter(|t| !t.is_empty()) + .collect::>() + .join(",") +} + +/// Convert a reqwest response into an axum response, copying status + +/// content-type + body. Intentionally drops Content-Disposition so that +/// images render inline rather than triggering a download. +async fn forward(resp: Result) -> Response { + match resp { + Ok(r) => { + let status = StatusCode::from_u16(r.status().as_u16()) + .unwrap_or(StatusCode::BAD_GATEWAY); + let ct = r + .headers() + .get("content-type") + .and_then(|v| v.to_str().ok()) + .unwrap_or("application/octet-stream") + .to_string(); + let bytes = r.bytes().await.unwrap_or_default(); + (status, [(header::CONTENT_TYPE, ct)], bytes).into_response() + } + Err(e) => ( + StatusCode::BAD_GATEWAY, + format!("CanService unreachable: {e}"), + ) + .into_response(), + } +} + +// ── Handlers ───────────────────────────────────────────────────────────── + +async fn serve_index() -> Html<&'static str> { + Html(html::INDEX_HTML) +} + +/// Accept `{ "text": "..." }` from the frontend, forward to CanService as +/// a multipart text/plain file so the stored content is raw text (not +/// JSON-wrapped). +async fn paste_text( + State(state): State, + Json(body): Json, +) -> Response { + let desc = if body.text.len() > 200 { + format!("{}...", &body.text[..body.text.char_indices().nth(200).map(|(i, _)| i).unwrap_or(body.text.len())]) + } else { + body.text.clone() + }; + + let tags = extract_tags(&desc); + + let part = match reqwest::multipart::Part::bytes(body.text.into_bytes()) + .file_name("paste.txt") + .mime_str("text/plain") + { + Ok(p) => p, + Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), + }; + + let mut form = reqwest::multipart::Form::new() + .part("file", part) + .text("application", "paste") + .text("description", desc); + + if !tags.is_empty() { + form = form.text("tags", tags); + } + + let resp = state + .client + .post(format!("{CAN_API}/ingest")) + .multipart(form) + .send() + .await; + + forward(resp).await +} + +/// Accept a multipart upload from the frontend (clipboard image). +/// Re-packages it into a new multipart request for CanService. +async fn paste_file( + State(state): State, + mut multipart: Multipart, +) -> Response { + let mut file_bytes: Option> = None; + let mut file_name = "clipboard.png".to_string(); + let mut content_type = "image/png".to_string(); + let mut description = String::new(); + + loop { + match multipart.next_field().await { + Ok(Some(field)) => { + let name = field.name().unwrap_or("").to_string(); + match name.as_str() { + "file" => { + if let Some(fname) = field.file_name() { + file_name = fname.to_string(); + } + if let Some(ct) = field.content_type() { + content_type = ct.to_string(); + } + match field.bytes().await { + Ok(b) => file_bytes = Some(b.to_vec()), + Err(e) => { + return (StatusCode::BAD_REQUEST, format!("Failed to read file: {e}")).into_response(); + } + } + } + "description" => { + description = field.text().await.unwrap_or_default(); + } + _ => {} + } + } + Ok(None) => break, + Err(e) => { + return (StatusCode::BAD_REQUEST, format!("Multipart error: {e}")).into_response(); + } + } + } + + let Some(bytes) = file_bytes else { + return (StatusCode::BAD_REQUEST, "Missing file field").into_response(); + }; + + let part = match reqwest::multipart::Part::bytes(bytes) + .file_name(file_name) + .mime_str(&content_type) + { + Ok(p) => p, + Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), + }; + + let tags = extract_tags(&description); + + let mut form = reqwest::multipart::Form::new() + .part("file", part) + .text("application", "paste"); + + if !description.is_empty() { + form = form.text("description", description); + } + if !tags.is_empty() { + form = form.text("tags", tags); + } + + let resp = state + .client + .post(format!("{CAN_API}/ingest")) + .multipart(form) + .send() + .await; + + forward(resp).await +} + +/// List items with application=paste, newest first. +async fn paste_list(State(state): State) -> Response { + let resp = state + .client + .get(format!( + "{CAN_API}/list?application=paste&order=desc&limit=100" + )) + .send() + .await; + + forward(resp).await +} + +/// Proxy asset download by hash. +async fn proxy_asset( + State(state): State, + Path(hash): Path, +) -> Response { + let resp = state + .client + .get(format!("{CAN_API}/asset/{hash}")) + .send() + .await; + + forward(resp).await +} + +/// Proxy thumbnail (200x200) by hash. +async fn proxy_thumb( + State(state): State, + Path(hash): Path, +) -> Response { + let resp = state + .client + .get(format!("{CAN_API}/asset/{hash}/thumb/200/200")) + .send() + .await; + + forward(resp).await +} + +// ── Main ───────────────────────────────────────────────────────────────── + +#[tokio::main] +async fn main() { + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| "paste=info".into()), + ) + .init(); + + let state = AppState { + client: reqwest::Client::new(), + }; + + let app = Router::new() + .route("/", get(serve_index)) + .route("/paste/text", post(paste_text)) + .route("/paste/file", post(paste_file)) + .route("/paste/list", get(paste_list)) + .route("/paste/asset/{hash}", get(proxy_asset)) + .route("/paste/thumb/{hash}", get(proxy_thumb)) + .layer(DefaultBodyLimit::max(100 * 1024 * 1024)) // 100 MB + .with_state(state); + + let addr = SocketAddr::from(([127, 0, 0, 1], 3211)); + tracing::info!("paste running at http://{addr}"); + tracing::info!("requires CanService at http://127.0.0.1:3210"); + + // Open browser (best-effort, won't crash on headless) + let url = format!("http://{addr}"); + let _ = open::that(&url); + + let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); + axum::serve(listener, app).await.unwrap(); +} diff --git a/go_example_1.ps1 b/go_example_1.ps1 new file mode 100644 index 0000000..172ad11 --- /dev/null +++ b/go_example_1.ps1 @@ -0,0 +1,69 @@ +# go_example_1.ps1 — Start CanService + Paste example, open browser + +$ErrorActionPreference = "Stop" +$root = $PSScriptRoot + +# Kill any leftover processes on our ports +Write-Host "Cleaning up stale processes..." -ForegroundColor Yellow +Get-NetTCPConnection -LocalPort 3210 -ErrorAction SilentlyContinue | + ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue } +Get-NetTCPConnection -LocalPort 3211 -ErrorAction SilentlyContinue | + ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue } +Start-Sleep -Milliseconds 500 + +Write-Host "Building CanService..." -ForegroundColor Cyan +cargo build --manifest-path "$root\Cargo.toml" + +Write-Host "Building Paste example..." -ForegroundColor Cyan +cargo build --manifest-path "$root\examples\paste\Cargo.toml" + +# Start CanService in background +Write-Host "Starting CanService on port 3210..." -ForegroundColor Green +$canService = Start-Process -FilePath "cargo" ` + -ArgumentList "run --manifest-path `"$root\Cargo.toml`"" ` + -WorkingDirectory $root ` + -PassThru -NoNewWindow + +# Wait for CanService to be ready +Write-Host "Waiting for CanService..." -ForegroundColor Yellow +$ready = $false +for ($i = 0; $i -lt 30; $i++) { + try { + $null = Invoke-WebRequest -Uri "http://127.0.0.1:3210/api/v1/can/0/list" -TimeoutSec 1 -ErrorAction Stop + $ready = $true + break + } catch { + Start-Sleep -Milliseconds 500 + } +} +if (-not $ready) { + Write-Host "CanService failed to start within 15s" -ForegroundColor Red + Stop-Process -Id $canService.Id -Force -ErrorAction SilentlyContinue + exit 1 +} +Write-Host "CanService ready." -ForegroundColor Green + +# Start Paste example (it opens the browser itself) +Write-Host "Starting Paste on port 3211..." -ForegroundColor Green +$paste = Start-Process -FilePath "cargo" ` + -ArgumentList "run --manifest-path `"$root\examples\paste\Cargo.toml`"" ` + -WorkingDirectory $root ` + -PassThru -NoNewWindow + +Write-Host "" +Write-Host "Running:" -ForegroundColor Cyan +Write-Host " CanService -> http://127.0.0.1:3210" +Write-Host " Paste UI -> http://127.0.0.1:3211" +Write-Host "" +Write-Host "Press Ctrl+C to stop both." -ForegroundColor Yellow + +# Wait for either process to exit, then clean up both +try { + while (-not $canService.HasExited -and -not $paste.HasExited) { + Start-Sleep -Seconds 1 + } +} finally { + Write-Host "Shutting down..." -ForegroundColor Yellow + Stop-Process -Id $canService.Id -Force -ErrorAction SilentlyContinue + Stop-Process -Id $paste.Id -Force -ErrorAction SilentlyContinue +} diff --git a/spec.md b/spec.md new file mode 100644 index 0000000..92c98fc --- /dev/null +++ b/spec.md @@ -0,0 +1,185 @@ +# CAN (Containerized Asset Network) Service Specification +**Version:** 1.0 (Final MVP) +**Target Language:** Rust + +## 1. System Overview +The CAN service is a robust, self-healing local network daemon designed to simulate a high-speed, append-oriented file system. It provides an HTTP REST and Protobuf API to ingest, manage, and retrieve assets (files and data). + +To bypass the slow nature of traditional OS file searches, it uses an embedded SQLite database for millisecond querying. To ensure 100% disaster-recovery readiness, critical metadata is redundantly written to the host's native OS file attributes. + +**MVP Scope:** This version supports a single, default container. To future-proof the API, all routes require a `{can_id}` parameter, which **must always be `0`**. Physically, all data is mapped flatly to the configured storage root. + +--- + +## 2. Directory Structure & Configuration +The system uses a flat directory structure within the configured root folder. + +**Physical Structure:** +```text +/var/lib/can_data/ # Defined by storage_root +├── .can.db # Master SQLite Index (Hidden) +├── .trash/ # Soft-deleted physical assets (Hidden) +├── .thumbs/ # Cached thumbnail images (Hidden, if enabled) +├── 1773014400123_a3b2... # Physical Asset +└── 1773014405999_f8c9... # Physical Asset +``` + +**Configuration (`config.yaml`):** +```yaml +storage_root: "/var/lib/can_data" # Absolute path to the storage folder +admin_token: "super_secret_rebuild" # Bearer token for admin operations +enable_thumbnail_cache: true # Toggle caching in .thumbs/ +rebuild_error_threshold: 50 # Tolerance before triggering a hard rebuild +verify_interval_hours: 12 # Frequency of full background hash verification +``` + +--- + +## 3. Storage Mechanics & Disaster Recovery + +### 3.1 Cryptographic Naming Convention +Files are written with a strict physical naming format to allow offline, mathematical verification of integrity. +**Format:** `{timestamp}_{sha256}_{truncated_tags}.{extension}` + +* `timestamp`: Epoch Unix timestamp in milliseconds (e.g., `1773014400123`). +* `sha256`: A SHA-256 hash calculated exactly as: `SHA256([timestamp_bytes] + [raw_file_content_bytes])`. +* `truncated_tags`: Tags joined by underscores (`_`). Non-alphanumeric characters stripped. Safely truncated to ensure the total filename stays safely under OS path limits (~255 chars). Omitted if no tags provided. +* `extension`: Derived from the `mime_type` or magic bytes (e.g., `.pdf`, `.json`). + +### 3.2 Native OS File Attributes +To guarantee the SQLite database can be rebuilt from scratch, critical metadata is bound directly to the file using OS-level attributes (Extended Attributes / `xattr` on Linux/macOS; NTFS Alternate Data Streams on Windows). + +**Required Attributes:** +* `can.application`: Software that ingested the file. +* `can.user`: User identity. +* `can.tags`: **The complete, unbounded, comma-separated list of tags.** +* `can.description`: Human-readable description. +* `can.human_filename`: The logical filename provided during ingestion. +* `can.human_path`: The logical folder path provided during ingestion. + +--- + +## 4. Metadata Indexing (`.can.db`) +A fully normalized SQLite database located at `{storage_root}/.can.db`. + +**Schema:** +```sql +CREATE TABLE assets ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + timestamp INTEGER NOT NULL, + hash TEXT NOT NULL UNIQUE, + mime_type TEXT NOT NULL, + application TEXT, + user_identity TEXT, + description TEXT, + actual_filename TEXT NOT NULL, + human_filename TEXT, + human_path TEXT, + is_trashed BOOLEAN NOT NULL DEFAULT 0, + is_corrupted BOOLEAN NOT NULL DEFAULT 0 +); + +CREATE TABLE tags ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE +); + +CREATE TABLE asset_tags ( + asset_id INTEGER NOT NULL, + tag_id INTEGER NOT NULL, + PRIMARY KEY (asset_id, tag_id), + FOREIGN KEY (asset_id) REFERENCES assets(id) ON DELETE CASCADE, + FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE +); + +-- Optimization Indexes +CREATE INDEX idx_hash ON assets(hash); +CREATE INDEX idx_timestamp ON assets(timestamp); +CREATE INDEX idx_application ON assets(application); +CREATE INDEX idx_user ON assets(user_identity); +CREATE INDEX idx_trashed ON assets(is_trashed); +CREATE INDEX idx_tag_name ON tags(name); +``` + +--- + +## 5. Background Verifier Subsystem +A low-priority background thread dedicated to data integrity. + +1. **Initial Scrub:** Runs on startup. Verifies `SHA256(timestamp + content)` for all files against their filenames. +2. **Continuous Monitoring:** Hooks into OS file system events (e.g., `inotify`). If a file is touched or altered by an external program, the verifier immediately rescans it. +3. **Periodic Scrub:** Runs every `verify_interval_hours` to catch silent bit rot. +4. **Corruption Handling:** If a hash mismatch is found, it flags `is_corrupted = 1` in `.can.db`. Corrupted files are explicitly marked in API responses and excluded from standard operations. + +--- + +## 6. API Endpoints + +**Protocol Negotation:** +All endpoints communicate in JSON by default. Clients can request/send **Protocol Buffers** by providing the HTTP headers: +* `Accept: application/x-protobuf` +* `Content-Type: application/x-protobuf` + +*(Note: Endpoint paths below use `{can_id}` which must be passed as `0`)* + +### 6.1 Ingest Data +* **Method:** `POST` +* **Path:** `/api/v1/can/0/ingest` +* **Content-Type:** `multipart/form-data` +* **Form Payload:** + * `file` (Binary File) - **Required** + * `mime_type` (String) - *Optional* + * `human_file_name` (String) - *Optional* + * `human_readable_path` (String) - *Optional* + * `application` (String) - *Optional* + * `user` (String) - *Optional* + * `tags` (String) - *Optional* (comma-separated) + * `description` (String) - *Optional* +* **Action:** Hashes file, writes to `{storage_root}`, attaches OS attributes, logs to DB. +* **Response (JSON):** + `{ "status": "success", "data": { "timestamp": 1773014400123, "hash": "abc...", "filename": "1773014400123_abc_tag.pdf" } }` + +### 6.2 Retrieve Physical Asset +* **Method:** `GET` +* **Path:** `/api/v1/can/0/asset/{hash}` +* **Action:** Streams the physical file. Sets `Content-Type` via DB mapping and `Content-Disposition` using `human_filename`. Returns 500/Warning if `is_corrupted = 1`. + +### 6.3 Retrieve Asset Metadata +* **Method:** `GET` +* **Path:** `/api/v1/can/0/asset/{hash}/meta` +* **Action:** Returns DB record. +* **Response (JSON):** + `{ "status": "success", "data": { "hash": "abc...", "mime_type": "image/jpeg", "application": "WebUI", "user": "Jason", "tags": ["tag1", "tag2"], "description": "...", "human_filename": "photo.jpg", "human_path": "/img/", "timestamp": 1773014400123, "is_trashed": false, "is_corrupted": false } }` + +### 6.4 Retrieve Thumbnail +* **Method:** `GET` +* **Path:** `/api/v1/can/0/asset/{hash}/thumb/{max_width}/{max_height}` +* **Action:** Resizes image strictly preserving aspect ratio. Falls back to static icon (SVG/PNG) for non-images. If `enable_thumbnail_cache=true`, reads/writes to `{storage_root}/.thumbs/{hash}_{max_width}x{max_height}.jpg`. Streams byte payload. + +### 6.5 Modify Metadata +* **Method:** `PATCH` +* **Path:** `/api/v1/can/0/asset/{hash}` +* **Body (JSON/Protobuf):** + `{ "tags": ["new_tag1", "new_tag2"], "description": "New description" }` +* **Action:** Updates `can.tags` and `can.description` OS Attributes. Updates SQLite `assets`, `tags`, and `asset_tags` tables inside a transaction. Physical filename remains unchanged. + +### 6.6 List Assets +* **Method:** `GET` +* **Path:** `/api/v1/can/0/list` +* **Query Parameters:** + * `limit` (Integer) - Default `50` + * `offset` (Integer) - Default `0` + * `offset_time` (Integer) - *Optional*. Epoch ms. High-speed cursor. Lists items strictly after/before this timestamp based on `order`. + * `order` (String) - `asc` or `desc`. Default `desc`. + * `application` (String) - *Optional*. Scopes list exclusively to files ingested by this Application ID. + * `include_trashed` (Boolean) - Default `false`. + * `include_corrupted` (Boolean) - Default `false`. +* **Response:** Paginated array of metadata objects (matching 6.3 output) + `pagination` block. + +### 6.7 Search Assets +* **Method:** `GET` +* **Path:** `/api/v1/can/0/search` +* **Query Parameters:** + * `hash` (String) - Exact or partial prefix. + * `start_time` (Integer) - Epoch ms. + * `end_time` (Integer) - diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..8feef56 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,112 @@ +use serde::Deserialize; +use std::path::{Path, PathBuf}; + +#[derive(Debug, Clone, Deserialize)] +pub struct Config { + pub storage_root: PathBuf, + #[serde(default = "default_admin_token")] + pub admin_token: String, + #[serde(default = "default_true")] + pub enable_thumbnail_cache: bool, + #[serde(default = "default_rebuild_threshold")] + pub rebuild_error_threshold: u32, + #[serde(default = "default_verify_interval")] + pub verify_interval_hours: u64, +} + +fn default_admin_token() -> String { + "changeme".to_string() +} +fn default_true() -> bool { + true +} +fn default_rebuild_threshold() -> u32 { + 50 +} +fn default_verify_interval() -> u64 { + 12 +} + +impl Config { + pub fn load(path: &Path) -> anyhow::Result { + let contents = std::fs::read_to_string(path)?; + let config: Config = serde_yaml::from_str(&contents)?; + Ok(config) + } + + pub fn db_path(&self) -> PathBuf { + self.storage_root.join(".can.db") + } + + pub fn trash_dir(&self) -> PathBuf { + self.storage_root.join(".trash") + } + + pub fn thumbs_dir(&self) -> PathBuf { + self.storage_root.join(".thumbs") + } + + pub fn ensure_dirs(&self) -> anyhow::Result<()> { + std::fs::create_dir_all(&self.storage_root)?; + std::fs::create_dir_all(self.trash_dir())?; + if self.enable_thumbnail_cache { + std::fs::create_dir_all(self.thumbs_dir())?; + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::TempDir; + + #[test] + fn test_load_config() { + let dir = TempDir::new().unwrap(); + let config_path = dir.path().join("config.yaml"); + std::fs::write( + &config_path, + r#" +storage_root: "/tmp/can_test" +admin_token: "test_token" +enable_thumbnail_cache: false +rebuild_error_threshold: 10 +verify_interval_hours: 6 +"#, + ) + .unwrap(); + + let config = Config::load(&config_path).unwrap(); + assert_eq!(config.storage_root, PathBuf::from("/tmp/can_test")); + assert_eq!(config.admin_token, "test_token"); + assert!(!config.enable_thumbnail_cache); + assert_eq!(config.rebuild_error_threshold, 10); + assert_eq!(config.verify_interval_hours, 6); + } + + #[test] + fn test_config_defaults() { + let dir = TempDir::new().unwrap(); + let config_path = dir.path().join("config.yaml"); + std::fs::write(&config_path, "storage_root: /tmp/test\n").unwrap(); + + let config = Config::load(&config_path).unwrap(); + assert_eq!(config.admin_token, "changeme"); + assert!(config.enable_thumbnail_cache); + assert_eq!(config.rebuild_error_threshold, 50); + assert_eq!(config.verify_interval_hours, 12); + } + + #[test] + fn test_derived_paths() { + let dir = TempDir::new().unwrap(); + let config_path = dir.path().join("config.yaml"); + std::fs::write(&config_path, "storage_root: /data/can\n").unwrap(); + let config = Config::load(&config_path).unwrap(); + + assert_eq!(config.db_path(), PathBuf::from("/data/can/.can.db")); + assert_eq!(config.trash_dir(), PathBuf::from("/data/can/.trash")); + assert_eq!(config.thumbs_dir(), PathBuf::from("/data/can/.thumbs")); + } +} diff --git a/src/db.rs b/src/db.rs new file mode 100644 index 0000000..97fb192 --- /dev/null +++ b/src/db.rs @@ -0,0 +1,663 @@ +use rusqlite::{params, Connection, OptionalExtension}; +use std::path::Path; +use std::sync::{Arc, Mutex}; + +use crate::models::{Asset, AssetMeta, ListParams, SearchParams}; + +pub type Db = Arc>; + +pub fn open(path: &Path) -> anyhow::Result { + let conn = Connection::open(path)?; + conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;")?; + init_schema(&conn)?; + Ok(Arc::new(Mutex::new(conn))) +} + +pub fn open_in_memory() -> anyhow::Result { + let conn = Connection::open_in_memory()?; + conn.execute_batch("PRAGMA foreign_keys=ON;")?; + init_schema(&conn)?; + Ok(Arc::new(Mutex::new(conn))) +} + +fn init_schema(conn: &Connection) -> rusqlite::Result<()> { + conn.execute_batch( + " + CREATE TABLE IF NOT EXISTS assets ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + timestamp INTEGER NOT NULL, + hash TEXT NOT NULL UNIQUE, + mime_type TEXT NOT NULL, + application TEXT, + user_identity TEXT, + description TEXT, + actual_filename TEXT NOT NULL, + human_filename TEXT, + human_path TEXT, + is_trashed BOOLEAN NOT NULL DEFAULT 0, + is_corrupted BOOLEAN NOT NULL DEFAULT 0 + ); + + CREATE TABLE IF NOT EXISTS tags ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE + ); + + CREATE TABLE IF NOT EXISTS asset_tags ( + asset_id INTEGER NOT NULL, + tag_id INTEGER NOT NULL, + PRIMARY KEY (asset_id, tag_id), + FOREIGN KEY (asset_id) REFERENCES assets(id) ON DELETE CASCADE, + FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE + ); + + CREATE INDEX IF NOT EXISTS idx_hash ON assets(hash); + CREATE INDEX IF NOT EXISTS idx_timestamp ON assets(timestamp); + CREATE INDEX IF NOT EXISTS idx_application ON assets(application); + CREATE INDEX IF NOT EXISTS idx_user ON assets(user_identity); + CREATE INDEX IF NOT EXISTS idx_trashed ON assets(is_trashed); + CREATE INDEX IF NOT EXISTS idx_tag_name ON tags(name); + ", + )?; + + // Migration: add size column (ignore error if column already exists) + let _ = conn.execute("ALTER TABLE assets ADD COLUMN size INTEGER NOT NULL DEFAULT 0", []); + + Ok(()) +} + +/// Insert a new asset. Returns the row id. +pub fn insert_asset(conn: &Connection, asset: &Asset) -> rusqlite::Result { + conn.execute( + "INSERT INTO assets (timestamp, hash, mime_type, application, user_identity, description, actual_filename, human_filename, human_path, size) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)", + params![ + asset.timestamp, + asset.hash, + asset.mime_type, + asset.application, + asset.user_identity, + asset.description, + asset.actual_filename, + asset.human_filename, + asset.human_path, + asset.size, + ], + )?; + Ok(conn.last_insert_rowid()) +} + +/// Look up an asset by its hash. +pub fn get_asset_by_hash(conn: &Connection, hash: &str) -> rusqlite::Result> { + conn.query_row( + "SELECT id, timestamp, hash, mime_type, application, user_identity, description, + actual_filename, human_filename, human_path, is_trashed, is_corrupted, size + FROM assets WHERE hash = ?1", + params![hash], + |row| { + Ok(Asset { + id: row.get(0)?, + timestamp: row.get(1)?, + hash: row.get(2)?, + mime_type: row.get(3)?, + application: row.get(4)?, + user_identity: row.get(5)?, + description: row.get(6)?, + actual_filename: row.get(7)?, + human_filename: row.get(8)?, + human_path: row.get(9)?, + is_trashed: row.get(10)?, + is_corrupted: row.get(11)?, + size: row.get(12)?, + }) + }, + ) + .optional() +} + +/// Get tags for an asset. +pub fn get_asset_tags(conn: &Connection, asset_id: i64) -> rusqlite::Result> { + let mut stmt = conn.prepare( + "SELECT t.name FROM tags t + JOIN asset_tags at ON at.tag_id = t.id + WHERE at.asset_id = ?1 + ORDER BY t.name", + )?; + let tags = stmt.query_map(params![asset_id], |row| row.get(0))?; + tags.collect() +} + +/// Upsert a tag and return its id. +pub fn upsert_tag(conn: &Connection, name: &str) -> rusqlite::Result { + conn.execute( + "INSERT OR IGNORE INTO tags (name) VALUES (?1)", + params![name], + )?; + conn.query_row("SELECT id FROM tags WHERE name = ?1", params![name], |row| { + row.get(0) + }) +} + +/// Replace all tags for an asset within a transaction. +pub fn set_asset_tags(conn: &Connection, asset_id: i64, tags: &[String]) -> rusqlite::Result<()> { + conn.execute( + "DELETE FROM asset_tags WHERE asset_id = ?1", + params![asset_id], + )?; + for tag in tags { + let tag_id = upsert_tag(conn, tag)?; + conn.execute( + "INSERT OR IGNORE INTO asset_tags (asset_id, tag_id) VALUES (?1, ?2)", + params![asset_id, tag_id], + )?; + } + Ok(()) +} + +/// Build an AssetMeta from an Asset row + tags. +pub fn asset_to_meta(conn: &Connection, asset: &Asset) -> rusqlite::Result { + let tags = get_asset_tags(conn, asset.id)?; + Ok(AssetMeta { + hash: asset.hash.clone(), + mime_type: asset.mime_type.clone(), + application: asset.application.clone(), + user: asset.user_identity.clone(), + tags, + description: asset.description.clone(), + human_filename: asset.human_filename.clone(), + human_path: asset.human_path.clone(), + timestamp: asset.timestamp, + is_trashed: asset.is_trashed, + is_corrupted: asset.is_corrupted, + size: asset.size, + }) +} + +/// Update description and/or tags for an asset. +pub fn update_asset_metadata( + conn: &Connection, + hash: &str, + description: Option<&str>, + tags: Option<&[String]>, +) -> rusqlite::Result<()> { + let asset = get_asset_by_hash(conn, hash)? + .ok_or(rusqlite::Error::QueryReturnedNoRows)?; + + if let Some(desc) = description { + conn.execute( + "UPDATE assets SET description = ?1 WHERE id = ?2", + params![desc, asset.id], + )?; + } + if let Some(tags) = tags { + set_asset_tags(conn, asset.id, tags)?; + } + Ok(()) +} + +/// Flag an asset as corrupted. +pub fn flag_corrupted(conn: &Connection, hash: &str, corrupted: bool) -> rusqlite::Result<()> { + conn.execute( + "UPDATE assets SET is_corrupted = ?1 WHERE hash = ?2", + params![corrupted, hash], + )?; + Ok(()) +} + +/// Update file size for an asset (used by verifier to backfill). +pub fn update_asset_size(conn: &Connection, hash: &str, size: i64) -> rusqlite::Result<()> { + conn.execute( + "UPDATE assets SET size = ?1 WHERE hash = ?2", + params![size, hash], + )?; + Ok(()) +} + +/// Soft-delete: mark as trashed. +pub fn trash_asset(conn: &Connection, hash: &str) -> rusqlite::Result<()> { + conn.execute( + "UPDATE assets SET is_trashed = 1 WHERE hash = ?1", + params![hash], + )?; + Ok(()) +} + +/// List assets with pagination and filtering. +pub fn list_assets(conn: &Connection, params: &ListParams) -> rusqlite::Result<(Vec, i64)> { + let limit = params.limit.unwrap_or(50); + let offset = params.offset.unwrap_or(0); + let order = match params.order.as_deref() { + Some("asc") => "ASC", + _ => "DESC", + }; + let include_trashed = params.include_trashed.unwrap_or(false); + let include_corrupted = params.include_corrupted.unwrap_or(false); + + let mut conditions = Vec::new(); + let mut bind_values: Vec> = Vec::new(); + + if !include_trashed { + conditions.push("is_trashed = 0"); + } + if !include_corrupted { + conditions.push("is_corrupted = 0"); + } + if let Some(ref app) = params.application { + conditions.push("application = ?"); + bind_values.push(Box::new(app.clone())); + } + if let Some(offset_time) = params.offset_time { + if order == "DESC" { + conditions.push("timestamp < ?"); + } else { + conditions.push("timestamp > ?"); + } + bind_values.push(Box::new(offset_time)); + } + + let where_clause = if conditions.is_empty() { + String::new() + } else { + format!("WHERE {}", conditions.join(" AND ")) + }; + + let count_sql = format!("SELECT COUNT(*) FROM assets {}", where_clause); + let refs: Vec<&dyn rusqlite::types::ToSql> = bind_values.iter().map(|b| b.as_ref()).collect(); + let total: i64 = conn.query_row(&count_sql, refs.as_slice(), |row| row.get(0))?; + + let query_sql = format!( + "SELECT id, timestamp, hash, mime_type, application, user_identity, description, + actual_filename, human_filename, human_path, is_trashed, is_corrupted, size + FROM assets {} ORDER BY timestamp {} LIMIT ? OFFSET ?", + where_clause, order + ); + + let mut all_binds: Vec> = bind_values; + all_binds.push(Box::new(limit)); + all_binds.push(Box::new(offset)); + let refs2: Vec<&dyn rusqlite::types::ToSql> = all_binds.iter().map(|b| b.as_ref()).collect(); + + let mut stmt = conn.prepare(&query_sql)?; + let assets = stmt + .query_map(refs2.as_slice(), |row| { + Ok(Asset { + id: row.get(0)?, + timestamp: row.get(1)?, + hash: row.get(2)?, + mime_type: row.get(3)?, + application: row.get(4)?, + user_identity: row.get(5)?, + description: row.get(6)?, + actual_filename: row.get(7)?, + human_filename: row.get(8)?, + human_path: row.get(9)?, + is_trashed: row.get(10)?, + is_corrupted: row.get(11)?, + size: row.get(12)?, + }) + })? + .collect::>>()?; + + Ok((assets, total)) +} + +/// Search assets with various filters. +pub fn search_assets( + conn: &Connection, + params: &SearchParams, +) -> rusqlite::Result<(Vec, i64)> { + let limit = params.limit.unwrap_or(50); + let offset = params.offset.unwrap_or(0); + let order = match params.order.as_deref() { + Some("asc") => "ASC", + _ => "DESC", + }; + let include_trashed = params.include_trashed.unwrap_or(false); + let include_corrupted = params.include_corrupted.unwrap_or(false); + + let mut conditions = Vec::new(); + let mut bind_values: Vec> = Vec::new(); + let mut needs_tag_join = false; + + if !include_trashed { + conditions.push("a.is_trashed = 0".to_string()); + } + if !include_corrupted { + conditions.push("a.is_corrupted = 0".to_string()); + } + if let Some(ref hash) = params.hash { + conditions.push("a.hash LIKE ?".to_string()); + bind_values.push(Box::new(format!("{}%", hash))); + } + if let Some(start) = params.start_time { + conditions.push("a.timestamp >= ?".to_string()); + bind_values.push(Box::new(start)); + } + if let Some(end) = params.end_time { + conditions.push("a.timestamp <= ?".to_string()); + bind_values.push(Box::new(end)); + } + if let Some(ref mime) = params.mime_type { + conditions.push("a.mime_type = ?".to_string()); + bind_values.push(Box::new(mime.clone())); + } + if let Some(ref user) = params.user { + conditions.push("a.user_identity = ?".to_string()); + bind_values.push(Box::new(user.clone())); + } + if let Some(ref app) = params.application { + conditions.push("a.application = ?".to_string()); + bind_values.push(Box::new(app.clone())); + } + + // Tag filtering: AND logic - asset must have ALL specified tags + let tag_names: Vec = params + .tags + .as_deref() + .unwrap_or("") + .split(',') + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect(); + + if !tag_names.is_empty() { + needs_tag_join = true; + let placeholders: Vec = tag_names.iter().map(|_| "?".to_string()).collect(); + conditions.push(format!( + "a.id IN ( + SELECT at.asset_id FROM asset_tags at + JOIN tags t ON t.id = at.tag_id + WHERE t.name IN ({}) + GROUP BY at.asset_id + HAVING COUNT(DISTINCT t.id) = ? + )", + placeholders.join(", ") + )); + for tag in &tag_names { + bind_values.push(Box::new(tag.clone())); + } + bind_values.push(Box::new(tag_names.len() as i64)); + } + + let _ = needs_tag_join; // subquery handles it + + let where_clause = if conditions.is_empty() { + String::new() + } else { + format!("WHERE {}", conditions.join(" AND ")) + }; + + let count_sql = format!("SELECT COUNT(*) FROM assets a {}", where_clause); + let refs: Vec<&dyn rusqlite::types::ToSql> = bind_values.iter().map(|b| b.as_ref()).collect(); + let total: i64 = conn.query_row(&count_sql, refs.as_slice(), |row| row.get(0))?; + + let query_sql = format!( + "SELECT a.id, a.timestamp, a.hash, a.mime_type, a.application, a.user_identity, + a.description, a.actual_filename, a.human_filename, a.human_path, + a.is_trashed, a.is_corrupted, a.size + FROM assets a {} ORDER BY a.timestamp {} LIMIT ? OFFSET ?", + where_clause, order + ); + + let mut all_binds = bind_values; + all_binds.push(Box::new(limit)); + all_binds.push(Box::new(offset)); + let refs2: Vec<&dyn rusqlite::types::ToSql> = all_binds.iter().map(|b| b.as_ref()).collect(); + + let mut stmt = conn.prepare(&query_sql)?; + let assets = stmt + .query_map(refs2.as_slice(), |row| { + Ok(Asset { + id: row.get(0)?, + timestamp: row.get(1)?, + hash: row.get(2)?, + mime_type: row.get(3)?, + application: row.get(4)?, + user_identity: row.get(5)?, + description: row.get(6)?, + actual_filename: row.get(7)?, + human_filename: row.get(8)?, + human_path: row.get(9)?, + is_trashed: row.get(10)?, + is_corrupted: row.get(11)?, + size: row.get(12)?, + }) + })? + .collect::>>()?; + + Ok((assets, total)) +} + +/// Get all non-trashed asset records (for verifier startup scan). +pub fn get_all_active_assets(conn: &Connection) -> rusqlite::Result> { + let mut stmt = conn.prepare( + "SELECT id, timestamp, hash, mime_type, application, user_identity, description, + actual_filename, human_filename, human_path, is_trashed, is_corrupted, size + FROM assets WHERE is_trashed = 0", + )?; + let assets = stmt + .query_map([], |row| { + Ok(Asset { + id: row.get(0)?, + timestamp: row.get(1)?, + hash: row.get(2)?, + mime_type: row.get(3)?, + application: row.get(4)?, + user_identity: row.get(5)?, + description: row.get(6)?, + actual_filename: row.get(7)?, + human_filename: row.get(8)?, + human_path: row.get(9)?, + is_trashed: row.get(10)?, + is_corrupted: row.get(11)?, + size: row.get(12)?, + }) + })? + .collect::>>()?; + Ok(assets) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn make_test_asset(ts: i64, hash: &str) -> Asset { + Asset { + id: 0, + timestamp: ts, + hash: hash.to_string(), + mime_type: "text/plain".to_string(), + application: Some("test_app".to_string()), + user_identity: Some("test_user".to_string()), + description: Some("test desc".to_string()), + actual_filename: format!("{}_{}.txt", ts, hash), + human_filename: Some("readme.txt".to_string()), + human_path: Some("/docs/".to_string()), + is_trashed: false, + is_corrupted: false, + size: 0, + } + } + + #[test] + fn test_insert_and_get_asset() { + let db = open_in_memory().unwrap(); + let conn = db.lock().unwrap(); + let asset = make_test_asset(1000, "abc123"); + let id = insert_asset(&conn, &asset).unwrap(); + assert!(id > 0); + + let found = get_asset_by_hash(&conn, "abc123").unwrap().unwrap(); + assert_eq!(found.hash, "abc123"); + assert_eq!(found.timestamp, 1000); + assert_eq!(found.mime_type, "text/plain"); + } + + #[test] + fn test_get_nonexistent_asset() { + let db = open_in_memory().unwrap(); + let conn = db.lock().unwrap(); + let found = get_asset_by_hash(&conn, "nonexistent").unwrap(); + assert!(found.is_none()); + } + + #[test] + fn test_tags() { + let db = open_in_memory().unwrap(); + let conn = db.lock().unwrap(); + let asset = make_test_asset(2000, "def456"); + let id = insert_asset(&conn, &asset).unwrap(); + + let tags = vec!["photo".to_string(), "vacation".to_string()]; + set_asset_tags(&conn, id, &tags).unwrap(); + + let fetched = get_asset_tags(&conn, id).unwrap(); + assert_eq!(fetched, vec!["photo", "vacation"]); + + // Replace tags + let new_tags = vec!["work".to_string()]; + set_asset_tags(&conn, id, &new_tags).unwrap(); + let fetched2 = get_asset_tags(&conn, id).unwrap(); + assert_eq!(fetched2, vec!["work"]); + } + + #[test] + fn test_update_metadata() { + let db = open_in_memory().unwrap(); + let conn = db.lock().unwrap(); + let asset = make_test_asset(3000, "ghi789"); + insert_asset(&conn, &asset).unwrap(); + + let new_tags = vec!["updated".to_string()]; + update_asset_metadata(&conn, "ghi789", Some("new desc"), Some(&new_tags)).unwrap(); + + let found = get_asset_by_hash(&conn, "ghi789").unwrap().unwrap(); + assert_eq!(found.description, Some("new desc".to_string())); + let tags = get_asset_tags(&conn, found.id).unwrap(); + assert_eq!(tags, vec!["updated"]); + } + + #[test] + fn test_flag_corrupted() { + let db = open_in_memory().unwrap(); + let conn = db.lock().unwrap(); + let asset = make_test_asset(4000, "corrupt1"); + insert_asset(&conn, &asset).unwrap(); + + flag_corrupted(&conn, "corrupt1", true).unwrap(); + let found = get_asset_by_hash(&conn, "corrupt1").unwrap().unwrap(); + assert!(found.is_corrupted); + + flag_corrupted(&conn, "corrupt1", false).unwrap(); + let found2 = get_asset_by_hash(&conn, "corrupt1").unwrap().unwrap(); + assert!(!found2.is_corrupted); + } + + #[test] + fn test_trash_asset() { + let db = open_in_memory().unwrap(); + let conn = db.lock().unwrap(); + let asset = make_test_asset(5000, "trash1"); + insert_asset(&conn, &asset).unwrap(); + + trash_asset(&conn, "trash1").unwrap(); + let found = get_asset_by_hash(&conn, "trash1").unwrap().unwrap(); + assert!(found.is_trashed); + } + + #[test] + fn test_list_assets_basic() { + let db = open_in_memory().unwrap(); + let conn = db.lock().unwrap(); + + for i in 0..5 { + let asset = make_test_asset(1000 + i, &format!("hash_{}", i)); + insert_asset(&conn, &asset).unwrap(); + } + + let params = ListParams { + limit: Some(3), + offset: Some(0), + offset_time: None, + order: Some("desc".to_string()), + application: None, + include_trashed: None, + include_corrupted: None, + }; + let (assets, total) = list_assets(&conn, ¶ms).unwrap(); + assert_eq!(total, 5); + assert_eq!(assets.len(), 3); + // DESC order: highest timestamp first + assert!(assets[0].timestamp > assets[1].timestamp); + } + + #[test] + fn test_list_excludes_trashed_by_default() { + let db = open_in_memory().unwrap(); + let conn = db.lock().unwrap(); + + let a1 = make_test_asset(100, "visible1"); + insert_asset(&conn, &a1).unwrap(); + let a2 = make_test_asset(200, "trashed1"); + insert_asset(&conn, &a2).unwrap(); + trash_asset(&conn, "trashed1").unwrap(); + + let params = ListParams { + limit: None, offset: None, offset_time: None, + order: None, application: None, + include_trashed: None, include_corrupted: None, + }; + let (assets, total) = list_assets(&conn, ¶ms).unwrap(); + assert_eq!(total, 1); + assert_eq!(assets[0].hash, "visible1"); + } + + #[test] + fn test_search_by_hash_prefix() { + let db = open_in_memory().unwrap(); + let conn = db.lock().unwrap(); + let a1 = make_test_asset(100, "abcdef123"); + let a2 = make_test_asset(200, "abcxyz789"); + let a3 = make_test_asset(300, "zzz000111"); + insert_asset(&conn, &a1).unwrap(); + insert_asset(&conn, &a2).unwrap(); + insert_asset(&conn, &a3).unwrap(); + + let params = SearchParams { + hash: Some("abc".to_string()), + start_time: None, end_time: None, tags: None, + mime_type: None, user: None, application: None, + limit: None, offset: None, order: None, + include_trashed: None, include_corrupted: None, + }; + let (assets, total) = search_assets(&conn, ¶ms).unwrap(); + assert_eq!(total, 2); + assert!(assets.iter().all(|a| a.hash.starts_with("abc"))); + } + + #[test] + fn test_search_by_tags() { + let db = open_in_memory().unwrap(); + let conn = db.lock().unwrap(); + + let a1 = make_test_asset(100, "tagged1"); + let id1 = insert_asset(&conn, &a1).unwrap(); + set_asset_tags(&conn, id1, &["red".to_string(), "blue".to_string()]).unwrap(); + + let a2 = make_test_asset(200, "tagged2"); + let id2 = insert_asset(&conn, &a2).unwrap(); + set_asset_tags(&conn, id2, &["red".to_string()]).unwrap(); + + // Search for both red AND blue -> only tagged1 + let params = SearchParams { + hash: None, start_time: None, end_time: None, + tags: Some("red,blue".to_string()), + mime_type: None, user: None, application: None, + limit: None, offset: None, order: None, + include_trashed: None, include_corrupted: None, + }; + let (assets, total) = search_assets(&conn, ¶ms).unwrap(); + assert_eq!(total, 1); + assert_eq!(assets[0].hash, "tagged1"); + } +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..8728c1d --- /dev/null +++ b/src/error.rs @@ -0,0 +1,42 @@ +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; +use crate::models::ErrorResponse; + +#[derive(Debug, thiserror::Error)] +pub enum AppError { + #[error("Not found: {0}")] + NotFound(String), + + #[error("Bad request: {0}")] + BadRequest(String), + + #[error("Asset is corrupted: {0}")] + Corrupted(String), + + #[error("Database error: {0}")] + Database(#[from] rusqlite::Error), + + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + + #[error("Internal error: {0}")] + Internal(String), +} + +impl IntoResponse for AppError { + fn into_response(self) -> Response { + let (status, message) = match &self { + AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg.clone()), + AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg.clone()), + AppError::Corrupted(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg.clone()), + AppError::Database(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()), + AppError::Io(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()), + AppError::Internal(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg.clone()), + }; + + tracing::error!(%status, error = %message, "request error"); + + let body = serde_json::to_string(&ErrorResponse::new(message)).unwrap_or_default(); + (status, [(axum::http::header::CONTENT_TYPE, "application/json")], body).into_response() + } +} diff --git a/src/hash.rs b/src/hash.rs new file mode 100644 index 0000000..a1a99d8 --- /dev/null +++ b/src/hash.rs @@ -0,0 +1,48 @@ +use sha2::{Digest, Sha256}; + +/// Compute SHA-256 hash as: SHA256(timestamp_bytes + content_bytes). +/// The timestamp is serialized as its string representation's bytes, +/// matching the spec: `SHA256([timestamp_bytes] + [raw_file_content_bytes])`. +pub fn compute_hash(timestamp_ms: i64, content: &[u8]) -> String { + let mut hasher = Sha256::new(); + hasher.update(timestamp_ms.to_be_bytes()); + hasher.update(content); + hex::encode(hasher.finalize()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_deterministic_hash() { + let ts = 1773014400123i64; + let content = b"hello world"; + let h1 = compute_hash(ts, content); + let h2 = compute_hash(ts, content); + assert_eq!(h1, h2); + assert_eq!(h1.len(), 64); // SHA-256 hex = 64 chars + } + + #[test] + fn test_different_timestamp_different_hash() { + let content = b"same content"; + let h1 = compute_hash(1000, content); + let h2 = compute_hash(2000, content); + assert_ne!(h1, h2); + } + + #[test] + fn test_different_content_different_hash() { + let ts = 1234567890i64; + let h1 = compute_hash(ts, b"content A"); + let h2 = compute_hash(ts, b"content B"); + assert_ne!(h1, h2); + } + + #[test] + fn test_empty_content() { + let h = compute_hash(0, b""); + assert_eq!(h.len(), 64); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..199acf1 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,20 @@ +pub mod config; +pub mod db; +pub mod error; +pub mod hash; +pub mod models; +pub mod routes; +pub mod storage; +pub mod verifier; +pub mod xattr; + +use std::sync::Arc; + +use crate::config::Config; +use crate::db::Db; + +#[derive(Clone)] +pub struct AppState { + pub config: Arc, + pub db: Db, +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..05fdab6 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,64 @@ +use std::net::SocketAddr; +use std::path::PathBuf; +use std::sync::Arc; + +use axum::extract::DefaultBodyLimit; +use axum::Router; +use tower_http::cors::CorsLayer; +use tower_http::trace::TraceLayer; + +use can_service::config::Config; +use can_service::{db, routes, verifier, AppState}; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // Initialize tracing + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| "can_service=info,tower_http=info".into()), + ) + .init(); + + // Load config + let config_path = std::env::args() + .nth(1) + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from("config.yaml")); + + let config = Config::load(&config_path)?; + tracing::info!("Loaded config, storage_root: {:?}", config.storage_root); + + // Ensure directories exist + config.ensure_dirs()?; + + // Open database + let db = db::open(&config.db_path())?; + tracing::info!("Database initialized at {:?}", config.db_path()); + + let config = Arc::new(config); + + // Start background verifier + verifier::start((*config).clone(), db.clone()); + + let state = AppState { + config: config.clone(), + db, + }; + + // Build router + let app = Router::new() + .merge(routes::router()) + .layer(DefaultBodyLimit::max(100 * 1024 * 1024)) // 100 MB + .layer(TraceLayer::new_for_http()) + .layer(CorsLayer::permissive()) + .with_state(state); + + let addr = SocketAddr::from(([0, 0, 0, 0], 3210)); + tracing::info!("CAN service listening on {}", addr); + + let listener = tokio::net::TcpListener::bind(addr).await?; + axum::serve(listener, app).await?; + + Ok(()) +} diff --git a/src/models.rs b/src/models.rs new file mode 100644 index 0000000..392153c --- /dev/null +++ b/src/models.rs @@ -0,0 +1,164 @@ +use serde::{Deserialize, Serialize}; + +/// Database representation of an asset. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Asset { + pub id: i64, + pub timestamp: i64, + pub hash: String, + pub mime_type: String, + pub application: Option, + pub user_identity: Option, + pub description: Option, + pub actual_filename: String, + pub human_filename: Option, + pub human_path: Option, + pub is_trashed: bool, + pub is_corrupted: bool, + pub size: i64, +} + +/// API-facing asset metadata response. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AssetMeta { + pub hash: String, + pub mime_type: String, + pub application: Option, + pub user: Option, + pub tags: Vec, + pub description: Option, + pub human_filename: Option, + pub human_path: Option, + pub timestamp: i64, + pub is_trashed: bool, + pub is_corrupted: bool, + pub size: i64, +} + +/// Standard API response wrapper. +#[derive(Debug, Serialize, Deserialize)] +pub struct ApiResponse { + pub status: String, + pub data: T, +} + +impl ApiResponse { + pub fn success(data: T) -> Self { + Self { + status: "success".to_string(), + data, + } + } +} + +/// Error response body. +#[derive(Debug, Serialize, Deserialize)] +pub struct ErrorResponse { + pub status: String, + pub error: String, +} + +impl ErrorResponse { + pub fn new(msg: impl Into) -> Self { + Self { + status: "error".to_string(), + error: msg.into(), + } + } +} + +/// Ingest success response data. +#[derive(Debug, Serialize, Deserialize)] +pub struct IngestResult { + pub timestamp: i64, + pub hash: String, + pub filename: String, +} + +/// Pagination metadata in list responses. +#[derive(Debug, Serialize, Deserialize)] +pub struct Pagination { + pub limit: i64, + pub offset: i64, + pub total: i64, +} + +/// Paginated list response. +#[derive(Debug, Serialize, Deserialize)] +pub struct ListResponse { + pub items: Vec, + pub pagination: Pagination, +} + +/// PATCH request body for metadata updates. +#[derive(Debug, Deserialize)] +pub struct MetadataUpdate { + pub tags: Option>, + pub description: Option, +} + +/// OS-level file attribute metadata (for xattr / NTFS ADS). +#[derive(Debug, Clone, Default, PartialEq)] +pub struct FileAttributes { + pub mime_type: Option, + pub application: Option, + pub user: Option, + pub tags: Option, + pub description: Option, + pub human_filename: Option, + pub human_path: Option, +} + +/// JSON-based data ingest request (agent-friendly, no multipart needed). +/// +/// `data` accepts any JSON value — object, array, string, number, etc. +/// It gets serialized to pretty JSON and stored as a `.json` file. +/// Minimal call: `{ "data": {"key": "value"} }` +#[derive(Debug, Deserialize)] +pub struct DataIngestRequest { + /// The payload to store. Any valid JSON value. + pub data: serde_json::Value, + /// Override MIME type. Defaults to `application/json`. + pub mime_type: Option, + /// Logical filename (e.g. "agent_config.json"). + pub human_file_name: Option, + /// Logical folder path (e.g. "/configs/"). + pub human_readable_path: Option, + /// Application that produced this data. + pub application: Option, + /// User / agent identity. + pub user: Option, + /// Comma-separated tags. + pub tags: Option, + /// Human-readable description. + pub description: Option, +} + +/// Query parameters for the list endpoint. +#[derive(Debug, Deserialize)] +pub struct ListParams { + pub limit: Option, + pub offset: Option, + pub offset_time: Option, + pub order: Option, + pub application: Option, + pub include_trashed: Option, + pub include_corrupted: Option, +} + +/// Query parameters for the search endpoint. +#[derive(Debug, Deserialize)] +pub struct SearchParams { + pub hash: Option, + pub start_time: Option, + pub end_time: Option, + pub tags: Option, + pub mime_type: Option, + pub user: Option, + pub application: Option, + pub limit: Option, + pub offset: Option, + pub order: Option, + pub include_trashed: Option, + pub include_corrupted: Option, +} diff --git a/src/routes/asset.rs b/src/routes/asset.rs new file mode 100644 index 0000000..b4d329a --- /dev/null +++ b/src/routes/asset.rs @@ -0,0 +1,101 @@ +use axum::body::Body; +use axum::extract::{Path, State}; +use axum::http::header; +use axum::response::{IntoResponse, Response}; +use axum::routing::{get, patch}; +use axum::{Json, Router}; +use tokio::fs::File; +use tokio_util::io::ReaderStream; + +use crate::error::AppError; +use crate::models::{ApiResponse, FileAttributes, MetadataUpdate}; +use crate::{db, xattr, AppState}; + +pub fn router() -> Router { + Router::new() + .route("/api/v1/can/0/asset/{hash}", get(get_asset)) + .route("/api/v1/can/0/asset/{hash}", patch(patch_asset)) +} + +/// GET /api/v1/can/0/asset/{hash} - Stream the physical file. +async fn get_asset( + State(state): State, + Path(hash): Path, +) -> Result { + let asset = { + let conn = state.db.lock().unwrap(); + db::get_asset_by_hash(&conn, &hash)? + .ok_or_else(|| AppError::NotFound(format!("Asset not found: {}", hash)))? + }; + + if asset.is_corrupted { + return Err(AppError::Corrupted(format!( + "Asset {} is flagged as corrupted", + hash + ))); + } + + let file_path = state.config.storage_root.join(&asset.actual_filename); + let file = File::open(&file_path).await.map_err(|e| { + AppError::Internal(format!("Failed to open file {}: {}", asset.actual_filename, e)) + })?; + + let stream = ReaderStream::new(file); + let body = Body::from_stream(stream); + + let content_type = asset.mime_type.clone(); + let disposition = match &asset.human_filename { + Some(name) => format!("attachment; filename=\"{}\"", name), + None => format!("attachment; filename=\"{}\"", asset.actual_filename), + }; + + Ok(( + [ + (header::CONTENT_TYPE, content_type), + (header::CONTENT_DISPOSITION, disposition), + ], + body, + ) + .into_response()) +} + +/// PATCH /api/v1/can/0/asset/{hash} - Update metadata (tags, description). +async fn patch_asset( + State(state): State, + Path(hash): Path, + Json(update): Json, +) -> Result>, AppError> { + let asset = { + let conn = state.db.lock().unwrap(); + db::get_asset_by_hash(&conn, &hash)? + .ok_or_else(|| AppError::NotFound(format!("Asset not found: {}", hash)))? + }; + + // Update DB + { + let conn = state.db.lock().unwrap(); + db::update_asset_metadata( + &conn, + &hash, + update.description.as_deref(), + update.tags.as_deref(), + )?; + } + + // Update OS attributes + let file_path = state.config.storage_root.join(&asset.actual_filename); + if file_path.exists() { + let mut attrs = FileAttributes::default(); + if let Some(ref desc) = update.description { + attrs.description = Some(desc.clone()); + } + if let Some(ref tags) = update.tags { + attrs.tags = Some(tags.join(",")); + } + if let Err(e) = xattr::write_attributes(&file_path, &attrs) { + tracing::warn!("Failed to update OS attributes: {}", e); + } + } + + Ok(Json(ApiResponse::success("updated".to_string()))) +} diff --git a/src/routes/ingest.rs b/src/routes/ingest.rs new file mode 100644 index 0000000..8bd6151 --- /dev/null +++ b/src/routes/ingest.rs @@ -0,0 +1,273 @@ +use axum::extract::{Multipart, State}; +use axum::routing::post; +use axum::{Json, Router}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use crate::error::AppError; +use crate::models::{ApiResponse, Asset, DataIngestRequest, FileAttributes, IngestResult}; +use crate::{db, hash, storage, xattr, AppState}; + +pub fn router() -> Router { + Router::new() + .route("/api/v1/can/0/ingest", post(ingest_multipart)) + .route("/api/v1/can/0/ingest/data", post(ingest_data)) +} + +// ── Shared ingest pipeline ────────────────────────────────────────────── + +/// All the parsed fields needed to ingest an asset, regardless of source. +struct IngestInput { + content: Vec, + mime_type: String, + human_file_name: Option, + human_readable_path: Option, + application: Option, + user: Option, + tags: Vec, + description: Option, +} + +/// Common pipeline: timestamp → hash → write file → xattr → DB insert. +fn do_ingest(state: &AppState, input: IngestInput) -> Result { + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis() as i64; + + let file_hash = hash::compute_hash(timestamp, &input.content); + + let actual_filename = + storage::build_filename(timestamp, &file_hash, &input.tags, &input.mime_type); + + let file_path = + storage::write_asset(&state.config.storage_root, &actual_filename, &input.content)?; + + // OS-level attributes (best-effort) + let attrs = FileAttributes { + mime_type: Some(input.mime_type.clone()), + application: input.application.clone(), + user: input.user.clone(), + tags: if input.tags.is_empty() { + None + } else { + Some(input.tags.join(",")) + }, + description: input.description.clone(), + human_filename: input.human_file_name.clone(), + human_path: input.human_readable_path.clone(), + }; + if let Err(e) = xattr::write_attributes(&file_path, &attrs) { + tracing::warn!("Failed to write OS attributes: {}", e); + } + + // Database insert + let asset = Asset { + id: 0, + timestamp, + hash: file_hash.clone(), + mime_type: input.mime_type, + application: input.application, + user_identity: input.user, + description: input.description, + actual_filename: actual_filename.clone(), + human_filename: input.human_file_name, + human_path: input.human_readable_path, + is_trashed: false, + is_corrupted: false, + size: input.content.len() as i64, + }; + + { + let conn = state.db.lock().unwrap(); + let asset_id = db::insert_asset(&conn, &asset)?; + if !input.tags.is_empty() { + db::set_asset_tags(&conn, asset_id, &input.tags)?; + } + } + + Ok(IngestResult { + timestamp, + hash: file_hash, + filename: actual_filename, + }) +} + +/// Parse a comma-separated tag string into a clean Vec. +fn parse_tags(raw: Option<&str>) -> Vec { + raw.unwrap_or("") + .split(',') + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect() +} + +// ── POST /api/v1/can/0/ingest (multipart — file uploads) ────────────── + +async fn ingest_multipart( + State(state): State, + mut multipart: Multipart, +) -> Result>, AppError> { + let mut file_data: Option> = None; + let mut mime_type: Option = None; + let mut human_file_name: Option = None; + let mut human_readable_path: Option = None; + let mut application: Option = None; + let mut user: Option = None; + let mut tags_str: Option = None; + let mut description: Option = None; + let mut original_filename: Option = None; + + while let Some(field) = multipart + .next_field() + .await + .map_err(|e| AppError::BadRequest(e.to_string()))? + { + let name = field.name().unwrap_or("").to_string(); + match name.as_str() { + "file" => { + if let Some(fname) = field.file_name() { + original_filename = Some(fname.to_string()); + } + if let Some(ct) = field.content_type() { + if mime_type.is_none() { + mime_type = Some(ct.to_string()); + } + } + file_data = Some( + field + .bytes() + .await + .map_err(|e| AppError::BadRequest(e.to_string()))? + .to_vec(), + ); + } + "mime_type" => { + let val = field + .text() + .await + .map_err(|e| AppError::BadRequest(e.to_string()))?; + if !val.is_empty() { + mime_type = Some(val); + } + } + "human_file_name" => { + human_file_name = Some( + field + .text() + .await + .map_err(|e| AppError::BadRequest(e.to_string()))?, + ); + } + "human_readable_path" => { + human_readable_path = Some( + field + .text() + .await + .map_err(|e| AppError::BadRequest(e.to_string()))?, + ); + } + "application" => { + application = Some( + field + .text() + .await + .map_err(|e| AppError::BadRequest(e.to_string()))?, + ); + } + "user" => { + user = Some( + field + .text() + .await + .map_err(|e| AppError::BadRequest(e.to_string()))?, + ); + } + "tags" => { + tags_str = Some( + field + .text() + .await + .map_err(|e| AppError::BadRequest(e.to_string()))?, + ); + } + "description" => { + description = Some( + field + .text() + .await + .map_err(|e| AppError::BadRequest(e.to_string()))?, + ); + } + _ => {} + } + } + + let content = file_data.ok_or_else(|| AppError::BadRequest("Missing 'file' field".into()))?; + + let resolved_mime = mime_type.unwrap_or_else(|| { + original_filename + .as_deref() + .and_then(|name| mime_guess::from_path(name).first_raw().map(|s| s.to_string())) + .unwrap_or_else(|| "application/octet-stream".to_string()) + }); + + let result = do_ingest( + &state, + IngestInput { + content, + mime_type: resolved_mime, + human_file_name, + human_readable_path, + application, + user, + tags: parse_tags(tags_str.as_deref()), + description, + }, + )?; + + Ok(Json(ApiResponse::success(result))) +} + +// ── POST /api/v1/can/0/ingest/data (JSON — agent-friendly) ──────────── + +/// JSON data ingest. Accepts any JSON value in `data`, serializes it to +/// pretty-printed JSON bytes, and stores it as a `.json` asset. +/// +/// Minimal agent call: +/// ```json +/// POST /api/v1/can/0/ingest/data +/// Content-Type: application/json +/// +/// { "data": { "key": "value" } } +/// ``` +/// +/// All metadata fields (tags, application, user, etc.) are optional — +/// same semantics as the multipart endpoint. +async fn ingest_data( + State(state): State, + Json(req): Json, +) -> Result>, AppError> { + // Serialize the data payload to pretty JSON bytes + let content = serde_json::to_vec_pretty(&req.data) + .map_err(|e| AppError::Internal(format!("Failed to serialize data: {}", e)))?; + + let mime = req + .mime_type + .unwrap_or_else(|| "application/json".to_string()); + + let result = do_ingest( + &state, + IngestInput { + content, + mime_type: mime, + human_file_name: req.human_file_name, + human_readable_path: req.human_readable_path, + application: req.application, + user: req.user, + tags: parse_tags(req.tags.as_deref()), + description: req.description, + }, + )?; + + Ok(Json(ApiResponse::success(result))) +} diff --git a/src/routes/list.rs b/src/routes/list.rs new file mode 100644 index 0000000..30283ad --- /dev/null +++ b/src/routes/list.rs @@ -0,0 +1,35 @@ +use axum::extract::{Query, State}; +use axum::routing::get; +use axum::{Json, Router}; + +use crate::error::AppError; +use crate::models::{ApiResponse, AssetMeta, ListParams, ListResponse, Pagination}; +use crate::{db, AppState}; + +pub fn router() -> Router { + Router::new().route("/api/v1/can/0/list", get(list_assets)) +} + +async fn list_assets( + State(state): State, + Query(params): Query, +) -> Result>, AppError> { + let conn = state.db.lock().unwrap(); + let (assets, total) = db::list_assets(&conn, ¶ms)?; + + let items: Vec = assets + .iter() + .map(|a| db::asset_to_meta(&conn, a)) + .collect::, _>>()?; + + let response = ListResponse { + items, + pagination: Pagination { + limit: params.limit.unwrap_or(50), + offset: params.offset.unwrap_or(0), + total, + }, + }; + + Ok(Json(ApiResponse::success(response))) +} diff --git a/src/routes/meta.rs b/src/routes/meta.rs new file mode 100644 index 0000000..bdfc1b8 --- /dev/null +++ b/src/routes/meta.rs @@ -0,0 +1,22 @@ +use axum::extract::{Path, State}; +use axum::routing::get; +use axum::{Json, Router}; + +use crate::error::AppError; +use crate::models::{ApiResponse, AssetMeta}; +use crate::{db, AppState}; + +pub fn router() -> Router { + Router::new().route("/api/v1/can/0/asset/{hash}/meta", get(get_meta)) +} + +async fn get_meta( + State(state): State, + Path(hash): Path, +) -> Result>, AppError> { + let conn = state.db.lock().unwrap(); + let asset = db::get_asset_by_hash(&conn, &hash)? + .ok_or_else(|| AppError::NotFound(format!("Asset not found: {}", hash)))?; + let meta = db::asset_to_meta(&conn, &asset)?; + Ok(Json(ApiResponse::success(meta))) +} diff --git a/src/routes/mod.rs b/src/routes/mod.rs new file mode 100644 index 0000000..0a784fe --- /dev/null +++ b/src/routes/mod.rs @@ -0,0 +1,19 @@ +pub mod ingest; +pub mod asset; +pub mod meta; +pub mod list; +pub mod search; +pub mod thumb; + +use axum::Router; +use crate::AppState; + +pub fn router() -> Router { + Router::new() + .merge(ingest::router()) + .merge(asset::router()) + .merge(meta::router()) + .merge(list::router()) + .merge(search::router()) + .merge(thumb::router()) +} diff --git a/src/routes/search.rs b/src/routes/search.rs new file mode 100644 index 0000000..f671246 --- /dev/null +++ b/src/routes/search.rs @@ -0,0 +1,38 @@ +use axum::extract::{Query, State}; +use axum::routing::get; +use axum::{Json, Router}; + +use crate::error::AppError; +use crate::models::{ApiResponse, AssetMeta, ListResponse, Pagination, SearchParams}; +use crate::{db, AppState}; + +pub fn router() -> Router { + Router::new().route("/api/v1/can/0/search", get(search_assets)) +} + +async fn search_assets( + State(state): State, + Query(params): Query, +) -> Result>, AppError> { + let conn = state.db.lock().unwrap(); + let limit = params.limit.unwrap_or(50); + let offset = params.offset.unwrap_or(0); + + let (assets, total) = db::search_assets(&conn, ¶ms)?; + + let items: Vec = assets + .iter() + .map(|a| db::asset_to_meta(&conn, a)) + .collect::, _>>()?; + + let response = ListResponse { + items, + pagination: Pagination { + limit, + offset, + total, + }, + }; + + Ok(Json(ApiResponse::success(response))) +} diff --git a/src/routes/thumb.rs b/src/routes/thumb.rs new file mode 100644 index 0000000..19236b7 --- /dev/null +++ b/src/routes/thumb.rs @@ -0,0 +1,97 @@ +use axum::body::Body; +use axum::extract::{Path, State}; +use axum::http::header; +use axum::response::{IntoResponse, Response}; +use axum::routing::get; +use axum::Router; +use image::imageops::FilterType; +use image::ImageFormat; +use std::io::Cursor; + +use crate::error::AppError; +use crate::{db, AppState}; + +pub fn router() -> Router { + Router::new().route( + "/api/v1/can/0/asset/{hash}/thumb/{max_width}/{max_height}", + get(get_thumb), + ) +} + +/// Static fallback SVG icon for non-image assets. +const FALLBACK_SVG: &str = r##" + + ? +"##; + +async fn get_thumb( + State(state): State, + Path((hash, max_width, max_height)): Path<(String, u32, u32)>, +) -> Result { + let asset = { + let conn = state.db.lock().unwrap(); + db::get_asset_by_hash(&conn, &hash)? + .ok_or_else(|| AppError::NotFound(format!("Asset not found: {}", hash)))? + }; + + // Check if MIME type is an image we can resize + let is_image = asset.mime_type.starts_with("image/") + && !asset.mime_type.contains("svg"); + + if !is_image { + // Return fallback SVG + return Ok(( + [(header::CONTENT_TYPE, "image/svg+xml".to_string())], + FALLBACK_SVG.to_string(), + ) + .into_response()); + } + + // Check thumbnail cache + if state.config.enable_thumbnail_cache { + let cache_name = format!("{}_{}x{}.jpg", hash, max_width, max_height); + let cache_path = state.config.thumbs_dir().join(&cache_name); + if cache_path.exists() { + let data = tokio::fs::read(&cache_path).await?; + return Ok(( + [(header::CONTENT_TYPE, "image/jpeg".to_string())], + Body::from(data), + ) + .into_response()); + } + } + + // Read original file + let file_path = state.config.storage_root.join(&asset.actual_filename); + let data = tokio::fs::read(&file_path).await.map_err(|e| { + AppError::Internal(format!("Failed to read file: {}", e)) + })?; + + // Decode and resize + let img = image::load_from_memory(&data) + .map_err(|e| AppError::Internal(format!("Failed to decode image: {}", e)))?; + + let thumb = img.resize(max_width, max_height, FilterType::Lanczos3); + + // Encode as JPEG + let mut buf = Cursor::new(Vec::new()); + thumb + .write_to(&mut buf, ImageFormat::Jpeg) + .map_err(|e| AppError::Internal(format!("Failed to encode thumbnail: {}", e)))?; + let jpeg_bytes = buf.into_inner(); + + // Cache the thumbnail + if state.config.enable_thumbnail_cache { + let cache_name = format!("{}_{}x{}.jpg", hash, max_width, max_height); + let cache_path = state.config.thumbs_dir().join(&cache_name); + if let Err(e) = tokio::fs::write(&cache_path, &jpeg_bytes).await { + tracing::warn!("Failed to cache thumbnail: {}", e); + } + } + + Ok(( + [(header::CONTENT_TYPE, "image/jpeg".to_string())], + Body::from(jpeg_bytes), + ) + .into_response()) +} diff --git a/src/storage.rs b/src/storage.rs new file mode 100644 index 0000000..b819387 --- /dev/null +++ b/src/storage.rs @@ -0,0 +1,192 @@ +use std::path::{Path, PathBuf}; + +/// Build the physical filename per the spec: +/// `{timestamp}_{sha256}_{truncated_tags}.{extension}` +pub fn build_filename( + timestamp: i64, + hash: &str, + tags: &[String], + mime_type: &str, +) -> String { + let extension = mime_to_extension(mime_type); + + let base = format!("{}_{}", timestamp, hash); + + if tags.is_empty() { + return format!("{}.{}", base, extension); + } + + // Sanitize tags: strip non-alphanumeric, join with underscore + let sanitized_tags: Vec = tags + .iter() + .map(|t| t.chars().filter(|c| c.is_alphanumeric()).collect::()) + .filter(|t| !t.is_empty()) + .collect(); + + if sanitized_tags.is_empty() { + return format!("{}.{}", base, extension); + } + + let tag_part = sanitized_tags.join("_"); + + // Truncate to keep total filename under ~200 chars (safely under 255) + let max_tag_len = 200usize.saturating_sub(base.len() + extension.len() + 2); // 2 for _ and . + let truncated = if tag_part.len() > max_tag_len { + &tag_part[..max_tag_len] + } else { + &tag_part + }; + + format!("{}_{}. {}", base, truncated, extension) + .replace(". ", ".") +} + +/// Derive file extension from MIME type. +pub fn mime_to_extension(mime: &str) -> &str { + match mime { + "application/pdf" => "pdf", + "application/json" => "json", + "application/xml" | "text/xml" => "xml", + "application/zip" => "zip", + "application/gzip" => "gz", + "application/octet-stream" => "bin", + "text/plain" => "txt", + "text/html" => "html", + "text/css" => "css", + "text/csv" => "csv", + "text/javascript" | "application/javascript" => "js", + "image/jpeg" => "jpg", + "image/png" => "png", + "image/gif" => "gif", + "image/webp" => "webp", + "image/svg+xml" => "svg", + "image/bmp" => "bmp", + "audio/mpeg" => "mp3", + "audio/wav" => "wav", + "audio/ogg" => "ogg", + "video/mp4" => "mp4", + "video/webm" => "webm", + _ => { + // Try to extract from mime_guess + mime_guess::get_mime_extensions_str(mime) + .and_then(|exts| exts.first().copied()) + .unwrap_or("bin") + } + } +} + +/// Write asset bytes to the storage root. Returns the full path. +pub fn write_asset(root: &Path, filename: &str, data: &[u8]) -> std::io::Result { + let path = root.join(filename); + std::fs::write(&path, data)?; + Ok(path) +} + +/// Read asset bytes from the storage root. +pub fn read_asset(root: &Path, filename: &str) -> std::io::Result> { + let path = root.join(filename); + std::fs::read(path) +} + +/// Move an asset file to the .trash directory. +pub fn trash_asset_file(root: &Path, filename: &str) -> std::io::Result<()> { + let src = root.join(filename); + let trash_dir = root.join(".trash"); + std::fs::create_dir_all(&trash_dir)?; + let dst = trash_dir.join(filename); + std::fs::rename(src, dst)?; + Ok(()) +} + +/// Parse a physical filename to extract the hash component. +/// Format: `{timestamp}_{sha256}_{tags}.{ext}` or `{timestamp}_{sha256}.{ext}` +pub fn parse_hash_from_filename(filename: &str) -> Option { + // Remove extension + let stem = filename.rsplit_once('.')?.0; + // Split by underscore: first part is timestamp, second is hash (64 hex chars) + let parts: Vec<&str> = stem.splitn(3, '_').collect(); + if parts.len() >= 2 && parts[1].len() == 64 { + Some(parts[1].to_string()) + } else { + None + } +} + +/// Parse a physical filename to extract the timestamp component. +pub fn parse_timestamp_from_filename(filename: &str) -> Option { + let stem = filename.rsplit_once('.')?.0; + let ts_str = stem.split('_').next()?; + ts_str.parse().ok() +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::TempDir; + + #[test] + fn test_build_filename_no_tags() { + let name = build_filename(1773014400123, "a3b2c4d5e6f7", &[], "application/pdf"); + assert_eq!(name, "1773014400123_a3b2c4d5e6f7.pdf"); + } + + #[test] + fn test_build_filename_with_tags() { + let tags = vec!["photo".to_string(), "vacation".to_string()]; + let name = build_filename(1773014400123, "a3b2c4d5e6f7", &tags, "image/jpeg"); + assert_eq!(name, "1773014400123_a3b2c4d5e6f7_photo_vacation.jpg"); + } + + #[test] + fn test_build_filename_strips_special_chars_from_tags() { + let tags = vec!["hello world!".to_string(), "test@123".to_string()]; + let name = build_filename(100, "abc", &tags, "text/plain"); + assert_eq!(name, "100_abc_helloworld_test123.txt"); + } + + #[test] + fn test_mime_to_extension() { + assert_eq!(mime_to_extension("image/png"), "png"); + assert_eq!(mime_to_extension("application/pdf"), "pdf"); + assert_eq!(mime_to_extension("text/plain"), "txt"); + assert_eq!(mime_to_extension("unknown/thing"), "bin"); + } + + #[test] + fn test_write_and_read_asset() { + let dir = TempDir::new().unwrap(); + let data = b"hello world"; + let path = write_asset(dir.path(), "test_file.txt", data).unwrap(); + assert!(path.exists()); + + let read_back = read_asset(dir.path(), "test_file.txt").unwrap(); + assert_eq!(read_back, data); + } + + #[test] + fn test_trash_asset_file() { + let dir = TempDir::new().unwrap(); + write_asset(dir.path(), "to_trash.txt", b"bye").unwrap(); + + trash_asset_file(dir.path(), "to_trash.txt").unwrap(); + assert!(!dir.path().join("to_trash.txt").exists()); + assert!(dir.path().join(".trash").join("to_trash.txt").exists()); + } + + #[test] + fn test_parse_hash_from_filename() { + let hash_64 = "a".repeat(64); + let filename = format!("1773014400123_{}.pdf", hash_64); + assert_eq!(parse_hash_from_filename(&filename), Some(hash_64.clone())); + + let filename_tags = format!("1773014400123_{}_photo_vacation.jpg", hash_64); + assert_eq!(parse_hash_from_filename(&filename_tags), Some(hash_64)); + } + + #[test] + fn test_parse_timestamp_from_filename() { + let hash_64 = "b".repeat(64); + let filename = format!("1773014400123_{}.pdf", hash_64); + assert_eq!(parse_timestamp_from_filename(&filename), Some(1773014400123)); + } +} diff --git a/src/verifier.rs b/src/verifier.rs new file mode 100644 index 0000000..3c91ab2 --- /dev/null +++ b/src/verifier.rs @@ -0,0 +1,308 @@ +use notify::{Event, EventKind, RecursiveMode, Watcher}; +use std::path::PathBuf; +// Mutex used via Db type alias +use std::time::Duration; +use tokio::sync::mpsc; + +use crate::config::Config; +use crate::db::{self, Db}; +use crate::hash::compute_hash; +use crate::models::FileAttributes; +use crate::storage::{parse_hash_from_filename, parse_timestamp_from_filename}; +use crate::xattr; + +/// Start the background verifier subsystem. +/// - Runs an initial full scrub +/// - Watches for filesystem changes +/// - Runs periodic scrubs +pub fn start(config: Config, db: Db) { + let config2 = config.clone(); + let db2 = db.clone(); + + // Initial scrub + let config3 = config.clone(); + let db3 = db.clone(); + tokio::spawn(async move { + tracing::info!("Verifier: starting initial scrub..."); + if let Err(e) = run_scrub(&config3, &db3).await { + tracing::error!("Verifier: initial scrub failed: {}", e); + } + tracing::info!("Verifier: initial scrub complete"); + }); + + // Periodic scrub + let interval_hours = config.verify_interval_hours; + tokio::spawn(async move { + let mut interval = + tokio::time::interval(Duration::from_secs(interval_hours * 3600)); + interval.tick().await; // Skip first immediate tick + loop { + interval.tick().await; + tracing::info!("Verifier: starting periodic scrub..."); + if let Err(e) = run_scrub(&config2, &db2).await { + tracing::error!("Verifier: periodic scrub failed: {}", e); + } + tracing::info!("Verifier: periodic scrub complete"); + } + }); + + // Filesystem watcher + tokio::spawn(async move { + if let Err(e) = run_watcher(config3_for_watcher(config), db).await { + tracing::error!("Verifier: filesystem watcher failed: {}", e); + } + }); +} + +fn config3_for_watcher(config: Config) -> Config { + config +} + +async fn run_watcher(config: Config, db: Db) -> anyhow::Result<()> { + let (tx, mut rx) = mpsc::channel::(100); + let storage_root = config.storage_root.clone(); + + // Spawn blocking watcher in a separate thread + let watcher_root = storage_root.clone(); + std::thread::spawn(move || { + let tx_clone = tx.clone(); + let mut watcher = notify::recommended_watcher(move |res: Result| { + if let Ok(event) = res { + match event.kind { + EventKind::Modify(_) | EventKind::Create(_) => { + for path in event.paths { + // Ignore hidden dirs (.trash, .thumbs, .can.db) + let filename = path + .file_name() + .and_then(|f| f.to_str()) + .unwrap_or(""); + if filename.starts_with('.') { + continue; + } + let _ = tx_clone.blocking_send(path); + } + } + _ => {} + } + } + }) + .expect("Failed to create filesystem watcher"); + + watcher + .watch(&watcher_root, RecursiveMode::NonRecursive) + .expect("Failed to watch storage root"); + + // Keep watcher alive + loop { + std::thread::sleep(Duration::from_secs(3600)); + } + }); + + // Process file change events + while let Some(path) = rx.recv().await { + let filename = match path.file_name().and_then(|f| f.to_str()) { + Some(f) => f.to_string(), + None => continue, + }; + + tracing::debug!("Verifier: checking modified file: {}", filename); + if let Err(e) = verify_single_file(&config, &db, &filename).await { + tracing::warn!("Verifier: error checking {}: {}", filename, e); + } + } + + Ok(()) +} + +/// Run a full scrub: verify every active asset's hash. +async fn run_scrub(config: &Config, db: &Db) -> anyhow::Result<()> { + let assets = { + let conn = db.lock().unwrap(); + db::get_all_active_assets(&conn)? + }; + + let mut corrupted_count = 0u32; + + for asset in &assets { + let file_path = config.storage_root.join(&asset.actual_filename); + if !file_path.exists() { + tracing::warn!( + "Verifier: file missing for asset {}: {}", + asset.hash, + asset.actual_filename + ); + continue; + } + + match tokio::fs::read(&file_path).await { + Ok(content) => { + let expected_hash = compute_hash(asset.timestamp, &content); + if expected_hash != asset.hash { + tracing::warn!( + "Verifier: CORRUPTION detected for {} (expected {}, got {})", + asset.actual_filename, + asset.hash, + expected_hash + ); + let conn = db.lock().unwrap(); + db::flag_corrupted(&conn, &asset.hash, true)?; + corrupted_count += 1; + } else if asset.is_corrupted { + // File was previously marked corrupted but now passes - clear flag + let conn = db.lock().unwrap(); + db::flag_corrupted(&conn, &asset.hash, false)?; + tracing::info!( + "Verifier: asset {} is no longer corrupted", + asset.hash + ); + } + } + Err(e) => { + tracing::warn!( + "Verifier: cannot read {}: {}", + asset.actual_filename, + e + ); + } + } + } + + if corrupted_count > 0 { + tracing::warn!( + "Verifier: scrub found {} corrupted assets out of {}", + corrupted_count, + assets.len() + ); + } else { + tracing::info!( + "Verifier: scrub passed for all {} assets", + assets.len() + ); + } + + // Sync DB metadata → OS-level file attributes + let mut attrs_synced = 0u32; + for asset in &assets { + let file_path = config.storage_root.join(&asset.actual_filename); + if !file_path.exists() { + continue; + } + + // Build expected attributes from DB + let tags = { + let conn = db.lock().unwrap(); + db::get_asset_tags(&conn, asset.id).unwrap_or_default() + }; + + let expected = FileAttributes { + mime_type: Some(asset.mime_type.clone()), + application: asset.application.clone(), + user: asset.user_identity.clone(), + tags: if tags.is_empty() { + None + } else { + Some(tags.join(",")) + }, + description: asset.description.clone(), + human_filename: asset.human_filename.clone(), + human_path: asset.human_path.clone(), + }; + + // Read current file attributes + let current = xattr::read_attributes(&file_path).unwrap_or_default(); + + if current != expected { + if let Err(e) = xattr::write_attributes(&file_path, &expected) { + tracing::warn!( + "Verifier: failed to sync attributes for {}: {}", + asset.actual_filename, + e + ); + } else { + attrs_synced += 1; + } + } + } + + if attrs_synced > 0 { + tracing::info!( + "Verifier: synced file attributes for {} assets", + attrs_synced + ); + } + + // Backfill missing file sizes + let mut sizes_backfilled = 0u32; + for asset in &assets { + if asset.size > 0 { + continue; + } + let file_path = config.storage_root.join(&asset.actual_filename); + if !file_path.exists() { + continue; + } + match file_path.metadata() { + Ok(meta) => { + let len = meta.len() as i64; + if len > 0 { + let conn = db.lock().unwrap(); + if let Err(e) = db::update_asset_size(&conn, &asset.hash, len) { + tracing::warn!( + "Verifier: failed to backfill size for {}: {}", + asset.hash, + e + ); + } else { + sizes_backfilled += 1; + } + } + } + Err(e) => { + tracing::warn!( + "Verifier: cannot stat {}: {}", + asset.actual_filename, + e + ); + } + } + } + if sizes_backfilled > 0 { + tracing::info!( + "Verifier: backfilled file sizes for {} assets", + sizes_backfilled + ); + } + + Ok(()) +} + +/// Verify a single file by its physical filename. +async fn verify_single_file( + config: &Config, + db: &Db, + filename: &str, +) -> anyhow::Result<()> { + let hash = match parse_hash_from_filename(filename) { + Some(h) => h, + None => return Ok(()), // Not a CAN-managed file + }; + let timestamp = match parse_timestamp_from_filename(filename) { + Some(t) => t, + None => return Ok(()), + }; + + let file_path = config.storage_root.join(filename); + let content = tokio::fs::read(&file_path).await?; + let computed = compute_hash(timestamp, &content); + + if computed != hash { + tracing::warn!( + "Verifier: corruption detected on change for {}", + filename + ); + let conn = db.lock().unwrap(); + db::flag_corrupted(&conn, &hash, true)?; + } + + Ok(()) +} diff --git a/src/xattr.rs b/src/xattr.rs new file mode 100644 index 0000000..62490c0 --- /dev/null +++ b/src/xattr.rs @@ -0,0 +1,187 @@ +use crate::models::FileAttributes; +use std::path::Path; + +/// Write CAN metadata as OS-level file attributes. +/// - Unix/macOS: Extended Attributes (xattr) +/// - Windows: NTFS Alternate Data Streams +pub fn write_attributes(path: &Path, attrs: &FileAttributes) -> std::io::Result<()> { + #[cfg(unix)] + { + write_xattr(path, attrs) + } + #[cfg(windows)] + { + write_ntfs_ads(path, attrs) + } +} + +/// Read CAN metadata from OS-level file attributes. +pub fn read_attributes(path: &Path) -> std::io::Result { + #[cfg(unix)] + { + read_xattr(path) + } + #[cfg(windows)] + { + read_ntfs_ads(path) + } +} + +// ── Unix implementation using xattr crate ── + +#[cfg(unix)] +fn write_xattr(path: &Path, attrs: &FileAttributes) -> std::io::Result<()> { + use xattr::FileExt; + let file = std::fs::File::open(path)?; + + if let Some(ref v) = attrs.mime_type { + file.set_xattr("user.can.mime_type", v.as_bytes())?; + } + if let Some(ref v) = attrs.application { + file.set_xattr("user.can.application", v.as_bytes())?; + } + if let Some(ref v) = attrs.user { + file.set_xattr("user.can.user", v.as_bytes())?; + } + if let Some(ref v) = attrs.tags { + file.set_xattr("user.can.tags", v.as_bytes())?; + } + if let Some(ref v) = attrs.description { + file.set_xattr("user.can.description", v.as_bytes())?; + } + if let Some(ref v) = attrs.human_filename { + file.set_xattr("user.can.human_filename", v.as_bytes())?; + } + if let Some(ref v) = attrs.human_path { + file.set_xattr("user.can.human_path", v.as_bytes())?; + } + Ok(()) +} + +#[cfg(unix)] +fn read_xattr(path: &Path) -> std::io::Result { + use xattr::FileExt; + let file = std::fs::File::open(path)?; + + let read_attr = |name: &str| -> Option { + file.get_xattr(name) + .ok() + .flatten() + .and_then(|bytes| String::from_utf8(bytes).ok()) + }; + + Ok(FileAttributes { + mime_type: read_attr("user.can.mime_type"), + application: read_attr("user.can.application"), + user: read_attr("user.can.user"), + tags: read_attr("user.can.tags"), + description: read_attr("user.can.description"), + human_filename: read_attr("user.can.human_filename"), + human_path: read_attr("user.can.human_path"), + }) +} + +// ── Windows implementation using NTFS Alternate Data Streams ── + +#[cfg(windows)] +fn write_ntfs_ads(path: &Path, attrs: &FileAttributes) -> std::io::Result<()> { + let base = path.to_string_lossy(); + + if let Some(ref v) = attrs.mime_type { + std::fs::write(format!("{}:can.mime_type", base), v)?; + } + if let Some(ref v) = attrs.application { + std::fs::write(format!("{}:can.application", base), v)?; + } + if let Some(ref v) = attrs.user { + std::fs::write(format!("{}:can.user", base), v)?; + } + if let Some(ref v) = attrs.tags { + std::fs::write(format!("{}:can.tags", base), v)?; + } + if let Some(ref v) = attrs.description { + std::fs::write(format!("{}:can.description", base), v)?; + } + if let Some(ref v) = attrs.human_filename { + std::fs::write(format!("{}:can.human_filename", base), v)?; + } + if let Some(ref v) = attrs.human_path { + std::fs::write(format!("{}:can.human_path", base), v)?; + } + Ok(()) +} + +#[cfg(windows)] +fn read_ntfs_ads(path: &Path) -> std::io::Result { + let base = path.to_string_lossy(); + + let read_stream = |name: &str| -> Option { + std::fs::read_to_string(format!("{}:{}", base, name)).ok() + }; + + Ok(FileAttributes { + mime_type: read_stream("can.mime_type"), + application: read_stream("can.application"), + user: read_stream("can.user"), + tags: read_stream("can.tags"), + description: read_stream("can.description"), + human_filename: read_stream("can.human_filename"), + human_path: read_stream("can.human_path"), + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::NamedTempFile; + + #[test] + fn test_write_and_read_attributes() { + let file = NamedTempFile::new().unwrap(); + std::fs::write(file.path(), b"test content").unwrap(); + + let attrs = FileAttributes { + mime_type: Some("image/jpeg".to_string()), + application: Some("TestApp".to_string()), + user: Some("jason".to_string()), + tags: Some("photo,vacation,2024".to_string()), + description: Some("A test file".to_string()), + human_filename: Some("my_photo.jpg".to_string()), + human_path: Some("/photos/trip/".to_string()), + }; + + write_attributes(file.path(), &attrs).unwrap(); + let read_back = read_attributes(file.path()).unwrap(); + + assert_eq!(read_back.mime_type, Some("image/jpeg".to_string())); + assert_eq!(read_back.application, Some("TestApp".to_string())); + assert_eq!(read_back.user, Some("jason".to_string())); + assert_eq!(read_back.tags, Some("photo,vacation,2024".to_string())); + assert_eq!(read_back.description, Some("A test file".to_string())); + assert_eq!(read_back.human_filename, Some("my_photo.jpg".to_string())); + assert_eq!(read_back.human_path, Some("/photos/trip/".to_string())); + } + + #[test] + fn test_partial_attributes() { + let file = NamedTempFile::new().unwrap(); + std::fs::write(file.path(), b"data").unwrap(); + + let attrs = FileAttributes { + mime_type: None, + application: Some("App".to_string()), + user: None, + tags: None, + description: None, + human_filename: None, + human_path: None, + }; + + write_attributes(file.path(), &attrs).unwrap(); + let read_back = read_attributes(file.path()).unwrap(); + + assert_eq!(read_back.application, Some("App".to_string())); + assert_eq!(read_back.user, None); + assert_eq!(read_back.tags, None); + } +} diff --git a/tests/integration.rs b/tests/integration.rs new file mode 100644 index 0000000..b3fd787 --- /dev/null +++ b/tests/integration.rs @@ -0,0 +1,741 @@ +use reqwest::multipart; +use std::sync::Arc; +use tempfile::TempDir; +use tokio::net::TcpListener; + +// We need to import the binary crate's internals. +// Since integration tests can't access `mod` items directly, we'll spin up +// the server using the library-like approach by duplicating setup logic. +// A cleaner approach is to test through HTTP. + +/// Helper: spin up a test server and return its base URL + temp dir handle. +async fn spawn_test_server() -> (String, TempDir) { + let tmp = TempDir::new().unwrap(); + let storage_root = tmp.path().to_path_buf(); + + // Create config.yaml in tempdir + let config_content = format!( + r#"storage_root: "{}" +admin_token: "test_token" +enable_thumbnail_cache: true +rebuild_error_threshold: 50 +verify_interval_hours: 999 +"#, + storage_root.to_string_lossy().replace('\\', "/") + ); + + let config_path = tmp.path().join("config.yaml"); + std::fs::write(&config_path, &config_content).unwrap(); + + // Load config + let config: can_service::config::Config = + serde_yaml::from_str(&config_content).unwrap(); + config.ensure_dirs().unwrap(); + + // Open DB + let db = can_service::db::open(&config.db_path()).unwrap(); + let config = Arc::new(config); + + let state = can_service::AppState { + config: config.clone(), + db, + }; + + // Build router + let app = axum::Router::new() + .merge(can_service::routes::router()) + .with_state(state); + + // Bind to random port + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + let base_url = format!("http://{}", addr); + + tokio::spawn(async move { + axum::serve(listener, app).await.unwrap(); + }); + + // Give the server a moment to start + tokio::time::sleep(std::time::Duration::from_millis(50)).await; + + (base_url, tmp) +} + +#[tokio::test] +async fn test_ingest_and_retrieve_metadata() { + let (base_url, _tmp) = spawn_test_server().await; + let client = reqwest::Client::new(); + + // Ingest a file + let file_part = multipart::Part::bytes(b"hello world".to_vec()) + .file_name("hello.txt") + .mime_str("text/plain") + .unwrap(); + + let form = multipart::Form::new() + .part("file", file_part) + .text("application", "TestApp") + .text("user", "jason") + .text("tags", "greeting,test") + .text("description", "A test file") + .text("human_file_name", "hello.txt") + .text("human_readable_path", "/docs/"); + + let resp = client + .post(format!("{}/api/v1/can/0/ingest", base_url)) + .multipart(form) + .send() + .await + .unwrap(); + + assert_eq!(resp.status(), 200); + let body: serde_json::Value = resp.json().await.unwrap(); + assert_eq!(body["status"], "success"); + + let hash = body["data"]["hash"].as_str().unwrap().to_string(); + let timestamp = body["data"]["timestamp"].as_i64().unwrap(); + assert!(!hash.is_empty()); + assert!(timestamp > 0); + + // Retrieve metadata + let resp = client + .get(format!("{}/api/v1/can/0/asset/{}/meta", base_url, hash)) + .send() + .await + .unwrap(); + + assert_eq!(resp.status(), 200); + let body: serde_json::Value = resp.json().await.unwrap(); + assert_eq!(body["status"], "success"); + assert_eq!(body["data"]["hash"], hash); + assert_eq!(body["data"]["mime_type"], "text/plain"); + assert_eq!(body["data"]["application"], "TestApp"); + assert_eq!(body["data"]["user"], "jason"); + assert_eq!(body["data"]["description"], "A test file"); + assert_eq!(body["data"]["human_filename"], "hello.txt"); + assert_eq!(body["data"]["human_path"], "/docs/"); + + let tags = body["data"]["tags"].as_array().unwrap(); + assert!(tags.contains(&serde_json::json!("greeting"))); + assert!(tags.contains(&serde_json::json!("test"))); +} + +#[tokio::test] +async fn test_retrieve_physical_asset() { + let (base_url, _tmp) = spawn_test_server().await; + let client = reqwest::Client::new(); + + let file_content = b"binary content here"; + let file_part = multipart::Part::bytes(file_content.to_vec()) + .file_name("data.bin") + .mime_str("application/octet-stream") + .unwrap(); + + let form = multipart::Form::new().part("file", file_part); + + let resp = client + .post(format!("{}/api/v1/can/0/ingest", base_url)) + .multipart(form) + .send() + .await + .unwrap(); + + let body: serde_json::Value = resp.json().await.unwrap(); + let hash = body["data"]["hash"].as_str().unwrap(); + + // Download the asset + let resp = client + .get(format!("{}/api/v1/can/0/asset/{}", base_url, hash)) + .send() + .await + .unwrap(); + + assert_eq!(resp.status(), 200); + let downloaded = resp.bytes().await.unwrap(); + assert_eq!(downloaded.as_ref(), file_content); +} + +#[tokio::test] +async fn test_patch_metadata() { + let (base_url, _tmp) = spawn_test_server().await; + let client = reqwest::Client::new(); + + // Ingest + let file_part = multipart::Part::bytes(b"patch me".to_vec()) + .file_name("patch.txt") + .mime_str("text/plain") + .unwrap(); + + let form = multipart::Form::new() + .part("file", file_part) + .text("tags", "original") + .text("description", "original desc"); + + let resp = client + .post(format!("{}/api/v1/can/0/ingest", base_url)) + .multipart(form) + .send() + .await + .unwrap(); + + let body: serde_json::Value = resp.json().await.unwrap(); + let hash = body["data"]["hash"].as_str().unwrap().to_string(); + + // Patch + let patch_body = serde_json::json!({ + "tags": ["updated", "new_tag"], + "description": "updated description" + }); + + let resp = client + .patch(format!("{}/api/v1/can/0/asset/{}", base_url, hash)) + .json(&patch_body) + .send() + .await + .unwrap(); + + assert_eq!(resp.status(), 200); + + // Verify + let resp = client + .get(format!("{}/api/v1/can/0/asset/{}/meta", base_url, hash)) + .send() + .await + .unwrap(); + + let body: serde_json::Value = resp.json().await.unwrap(); + assert_eq!(body["data"]["description"], "updated description"); + let tags = body["data"]["tags"].as_array().unwrap(); + assert!(tags.contains(&serde_json::json!("updated"))); + assert!(tags.contains(&serde_json::json!("new_tag"))); + assert!(!tags.contains(&serde_json::json!("original"))); +} + +#[tokio::test] +async fn test_list_assets_pagination() { + let (base_url, _tmp) = spawn_test_server().await; + let client = reqwest::Client::new(); + + // Ingest 5 files + for i in 0..5 { + let content = format!("file content {}", i); + let file_part = multipart::Part::bytes(content.into_bytes()) + .file_name(format!("file_{}.txt", i)) + .mime_str("text/plain") + .unwrap(); + + let form = multipart::Form::new() + .part("file", file_part) + .text("application", "ListTest"); + + client + .post(format!("{}/api/v1/can/0/ingest", base_url)) + .multipart(form) + .send() + .await + .unwrap(); + + // Small delay so timestamps differ + tokio::time::sleep(std::time::Duration::from_millis(10)).await; + } + + // List with limit=2 + let resp = client + .get(format!( + "{}/api/v1/can/0/list?limit=2&offset=0", + base_url + )) + .send() + .await + .unwrap(); + + assert_eq!(resp.status(), 200); + let body: serde_json::Value = resp.json().await.unwrap(); + let items = body["data"]["items"].as_array().unwrap(); + assert_eq!(items.len(), 2); + assert_eq!(body["data"]["pagination"]["total"], 5); + assert_eq!(body["data"]["pagination"]["limit"], 2); + + // List with offset=3 + let resp = client + .get(format!( + "{}/api/v1/can/0/list?limit=10&offset=3", + base_url + )) + .send() + .await + .unwrap(); + + let body: serde_json::Value = resp.json().await.unwrap(); + let items = body["data"]["items"].as_array().unwrap(); + assert_eq!(items.len(), 2); // 5 total, offset 3 = 2 remaining + + // List with application filter + let resp = client + .get(format!( + "{}/api/v1/can/0/list?application=ListTest", + base_url + )) + .send() + .await + .unwrap(); + + let body: serde_json::Value = resp.json().await.unwrap(); + assert_eq!(body["data"]["pagination"]["total"], 5); +} + +#[tokio::test] +async fn test_search_assets() { + let (base_url, _tmp) = spawn_test_server().await; + let client = reqwest::Client::new(); + + // Ingest files with different tags and metadata + let ingest = |name: &str, tags: &str, mime: &str| { + let base = base_url.clone(); + let client = client.clone(); + let name = name.to_string(); + let tags = tags.to_string(); + let mime = mime.to_string(); + async move { + let file_part = multipart::Part::bytes(format!("content of {}", name).into_bytes()) + .file_name(name.clone()) + .mime_str(&mime) + .unwrap(); + + let form = multipart::Form::new() + .part("file", file_part) + .text("tags", tags) + .text("user", "tester"); + + let resp = client + .post(format!("{}/api/v1/can/0/ingest", base)) + .multipart(form) + .send() + .await + .unwrap(); + + let body: serde_json::Value = resp.json().await.unwrap(); + body["data"]["hash"].as_str().unwrap().to_string() + } + }; + + let _h1 = ingest("photo.jpg", "nature,landscape", "image/jpeg").await; + tokio::time::sleep(std::time::Duration::from_millis(10)).await; + let _h2 = ingest("doc.pdf", "work,report", "application/pdf").await; + tokio::time::sleep(std::time::Duration::from_millis(10)).await; + let _h3 = ingest("nature.png", "nature,macro", "image/png").await; + + // Search by tags + let resp = client + .get(format!( + "{}/api/v1/can/0/search?tags=nature", + base_url + )) + .send() + .await + .unwrap(); + + let body: serde_json::Value = resp.json().await.unwrap(); + assert_eq!(body["data"]["pagination"]["total"], 2); + + // Search by mime_type + let resp = client + .get(format!( + "{}/api/v1/can/0/search?mime_type=application/pdf", + base_url + )) + .send() + .await + .unwrap(); + + let body: serde_json::Value = resp.json().await.unwrap(); + assert_eq!(body["data"]["pagination"]["total"], 1); + + // Search by user + let resp = client + .get(format!( + "{}/api/v1/can/0/search?user=tester", + base_url + )) + .send() + .await + .unwrap(); + + let body: serde_json::Value = resp.json().await.unwrap(); + assert_eq!(body["data"]["pagination"]["total"], 3); +} + +#[tokio::test] +async fn test_asset_not_found() { + let (base_url, _tmp) = spawn_test_server().await; + let client = reqwest::Client::new(); + + let resp = client + .get(format!( + "{}/api/v1/can/0/asset/nonexistent_hash", + base_url + )) + .send() + .await + .unwrap(); + + assert_eq!(resp.status(), 404); + + let resp = client + .get(format!( + "{}/api/v1/can/0/asset/nonexistent_hash/meta", + base_url + )) + .send() + .await + .unwrap(); + + assert_eq!(resp.status(), 404); +} + +#[tokio::test] +async fn test_thumbnail_fallback_svg() { + let (base_url, _tmp) = spawn_test_server().await; + let client = reqwest::Client::new(); + + // Ingest a non-image file + let file_part = multipart::Part::bytes(b"not an image".to_vec()) + .file_name("doc.txt") + .mime_str("text/plain") + .unwrap(); + + let form = multipart::Form::new().part("file", file_part); + + let resp = client + .post(format!("{}/api/v1/can/0/ingest", base_url)) + .multipart(form) + .send() + .await + .unwrap(); + + let body: serde_json::Value = resp.json().await.unwrap(); + let hash = body["data"]["hash"].as_str().unwrap(); + + // Request thumbnail - should get SVG fallback + let resp = client + .get(format!( + "{}/api/v1/can/0/asset/{}/thumb/128/128", + base_url, hash + )) + .send() + .await + .unwrap(); + + assert_eq!(resp.status(), 200); + let content_type = resp.headers().get("content-type").unwrap().to_str().unwrap(); + assert!(content_type.contains("svg")); +} + +#[tokio::test] +async fn test_list_order() { + let (base_url, _tmp) = spawn_test_server().await; + let client = reqwest::Client::new(); + + // Ingest 3 files with delays + for i in 0..3 { + let file_part = multipart::Part::bytes(format!("order test {}", i).into_bytes()) + .file_name(format!("order_{}.txt", i)) + .mime_str("text/plain") + .unwrap(); + + let form = multipart::Form::new().part("file", file_part); + + client + .post(format!("{}/api/v1/can/0/ingest", base_url)) + .multipart(form) + .send() + .await + .unwrap(); + + tokio::time::sleep(std::time::Duration::from_millis(15)).await; + } + + // List descending (default) + let resp = client + .get(format!("{}/api/v1/can/0/list?order=desc", base_url)) + .send() + .await + .unwrap(); + + let body: serde_json::Value = resp.json().await.unwrap(); + let items = body["data"]["items"].as_array().unwrap(); + assert_eq!(items.len(), 3); + let ts0 = items[0]["timestamp"].as_i64().unwrap(); + let ts1 = items[1]["timestamp"].as_i64().unwrap(); + let ts2 = items[2]["timestamp"].as_i64().unwrap(); + assert!(ts0 > ts1); + assert!(ts1 > ts2); + + // List ascending + let resp = client + .get(format!("{}/api/v1/can/0/list?order=asc", base_url)) + .send() + .await + .unwrap(); + + let body: serde_json::Value = resp.json().await.unwrap(); + let items = body["data"]["items"].as_array().unwrap(); + let ts0 = items[0]["timestamp"].as_i64().unwrap(); + let ts1 = items[1]["timestamp"].as_i64().unwrap(); + assert!(ts0 < ts1); +} + +// ── JSON data ingest tests ────────────────────────────────────────────── + +#[tokio::test] +async fn test_data_ingest_minimal() { + let (base_url, _tmp) = spawn_test_server().await; + let client = reqwest::Client::new(); + + // Minimal call: just data + let resp = client + .post(format!("{}/api/v1/can/0/ingest/data", base_url)) + .json(&serde_json::json!({ + "data": { "key": "value", "count": 42 } + })) + .send() + .await + .unwrap(); + + assert_eq!(resp.status(), 200); + let body: serde_json::Value = resp.json().await.unwrap(); + assert_eq!(body["status"], "success"); + + let hash = body["data"]["hash"].as_str().unwrap(); + let filename = body["data"]["filename"].as_str().unwrap(); + assert!(!hash.is_empty()); + assert!(filename.ends_with(".json")); + + // Retrieve and verify it's stored as pretty JSON + let resp = client + .get(format!("{}/api/v1/can/0/asset/{}", base_url, hash)) + .send() + .await + .unwrap(); + + assert_eq!(resp.status(), 200); + let stored: serde_json::Value = resp.json().await.unwrap(); + assert_eq!(stored["key"], "value"); + assert_eq!(stored["count"], 42); + + // Verify metadata defaults to application/json + let resp = client + .get(format!("{}/api/v1/can/0/asset/{}/meta", base_url, hash)) + .send() + .await + .unwrap(); + + let meta: serde_json::Value = resp.json().await.unwrap(); + assert_eq!(meta["data"]["mime_type"], "application/json"); +} + +#[tokio::test] +async fn test_data_ingest_with_all_metadata() { + let (base_url, _tmp) = spawn_test_server().await; + let client = reqwest::Client::new(); + + let resp = client + .post(format!("{}/api/v1/can/0/ingest/data", base_url)) + .json(&serde_json::json!({ + "data": { + "agent_id": "planner-v2", + "session": "abc-123", + "output": ["step1", "step2", "step3"] + }, + "application": "AgentOrchestrator", + "user": "agent_planner", + "tags": "agent,plan,session", + "description": "Planning agent output for session abc-123", + "human_file_name": "plan_output.json", + "human_readable_path": "/agents/planner/" + })) + .send() + .await + .unwrap(); + + assert_eq!(resp.status(), 200); + let body: serde_json::Value = resp.json().await.unwrap(); + let hash = body["data"]["hash"].as_str().unwrap(); + + // Verify all metadata persisted + let resp = client + .get(format!("{}/api/v1/can/0/asset/{}/meta", base_url, hash)) + .send() + .await + .unwrap(); + + let meta: serde_json::Value = resp.json().await.unwrap(); + assert_eq!(meta["data"]["application"], "AgentOrchestrator"); + assert_eq!(meta["data"]["user"], "agent_planner"); + assert_eq!(meta["data"]["description"], "Planning agent output for session abc-123"); + assert_eq!(meta["data"]["human_filename"], "plan_output.json"); + assert_eq!(meta["data"]["human_path"], "/agents/planner/"); + assert_eq!(meta["data"]["mime_type"], "application/json"); + + let tags = meta["data"]["tags"].as_array().unwrap(); + assert!(tags.contains(&serde_json::json!("agent"))); + assert!(tags.contains(&serde_json::json!("plan"))); + assert!(tags.contains(&serde_json::json!("session"))); +} + +#[tokio::test] +async fn test_data_ingest_various_json_types() { + let (base_url, _tmp) = spawn_test_server().await; + let client = reqwest::Client::new(); + + // Store a plain string + let resp = client + .post(format!("{}/api/v1/can/0/ingest/data", base_url)) + .json(&serde_json::json!({ + "data": "just a plain string log entry", + "tags": "log" + })) + .send() + .await + .unwrap(); + + assert_eq!(resp.status(), 200); + let body: serde_json::Value = resp.json().await.unwrap(); + let hash_str = body["data"]["hash"].as_str().unwrap(); + + // Retrieve and verify the string was stored + let resp = client + .get(format!("{}/api/v1/can/0/asset/{}", base_url, hash_str)) + .send() + .await + .unwrap(); + + let stored: serde_json::Value = resp.json().await.unwrap(); + assert_eq!(stored, "just a plain string log entry"); + + // Store an array + let resp = client + .post(format!("{}/api/v1/can/0/ingest/data", base_url)) + .json(&serde_json::json!({ + "data": [1, 2, 3, "four", null, true] + })) + .send() + .await + .unwrap(); + + assert_eq!(resp.status(), 200); + let body: serde_json::Value = resp.json().await.unwrap(); + let hash_arr = body["data"]["hash"].as_str().unwrap(); + + let resp = client + .get(format!("{}/api/v1/can/0/asset/{}", base_url, hash_arr)) + .send() + .await + .unwrap(); + + let stored: serde_json::Value = resp.json().await.unwrap(); + assert_eq!(stored, serde_json::json!([1, 2, 3, "four", null, true])); + + // Store a number + let resp = client + .post(format!("{}/api/v1/can/0/ingest/data", base_url)) + .json(&serde_json::json!({ + "data": 99.5 + })) + .send() + .await + .unwrap(); + + assert_eq!(resp.status(), 200); +} + +#[tokio::test] +async fn test_data_ingest_shows_up_in_list_and_search() { + let (base_url, _tmp) = spawn_test_server().await; + let client = reqwest::Client::new(); + + // Ingest via JSON data endpoint + client + .post(format!("{}/api/v1/can/0/ingest/data", base_url)) + .json(&serde_json::json!({ + "data": { "sensor": "temperature", "value": 22.5 }, + "application": "IoTAgent", + "tags": "sensor,temperature" + })) + .send() + .await + .unwrap(); + + // Also ingest via multipart + let file_part = multipart::Part::bytes(b"binary sensor log".to_vec()) + .file_name("sensor.bin") + .mime_str("application/octet-stream") + .unwrap(); + let form = multipart::Form::new() + .part("file", file_part) + .text("application", "IoTAgent") + .text("tags", "sensor,binary"); + client + .post(format!("{}/api/v1/can/0/ingest", base_url)) + .multipart(form) + .send() + .await + .unwrap(); + + tokio::time::sleep(std::time::Duration::from_millis(20)).await; + + // Both should show up in list + let resp = client + .get(format!("{}/api/v1/can/0/list?application=IoTAgent", base_url)) + .send() + .await + .unwrap(); + + let body: serde_json::Value = resp.json().await.unwrap(); + assert_eq!(body["data"]["pagination"]["total"], 2); + + // Search by tag should find the JSON one + let resp = client + .get(format!("{}/api/v1/can/0/search?tags=temperature", base_url)) + .send() + .await + .unwrap(); + + let body: serde_json::Value = resp.json().await.unwrap(); + assert_eq!(body["data"]["pagination"]["total"], 1); + assert_eq!(body["data"]["items"][0]["mime_type"], "application/json"); +} + +#[tokio::test] +async fn test_data_ingest_custom_mime_type() { + let (base_url, _tmp) = spawn_test_server().await; + let client = reqwest::Client::new(); + + // Agent stores data but overrides mime_type to text/plain + let resp = client + .post(format!("{}/api/v1/can/0/ingest/data", base_url)) + .json(&serde_json::json!({ + "data": "This is a plain text log line from the agent", + "mime_type": "text/plain", + "human_file_name": "agent.log" + })) + .send() + .await + .unwrap(); + + assert_eq!(resp.status(), 200); + let body: serde_json::Value = resp.json().await.unwrap(); + let filename = body["data"]["filename"].as_str().unwrap(); + assert!(filename.ends_with(".txt"), "Expected .txt extension, got {}", filename); + + let hash = body["data"]["hash"].as_str().unwrap(); + let resp = client + .get(format!("{}/api/v1/can/0/asset/{}/meta", base_url, hash)) + .send() + .await + .unwrap(); + + let meta: serde_json::Value = resp.json().await.unwrap(); + assert_eq!(meta["data"]["mime_type"], "text/plain"); + assert_eq!(meta["data"]["human_filename"], "agent.log"); +}