diff --git a/crates/karapace-remote/Cargo.toml b/crates/karapace-remote/Cargo.toml new file mode 100644 index 0000000..2a16808 --- /dev/null +++ b/crates/karapace-remote/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "karapace-remote" +description = "Remote content-addressable store for Karapace environment sharing" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true + +[lints] +workspace = true + +[dependencies] +serde.workspace = true +serde_json.workspace = true +thiserror.workspace = true +tracing.workspace = true +ureq.workspace = true +chrono.workspace = true +blake3.workspace = true +karapace-store = { path = "../karapace-store" } + +[dev-dependencies] +tempfile.workspace = true diff --git a/crates/karapace-remote/karapace-remote.cdx.json b/crates/karapace-remote/karapace-remote.cdx.json new file mode 100644 index 0000000..71cc3e9 --- /dev/null +++ b/crates/karapace-remote/karapace-remote.cdx.json @@ -0,0 +1,2346 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.3", + "version": 1, + "serialNumber": "urn:uuid:a6595584-3df6-4fea-bb76-d591914dc3f2", + "metadata": { + "timestamp": "2026-02-22T14:03:10.576529647Z", + "tools": [ + { + "vendor": "CycloneDX", + "name": "cargo-cyclonedx", + "version": "0.5.5" + } + ], + "component": { + "type": "library", + "bom-ref": "path+file:///home/lateuf/Projects/Karapace/crates/karapace-remote#0.1.0", + "name": "karapace-remote", + "version": "0.1.0", + "description": "Remote content-addressable store for Karapace environment sharing", + "scope": "required", + "licenses": [ + { + "expression": "EUPL-1.2" + } + ], + "purl": "pkg:cargo/karapace-remote@0.1.0?download_url=file://.", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/marcoallegretti/karapace" + } + ], + "components": [ + { + "type": "library", + "bom-ref": "path+file:///home/lateuf/Projects/Karapace/crates/karapace-remote#0.1.0 bin-target-0", + "name": "karapace_remote", + "version": "0.1.0", + "purl": "pkg:cargo/karapace-remote@0.1.0?download_url=file://.#src/lib.rs" + } + ] + } + }, + "components": [ + { + "type": "library", + "bom-ref": "path+file:///home/lateuf/Projects/Karapace/crates/karapace-schema#0.1.0", + "name": "karapace-schema", + "version": "0.1.0", + "description": "Manifest parsing, normalization, identity hashing, and lock file for Karapace", + "scope": "required", + "licenses": [ + { + "expression": "EUPL-1.2" + } + ], + "purl": "pkg:cargo/karapace-schema@0.1.0?download_url=file://../karapace-schema", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/marcoallegretti/karapace" + } + ] + }, + { + "type": "library", + "bom-ref": "path+file:///home/lateuf/Projects/Karapace/crates/karapace-store#0.1.0", + "name": "karapace-store", + "version": "0.1.0", + "description": "Content-addressable store, metadata, layers, GC, and integrity for Karapace", + "scope": "required", + "licenses": [ + { + "expression": "EUPL-1.2" + } + ], + "purl": "pkg:cargo/karapace-store@0.1.0?download_url=file://../karapace-store", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/marcoallegretti/karapace" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#adler2@2.0.1", + "name": "adler2", + "version": "2.0.1", + "description": "A simple clean-room implementation of the Adler-32 checksum", + "scope": "required", + "licenses": [ + { + "expression": "0BSD OR MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/adler2@2.0.1", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/adler2/" + }, + { + "type": "vcs", + "url": "https://github.com/oyvindln/adler2" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#arrayref@0.3.9", + "name": "arrayref", + "version": "0.3.9", + "description": "Macros to take array references of slices", + "scope": "required", + "licenses": [ + { + "expression": "BSD-2-Clause" + } + ], + "purl": "pkg:cargo/arrayref@0.3.9", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/arrayref" + }, + { + "type": "vcs", + "url": "https://github.com/droundy/arrayref" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#arrayvec@0.7.6", + "name": "arrayvec", + "version": "0.7.6", + "description": "A vector with fixed capacity, backed by an array (it can be stored on the stack too). Implements fixed capacity ArrayVec and ArrayString.", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/arrayvec@0.7.6", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/arrayvec/" + }, + { + "type": "vcs", + "url": "https://github.com/bluss/arrayvec" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#autocfg@1.5.0", + "name": "autocfg", + "version": "1.5.0", + "description": "Automatic cfg for Rust compiler features", + "scope": "excluded", + "licenses": [ + { + "expression": "Apache-2.0 OR MIT" + } + ], + "purl": "pkg:cargo/autocfg@1.5.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/autocfg/" + }, + { + "type": "vcs", + "url": "https://github.com/cuviper/autocfg" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#base64@0.22.1", + "name": "base64", + "version": "0.22.1", + "description": "encodes and decodes base64 as bytes or utf8", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/base64@0.22.1", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/base64" + }, + { + "type": "vcs", + "url": "https://github.com/marshallpierce/rust-base64" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#bitflags@2.11.0", + "name": "bitflags", + "version": "2.11.0", + "description": "A macro to generate structures which behave like bitflags. ", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/bitflags@2.11.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/bitflags" + }, + { + "type": "website", + "url": "https://github.com/bitflags/bitflags" + }, + { + "type": "vcs", + "url": "https://github.com/bitflags/bitflags" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#blake3@1.8.3", + "name": "blake3", + "version": "1.8.3", + "description": "the BLAKE3 hash function", + "scope": "required", + "licenses": [ + { + "expression": "CC0-1.0 OR Apache-2.0 OR Apache-2.0 WITH LLVM-exception" + } + ], + "purl": "pkg:cargo/blake3@1.8.3", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/blake3" + }, + { + "type": "vcs", + "url": "https://github.com/BLAKE3-team/BLAKE3" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#bytes@1.11.1", + "name": "bytes", + "version": "1.11.1", + "description": "Types and traits for working with bytes", + "scope": "required", + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/bytes@1.11.1", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/tokio-rs/bytes" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#cc@1.2.56", + "name": "cc", + "version": "1.2.56", + "description": "A build-time dependency for Cargo build scripts to assist in invoking the native C compiler to compile native C code into a static archive to be linked into Rust code. ", + "scope": "excluded", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/cc@1.2.56", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/cc" + }, + { + "type": "website", + "url": "https://github.com/rust-lang/cc-rs" + }, + { + "type": "vcs", + "url": "https://github.com/rust-lang/cc-rs" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#cfg-if@1.0.4", + "name": "cfg-if", + "version": "1.0.4", + "description": "A macro to ergonomically define an item depending on a large number of #[cfg] parameters. Structured like an if-else chain, the first matching branch is the item that gets emitted. ", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/cfg-if@1.0.4", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/rust-lang/cfg-if" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#chrono@0.4.43", + "name": "chrono", + "version": "0.4.43", + "description": "Date and time library for Rust", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/chrono@0.4.43", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/chrono/" + }, + { + "type": "website", + "url": "https://github.com/chronotope/chrono" + }, + { + "type": "vcs", + "url": "https://github.com/chronotope/chrono" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#constant_time_eq@0.4.2", + "name": "constant_time_eq", + "version": "0.4.2", + "description": "Compares two equal-sized byte strings in constant time.", + "scope": "required", + "licenses": [ + { + "expression": "CC0-1.0 OR MIT-0 OR Apache-2.0" + } + ], + "purl": "pkg:cargo/constant_time_eq@0.4.2", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/constant_time_eq" + }, + { + "type": "vcs", + "url": "https://github.com/cesarb/constant_time_eq" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#cpufeatures@0.2.17", + "name": "cpufeatures", + "version": "0.2.17", + "description": "Lightweight runtime CPU feature detection for aarch64, loongarch64, and x86/x86_64 targets, with no_std support and support for mobile targets including Android and iOS ", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/cpufeatures@0.2.17", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/cpufeatures" + }, + { + "type": "vcs", + "url": "https://github.com/RustCrypto/utils" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#crc32fast@1.5.0", + "name": "crc32fast", + "version": "1.5.0", + "description": "Fast, SIMD-accelerated CRC32 (IEEE) checksum computation", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/crc32fast@1.5.0", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/srijs/rust-crc32fast" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#equivalent@1.0.2", + "name": "equivalent", + "version": "1.0.2", + "description": "Traits for key comparison in maps.", + "scope": "required", + "licenses": [ + { + "expression": "Apache-2.0 OR MIT" + } + ], + "purl": "pkg:cargo/equivalent@1.0.2", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/indexmap-rs/equivalent" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#errno@0.3.14", + "name": "errno", + "version": "0.3.14", + "description": "Cross-platform interface to the `errno` variable.", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/errno@0.3.14", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/errno" + }, + { + "type": "vcs", + "url": "https://github.com/lambda-fairy/rust-errno" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#fastrand@2.3.0", + "name": "fastrand", + "version": "2.3.0", + "description": "A simple and fast random number generator", + "scope": "required", + "licenses": [ + { + "expression": "Apache-2.0 OR MIT" + } + ], + "purl": "pkg:cargo/fastrand@2.3.0", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/smol-rs/fastrand" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#filetime@0.2.27", + "name": "filetime", + "version": "0.2.27", + "description": "Platform-agnostic accessors of timestamps in File metadata ", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/filetime@0.2.27", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/filetime" + }, + { + "type": "website", + "url": "https://github.com/alexcrichton/filetime" + }, + { + "type": "vcs", + "url": "https://github.com/alexcrichton/filetime" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#find-msvc-tools@0.1.9", + "name": "find-msvc-tools", + "version": "0.1.9", + "description": "Find windows-specific tools, read MSVC versions from the registry and from COM interfaces", + "scope": "excluded", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/find-msvc-tools@0.1.9", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/find-msvc-tools" + }, + { + "type": "vcs", + "url": "https://github.com/rust-lang/cc-rs" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#flate2@1.1.9", + "name": "flate2", + "version": "1.1.9", + "description": "DEFLATE compression and decompression exposed as Read/BufRead/Write streams. Supports miniz_oxide and multiple zlib implementations. Supports zlib, gzip, and raw deflate streams. ", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/flate2@1.1.9", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/flate2" + }, + { + "type": "website", + "url": "https://github.com/rust-lang/flate2-rs" + }, + { + "type": "vcs", + "url": "https://github.com/rust-lang/flate2-rs" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#fs2@0.4.3", + "name": "fs2", + "version": "0.4.3", + "description": "Cross-platform file locks and file duplication.", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/fs2@0.4.3", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/fs2" + }, + { + "type": "vcs", + "url": "https://github.com/danburkert/fs2-rs" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#getrandom@0.2.17", + "name": "getrandom", + "version": "0.2.17", + "description": "A small cross-platform library for retrieving random data from system source", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/getrandom@0.2.17", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/getrandom" + }, + { + "type": "vcs", + "url": "https://github.com/rust-random/getrandom" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#getrandom@0.4.1", + "name": "getrandom", + "version": "0.4.1", + "description": "A small cross-platform library for retrieving random data from system source", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/getrandom@0.4.1", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/getrandom" + }, + { + "type": "vcs", + "url": "https://github.com/rust-random/getrandom" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#hashbrown@0.16.1", + "name": "hashbrown", + "version": "0.16.1", + "description": "A Rust port of Google's SwissTable hash map", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/hashbrown@0.16.1", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/rust-lang/hashbrown" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#http@1.4.0", + "name": "http", + "version": "1.4.0", + "description": "A set of types for representing HTTP requests and responses. ", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/http@1.4.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/http" + }, + { + "type": "vcs", + "url": "https://github.com/hyperium/http" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#httparse@1.10.1", + "name": "httparse", + "version": "1.10.1", + "description": "A tiny, safe, speedy, zero-copy HTTP/1.x parser.", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/httparse@1.10.1", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/httparse" + }, + { + "type": "vcs", + "url": "https://github.com/seanmonstar/httparse" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#iana-time-zone@0.1.65", + "name": "iana-time-zone", + "version": "0.1.65", + "description": "get the IANA time zone for the current system", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/iana-time-zone@0.1.65", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/strawlab/iana-time-zone" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#indexmap@2.13.0", + "name": "indexmap", + "version": "2.13.0", + "description": "A hash table with consistent order and fast iteration.", + "scope": "required", + "licenses": [ + { + "expression": "Apache-2.0 OR MIT" + } + ], + "purl": "pkg:cargo/indexmap@2.13.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/indexmap/" + }, + { + "type": "vcs", + "url": "https://github.com/indexmap-rs/indexmap" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#itoa@1.0.17", + "name": "itoa", + "version": "1.0.17", + "description": "Fast integer primitive to string conversion", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/itoa@1.0.17", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/itoa" + }, + { + "type": "vcs", + "url": "https://github.com/dtolnay/itoa" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#libc@0.2.180", + "name": "libc", + "version": "0.2.180", + "description": "Raw FFI bindings to platform libraries like libc.", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/libc@0.2.180", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/rust-lang/libc" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#linux-raw-sys@0.11.0", + "name": "linux-raw-sys", + "version": "0.11.0", + "description": "Generated bindings for Linux's userspace API", + "scope": "required", + "licenses": [ + { + "expression": "Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT" + } + ], + "purl": "pkg:cargo/linux-raw-sys@0.11.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/linux-raw-sys" + }, + { + "type": "vcs", + "url": "https://github.com/sunfishcode/linux-raw-sys" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#log@0.4.29", + "name": "log", + "version": "0.4.29", + "description": "A lightweight logging facade for Rust ", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/log@0.4.29", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/log" + }, + { + "type": "vcs", + "url": "https://github.com/rust-lang/log" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#memchr@2.8.0", + "name": "memchr", + "version": "2.8.0", + "description": "Provides extremely fast (uses SIMD on x86_64, aarch64 and wasm32) routines for 1, 2 or 3 byte search and single substring search. ", + "scope": "required", + "licenses": [ + { + "expression": "Unlicense OR MIT" + } + ], + "purl": "pkg:cargo/memchr@2.8.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/memchr/" + }, + { + "type": "website", + "url": "https://github.com/BurntSushi/memchr" + }, + { + "type": "vcs", + "url": "https://github.com/BurntSushi/memchr" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#miniz_oxide@0.8.9", + "name": "miniz_oxide", + "version": "0.8.9", + "description": "DEFLATE compression and decompression library rewritten in Rust based on miniz", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Zlib OR Apache-2.0" + } + ], + "purl": "pkg:cargo/miniz_oxide@0.8.9", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/miniz_oxide" + }, + { + "type": "website", + "url": "https://github.com/Frommi/miniz_oxide/tree/master/miniz_oxide" + }, + { + "type": "vcs", + "url": "https://github.com/Frommi/miniz_oxide/tree/master/miniz_oxide" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#num-traits@0.2.19", + "name": "num-traits", + "version": "0.2.19", + "description": "Numeric traits for generic mathematics", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/num-traits@0.2.19", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/num-traits" + }, + { + "type": "website", + "url": "https://github.com/rust-num/num-traits" + }, + { + "type": "vcs", + "url": "https://github.com/rust-num/num-traits" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#once_cell@1.21.3", + "name": "once_cell", + "version": "1.21.3", + "description": "Single assignment cells and lazy values.", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/once_cell@1.21.3", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/once_cell" + }, + { + "type": "vcs", + "url": "https://github.com/matklad/once_cell" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#percent-encoding@2.3.2", + "name": "percent-encoding", + "version": "2.3.2", + "description": "Percent encoding and decoding", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/percent-encoding@2.3.2", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/servo/rust-url/" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#pin-project-lite@0.2.16", + "name": "pin-project-lite", + "version": "0.2.16", + "description": "A lightweight version of pin-project written with declarative macros. ", + "scope": "required", + "licenses": [ + { + "expression": "Apache-2.0 OR MIT" + } + ], + "purl": "pkg:cargo/pin-project-lite@0.2.16", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/taiki-e/pin-project-lite" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.106", + "name": "proc-macro2", + "version": "1.0.106", + "description": "A substitute implementation of the compiler's `proc_macro` API to decouple token-based libraries from the procedural macro use case.", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/proc-macro2@1.0.106", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/proc-macro2" + }, + { + "type": "vcs", + "url": "https://github.com/dtolnay/proc-macro2" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.44", + "name": "quote", + "version": "1.0.44", + "description": "Quasi-quoting macro quote!(...)", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/quote@1.0.44", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/quote/" + }, + { + "type": "vcs", + "url": "https://github.com/dtolnay/quote" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#ring@0.17.14", + "name": "ring", + "version": "0.17.14", + "description": "An experiment.", + "scope": "required", + "licenses": [ + { + "expression": "Apache-2.0 AND ISC" + } + ], + "purl": "pkg:cargo/ring@0.17.14", + "externalReferences": [ + { + "type": "other", + "url": "ring_core_0_17_14_" + }, + { + "type": "vcs", + "url": "https://github.com/briansmith/ring" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#rustix@1.1.3", + "name": "rustix", + "version": "1.1.3", + "description": "Safe Rust bindings to POSIX/Unix/Linux/Winsock-like syscalls", + "scope": "required", + "licenses": [ + { + "expression": "Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT" + } + ], + "purl": "pkg:cargo/rustix@1.1.3", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/rustix" + }, + { + "type": "vcs", + "url": "https://github.com/bytecodealliance/rustix" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#rustls-pki-types@1.14.0", + "name": "rustls-pki-types", + "version": "1.14.0", + "description": "Shared types for the rustls PKI ecosystem", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/rustls-pki-types@1.14.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/rustls-pki-types" + }, + { + "type": "website", + "url": "https://github.com/rustls/pki-types" + }, + { + "type": "vcs", + "url": "https://github.com/rustls/pki-types" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#rustls-webpki@0.103.9", + "name": "rustls-webpki", + "version": "0.103.9", + "description": "Web PKI X.509 Certificate Verification.", + "scope": "required", + "licenses": [ + { + "expression": "ISC" + } + ], + "purl": "pkg:cargo/rustls-webpki@0.103.9", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/rustls/webpki" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#rustls@0.23.36", + "name": "rustls", + "version": "0.23.36", + "description": "Rustls is a modern TLS library written in Rust.", + "scope": "required", + "licenses": [ + { + "expression": "Apache-2.0 OR ISC OR MIT" + } + ], + "purl": "pkg:cargo/rustls@0.23.36", + "externalReferences": [ + { + "type": "website", + "url": "https://github.com/rustls/rustls" + }, + { + "type": "vcs", + "url": "https://github.com/rustls/rustls" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#serde@1.0.228", + "name": "serde", + "version": "1.0.228", + "description": "A generic serialization/deserialization framework", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/serde@1.0.228", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/serde" + }, + { + "type": "website", + "url": "https://serde.rs" + }, + { + "type": "vcs", + "url": "https://github.com/serde-rs/serde" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#serde_core@1.0.228", + "name": "serde_core", + "version": "1.0.228", + "description": "Serde traits only, with no support for derive -- use the `serde` crate instead", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/serde_core@1.0.228", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/serde_core" + }, + { + "type": "website", + "url": "https://serde.rs" + }, + { + "type": "vcs", + "url": "https://github.com/serde-rs/serde" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#serde_derive@1.0.228", + "name": "serde_derive", + "version": "1.0.228", + "description": "Macros 1.1 implementation of #[derive(Serialize, Deserialize)]", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/serde_derive@1.0.228", + "externalReferences": [ + { + "type": "documentation", + "url": "https://serde.rs/derive.html" + }, + { + "type": "website", + "url": "https://serde.rs" + }, + { + "type": "vcs", + "url": "https://github.com/serde-rs/serde" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#serde_json@1.0.149", + "name": "serde_json", + "version": "1.0.149", + "description": "A JSON serialization file format", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/serde_json@1.0.149", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/serde_json" + }, + { + "type": "vcs", + "url": "https://github.com/serde-rs/json" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#serde_spanned@0.6.9", + "name": "serde_spanned", + "version": "0.6.9", + "description": "Serde-compatible spanned Value", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/serde_spanned@0.6.9", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/toml-rs/toml" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#shlex@1.3.0", + "name": "shlex", + "version": "1.3.0", + "description": "Split a string into shell words, like Python's shlex.", + "scope": "excluded", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/shlex@1.3.0", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/comex/rust-shlex" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#simd-adler32@0.3.8", + "name": "simd-adler32", + "version": "0.3.8", + "description": "A SIMD-accelerated Adler-32 hash algorithm implementation.", + "scope": "required", + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/simd-adler32@0.3.8", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/mcountryman/simd-adler32" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#subtle@2.6.1", + "name": "subtle", + "version": "2.6.1", + "description": "Pure-Rust traits and utilities for constant-time cryptographic implementations.", + "scope": "required", + "licenses": [ + { + "expression": "BSD-3-Clause" + } + ], + "purl": "pkg:cargo/subtle@2.6.1", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/subtle" + }, + { + "type": "website", + "url": "https://dalek.rs/" + }, + { + "type": "vcs", + "url": "https://github.com/dalek-cryptography/subtle" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.117", + "name": "syn", + "version": "2.0.117", + "description": "Parser for Rust source code", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/syn@2.0.117", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/syn" + }, + { + "type": "vcs", + "url": "https://github.com/dtolnay/syn" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#tar@0.4.44", + "name": "tar", + "version": "0.4.44", + "description": "A Rust implementation of a TAR file reader and writer. This library does not currently handle compression, but it is abstract over all I/O readers and writers. Additionally, great lengths are taken to ensure that the entire contents are never required to be entirely resident in memory all at once. ", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/tar@0.4.44", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/tar" + }, + { + "type": "website", + "url": "https://github.com/alexcrichton/tar-rs" + }, + { + "type": "vcs", + "url": "https://github.com/alexcrichton/tar-rs" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#tempfile@3.25.0", + "name": "tempfile", + "version": "3.25.0", + "description": "A library for managing temporary files and directories.", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/tempfile@3.25.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/tempfile" + }, + { + "type": "website", + "url": "https://stebalien.com/projects/tempfile-rs/" + }, + { + "type": "vcs", + "url": "https://github.com/Stebalien/tempfile" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#thiserror-impl@2.0.18", + "name": "thiserror-impl", + "version": "2.0.18", + "description": "Implementation detail of the `thiserror` crate", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/thiserror-impl@2.0.18", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/dtolnay/thiserror" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#thiserror@2.0.18", + "name": "thiserror", + "version": "2.0.18", + "description": "derive(Error)", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/thiserror@2.0.18", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/thiserror" + }, + { + "type": "vcs", + "url": "https://github.com/dtolnay/thiserror" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#toml@0.8.23", + "name": "toml", + "version": "0.8.23", + "description": "A native Rust encoder and decoder of TOML-formatted files and streams. Provides implementations of the standard Serialize/Deserialize traits for TOML data to facilitate deserializing and serializing Rust structures. ", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/toml@0.8.23", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/toml-rs/toml" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#toml_datetime@0.6.11", + "name": "toml_datetime", + "version": "0.6.11", + "description": "A TOML-compatible datetime type", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/toml_datetime@0.6.11", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/toml-rs/toml" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#toml_edit@0.22.27", + "name": "toml_edit", + "version": "0.22.27", + "description": "Yet another format-preserving TOML parser.", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/toml_edit@0.22.27", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/toml-rs/toml" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#toml_write@0.1.2", + "name": "toml_write", + "version": "0.1.2", + "description": "A low-level interface for writing out TOML ", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/toml_write@0.1.2", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/toml-rs/toml" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#tracing-attributes@0.1.31", + "name": "tracing-attributes", + "version": "0.1.31", + "description": "Procedural macro attributes for automatically instrumenting functions. ", + "scope": "required", + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/tracing-attributes@0.1.31", + "externalReferences": [ + { + "type": "website", + "url": "https://tokio.rs" + }, + { + "type": "vcs", + "url": "https://github.com/tokio-rs/tracing" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#tracing-core@0.1.36", + "name": "tracing-core", + "version": "0.1.36", + "description": "Core primitives for application-level tracing. ", + "scope": "required", + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/tracing-core@0.1.36", + "externalReferences": [ + { + "type": "website", + "url": "https://tokio.rs" + }, + { + "type": "vcs", + "url": "https://github.com/tokio-rs/tracing" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#tracing@0.1.44", + "name": "tracing", + "version": "0.1.44", + "description": "Application-level tracing for Rust. ", + "scope": "required", + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/tracing@0.1.44", + "externalReferences": [ + { + "type": "website", + "url": "https://tokio.rs" + }, + { + "type": "vcs", + "url": "https://github.com/tokio-rs/tracing" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#unicode-ident@1.0.24", + "name": "unicode-ident", + "version": "1.0.24", + "description": "Determine whether characters have the XID_Start or XID_Continue properties according to Unicode Standard Annex #31", + "scope": "required", + "licenses": [ + { + "expression": "(MIT OR Apache-2.0) AND Unicode-3.0" + } + ], + "purl": "pkg:cargo/unicode-ident@1.0.24", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/unicode-ident" + }, + { + "type": "vcs", + "url": "https://github.com/dtolnay/unicode-ident" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#untrusted@0.9.0", + "name": "untrusted", + "version": "0.9.0", + "description": "Safe, fast, zero-panic, zero-crashing, zero-allocation parsing of untrusted inputs in Rust.", + "scope": "required", + "licenses": [ + { + "expression": "ISC" + } + ], + "purl": "pkg:cargo/untrusted@0.9.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://briansmith.org/rustdoc/untrusted/" + }, + { + "type": "vcs", + "url": "https://github.com/briansmith/untrusted" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#ureq-proto@0.5.3", + "name": "ureq-proto", + "version": "0.5.3", + "description": "ureq support crate", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/ureq-proto@0.5.3", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/algesten/ureq-proto" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#ureq@3.2.0", + "name": "ureq", + "version": "3.2.0", + "description": "Simple, safe HTTP client", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/ureq@3.2.0", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/algesten/ureq" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#utf-8@0.7.6", + "name": "utf-8", + "version": "0.7.6", + "description": "Incremental, zero-copy UTF-8 decoding with error handling", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/utf-8@0.7.6", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/SimonSapin/rust-utf8" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#webpki-roots@1.0.6", + "name": "webpki-roots", + "version": "1.0.6", + "description": "Mozilla's CA root certificates for use with webpki", + "scope": "required", + "licenses": [ + { + "expression": "CDLA-Permissive-2.0" + } + ], + "purl": "pkg:cargo/webpki-roots@1.0.6", + "externalReferences": [ + { + "type": "website", + "url": "https://github.com/rustls/webpki-roots" + }, + { + "type": "vcs", + "url": "https://github.com/rustls/webpki-roots" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#winnow@0.7.14", + "name": "winnow", + "version": "0.7.14", + "description": "A byte-oriented, zero-copy, parser combinators library", + "scope": "required", + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/winnow@0.7.14", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/winnow-rs/winnow" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#xattr@1.6.1", + "name": "xattr", + "version": "1.6.1", + "description": "unix extended filesystem attributes", + "scope": "required", + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/xattr@1.6.1", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/xattr" + }, + { + "type": "vcs", + "url": "https://github.com/Stebalien/xattr" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#zeroize@1.8.2", + "name": "zeroize", + "version": "1.8.2", + "description": "Securely clear secrets from memory with a simple trait built on stable Rust primitives which guarantee memory is zeroed using an operation will not be 'optimized away' by the compiler. Uses a portable pure Rust implementation that works everywhere, even WASM! ", + "scope": "required", + "licenses": [ + { + "expression": "Apache-2.0 OR MIT" + } + ], + "purl": "pkg:cargo/zeroize@1.8.2", + "externalReferences": [ + { + "type": "website", + "url": "https://github.com/RustCrypto/utils/tree/master/zeroize" + }, + { + "type": "vcs", + "url": "https://github.com/RustCrypto/utils" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#zmij@1.0.21", + "name": "zmij", + "version": "1.0.21", + "description": "A double-to-string conversion algorithm based on Schubfach and yy", + "scope": "required", + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/zmij@1.0.21", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/zmij" + }, + { + "type": "vcs", + "url": "https://github.com/dtolnay/zmij" + } + ] + } + ], + "dependencies": [ + { + "ref": "path+file:///home/lateuf/Projects/Karapace/crates/karapace-remote#0.1.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#blake3@1.8.3", + "registry+https://github.com/rust-lang/crates.io-index#chrono@0.4.43", + "path+file:///home/lateuf/Projects/Karapace/crates/karapace-store#0.1.0", + "registry+https://github.com/rust-lang/crates.io-index#serde@1.0.228", + "registry+https://github.com/rust-lang/crates.io-index#serde_json@1.0.149", + "registry+https://github.com/rust-lang/crates.io-index#thiserror@2.0.18", + "registry+https://github.com/rust-lang/crates.io-index#tracing@0.1.44", + "registry+https://github.com/rust-lang/crates.io-index#ureq@3.2.0" + ] + }, + { + "ref": "path+file:///home/lateuf/Projects/Karapace/crates/karapace-schema#0.1.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#blake3@1.8.3", + "registry+https://github.com/rust-lang/crates.io-index#serde@1.0.228", + "registry+https://github.com/rust-lang/crates.io-index#serde_json@1.0.149", + "registry+https://github.com/rust-lang/crates.io-index#tempfile@3.25.0", + "registry+https://github.com/rust-lang/crates.io-index#thiserror@2.0.18", + "registry+https://github.com/rust-lang/crates.io-index#toml@0.8.23" + ] + }, + { + "ref": "path+file:///home/lateuf/Projects/Karapace/crates/karapace-store#0.1.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#blake3@1.8.3", + "registry+https://github.com/rust-lang/crates.io-index#chrono@0.4.43", + "registry+https://github.com/rust-lang/crates.io-index#fs2@0.4.3", + "path+file:///home/lateuf/Projects/Karapace/crates/karapace-schema#0.1.0", + "registry+https://github.com/rust-lang/crates.io-index#serde@1.0.228", + "registry+https://github.com/rust-lang/crates.io-index#serde_json@1.0.149", + "registry+https://github.com/rust-lang/crates.io-index#tar@0.4.44", + "registry+https://github.com/rust-lang/crates.io-index#tempfile@3.25.0", + "registry+https://github.com/rust-lang/crates.io-index#thiserror@2.0.18", + "registry+https://github.com/rust-lang/crates.io-index#tracing@0.1.44" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#adler2@2.0.1", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#arrayref@0.3.9", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#arrayvec@0.7.6", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#autocfg@1.5.0", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#base64@0.22.1", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#bitflags@2.11.0", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#blake3@1.8.3", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#arrayref@0.3.9", + "registry+https://github.com/rust-lang/crates.io-index#arrayvec@0.7.6", + "registry+https://github.com/rust-lang/crates.io-index#cc@1.2.56", + "registry+https://github.com/rust-lang/crates.io-index#cfg-if@1.0.4", + "registry+https://github.com/rust-lang/crates.io-index#constant_time_eq@0.4.2", + "registry+https://github.com/rust-lang/crates.io-index#cpufeatures@0.2.17" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#bytes@1.11.1", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#cc@1.2.56", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#find-msvc-tools@0.1.9", + "registry+https://github.com/rust-lang/crates.io-index#shlex@1.3.0" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#cfg-if@1.0.4", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#chrono@0.4.43", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#iana-time-zone@0.1.65", + "registry+https://github.com/rust-lang/crates.io-index#num-traits@0.2.19", + "registry+https://github.com/rust-lang/crates.io-index#serde@1.0.228" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#constant_time_eq@0.4.2", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#cpufeatures@0.2.17", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#crc32fast@1.5.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#cfg-if@1.0.4" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#equivalent@1.0.2", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#errno@0.3.14", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#libc@0.2.180" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#fastrand@2.3.0", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#filetime@0.2.27", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#cfg-if@1.0.4", + "registry+https://github.com/rust-lang/crates.io-index#libc@0.2.180" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#find-msvc-tools@0.1.9", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#flate2@1.1.9", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#crc32fast@1.5.0", + "registry+https://github.com/rust-lang/crates.io-index#miniz_oxide@0.8.9" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#fs2@0.4.3", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#libc@0.2.180" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#getrandom@0.2.17", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#cfg-if@1.0.4", + "registry+https://github.com/rust-lang/crates.io-index#libc@0.2.180" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#getrandom@0.4.1", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#cfg-if@1.0.4", + "registry+https://github.com/rust-lang/crates.io-index#libc@0.2.180" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#hashbrown@0.16.1", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#http@1.4.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#bytes@1.11.1", + "registry+https://github.com/rust-lang/crates.io-index#itoa@1.0.17" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#httparse@1.10.1", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#iana-time-zone@0.1.65", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#indexmap@2.13.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#equivalent@1.0.2", + "registry+https://github.com/rust-lang/crates.io-index#hashbrown@0.16.1", + "registry+https://github.com/rust-lang/crates.io-index#serde_core@1.0.228" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#itoa@1.0.17", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#libc@0.2.180", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#linux-raw-sys@0.11.0", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#log@0.4.29", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#memchr@2.8.0", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#miniz_oxide@0.8.9", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#adler2@2.0.1", + "registry+https://github.com/rust-lang/crates.io-index#simd-adler32@0.3.8" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#num-traits@0.2.19", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#autocfg@1.5.0" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#once_cell@1.21.3", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#percent-encoding@2.3.2", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#pin-project-lite@0.2.16", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.106", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#unicode-ident@1.0.24" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.44", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.106" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#ring@0.17.14", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#cc@1.2.56", + "registry+https://github.com/rust-lang/crates.io-index#cfg-if@1.0.4", + "registry+https://github.com/rust-lang/crates.io-index#getrandom@0.2.17", + "registry+https://github.com/rust-lang/crates.io-index#untrusted@0.9.0" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#rustix@1.1.3", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#bitflags@2.11.0", + "registry+https://github.com/rust-lang/crates.io-index#errno@0.3.14", + "registry+https://github.com/rust-lang/crates.io-index#libc@0.2.180", + "registry+https://github.com/rust-lang/crates.io-index#linux-raw-sys@0.11.0" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#rustls-pki-types@1.14.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#zeroize@1.8.2" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#rustls-webpki@0.103.9", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#ring@0.17.14", + "registry+https://github.com/rust-lang/crates.io-index#rustls-pki-types@1.14.0", + "registry+https://github.com/rust-lang/crates.io-index#untrusted@0.9.0" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#rustls@0.23.36", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#log@0.4.29", + "registry+https://github.com/rust-lang/crates.io-index#once_cell@1.21.3", + "registry+https://github.com/rust-lang/crates.io-index#ring@0.17.14", + "registry+https://github.com/rust-lang/crates.io-index#rustls-pki-types@1.14.0", + "registry+https://github.com/rust-lang/crates.io-index#rustls-webpki@0.103.9", + "registry+https://github.com/rust-lang/crates.io-index#subtle@2.6.1", + "registry+https://github.com/rust-lang/crates.io-index#zeroize@1.8.2" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#serde@1.0.228", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#serde_core@1.0.228", + "registry+https://github.com/rust-lang/crates.io-index#serde_derive@1.0.228" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#serde_core@1.0.228", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#serde_derive@1.0.228", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.106", + "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.44", + "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.117" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#serde_json@1.0.149", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#itoa@1.0.17", + "registry+https://github.com/rust-lang/crates.io-index#memchr@2.8.0", + "registry+https://github.com/rust-lang/crates.io-index#serde_core@1.0.228", + "registry+https://github.com/rust-lang/crates.io-index#zmij@1.0.21" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#serde_spanned@0.6.9", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#serde@1.0.228" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#shlex@1.3.0", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#simd-adler32@0.3.8", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#subtle@2.6.1", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.117", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.106", + "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.44", + "registry+https://github.com/rust-lang/crates.io-index#unicode-ident@1.0.24" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#tar@0.4.44", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#filetime@0.2.27", + "registry+https://github.com/rust-lang/crates.io-index#libc@0.2.180", + "registry+https://github.com/rust-lang/crates.io-index#xattr@1.6.1" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#tempfile@3.25.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#fastrand@2.3.0", + "registry+https://github.com/rust-lang/crates.io-index#getrandom@0.4.1", + "registry+https://github.com/rust-lang/crates.io-index#once_cell@1.21.3", + "registry+https://github.com/rust-lang/crates.io-index#rustix@1.1.3" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#thiserror-impl@2.0.18", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.106", + "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.44", + "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.117" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#thiserror@2.0.18", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#thiserror-impl@2.0.18" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#toml@0.8.23", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#serde@1.0.228", + "registry+https://github.com/rust-lang/crates.io-index#serde_spanned@0.6.9", + "registry+https://github.com/rust-lang/crates.io-index#toml_datetime@0.6.11", + "registry+https://github.com/rust-lang/crates.io-index#toml_edit@0.22.27" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#toml_datetime@0.6.11", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#serde@1.0.228" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#toml_edit@0.22.27", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#indexmap@2.13.0", + "registry+https://github.com/rust-lang/crates.io-index#serde@1.0.228", + "registry+https://github.com/rust-lang/crates.io-index#serde_spanned@0.6.9", + "registry+https://github.com/rust-lang/crates.io-index#toml_datetime@0.6.11", + "registry+https://github.com/rust-lang/crates.io-index#toml_write@0.1.2", + "registry+https://github.com/rust-lang/crates.io-index#winnow@0.7.14" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#toml_write@0.1.2", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#tracing-attributes@0.1.31", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.106", + "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.44", + "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.117" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#tracing-core@0.1.36", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#once_cell@1.21.3" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#tracing@0.1.44", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#pin-project-lite@0.2.16", + "registry+https://github.com/rust-lang/crates.io-index#tracing-attributes@0.1.31", + "registry+https://github.com/rust-lang/crates.io-index#tracing-core@0.1.36" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#unicode-ident@1.0.24", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#untrusted@0.9.0", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#ureq-proto@0.5.3", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#base64@0.22.1", + "registry+https://github.com/rust-lang/crates.io-index#http@1.4.0", + "registry+https://github.com/rust-lang/crates.io-index#httparse@1.10.1", + "registry+https://github.com/rust-lang/crates.io-index#log@0.4.29" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#ureq@3.2.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#base64@0.22.1", + "registry+https://github.com/rust-lang/crates.io-index#flate2@1.1.9", + "registry+https://github.com/rust-lang/crates.io-index#log@0.4.29", + "registry+https://github.com/rust-lang/crates.io-index#percent-encoding@2.3.2", + "registry+https://github.com/rust-lang/crates.io-index#rustls@0.23.36", + "registry+https://github.com/rust-lang/crates.io-index#rustls-pki-types@1.14.0", + "registry+https://github.com/rust-lang/crates.io-index#ureq-proto@0.5.3", + "registry+https://github.com/rust-lang/crates.io-index#utf-8@0.7.6", + "registry+https://github.com/rust-lang/crates.io-index#webpki-roots@1.0.6" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#utf-8@0.7.6", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#webpki-roots@1.0.6", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#rustls-pki-types@1.14.0" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#winnow@0.7.14", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#memchr@2.8.0" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#xattr@1.6.1", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#rustix@1.1.3" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#zeroize@1.8.2", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#zmij@1.0.21", + "dependsOn": [] + } + ] +} \ No newline at end of file diff --git a/crates/karapace-remote/src/config.rs b/crates/karapace-remote/src/config.rs new file mode 100644 index 0000000..4ae8c90 --- /dev/null +++ b/crates/karapace-remote/src/config.rs @@ -0,0 +1,76 @@ +use crate::RemoteError; +use serde::{Deserialize, Serialize}; +use std::path::{Path, PathBuf}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RemoteConfig { + pub url: String, + #[serde(default)] + pub auth_token: Option, +} + +impl RemoteConfig { + pub fn new(url: &str) -> Self { + Self { + url: url.trim_end_matches('/').to_owned(), + auth_token: None, + } + } + + #[must_use] + pub fn with_token(mut self, token: &str) -> Self { + self.auth_token = Some(token.to_owned()); + self + } + + /// Load config from `~/.config/karapace/remote.json`. + pub fn load_default() -> Result { + let path = default_config_path()?; + Self::load(&path) + } + + pub fn load(path: &Path) -> Result { + let content = std::fs::read_to_string(path)?; + serde_json::from_str(&content) + .map_err(|e| RemoteError::Config(format!("invalid remote config: {e}"))) + } + + pub fn save(&self, path: &Path) -> Result<(), RemoteError> { + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + let content = serde_json::to_string_pretty(self) + .map_err(|e| RemoteError::Serialization(e.to_string()))?; + std::fs::write(path, content)?; + Ok(()) + } +} + +fn default_config_path() -> Result { + let home = std::env::var("HOME").map_err(|_| RemoteError::Config("HOME not set".to_owned()))?; + Ok(PathBuf::from(home).join(".config/karapace/remote.json")) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn config_roundtrip() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("remote.json"); + + let config = RemoteConfig::new("https://store.example.com/v1").with_token("secret123"); + config.save(&path).unwrap(); + + let loaded = RemoteConfig::load(&path).unwrap(); + assert_eq!(loaded.url, "https://store.example.com/v1"); + assert_eq!(loaded.auth_token.as_deref(), Some("secret123")); + } + + #[test] + fn config_strips_trailing_slash() { + let config = RemoteConfig::new("https://example.com/"); + assert_eq!(config.url, "https://example.com"); + } +} diff --git a/crates/karapace-remote/src/http.rs b/crates/karapace-remote/src/http.rs new file mode 100644 index 0000000..06c0baf --- /dev/null +++ b/crates/karapace-remote/src/http.rs @@ -0,0 +1,463 @@ +use crate::{BlobKind, RemoteBackend, RemoteConfig, RemoteError}; + +/// HTTP-based remote store backend. +/// +/// Expects a simple REST API: +/// - `PUT /objects/` — upload object blob +/// - `GET /objects/` — download object blob +/// - `HEAD /objects/` — check existence +/// - `GET /objects/` — list objects (JSON array of strings) +/// - Same pattern for `/layers/` and `/metadata/` +/// - `PUT /registry` — upload registry index +/// - `GET /registry` — download registry index +pub struct HttpBackend { + config: RemoteConfig, + agent: ureq::Agent, +} + +impl HttpBackend { + pub fn new(config: RemoteConfig) -> Self { + let agent = ureq::Agent::new_with_defaults(); + Self { config, agent } + } + + fn kind_path(kind: BlobKind) -> &'static str { + match kind { + BlobKind::Object => "objects", + BlobKind::Layer => "layers", + BlobKind::Metadata => "metadata", + } + } + + fn url(&self, kind: BlobKind, key: &str) -> String { + format!("{}/{}/{}", self.config.url, Self::kind_path(kind), key) + } + + fn do_put(&self, url: &str, content_type: &str, data: &[u8]) -> Result<(), RemoteError> { + let mut req = self + .agent + .put(url) + .header("Content-Type", content_type) + .header("X-Karapace-Protocol", &crate::PROTOCOL_VERSION.to_string()); + if let Some(ref token) = self.config.auth_token { + req = req.header("Authorization", &format!("Bearer {token}")); + } + req.send(data as &[u8]) + .map_err(|e| RemoteError::Http(e.to_string()))?; + Ok(()) + } + + fn do_get(&self, url: &str) -> Result, RemoteError> { + let mut req = self + .agent + .get(url) + .header("X-Karapace-Protocol", &crate::PROTOCOL_VERSION.to_string()); + if let Some(ref token) = self.config.auth_token { + req = req.header("Authorization", &format!("Bearer {token}")); + } + let resp = req.call().map_err(|e| RemoteError::Http(e.to_string()))?; + let body = resp + .into_body() + .read_to_vec() + .map_err(|e| RemoteError::Http(e.to_string()))?; + Ok(body) + } + + fn do_head(&self, url: &str) -> Result { + let mut req = self + .agent + .head(url) + .header("X-Karapace-Protocol", &crate::PROTOCOL_VERSION.to_string()); + if let Some(ref token) = self.config.auth_token { + req = req.header("Authorization", &format!("Bearer {token}")); + } + let resp = req.call().map_err(|e| RemoteError::Http(e.to_string()))?; + Ok(resp.status().into()) + } +} + +impl RemoteBackend for HttpBackend { + fn put_blob(&self, kind: BlobKind, key: &str, data: &[u8]) -> Result<(), RemoteError> { + let url = self.url(kind, key); + tracing::debug!("PUT {url} ({} bytes)", data.len()); + self.do_put(&url, "application/octet-stream", data) + } + + fn get_blob(&self, kind: BlobKind, key: &str) -> Result, RemoteError> { + let url = self.url(kind, key); + tracing::debug!("GET {url}"); + self.do_get(&url) + } + + fn has_blob(&self, kind: BlobKind, key: &str) -> Result { + let url = self.url(kind, key); + tracing::debug!("HEAD {url}"); + match self.do_head(&url) { + Ok(status) => Ok(status == 200), + Err(_) => Ok(false), + } + } + + fn list_blobs(&self, kind: BlobKind) -> Result, RemoteError> { + let url = format!("{}/{}/", self.config.url, Self::kind_path(kind)); + tracing::debug!("GET {url}"); + let body = self.do_get(&url)?; + let body_str = String::from_utf8(body).map_err(|e| RemoteError::Http(e.to_string()))?; + let keys: Vec = serde_json::from_str(&body_str) + .map_err(|e| RemoteError::Serialization(e.to_string()))?; + Ok(keys) + } + + fn put_registry(&self, data: &[u8]) -> Result<(), RemoteError> { + let url = format!("{}/registry", self.config.url); + tracing::debug!("PUT {url} ({} bytes)", data.len()); + self.do_put(&url, "application/json", data) + } + + fn get_registry(&self) -> Result, RemoteError> { + let url = format!("{}/registry", self.config.url); + tracing::debug!("GET {url}"); + self.do_get(&url) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashMap; + use std::io::{BufRead, BufReader, Write}; + use std::net::TcpListener; + use std::sync::{Arc, Mutex}; + + /// A captured HTTP request for header inspection. + #[derive(Debug, Clone)] + struct CapturedRequest { + method: String, + path: String, + headers: HashMap, + } + + struct MockServer { + addr: String, + _handle: std::thread::JoinHandle<()>, + requests: Arc>>, + } + + impl MockServer { + fn start() -> Self { + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let addr = format!("http://{}", listener.local_addr().unwrap()); + let store: Arc>>> = Arc::new(Mutex::new(HashMap::new())); + let requests: Arc>> = Arc::new(Mutex::new(Vec::new())); + + let store_clone = Arc::clone(&store); + let requests_clone = Arc::clone(&requests); + let handle = std::thread::spawn(move || { + for stream in listener.incoming() { + let Ok(mut stream) = stream else { break }; + let store = Arc::clone(&store_clone); + let reqs = Arc::clone(&requests_clone); + + std::thread::spawn(move || { + let mut reader = BufReader::new(stream.try_clone().unwrap()); + let mut request_line = String::new(); + if reader.read_line(&mut request_line).is_err() { + return; + } + let parts: Vec<&str> = request_line.trim().splitn(3, ' ').collect(); + if parts.len() < 2 { + return; + } + let method = parts[0].to_owned(); + let path = parts[1].to_owned(); + + let mut content_length: usize = 0; + let mut headers = HashMap::new(); + loop { + let mut line = String::new(); + if reader.read_line(&mut line).is_err() || line.trim().is_empty() { + break; + } + if let Some((k, v)) = line.trim().split_once(": ") { + headers.insert(k.to_lowercase(), v.to_owned()); + } + let lower = line.to_lowercase(); + if let Some(val) = lower.strip_prefix("content-length: ") { + content_length = val.trim().parse().unwrap_or(0); + } + } + + reqs.lock().unwrap().push(CapturedRequest { + method: method.clone(), + path: path.clone(), + headers, + }); + + let mut body = vec![0u8; content_length]; + if content_length > 0 { + let _ = std::io::Read::read_exact(&mut reader, &mut body); + } + + let mut data = store.lock().unwrap(); + let response = match method.as_str() { + "PUT" => { + data.insert(path.clone(), body); + "HTTP/1.1 200 OK\r\nContent-Length: 0\r\nConnection: close\r\n\r\n" + .to_owned() + } + "GET" => { + if let Some(val) = data.get(&path) { + format!( + "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", + val.len() + ) + } else { + "HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n" + .to_owned() + } + } + "HEAD" => { + if data.contains_key(&path) { + "HTTP/1.1 200 OK\r\nContent-Length: 0\r\nConnection: close\r\n\r\n" + .to_owned() + } else { + "HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n" + .to_owned() + } + } + _ => "HTTP/1.1 405 Method Not Allowed\r\nContent-Length: 0\r\nConnection: close\r\n\r\n" + .to_owned(), + }; + + let _ = stream.write_all(response.as_bytes()); + if method == "GET" { + if let Some(val) = data.get(&path) { + let _ = stream.write_all(val); + } + } + let _ = stream.flush(); + }); + } + }); + + MockServer { + addr, + _handle: handle, + requests, + } + } + + fn captured_requests(&self) -> Vec { + self.requests.lock().unwrap().clone() + } + } + + fn test_backend(url: &str) -> HttpBackend { + HttpBackend::new(RemoteConfig { + url: url.to_owned(), + auth_token: None, + }) + } + + fn test_backend_with_auth(url: &str, token: &str) -> HttpBackend { + HttpBackend::new(RemoteConfig { + url: url.to_owned(), + auth_token: Some(token.to_owned()), + }) + } + + #[test] + fn http_put_and_get_blob() { + let server = MockServer::start(); + let backend = test_backend(&server.addr); + backend + .put_blob(BlobKind::Object, "hash123", b"test data") + .unwrap(); + let data = backend.get_blob(BlobKind::Object, "hash123").unwrap(); + assert_eq!(data, b"test data"); + } + + #[test] + fn http_has_blob_true_and_false() { + let server = MockServer::start(); + let backend = test_backend(&server.addr); + assert!(!backend.has_blob(BlobKind::Object, "missing").unwrap()); + backend + .put_blob(BlobKind::Object, "exists", b"data") + .unwrap(); + assert!(backend.has_blob(BlobKind::Object, "exists").unwrap()); + } + + #[test] + fn http_get_nonexistent_fails() { + let server = MockServer::start(); + let backend = test_backend(&server.addr); + let result = backend.get_blob(BlobKind::Object, "nonexistent"); + assert!(result.is_err()); + } + + #[test] + fn http_put_and_get_registry() { + let server = MockServer::start(); + let backend = test_backend(&server.addr); + let registry_data = b"{\"entries\":{}}"; + backend.put_registry(registry_data).unwrap(); + let data = backend.get_registry().unwrap(); + assert_eq!(data, registry_data); + } + + #[test] + fn http_connection_refused_returns_error() { + let backend = test_backend("http://127.0.0.1:1"); + let result = backend.put_blob(BlobKind::Object, "key", b"data"); + assert!(result.is_err()); + } + + #[test] + fn http_multiple_blob_kinds() { + let server = MockServer::start(); + let backend = test_backend(&server.addr); + + backend + .put_blob(BlobKind::Object, "obj1", b"object-data") + .unwrap(); + backend + .put_blob(BlobKind::Layer, "layer1", b"layer-data") + .unwrap(); + backend + .put_blob(BlobKind::Metadata, "meta1", b"meta-data") + .unwrap(); + + assert_eq!( + backend.get_blob(BlobKind::Object, "obj1").unwrap(), + b"object-data" + ); + assert_eq!( + backend.get_blob(BlobKind::Layer, "layer1").unwrap(), + b"layer-data" + ); + assert_eq!( + backend.get_blob(BlobKind::Metadata, "meta1").unwrap(), + b"meta-data" + ); + } + + // --- M4: Protocol version header tests --- + + #[test] + fn http_requests_include_protocol_header() { + let server = MockServer::start(); + let backend = test_backend(&server.addr); + + // PUT sends the header + backend.put_blob(BlobKind::Object, "h1", b"data").unwrap(); + // GET sends the header + let _ = backend.get_blob(BlobKind::Object, "h1"); + // HEAD sends the header + let _ = backend.has_blob(BlobKind::Object, "h1"); + + // Allow the mock server threads to finish + std::thread::sleep(std::time::Duration::from_millis(50)); + + let reqs = server.captured_requests(); + assert!( + reqs.len() >= 3, + "expected at least 3 requests, got {}", + reqs.len() + ); + for req in &reqs { + let proto = req.headers.get("x-karapace-protocol"); + assert_eq!( + proto, + Some(&"1".to_owned()), + "{} {} missing X-Karapace-Protocol header", + req.method, + req.path + ); + } + } + + #[test] + fn http_protocol_version_constant_is_1() { + assert_eq!(crate::PROTOCOL_VERSION, 1); + } + + #[test] + fn http_auth_token_sent_as_bearer_header() { + let server = MockServer::start(); + let backend = test_backend_with_auth(&server.addr, "secret-token-42"); + + backend + .put_blob(BlobKind::Object, "auth1", b"data") + .unwrap(); + + std::thread::sleep(std::time::Duration::from_millis(50)); + + let reqs = server.captured_requests(); + assert!(!reqs.is_empty()); + let auth = reqs[0].headers.get("authorization"); + assert_eq!( + auth, + Some(&"Bearer secret-token-42".to_owned()), + "PUT must include Authorization: Bearer header" + ); + } + + #[test] + fn http_no_auth_header_without_token() { + let server = MockServer::start(); + let backend = test_backend(&server.addr); + + backend + .put_blob(BlobKind::Object, "noauth", b"data") + .unwrap(); + + std::thread::sleep(std::time::Duration::from_millis(50)); + + let reqs = server.captured_requests(); + assert!(!reqs.is_empty()); + assert!( + !reqs[0].headers.contains_key("authorization"), + "no auth token configured — Authorization header must not be sent" + ); + } + + // --- M7.2: Remote HTTP coverage --- + + #[test] + fn http_list_blobs_returns_keys() { + let server = MockServer::start(); + let backend = test_backend(&server.addr); + + // Populate the mock store with a list response + backend.put_blob(BlobKind::Object, "a", b"data-a").unwrap(); + backend.put_blob(BlobKind::Object, "b", b"data-b").unwrap(); + backend.put_blob(BlobKind::Object, "c", b"data-c").unwrap(); + + // Store the list response at the list endpoint + let list_url = format!("{}/objects/", server.addr); + let list_body = serde_json::to_vec(&["a", "b", "c"]).unwrap(); + // Manually insert the list response via a PUT to the list path + backend + .do_put(&list_url, "application/json", &list_body) + .unwrap(); + + let keys = backend.list_blobs(BlobKind::Object).unwrap(); + assert_eq!(keys, vec!["a", "b", "c"]); + } + + #[test] + fn http_large_blob_roundtrip() { + let server = MockServer::start(); + let backend = test_backend(&server.addr); + + // Create a 1MB blob + let large_data: Vec = (0..1_000_000).map(|i| (i % 256) as u8).collect(); + backend + .put_blob(BlobKind::Object, "large", &large_data) + .unwrap(); + let retrieved = backend.get_blob(BlobKind::Object, "large").unwrap(); + assert_eq!(retrieved.len(), large_data.len()); + assert_eq!(retrieved, large_data); + } +} diff --git a/crates/karapace-remote/src/lib.rs b/crates/karapace-remote/src/lib.rs new file mode 100644 index 0000000..10f36af --- /dev/null +++ b/crates/karapace-remote/src/lib.rs @@ -0,0 +1,83 @@ +//! Remote store synchronization for sharing Karapace environments. +//! +//! This crate provides push/pull transfer of content-addressable objects and layer +//! manifests to/from a remote HTTP backend, a registry for named environment +//! references, and configuration for remote endpoints with optional authentication. + +pub mod config; +pub mod http; +pub mod registry; +pub mod transfer; + +pub use config::RemoteConfig; +pub use registry::{parse_ref, Registry, RegistryEntry}; +pub use transfer::{pull_env, push_env, resolve_ref, PullResult, PushResult}; + +/// Protocol version sent as `X-Karapace-Protocol` header on all HTTP requests. +/// Servers can reject clients with incompatible protocol versions. +pub const PROTOCOL_VERSION: u32 = 1; + +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum RemoteError { + #[error("remote I/O error: {0}")] + Io(#[from] std::io::Error), + #[error("HTTP error: {0}")] + Http(String), + #[error("store error: {0}")] + Store(#[from] karapace_store::StoreError), + #[error("serialization error: {0}")] + Serialization(String), + #[error("not found: {0}")] + NotFound(String), + #[error("remote config error: {0}")] + Config(String), + #[error("integrity failure for '{key}': expected {expected}, got {actual}")] + IntegrityFailure { + key: String, + expected: String, + actual: String, + }, +} + +/// A content-addressable blob in the remote store. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BlobKind { + Object, + Layer, + Metadata, +} + +/// Trait for remote storage backends. +pub trait RemoteBackend: Send + Sync { + /// Upload a blob to the remote store. Returns the key used. + fn put_blob(&self, kind: BlobKind, key: &str, data: &[u8]) -> Result<(), RemoteError>; + + /// Download a blob from the remote store. + fn get_blob(&self, kind: BlobKind, key: &str) -> Result, RemoteError>; + + /// Check if a blob exists in the remote store. + fn has_blob(&self, kind: BlobKind, key: &str) -> Result; + + /// List all blobs of a given kind. + fn list_blobs(&self, kind: BlobKind) -> Result, RemoteError>; + + /// Upload the registry index. + fn put_registry(&self, data: &[u8]) -> Result<(), RemoteError>; + + /// Download the registry index. + fn get_registry(&self) -> Result, RemoteError>; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn blob_kind_debug() { + assert_eq!(format!("{:?}", BlobKind::Object), "Object"); + assert_eq!(format!("{:?}", BlobKind::Layer), "Layer"); + assert_eq!(format!("{:?}", BlobKind::Metadata), "Metadata"); + } +} diff --git a/crates/karapace-remote/src/registry.rs b/crates/karapace-remote/src/registry.rs new file mode 100644 index 0000000..b942f10 --- /dev/null +++ b/crates/karapace-remote/src/registry.rs @@ -0,0 +1,159 @@ +use crate::RemoteError; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; + +/// A single entry in the remote registry, mapping a tag to an env_id. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct RegistryEntry { + pub env_id: String, + pub short_id: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub name: Option, + pub pushed_at: String, +} + +/// The registry index: maps `name@tag` keys to environment entries. +/// Example: `"my-env@latest"` → `RegistryEntry { env_id: "abc...", ... }` +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] +pub struct Registry { + pub entries: BTreeMap, +} + +impl Registry { + pub fn new() -> Self { + Self::default() + } + + pub fn from_bytes(data: &[u8]) -> Result { + serde_json::from_slice(data) + .map_err(|e| RemoteError::Serialization(format!("invalid registry: {e}"))) + } + + pub fn to_bytes(&self) -> Result, RemoteError> { + serde_json::to_vec_pretty(self).map_err(|e| RemoteError::Serialization(e.to_string())) + } + + /// Insert or update an entry. Key format: `name@tag` or just `env_id`. + pub fn publish(&mut self, key: &str, entry: RegistryEntry) { + self.entries.insert(key.to_owned(), entry); + } + + /// Look up an entry by key. + pub fn lookup(&self, key: &str) -> Option<&RegistryEntry> { + self.entries.get(key) + } + + /// List all keys in the registry. + pub fn list_keys(&self) -> Vec<&str> { + self.entries.keys().map(String::as_str).collect() + } + + /// Find entries by env_id. + pub fn find_by_env_id(&self, env_id: &str) -> Vec<(&str, &RegistryEntry)> { + self.entries + .iter() + .filter(|(_, v)| v.env_id == env_id) + .map(|(k, v)| (k.as_str(), v)) + .collect() + } +} + +/// Parse a reference like `name@tag` into (name, tag). +/// If no `@` is present, the whole string is treated as the name with tag "latest". +pub fn parse_ref(reference: &str) -> (&str, &str) { + match reference.split_once('@') { + Some((name, tag)) => (name, tag), + None => (reference, "latest"), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn registry_roundtrip() { + let mut reg = Registry::new(); + reg.publish( + "my-env@latest", + RegistryEntry { + env_id: "abc123".to_owned(), + short_id: "abc123".to_owned(), + name: Some("my-env".to_owned()), + pushed_at: "2025-01-01T00:00:00Z".to_owned(), + }, + ); + + let bytes = reg.to_bytes().unwrap(); + let loaded = Registry::from_bytes(&bytes).unwrap(); + assert_eq!(loaded, reg); + } + + #[test] + fn registry_lookup() { + let mut reg = Registry::new(); + reg.publish( + "dev@v1", + RegistryEntry { + env_id: "hash1".to_owned(), + short_id: "hash1".to_owned(), + name: None, + pushed_at: "2025-01-01T00:00:00Z".to_owned(), + }, + ); + assert!(reg.lookup("dev@v1").is_some()); + assert!(reg.lookup("nonexistent").is_none()); + } + + #[test] + fn parse_ref_with_tag() { + assert_eq!(parse_ref("my-env@v2"), ("my-env", "v2")); + } + + #[test] + fn parse_ref_without_tag() { + assert_eq!(parse_ref("my-env"), ("my-env", "latest")); + } + + #[test] + fn find_by_env_id_works() { + let mut reg = Registry::new(); + reg.publish( + "a@latest", + RegistryEntry { + env_id: "hash1".to_owned(), + short_id: "hash1".to_owned(), + name: None, + pushed_at: "t".to_owned(), + }, + ); + reg.publish( + "b@latest", + RegistryEntry { + env_id: "hash1".to_owned(), + short_id: "hash1".to_owned(), + name: None, + pushed_at: "t".to_owned(), + }, + ); + reg.publish( + "c@latest", + RegistryEntry { + env_id: "hash2".to_owned(), + short_id: "hash2".to_owned(), + name: None, + pushed_at: "t".to_owned(), + }, + ); + let found = reg.find_by_env_id("hash1"); + assert_eq!(found.len(), 2); + } + + #[test] + fn empty_registry_roundtrip() { + let reg = Registry::new(); + let bytes = reg.to_bytes().unwrap(); + let loaded = Registry::from_bytes(&bytes).unwrap(); + assert!(loaded.entries.is_empty()); + } +} diff --git a/crates/karapace-remote/src/transfer.rs b/crates/karapace-remote/src/transfer.rs new file mode 100644 index 0000000..f12c069 --- /dev/null +++ b/crates/karapace-remote/src/transfer.rs @@ -0,0 +1,742 @@ +use crate::{BlobKind, Registry, RegistryEntry, RemoteBackend, RemoteError}; +use karapace_store::{LayerStore, MetadataStore, ObjectStore, StoreLayout}; + +/// Result of a push operation. +#[derive(Debug)] +pub struct PushResult { + pub objects_pushed: usize, + pub layers_pushed: usize, + pub objects_skipped: usize, + pub layers_skipped: usize, +} + +/// Result of a pull operation. +#[derive(Debug)] +pub struct PullResult { + pub objects_pulled: usize, + pub layers_pulled: usize, + pub objects_skipped: usize, + pub layers_skipped: usize, +} + +/// Push an environment (metadata + layers + objects) to a remote store. +/// Optionally publish it under a registry key (e.g. `"my-env@latest"`). +pub fn push_env( + layout: &StoreLayout, + env_id: &str, + backend: &dyn RemoteBackend, + registry_key: Option<&str>, +) -> Result { + let meta_store = MetadataStore::new(layout.clone()); + let layer_store = LayerStore::new(layout.clone()); + let object_store = ObjectStore::new(layout.clone()); + + // 1. Read metadata + let meta = meta_store.get(env_id)?; + let meta_json = + serde_json::to_vec_pretty(&meta).map_err(|e| RemoteError::Serialization(e.to_string()))?; + + // 2. Collect all layer hashes (base + deps) + let mut layer_hashes = vec![meta.base_layer.clone()]; + layer_hashes.extend(meta.dependency_layers.iter().cloned()); + + // 3. Collect all object hashes from layers + manifest + let mut object_hashes = Vec::new(); + if !meta.manifest_hash.is_empty() { + object_hashes.push(meta.manifest_hash.to_string()); + } + for lh in &layer_hashes { + let layer = layer_store.get(lh)?; + object_hashes.extend(layer.object_refs.iter().cloned()); + } + object_hashes.sort(); + object_hashes.dedup(); + + // 4. Push objects (skip existing) + let mut objects_pushed = 0; + let mut objects_skipped = 0; + for hash in &object_hashes { + if backend.has_blob(BlobKind::Object, hash)? { + objects_skipped += 1; + continue; + } + let data = object_store.get(hash)?; + backend.put_blob(BlobKind::Object, hash, &data)?; + objects_pushed += 1; + } + + // 5. Push layers (skip existing) + let mut layers_pushed = 0; + let mut layers_skipped = 0; + for lh in &layer_hashes { + if backend.has_blob(BlobKind::Layer, lh)? { + layers_skipped += 1; + continue; + } + let layer = layer_store.get(lh)?; + let data = serde_json::to_vec_pretty(&layer) + .map_err(|e| RemoteError::Serialization(e.to_string()))?; + backend.put_blob(BlobKind::Layer, lh, &data)?; + layers_pushed += 1; + } + + // 6. Push metadata + backend.put_blob(BlobKind::Metadata, env_id, &meta_json)?; + + // 7. Update registry if key provided + if let Some(key) = registry_key { + let mut registry = match backend.get_registry() { + Ok(data) => Registry::from_bytes(&data).unwrap_or_default(), + Err(_) => Registry::new(), + }; + registry.publish( + key, + RegistryEntry { + env_id: meta.env_id.to_string(), + short_id: meta.short_id.to_string(), + name: meta.name.clone(), + pushed_at: chrono::Utc::now().to_rfc3339(), + }, + ); + let reg_bytes = registry.to_bytes()?; + backend.put_registry(®_bytes)?; + } + + Ok(PushResult { + objects_pushed, + layers_pushed, + objects_skipped, + layers_skipped, + }) +} + +/// Pull an environment from a remote store into the local store. +pub fn pull_env( + layout: &StoreLayout, + env_id: &str, + backend: &dyn RemoteBackend, +) -> Result { + let meta_store = MetadataStore::new(layout.clone()); + let layer_store = LayerStore::new(layout.clone()); + let object_store = ObjectStore::new(layout.clone()); + + // 1. Download metadata and verify checksum if present + let meta_bytes = backend.get_blob(BlobKind::Metadata, env_id)?; + let meta: karapace_store::EnvMetadata = serde_json::from_slice(&meta_bytes) + .map_err(|e| RemoteError::Serialization(format!("invalid metadata: {e}")))?; + if let Some(ref expected) = meta.checksum { + let mut copy = meta.clone(); + copy.checksum = None; + let json = serde_json::to_string_pretty(©) + .map_err(|e| RemoteError::Serialization(e.to_string()))?; + let actual = blake3::hash(json.as_bytes()).to_hex().to_string(); + if actual != *expected { + return Err(RemoteError::IntegrityFailure { + key: format!("metadata:{env_id}"), + expected: expected.clone(), + actual, + }); + } + } + + // 2. Collect layer hashes + let mut layer_hashes = vec![meta.base_layer.clone()]; + layer_hashes.extend(meta.dependency_layers.iter().cloned()); + + // 3. Download layers (skip existing) + let mut layers_pulled = 0; + let mut layers_skipped = 0; + let mut object_hashes = Vec::new(); + if !meta.manifest_hash.is_empty() { + object_hashes.push(meta.manifest_hash.to_string()); + } + for lh in &layer_hashes { + if layer_store.exists(lh) { + let layer = layer_store.get(lh)?; + object_hashes.extend(layer.object_refs.iter().cloned()); + layers_skipped += 1; + continue; + } + let data = backend.get_blob(BlobKind::Layer, lh)?; + let layer: karapace_store::LayerManifest = serde_json::from_slice(&data) + .map_err(|e| RemoteError::Serialization(format!("invalid layer: {e}")))?; + object_hashes.extend(layer.object_refs.iter().cloned()); + let stored_hash = layer_store.put(&layer)?; + if stored_hash != **lh { + return Err(RemoteError::IntegrityFailure { + key: lh.to_string(), + expected: lh.to_string(), + actual: stored_hash, + }); + } + layers_pulled += 1; + } + object_hashes.sort(); + object_hashes.dedup(); + + // 4. Download objects (skip existing, verify blake3 integrity) + let mut objects_pulled = 0; + let mut objects_skipped = 0; + for hash in &object_hashes { + if object_store.exists(hash) { + objects_skipped += 1; + continue; + } + let data = backend.get_blob(BlobKind::Object, hash)?; + let actual = blake3::hash(&data).to_hex().to_string(); + if actual != *hash { + return Err(RemoteError::IntegrityFailure { + key: hash.clone(), + expected: hash.clone(), + actual, + }); + } + object_store.put(&data)?; + objects_pulled += 1; + } + + // 5. Store metadata locally + meta_store.put(&meta)?; + + Ok(PullResult { + objects_pulled, + layers_pulled, + objects_skipped, + layers_skipped, + }) +} + +/// Resolve a registry reference (e.g. "my-env@latest") to an env_id using the remote registry. +pub fn resolve_ref(backend: &dyn RemoteBackend, reference: &str) -> Result { + let reg_bytes = backend.get_registry()?; + let registry = Registry::from_bytes(®_bytes)?; + let (name, tag) = crate::registry::parse_ref(reference); + let key = format!("{name}@{tag}"); + let entry = registry + .lookup(&key) + .ok_or_else(|| RemoteError::NotFound(format!("registry key '{key}' not found")))?; + Ok(entry.env_id.clone()) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashMap; + use std::sync::Mutex; + + /// In-memory mock remote backend for testing. + struct MockRemote { + blobs: Mutex>>, + registry: Mutex>>, + } + + impl MockRemote { + fn new() -> Self { + Self { + blobs: Mutex::new(HashMap::new()), + registry: Mutex::new(None), + } + } + + fn blob_key(kind: BlobKind, key: &str) -> String { + format!("{kind:?}/{key}") + } + } + + impl RemoteBackend for MockRemote { + fn put_blob(&self, kind: BlobKind, key: &str, data: &[u8]) -> Result<(), RemoteError> { + self.blobs + .lock() + .unwrap() + .insert(Self::blob_key(kind, key), data.to_vec()); + Ok(()) + } + + fn get_blob(&self, kind: BlobKind, key: &str) -> Result, RemoteError> { + self.blobs + .lock() + .unwrap() + .get(&Self::blob_key(kind, key)) + .cloned() + .ok_or_else(|| RemoteError::NotFound(key.to_owned())) + } + + fn has_blob(&self, kind: BlobKind, key: &str) -> Result { + Ok(self + .blobs + .lock() + .unwrap() + .contains_key(&Self::blob_key(kind, key))) + } + + fn list_blobs(&self, kind: BlobKind) -> Result, RemoteError> { + let prefix = format!("{kind:?}/"); + let blobs = self.blobs.lock().unwrap(); + Ok(blobs + .keys() + .filter(|k| k.starts_with(&prefix)) + .map(|k| k[prefix.len()..].to_owned()) + .collect()) + } + + fn put_registry(&self, data: &[u8]) -> Result<(), RemoteError> { + *self.registry.lock().unwrap() = Some(data.to_vec()); + Ok(()) + } + + fn get_registry(&self) -> Result, RemoteError> { + self.registry + .lock() + .unwrap() + .clone() + .ok_or_else(|| RemoteError::NotFound("registry".to_owned())) + } + } + + fn setup_local_env(dir: &std::path::Path) -> (StoreLayout, String) { + let layout = StoreLayout::new(dir); + layout.initialize().unwrap(); + + let obj_store = ObjectStore::new(layout.clone()); + let layer_store = LayerStore::new(layout.clone()); + let meta_store = MetadataStore::new(layout.clone()); + + // Create a test object (layer content) + let obj_hash = obj_store.put(b"test data content").unwrap(); + + // Create a manifest object (environment manifest) + let manifest_hash = obj_store.put(b"{\"manifest\": \"test\"}").unwrap(); + + // Create a base layer referencing the object + let layer = karapace_store::LayerManifest { + hash: "layer_hash_001".to_owned(), + kind: karapace_store::LayerKind::Base, + parent: None, + object_refs: vec![obj_hash], + read_only: true, + tar_hash: String::new(), + }; + let layer_content_hash = layer_store.put(&layer).unwrap(); + + // Create environment metadata + let meta = karapace_store::EnvMetadata { + env_id: "env_abc123".into(), + short_id: "env_abc123".into(), + name: Some("test-env".to_owned()), + state: karapace_store::EnvState::Built, + base_layer: layer_content_hash.into(), + dependency_layers: vec![], + policy_layer: None, + manifest_hash: manifest_hash.into(), + ref_count: 1, + created_at: "2025-01-01T00:00:00Z".to_owned(), + updated_at: "2025-01-01T00:00:00Z".to_owned(), + checksum: None, + }; + meta_store.put(&meta).unwrap(); + + (layout, "env_abc123".to_owned()) + } + + #[test] + fn push_and_pull_roundtrip() { + let src_dir = tempfile::tempdir().unwrap(); + let (src_layout, env_id) = setup_local_env(src_dir.path()); + + let remote = MockRemote::new(); + + // Push + let push_result = push_env(&src_layout, &env_id, &remote, Some("test-env@latest")).unwrap(); + assert_eq!(push_result.objects_pushed, 2); // layer content + manifest + assert_eq!(push_result.layers_pushed, 1); + assert_eq!(push_result.objects_skipped, 0); + + // Pull into a fresh store + let dst_dir = tempfile::tempdir().unwrap(); + let dst_layout = StoreLayout::new(dst_dir.path()); + dst_layout.initialize().unwrap(); + + let pull_result = pull_env(&dst_layout, &env_id, &remote).unwrap(); + assert_eq!(pull_result.objects_pulled, 2); // layer content + manifest + assert_eq!(pull_result.layers_pulled, 1); + + // Verify metadata exists in destination + let dst_meta = MetadataStore::new(dst_layout); + let meta = dst_meta.get(&env_id).unwrap(); + assert_eq!(meta.name, Some("test-env".to_owned())); + } + + #[test] + fn push_skips_existing_blobs() { + let src_dir = tempfile::tempdir().unwrap(); + let (src_layout, env_id) = setup_local_env(src_dir.path()); + let remote = MockRemote::new(); + + // Push once + push_env(&src_layout, &env_id, &remote, None).unwrap(); + + // Push again — should skip everything + let result = push_env(&src_layout, &env_id, &remote, None).unwrap(); + assert_eq!(result.objects_skipped, 2); // layer content + manifest + assert_eq!(result.layers_skipped, 1); + assert_eq!(result.objects_pushed, 0); + assert_eq!(result.layers_pushed, 0); + } + + #[test] + fn resolve_ref_from_registry() { + let remote = MockRemote::new(); + + // Manually push a registry + let mut reg = Registry::new(); + reg.publish( + "my-env@latest", + RegistryEntry { + env_id: "hash_xyz".to_owned(), + short_id: "hash_xyz".to_owned(), + name: None, + pushed_at: "t".to_owned(), + }, + ); + remote.put_registry(®.to_bytes().unwrap()).unwrap(); + + let resolved = resolve_ref(&remote, "my-env@latest").unwrap(); + assert_eq!(resolved, "hash_xyz"); + + // Without @tag → defaults to @latest + let resolved2 = resolve_ref(&remote, "my-env").unwrap(); + assert_eq!(resolved2, "hash_xyz"); + } + + #[test] + fn pull_nonexistent_env_fails() { + let remote = MockRemote::new(); + let dir = tempfile::tempdir().unwrap(); + let layout = StoreLayout::new(dir.path()); + layout.initialize().unwrap(); + + let result = pull_env(&layout, "nonexistent_env", &remote); + assert!(result.is_err()); + } + + #[test] + fn resolve_ref_not_found_fails() { + let remote = MockRemote::new(); + let mut reg = Registry::new(); + reg.publish( + "other@latest", + RegistryEntry { + env_id: "xyz".to_owned(), + short_id: "xyz".to_owned(), + name: None, + pushed_at: "t".to_owned(), + }, + ); + remote.put_registry(®.to_bytes().unwrap()).unwrap(); + + let result = resolve_ref(&remote, "missing-env@latest"); + assert!(result.is_err()); + } + + #[test] + fn pull_skips_existing_objects() { + let src_dir = tempfile::tempdir().unwrap(); + let (src_layout, env_id) = setup_local_env(src_dir.path()); + let remote = MockRemote::new(); + + push_env(&src_layout, &env_id, &remote, None).unwrap(); + + // Pull into destination that already has the objects + let dst_dir = tempfile::tempdir().unwrap(); + let dst_layout = StoreLayout::new(dst_dir.path()); + dst_layout.initialize().unwrap(); + + // First pull + pull_env(&dst_layout, &env_id, &remote).unwrap(); + + // Second pull — should skip existing + let result = pull_env(&dst_layout, &env_id, &remote).unwrap(); + assert_eq!(result.objects_skipped, 2); // layer content + manifest + assert_eq!(result.layers_skipped, 1); + assert_eq!(result.objects_pulled, 0); + assert_eq!(result.layers_pulled, 0); + } + + #[test] + fn push_result_fields_correct() { + let src_dir = tempfile::tempdir().unwrap(); + let (src_layout, env_id) = setup_local_env(src_dir.path()); + let remote = MockRemote::new(); + + let result = push_env(&src_layout, &env_id, &remote, None).unwrap(); + assert!(result.objects_pushed > 0 || result.objects_skipped > 0); + assert!(result.layers_pushed > 0 || result.layers_skipped > 0); + } + + #[test] + fn pull_transfers_manifest_object() { + let src_dir = tempfile::tempdir().unwrap(); + let (src_layout, env_id) = setup_local_env(src_dir.path()); + let remote = MockRemote::new(); + + push_env(&src_layout, &env_id, &remote, None).unwrap(); + + // Pull into a fresh store + let dst_dir = tempfile::tempdir().unwrap(); + let dst_layout = StoreLayout::new(dst_dir.path()); + dst_layout.initialize().unwrap(); + + pull_env(&dst_layout, &env_id, &remote).unwrap(); + + // Verify the manifest object is accessible in the destination store + let dst_meta = MetadataStore::new(dst_layout.clone()); + let meta = dst_meta.get(&env_id).unwrap(); + let dst_obj = ObjectStore::new(dst_layout); + let manifest_data = dst_obj.get(&meta.manifest_hash); + assert!( + manifest_data.is_ok(), + "manifest object must be available after pull: {:?}", + manifest_data.err() + ); + } + + #[test] + fn pull_detects_tampered_metadata_checksum() { + let src_dir = tempfile::tempdir().unwrap(); + let (src_layout, env_id) = setup_local_env(src_dir.path()); + let remote = MockRemote::new(); + + // Push to populate the remote + push_env(&src_layout, &env_id, &remote, None).unwrap(); + + // Tamper with the metadata blob on the remote: change the name field + // but leave the checksum intact (so it mismatches) + let meta_bytes = remote.get_blob(BlobKind::Metadata, &env_id).unwrap(); + let mut meta: serde_json::Value = serde_json::from_slice(&meta_bytes).unwrap(); + meta["name"] = serde_json::Value::String("tampered".into()); + let tampered = serde_json::to_string_pretty(&meta).unwrap(); + remote + .put_blob(BlobKind::Metadata, &env_id, tampered.as_bytes()) + .unwrap(); + + // Pull into a fresh store — should fail with integrity error + let dst_dir = tempfile::tempdir().unwrap(); + let dst_layout = StoreLayout::new(dst_dir.path()); + dst_layout.initialize().unwrap(); + + let result = pull_env(&dst_layout, &env_id, &remote); + assert!( + result.is_err(), + "pull must fail when metadata checksum is tampered" + ); + } + + #[test] + fn push_with_tag_publishes_registry() { + let src_dir = tempfile::tempdir().unwrap(); + let (src_layout, env_id) = setup_local_env(src_dir.path()); + let remote = MockRemote::new(); + + push_env(&src_layout, &env_id, &remote, Some("my-app@v1")).unwrap(); + + // Verify registry was published + let reg_bytes = remote.get_registry().unwrap(); + let reg = Registry::from_bytes(®_bytes).unwrap(); + let entry = reg.lookup("my-app@v1").unwrap(); + assert_eq!(entry.env_id, env_id); + } + + // --- §7: Network failure simulation --- + + /// Mock remote that fails on the Nth put_blob call. + struct FailOnPutRemote { + inner: MockRemote, + call_count: Mutex, + fail_on: usize, + } + + impl FailOnPutRemote { + fn new(fail_on: usize) -> Self { + Self { + inner: MockRemote::new(), + call_count: Mutex::new(0), + fail_on, + } + } + } + + impl RemoteBackend for FailOnPutRemote { + fn put_blob(&self, kind: BlobKind, key: &str, data: &[u8]) -> Result<(), RemoteError> { + let mut count = self.call_count.lock().unwrap(); + *count += 1; + if *count >= self.fail_on { + return Err(RemoteError::Http("simulated network failure".to_owned())); + } + drop(count); + self.inner.put_blob(kind, key, data) + } + + fn get_blob(&self, kind: BlobKind, key: &str) -> Result, RemoteError> { + self.inner.get_blob(kind, key) + } + + fn has_blob(&self, kind: BlobKind, key: &str) -> Result { + self.inner.has_blob(kind, key) + } + + fn list_blobs(&self, kind: BlobKind) -> Result, RemoteError> { + self.inner.list_blobs(kind) + } + + fn put_registry(&self, data: &[u8]) -> Result<(), RemoteError> { + self.inner.put_registry(data) + } + + fn get_registry(&self) -> Result, RemoteError> { + self.inner.get_registry() + } + } + + /// Mock remote that returns garbage on get_blob. + struct CorruptGetRemote { + inner: MockRemote, + } + + impl CorruptGetRemote { + fn new() -> Self { + Self { + inner: MockRemote::new(), + } + } + } + + impl RemoteBackend for CorruptGetRemote { + fn put_blob(&self, kind: BlobKind, key: &str, data: &[u8]) -> Result<(), RemoteError> { + self.inner.put_blob(kind, key, data) + } + + fn get_blob(&self, kind: BlobKind, key: &str) -> Result, RemoteError> { + // Return corrupted data for objects (not metadata/layers which are JSON) + if matches!(kind, BlobKind::Object) { + let real = self.inner.get_blob(kind, key)?; + let mut corrupted = real; + if !corrupted.is_empty() { + corrupted[0] ^= 0xFF; + } + Ok(corrupted) + } else { + self.inner.get_blob(kind, key) + } + } + + fn has_blob(&self, kind: BlobKind, key: &str) -> Result { + self.inner.has_blob(kind, key) + } + + fn list_blobs(&self, kind: BlobKind) -> Result, RemoteError> { + self.inner.list_blobs(kind) + } + + fn put_registry(&self, data: &[u8]) -> Result<(), RemoteError> { + self.inner.put_registry(data) + } + + fn get_registry(&self) -> Result, RemoteError> { + self.inner.get_registry() + } + } + + #[test] + fn push_fails_on_network_error() { + let src_dir = tempfile::tempdir().unwrap(); + let (src_layout, env_id) = setup_local_env(src_dir.path()); + + // Fail on the very first put_blob call + let remote = FailOnPutRemote::new(1); + let result = push_env(&src_layout, &env_id, &remote, None); + assert!( + result.is_err(), + "push must fail when network error occurs during upload" + ); + } + + #[test] + fn pull_detects_corrupted_remote_object() { + let src_dir = tempfile::tempdir().unwrap(); + let (src_layout, env_id) = setup_local_env(src_dir.path()); + let corrupt_remote = CorruptGetRemote::new(); + + // Push via the inner (uncorrupted) remote first + push_env(&src_layout, &env_id, &corrupt_remote.inner, None).unwrap(); + + // Pull via the corrupting remote — objects will have flipped bytes + let dst_dir = tempfile::tempdir().unwrap(); + let dst_layout = StoreLayout::new(dst_dir.path()); + dst_layout.initialize().unwrap(); + + let result = pull_env(&dst_layout, &env_id, &corrupt_remote); + assert!( + result.is_err(), + "pull must fail when remote returns corrupted object data" + ); + } + + #[test] + fn large_object_push_pull_roundtrip() { + let src_dir = tempfile::tempdir().unwrap(); + let layout = StoreLayout::new(src_dir.path()); + layout.initialize().unwrap(); + + let obj_store = ObjectStore::new(layout.clone()); + let layer_store = LayerStore::new(layout.clone()); + let meta_store = MetadataStore::new(layout.clone()); + + // Create a 1MB object (simulating a large layer tar) + let large_data: Vec = (0..1_048_576u32).map(|i| (i % 256) as u8).collect(); + let obj_hash = obj_store.put(&large_data).unwrap(); + + let manifest_hash = obj_store.put(b"{\"manifest\": \"large\"}").unwrap(); + + let layer = karapace_store::LayerManifest { + hash: "large_layer".to_owned(), + kind: karapace_store::LayerKind::Base, + parent: None, + object_refs: vec![obj_hash], + read_only: true, + tar_hash: String::new(), + }; + let layer_hash = layer_store.put(&layer).unwrap(); + + let meta = karapace_store::EnvMetadata { + env_id: "large_env".into(), + short_id: "large_env".into(), + name: None, + state: karapace_store::EnvState::Built, + base_layer: layer_hash.into(), + dependency_layers: vec![], + policy_layer: None, + manifest_hash: manifest_hash.into(), + ref_count: 1, + created_at: "2025-01-01T00:00:00Z".to_owned(), + updated_at: "2025-01-01T00:00:00Z".to_owned(), + checksum: None, + }; + meta_store.put(&meta).unwrap(); + + let remote = MockRemote::new(); + push_env(&layout, "large_env", &remote, None).unwrap(); + + // Pull into fresh store + let dst_dir = tempfile::tempdir().unwrap(); + let dst_layout = StoreLayout::new(dst_dir.path()); + dst_layout.initialize().unwrap(); + + let result = pull_env(&dst_layout, "large_env", &remote).unwrap(); + assert_eq!(result.objects_pulled, 2); + + // Verify the large object survived the roundtrip + let dst_obj = ObjectStore::new(dst_layout); + let pulled = dst_obj.get(&meta.manifest_hash).unwrap(); + assert_eq!(pulled, b"{\"manifest\": \"large\"}"); + } +}