Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,12 @@ jobs:
with:
toolchain: stable
targets: wasm32-unknown-unknown
components: clippy
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
- run: cargo check -p multistore-cf-workers --target wasm32-unknown-unknown
# The Workers crates are excluded from `default-members`, so the native
# clippy job never sees them. Lint them on their real target.
- run: cargo clippy -p multistore-cf-workers -p multistore-cf-workers-example --target wasm32-unknown-unknown -- -D warnings

audit:
name: Audit
Expand Down
9 changes: 9 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,22 @@ cargo check
# Check (WASM)
cargo check -p multistore-cf-workers --target wasm32-unknown-unknown

# Lint (WASM) — the Workers crates are not in `default-members`, so lint them explicitly
cargo clippy -p multistore-cf-workers -p multistore-cf-workers-example --target wasm32-unknown-unknown -- -D warnings

# Unit + doc tests
cargo test

# Integration tests (RustFS via docker compose behind wrangler dev; mirrors CI)
make test-integration
```

### Lints and MSRV

Lint levels live in `[workspace.lints]` in the root `Cargo.toml`; every crate opts in with `[lints] workspace = true`. Beyond clippy's defaults the workspace warns on `missing_docs` (document every `pub` item) and `clippy::unwrap_used` (return an error, or use `expect` with a message that states the invariant). Tests are exempt from the unwrap lint via `clippy.toml`. CI runs clippy with `-D warnings`, so a new warning fails the build.

The minimum supported Rust version is declared as `rust-version` in the root `Cargo.toml`. It is set by the dependency tree; raise it only when a dependency forces it.

## Release Process

### Publishable Crates
Expand Down
11 changes: 11 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@ resolver = "2"
version = "0.7.2"
edition = "2021"
license = "MIT"
# Floor set by the dependency tree (crc-fast via object_store); verified with `cargo +1.89 check`.
rust-version = "1.89"

# Opted into per crate with `[lints] workspace = true`.
[workspace.lints.rust]
missing_docs = "warn"

[workspace.lints.clippy]
# Production code returns errors; `expect` with a message documents a real invariant.
# Tests are exempt via `allow-unwrap-in-tests` in clippy.toml.
unwrap_used = "warn"

[workspace.dependencies]
# Core
Expand Down
4 changes: 3 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ fmt-fix:

clippy:
cargo clippy -- -D warnings
clippy-wasm:
cargo clippy -p multistore-cf-workers -p multistore-cf-workers-example --target wasm32-unknown-unknown -- -D warnings
clippy-fix:
cargo clippy --fix --allow-dirty --allow-staged

Expand All @@ -35,7 +37,7 @@ run-server:
run-workers:
npx wrangler dev --cwd examples/cf-workers

ci-fast: fmt clippy check-wasm
ci-fast: fmt clippy clippy-wasm check-wasm
ci: ci-fast test

docs:
Expand Down
1 change: 1 addition & 0 deletions clippy.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
allow-unwrap-in-tests = true
3 changes: 3 additions & 0 deletions crates/cf-workers/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,6 @@ features = ["wasm_js"]
[dependencies.ring]
version = "0.17"
features = ["wasm32_unknown_unknown_js"]

[lints]
workspace = true
2 changes: 1 addition & 1 deletion crates/cf-workers/src/headers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ impl WsHeaders {

impl From<&HeaderMap> for WsHeaders {
fn from(headers: &HeaderMap) -> Self {
let ws = web_sys::Headers::new().unwrap();
let ws = web_sys::Headers::new().expect("Headers::new() cannot fail");
for (key, value) in headers.iter() {
if let Ok(v) = value.to_str() {
let _ = ws.set(key.as_str(), v);
Expand Down
9 changes: 4 additions & 5 deletions crates/cf-workers/src/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,12 @@ pub(crate) fn response_from_proxy_result(result: ProxyResult) -> web_sys::Respon
resp_init.set_headers(&WsHeaders::from(&result.headers).into_inner().into());

match result.body {
ProxyResponseBody::Empty => {
web_sys::Response::new_with_opt_str_and_init(None, &resp_init).unwrap()
}
ProxyResponseBody::Empty => web_sys::Response::new_with_opt_str_and_init(None, &resp_init)
.unwrap_or_else(|_| error_response(500, "Internal Server Error")),
ProxyResponseBody::Bytes(bytes) => {
let uint8 = js_sys::Uint8Array::from(bytes.as_ref());
web_sys::Response::new_with_opt_buffer_source_and_init(Some(&uint8), &resp_init)
.unwrap()
.unwrap_or_else(|_| error_response(500, "Internal Server Error"))
}
}
}
Expand Down Expand Up @@ -69,7 +68,7 @@ pub(crate) fn error_response(status: u16, message: &str) -> web_sys::Response {
let init = web_sys::ResponseInit::new();
init.set_status(status);
web_sys::Response::new_with_opt_str_and_init(Some(message), &init)
.unwrap_or_else(|_| web_sys::Response::new().unwrap())
.unwrap_or_else(|_| web_sys::Response::new().expect("Response::new() cannot fail"))
}

/// Extension trait for converting a [`GatewayResponse`] into a `web_sys::Response`.
Expand Down
3 changes: 3 additions & 0 deletions crates/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,6 @@ object_store.workspace = true
reqwest.workspace = true
futures.workspace = true
matchit.workspace = true

[lints]
workspace = true
6 changes: 5 additions & 1 deletion crates/core/src/api/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -273,12 +273,16 @@ pub fn validate_key(key: &str) -> Result<(), ProxyError> {
Ok(())
}

/// How the bucket name was addressed in the incoming request.
#[derive(Debug, Clone)]
pub enum HostStyle {
/// Path-style: `/{bucket}/{key}`
Path,
/// Virtual-hosted-style: bucket extracted from Host header.
VirtualHosted { bucket: String },
VirtualHosted {
/// The bucket name taken from the `Host` header.
bucket: String,
},
}

fn parse_path_style(path: &str) -> Result<(String, String), ProxyError> {
Expand Down
15 changes: 15 additions & 0 deletions crates/core/src/api/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,16 @@ pub use crate::types::BucketOwner;
#[derive(Debug, Serialize)]
#[serde(rename = "Error")]
pub struct ErrorResponse {
/// S3 error code (e.g. `NoSuchKey`).
#[serde(rename = "Code")]
pub code: String,
/// Human-readable description of the error.
#[serde(rename = "Message")]
pub message: String,
/// The request path the error relates to.
#[serde(rename = "Resource")]
pub resource: String,
/// The proxy request identifier, for correlating with logs.
#[serde(rename = "RequestId")]
pub request_id: String,
}
Expand Down Expand Up @@ -69,10 +73,13 @@ impl ErrorResponse {
#[derive(Debug, Serialize)]
#[serde(rename = "InitiateMultipartUploadResult")]
pub struct InitiateMultipartUploadResult {
/// The virtual bucket the upload targets.
#[serde(rename = "Bucket")]
pub bucket: String,
/// The object key being uploaded.
#[serde(rename = "Key")]
pub key: String,
/// Backend-issued identifier for the multipart upload.
#[serde(rename = "UploadId")]
pub upload_id: String,
}
Expand All @@ -91,12 +98,16 @@ impl InitiateMultipartUploadResult {
#[derive(Debug, Serialize)]
#[serde(rename = "CompleteMultipartUploadResult")]
pub struct CompleteMultipartUploadResult {
/// URL of the completed object.
#[serde(rename = "Location")]
pub location: String,
/// The virtual bucket the object was written to.
#[serde(rename = "Bucket")]
pub bucket: String,
/// The object key.
#[serde(rename = "Key")]
pub key: String,
/// ETag of the assembled object.
#[serde(rename = "ETag")]
pub etag: String,
}
Expand All @@ -115,6 +126,7 @@ impl CompleteMultipartUploadResult {
#[derive(Debug, serde::Deserialize)]
#[serde(rename = "CompleteMultipartUpload")]
pub struct CompleteMultipartUploadRequest {
/// The uploaded parts, in ascending part-number order.
#[serde(rename = "Part")]
pub parts: Vec<CompletePart>,
}
Expand All @@ -134,15 +146,18 @@ pub struct CompletePart {
#[derive(Debug, Serialize)]
#[serde(rename = "ListAllMyBucketsResult")]
pub struct ListAllMyBucketsResult {
/// The owner reported for every listed bucket.
#[serde(rename = "Owner")]
pub owner: BucketOwner,
/// The buckets visible to the caller.
#[serde(rename = "Buckets")]
pub buckets: BucketList,
}

/// Wrapper for the `<Buckets>` element in a ListAllMyBucketsResult response.
#[derive(Debug, Serialize)]
pub struct BucketList {
/// One `<Bucket>` element per virtual bucket.
#[serde(rename = "Bucket")]
pub buckets: Vec<BucketEntry>,
}
Expand Down
4 changes: 4 additions & 0 deletions crates/core/src/auth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ use crate::types::TemporaryCredentials;
/// Implementations handle token decryption/lookup. The core proxy calls this
/// during identity resolution without knowing the token format.
pub trait TemporaryCredentialResolver: MaybeSend + MaybeSync {
/// Resolve a session token into its temporary credentials.
///
/// Returns `Ok(None)` when the token is well-formed but unknown or
/// expired, and an error when it cannot be decoded at all.
fn resolve(&self, token: &str) -> Result<Option<TemporaryCredentials>, ProxyError>;
}

Expand Down
1 change: 1 addition & 0 deletions crates/core/src/auth/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ impl crate::registry::CredentialRegistry for MockConfig {
// ── Test signing helper ───────────────────────────────────────────

/// Build a valid SigV4 Authorization header value for testing.
#[allow(clippy::too_many_arguments)] // test helper mirrors the SigV4 parameter list
fn sign_request(
method: &http::Method,
uri_path: &str,
Expand Down
6 changes: 6 additions & 0 deletions crates/core/src/backend/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,11 @@ pub trait ProxyBackend: Clone + MaybeSend + MaybeSync + 'static {

/// Response from a raw HTTP request to a backend.
pub struct RawResponse {
/// HTTP status code from the backend.
pub status: u16,
/// Response headers from the backend.
pub headers: HeaderMap,
/// The fully buffered response body.
pub body: Bytes,
}

Expand All @@ -113,9 +116,12 @@ pub struct ForwardResponse<S> {
/// connector), then call [`build`](Self::build) or
/// [`build_signer`](Self::build_signer).
pub enum StoreBuilder {
/// Amazon S3 or an S3-compatible store.
S3(AmazonS3Builder),
/// Azure Blob Storage.
#[cfg(feature = "azure")]
Azure(MicrosoftAzureBuilder),
/// Google Cloud Storage.
#[cfg(feature = "gcp")]
Gcs(GoogleCloudStorageBuilder),
}
Expand Down
4 changes: 4 additions & 0 deletions crates/core/src/maybe_send.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,27 @@

// --- Native targets: MaybeSend = Send, MaybeSync = Sync ---

/// `Send` on native targets, no bound on wasm32.
#[cfg(not(target_arch = "wasm32"))]
pub trait MaybeSend: Send {}
#[cfg(not(target_arch = "wasm32"))]
impl<T: Send> MaybeSend for T {}

/// `Sync` on native targets, no bound on wasm32.
#[cfg(not(target_arch = "wasm32"))]
pub trait MaybeSync: Sync {}
#[cfg(not(target_arch = "wasm32"))]
impl<T: Sync> MaybeSync for T {}

// --- WASM targets: MaybeSend and MaybeSync are no-ops ---

/// `Send` on native targets, no bound on wasm32.
#[cfg(target_arch = "wasm32")]
pub trait MaybeSend {}
#[cfg(target_arch = "wasm32")]
impl<T> MaybeSend for T {}

/// `Sync` on native targets, no bound on wasm32.
#[cfg(target_arch = "wasm32")]
pub trait MaybeSync {}
#[cfg(target_arch = "wasm32")]
Expand Down
8 changes: 7 additions & 1 deletion crates/core/src/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ pub struct Router {
}

impl Router {
/// Create an empty router with no registered routes.
pub fn new() -> Self {
Self {
inner: matchit::Router::new(),
Expand All @@ -40,7 +41,12 @@ impl Router {
/// Register a handler for a path pattern.
///
/// Supports matchit syntax: `/exact`, `/prefix/{param}`, `/{*catch_all}`.
/// Panics if the path conflicts with an already-registered route.
///
/// # Panics
///
/// Panics if `path` conflicts with an already-registered route. Routes are
/// registered at startup from static configuration, so a conflict is a
/// programming error rather than a runtime condition to recover from.
pub fn route(mut self, path: &str, handler: impl RouteHandler + 'static) -> Self {
self.inner
.insert(path, Box::new(handler))
Expand Down
Loading
Loading