From 159c73142df50ef323e7c591d325cf658a005010 Mon Sep 17 00:00:00 2001 From: Aram Grigoryan <132480+aram356@users.noreply.github.com> Date: Tue, 9 Jun 2026 23:08:27 -0700 Subject: [PATCH] Sync EdgeZero PR 257 updates --- .../setup-integration-test-env/action.yml | 8 +- .github/dependabot.yml | 2 +- .github/pull_request_template.md | 4 +- .github/workflows/format.yml | 4 +- .github/workflows/integration-tests.yml | 20 +- .github/workflows/test.yml | 8 +- .gitignore | 10 +- .tool-versions | 7 +- AGENTS.md | 2 +- CLAUDE.md | 28 +- Cargo.lock | 40 +- Cargo.toml | 170 ++++++-- clippy.toml | 47 +- .../src/error.rs | 4 +- .../trusted-server-adapter-fastly/src/main.rs | 48 ++- .../src/management_api.rs | 38 +- .../src/platform.rs | 37 +- .../src/route_tests.rs | 13 +- crates/trusted-server-core/Cargo.toml | 4 +- .../benches/consent_decode.rs | 2 +- crates/trusted-server-core/build.rs | 19 +- .../src/auction/context.rs | 35 +- .../src/auction/endpoints.rs | 51 ++- .../src/auction/formats.rs | 28 +- .../src/auction/orchestrator.rs | 133 +++--- .../trusted-server-core/src/auction/types.rs | 6 +- .../src/auction_config_types.rs | 7 +- crates/trusted-server-core/src/auth.rs | 9 +- crates/trusted-server-core/src/backend.rs | 38 +- .../src/consent/extraction.rs | 3 +- crates/trusted-server-core/src/consent/gpp.rs | 11 +- .../src/consent/jurisdiction.rs | 5 +- crates/trusted-server-core/src/consent/mod.rs | 16 +- crates/trusted-server-core/src/consent/tcf.rs | 46 +- .../trusted-server-core/src/consent/types.rs | 4 +- .../src/consent/us_privacy.rs | 6 +- .../trusted-server-core/src/consent_config.rs | 2 +- crates/trusted-server-core/src/cookies.rs | 29 +- crates/trusted-server-core/src/creative.rs | 119 +++-- .../trusted-server-core/src/ec/batch_sync.rs | 7 +- crates/trusted-server-core/src/ec/cookies.rs | 11 +- crates/trusted-server-core/src/ec/eids.rs | 2 +- .../trusted-server-core/src/ec/generation.rs | 10 +- crates/trusted-server-core/src/ec/identify.rs | 14 +- crates/trusted-server-core/src/ec/kv.rs | 85 ++-- crates/trusted-server-core/src/ec/kv_types.rs | 16 +- crates/trusted-server-core/src/ec/mod.rs | 16 +- crates/trusted-server-core/src/ec/partner.rs | 2 +- .../trusted-server-core/src/ec/prebid_eids.rs | 14 +- .../trusted-server-core/src/ec/pull_sync.rs | 40 +- crates/trusted-server-core/src/ec/registry.rs | 20 +- crates/trusted-server-core/src/error.rs | 44 +- crates/trusted-server-core/src/geo.rs | 12 +- .../trusted-server-core/src/host_rewrite.rs | 2 +- .../trusted-server-core/src/html_processor.rs | 61 ++- crates/trusted-server-core/src/http_util.rs | 43 +- .../src/integrations/adserver_mock.rs | 120 +++--- .../src/integrations/aps.rs | 142 +++--- .../src/integrations/datadome.rs | 84 ++-- .../src/integrations/didomi.rs | 8 +- .../src/integrations/google_tag_manager.rs | 207 ++++----- .../src/integrations/gpt.rs | 34 +- .../src/integrations/lockr.rs | 36 +- .../integrations/nextjs/html_post_process.rs | 69 ++- .../src/integrations/nextjs/mod.rs | 81 ++-- .../src/integrations/nextjs/rsc.rs | 57 +-- .../integrations/nextjs/rsc_placeholders.rs | 8 +- .../integrations/nextjs/script_rewriter.rs | 10 +- .../src/integrations/nextjs/shared.rs | 25 +- .../src/integrations/permutive.rs | 122 ++---- .../src/integrations/prebid.rs | 398 ++++++++--------- .../src/integrations/registry.rs | 34 +- .../src/integrations/sourcepoint.rs | 67 ++- .../src/integrations/testlight.rs | 12 +- crates/trusted-server-core/src/lib.rs | 1 + crates/trusted-server-core/src/models.rs | 44 +- crates/trusted-server-core/src/openrtb.rs | 52 ++- .../trusted-server-core/src/platform/mod.rs | 10 +- .../src/platform/test_support.rs | 28 +- .../trusted-server-core/src/platform/types.rs | 4 +- crates/trusted-server-core/src/proxy.rs | 209 ++++----- crates/trusted-server-core/src/publisher.rs | 49 +-- crates/trusted-server-core/src/redacted.rs | 32 +- .../src/request_signing/discovery.rs | 2 +- .../src/request_signing/endpoints.rs | 63 ++- .../src/request_signing/jwks.rs | 12 +- .../src/request_signing/mod.rs | 4 +- .../src/request_signing/rotation.rs | 116 +++-- .../src/request_signing/signing.rs | 66 +-- crates/trusted-server-core/src/rsc_flight.rs | 8 +- crates/trusted-server-core/src/settings.rs | 408 ++++++------------ .../trusted-server-core/src/settings_data.rs | 10 +- .../src/storage/secret_store.rs | 4 +- .../src/streaming_processor.rs | 86 ++-- .../src/streaming_replacer.rs | 92 ++-- .../trusted-server-core/src/test_support.rs | 2 +- .../.dockerignore | 0 .../Cargo.lock | 65 ++- .../Cargo.toml | 2 +- .../README.md | 14 +- .../browser/global-setup.ts | 0 .../browser/global-teardown.ts | 0 .../browser/helpers/infra.ts | 0 .../browser/helpers/state.ts | 0 .../browser/helpers/wait-for-ready.ts | 0 .../browser/package-lock.json | 0 .../browser/package.json | 0 .../browser/playwright.config.ts | 0 .../tests/nextjs/api-passthrough.spec.ts | 0 .../tests/nextjs/form-rewriting.spec.ts | 0 .../browser/tests/nextjs/navigation.spec.ts | 0 .../tests/shared/script-bundle.spec.ts | 0 .../tests/shared/script-injection.spec.ts | 0 .../tests/wordpress/admin-injection.spec.ts | 0 .../browser/tsconfig.json | 0 .../fixtures/configs/viceroy-template.toml | 0 .../fixtures/frameworks/nextjs/Dockerfile | 4 +- .../frameworks/nextjs/app/about/page.tsx | 0 .../frameworks/nextjs/app/api/data/route.ts | 0 .../frameworks/nextjs/app/api/hello/route.ts | 0 .../nextjs/app/components/Navigation.tsx | 0 .../nextjs/app/components/RouteScript.tsx | 0 .../frameworks/nextjs/app/contact/page.tsx | 0 .../frameworks/nextjs/app/dashboard/page.tsx | 0 .../fixtures/frameworks/nextjs/app/layout.tsx | 0 .../fixtures/frameworks/nextjs/app/page.tsx | 0 .../frameworks/nextjs/next.config.mjs | 0 .../frameworks/nextjs/package-lock.json | 0 .../fixtures/frameworks/nextjs/package.json | 0 .../fixtures/frameworks/wordpress/Dockerfile | 2 +- .../frameworks/wordpress/theme/index.php | 0 .../wordpress/theme/wp-admin/index.php | 0 .../tests/common/assertions.rs | 0 .../tests/common/ec.rs | 0 .../tests/common/mod.rs | 0 .../tests/common/runtime.rs | 0 .../tests/environments/fastly.rs | 0 .../tests/environments/mod.rs | 0 .../tests/frameworks/mod.rs | 0 .../tests/frameworks/nextjs.rs | 2 +- .../tests/frameworks/scenarios.rs | 0 .../tests/frameworks/wordpress.rs | 2 +- .../tests/integration.rs | 0 crates/{js => trusted-server-js}/.gitignore | 0 crates/{js => trusted-server-js}/Cargo.toml | 0 crates/{js => trusted-server-js}/build.rs | 145 ++++--- .../{js => trusted-server-js}/lib/.gitignore | 0 .../lib/.prettierignore | 0 .../lib/.prettierrc.json | 0 .../lib/build-all.mjs | 0 .../lib/eslint.config.js | 0 .../lib/package-lock.json | 0 .../lib/package.json | 0 .../lib/src/core/auction.ts | 0 .../lib/src/core/config.ts | 0 .../lib/src/core/context.ts | 0 .../lib/src/core/global.d.ts | 0 .../lib/src/core/index.ts | 0 .../lib/src/core/log.ts | 0 .../lib/src/core/queue.ts | 0 .../lib/src/core/registry.ts | 0 .../lib/src/core/render.ts | 0 .../lib/src/core/request.ts | 0 .../lib/src/core/styles/normalize.css | 0 .../lib/src/core/templates/iframe.html | 0 .../lib/src/core/types.ts | 0 .../lib/src/core/util.ts | 0 .../lib/src/index.ts | 0 .../lib/src/integrations/creative/click.ts | 0 .../creative/dynamic_src_guard.ts | 0 .../lib/src/integrations/creative/iframe.ts | 0 .../lib/src/integrations/creative/image.ts | 0 .../lib/src/integrations/creative/index.ts | 0 .../src/integrations/creative/proxy_sign.ts | 0 .../lib/src/integrations/datadome/index.ts | 0 .../src/integrations/datadome/script_guard.ts | 0 .../lib/src/integrations/didomi/index.ts | 0 .../integrations/google_tag_manager/index.ts | 0 .../google_tag_manager/script_guard.ts | 0 .../lib/src/integrations/gpt/index.ts | 0 .../lib/src/integrations/gpt/script_guard.ts | 0 .../lib/src/integrations/lockr/index.ts | 0 .../src/integrations/lockr/script_guard.ts | 0 .../lib/src/integrations/permutive/index.ts | 0 .../integrations/permutive/script_guard.ts | 0 .../src/integrations/permutive/segments.ts | 0 .../prebid/_adapters.generated.ts | 0 .../prebid/_user_ids.generated.ts | 0 .../lib/src/integrations/prebid/index.ts | 0 .../prebid/prebid_modules/aliases.d.ts | 0 .../prebid_modules/liveIntentIdSystem.ts | 0 .../integrations/prebid/user_id_modules.json | 0 .../integrations/prebid/user_id_modules.ts | 0 .../lib/src/integrations/sourcepoint/index.ts | 0 .../integrations/sourcepoint/script_guard.ts | 0 .../lib/src/integrations/testlight/index.ts | 0 .../lib/src/shared/async.ts | 0 .../lib/src/shared/beacon_guard.ts | 0 .../src/shared/dom_insertion_dispatcher.ts | 0 .../lib/src/shared/globals.ts | 0 .../lib/src/shared/scheduler.ts | 0 .../lib/src/shared/script_guard.ts | 0 .../lib/test/core/auction.test.ts | 0 .../lib/test/core/config.test.ts | 0 .../lib/test/core/context.test.ts | 0 .../lib/test/core/index.test.ts | 0 .../lib/test/core/registry.test.ts | 0 .../lib/test/core/render.test.ts | 0 .../lib/test/core/request.test.ts | 0 .../test/integrations/creative/click.test.ts | 0 .../lib/test/integrations/creative/helpers.ts | 0 .../test/integrations/creative/iframe.test.ts | 0 .../test/integrations/creative/image.test.ts | 0 .../integrations/creative/proxy_sign.test.ts | 0 .../datadome/script_guard.test.ts | 0 .../test/integrations/didomi/index.test.ts | 0 .../google_tag_manager/script_guard.test.ts | 0 .../lib/test/integrations/gpt/index.test.ts | 0 .../integrations/gpt/script_guard.test.ts | 0 .../integrations/lockr/script_guard.test.ts | 0 .../integrations/permutive/segments.test.ts | 0 .../test/integrations/prebid/index.test.ts | 0 .../prebid/user_id_modules.test.ts | 0 .../integrations/sourcepoint/index.test.ts | 0 .../sourcepoint/script_guard.test.ts | 0 .../lib/test/shared/async.test.ts | 0 .../lib/test/shared/beacon_guard.test.ts | 0 .../shared/dom_insertion_dispatcher.test.ts | 0 .../lib/test/shared/scheduler.test.ts | 0 .../lib/tsconfig.json | 0 .../lib/vite.config.ts | 0 .../lib/vitest.config.ts | 0 .../{js => trusted-server-js}/src/bundle.rs | 16 +- crates/{js => trusted-server-js}/src/lib.rs | 5 + .../.gitignore | 0 .../Cargo.lock | 14 +- .../Cargo.toml | 2 +- .../src/main.rs | 14 +- .../Cargo.toml | 0 .../README.md | 6 +- .../generate.sh | 4 +- .../proto/openrtb.proto | 0 .../src/codegen.rs | 0 .../src/generated.rs | 4 +- .../src/lib.rs | 163 ++++--- docs/guide/creative-processing.md | 2 +- docs/guide/error-reference.md | 4 +- docs/guide/getting-started.md | 2 +- docs/guide/integration-guide.md | 8 +- docs/guide/integrations/gpt.md | 2 +- docs/guide/integrations/prebid.md | 2 +- docs/guide/testing.md | 2 +- .../plans/2026-03-25-streaming-response.md | 4 +- .../2026-04-15-sourcepoint-gpp-consent.md | 28 +- .../specs/2026-01-15-attestation-design.md | 2 +- .../2026-03-19-edgezero-migration-design.md | 2 +- .../2026-03-24-ssc-technical-spec-design.md | 2 +- ...26-04-15-sourcepoint-gpp-consent-design.md | 6 +- rust-toolchain.toml | 2 +- .../check-integration-dependency-versions.sh | 6 +- scripts/integration-tests-browser.sh | 10 +- scripts/integration-tests.sh | 8 +- 262 files changed, 2243 insertions(+), 2591 deletions(-) rename crates/{integration-tests => trusted-server-integration-tests}/.dockerignore (100%) rename crates/{integration-tests => trusted-server-integration-tests}/Cargo.lock (99%) rename crates/{integration-tests => trusted-server-integration-tests}/Cargo.toml (92%) rename crates/{integration-tests => trusted-server-integration-tests}/README.md (94%) rename crates/{integration-tests => trusted-server-integration-tests}/browser/global-setup.ts (100%) rename crates/{integration-tests => trusted-server-integration-tests}/browser/global-teardown.ts (100%) rename crates/{integration-tests => trusted-server-integration-tests}/browser/helpers/infra.ts (100%) rename crates/{integration-tests => trusted-server-integration-tests}/browser/helpers/state.ts (100%) rename crates/{integration-tests => trusted-server-integration-tests}/browser/helpers/wait-for-ready.ts (100%) rename crates/{integration-tests => trusted-server-integration-tests}/browser/package-lock.json (100%) rename crates/{integration-tests => trusted-server-integration-tests}/browser/package.json (100%) rename crates/{integration-tests => trusted-server-integration-tests}/browser/playwright.config.ts (100%) rename crates/{integration-tests => trusted-server-integration-tests}/browser/tests/nextjs/api-passthrough.spec.ts (100%) rename crates/{integration-tests => trusted-server-integration-tests}/browser/tests/nextjs/form-rewriting.spec.ts (100%) rename crates/{integration-tests => trusted-server-integration-tests}/browser/tests/nextjs/navigation.spec.ts (100%) rename crates/{integration-tests => trusted-server-integration-tests}/browser/tests/shared/script-bundle.spec.ts (100%) rename crates/{integration-tests => trusted-server-integration-tests}/browser/tests/shared/script-injection.spec.ts (100%) rename crates/{integration-tests => trusted-server-integration-tests}/browser/tests/wordpress/admin-injection.spec.ts (100%) rename crates/{integration-tests => trusted-server-integration-tests}/browser/tsconfig.json (100%) rename crates/{integration-tests => trusted-server-integration-tests}/fixtures/configs/viceroy-template.toml (100%) rename crates/{integration-tests => trusted-server-integration-tests}/fixtures/frameworks/nextjs/Dockerfile (85%) rename crates/{integration-tests => trusted-server-integration-tests}/fixtures/frameworks/nextjs/app/about/page.tsx (100%) rename crates/{integration-tests => trusted-server-integration-tests}/fixtures/frameworks/nextjs/app/api/data/route.ts (100%) rename crates/{integration-tests => trusted-server-integration-tests}/fixtures/frameworks/nextjs/app/api/hello/route.ts (100%) rename crates/{integration-tests => trusted-server-integration-tests}/fixtures/frameworks/nextjs/app/components/Navigation.tsx (100%) rename crates/{integration-tests => trusted-server-integration-tests}/fixtures/frameworks/nextjs/app/components/RouteScript.tsx (100%) rename crates/{integration-tests => trusted-server-integration-tests}/fixtures/frameworks/nextjs/app/contact/page.tsx (100%) rename crates/{integration-tests => trusted-server-integration-tests}/fixtures/frameworks/nextjs/app/dashboard/page.tsx (100%) rename crates/{integration-tests => trusted-server-integration-tests}/fixtures/frameworks/nextjs/app/layout.tsx (100%) rename crates/{integration-tests => trusted-server-integration-tests}/fixtures/frameworks/nextjs/app/page.tsx (100%) rename crates/{integration-tests => trusted-server-integration-tests}/fixtures/frameworks/nextjs/next.config.mjs (100%) rename crates/{integration-tests => trusted-server-integration-tests}/fixtures/frameworks/nextjs/package-lock.json (100%) rename crates/{integration-tests => trusted-server-integration-tests}/fixtures/frameworks/nextjs/package.json (100%) rename crates/{integration-tests => trusted-server-integration-tests}/fixtures/frameworks/wordpress/Dockerfile (87%) rename crates/{integration-tests => trusted-server-integration-tests}/fixtures/frameworks/wordpress/theme/index.php (100%) rename crates/{integration-tests => trusted-server-integration-tests}/fixtures/frameworks/wordpress/theme/wp-admin/index.php (100%) rename crates/{integration-tests => trusted-server-integration-tests}/tests/common/assertions.rs (100%) rename crates/{integration-tests => trusted-server-integration-tests}/tests/common/ec.rs (100%) rename crates/{integration-tests => trusted-server-integration-tests}/tests/common/mod.rs (100%) rename crates/{integration-tests => trusted-server-integration-tests}/tests/common/runtime.rs (100%) rename crates/{integration-tests => trusted-server-integration-tests}/tests/environments/fastly.rs (100%) rename crates/{integration-tests => trusted-server-integration-tests}/tests/environments/mod.rs (100%) rename crates/{integration-tests => trusted-server-integration-tests}/tests/frameworks/mod.rs (100%) rename crates/{integration-tests => trusted-server-integration-tests}/tests/frameworks/nextjs.rs (95%) rename crates/{integration-tests => trusted-server-integration-tests}/tests/frameworks/scenarios.rs (100%) rename crates/{integration-tests => trusted-server-integration-tests}/tests/frameworks/wordpress.rs (95%) rename crates/{integration-tests => trusted-server-integration-tests}/tests/integration.rs (100%) rename crates/{js => trusted-server-js}/.gitignore (100%) rename crates/{js => trusted-server-js}/Cargo.toml (100%) rename crates/{js => trusted-server-js}/build.rs (55%) rename crates/{js => trusted-server-js}/lib/.gitignore (100%) rename crates/{js => trusted-server-js}/lib/.prettierignore (100%) rename crates/{js => trusted-server-js}/lib/.prettierrc.json (100%) rename crates/{js => trusted-server-js}/lib/build-all.mjs (100%) rename crates/{js => trusted-server-js}/lib/eslint.config.js (100%) rename crates/{js => trusted-server-js}/lib/package-lock.json (100%) rename crates/{js => trusted-server-js}/lib/package.json (100%) rename crates/{js => trusted-server-js}/lib/src/core/auction.ts (100%) rename crates/{js => trusted-server-js}/lib/src/core/config.ts (100%) rename crates/{js => trusted-server-js}/lib/src/core/context.ts (100%) rename crates/{js => trusted-server-js}/lib/src/core/global.d.ts (100%) rename crates/{js => trusted-server-js}/lib/src/core/index.ts (100%) rename crates/{js => trusted-server-js}/lib/src/core/log.ts (100%) rename crates/{js => trusted-server-js}/lib/src/core/queue.ts (100%) rename crates/{js => trusted-server-js}/lib/src/core/registry.ts (100%) rename crates/{js => trusted-server-js}/lib/src/core/render.ts (100%) rename crates/{js => trusted-server-js}/lib/src/core/request.ts (100%) rename crates/{js => trusted-server-js}/lib/src/core/styles/normalize.css (100%) rename crates/{js => trusted-server-js}/lib/src/core/templates/iframe.html (100%) rename crates/{js => trusted-server-js}/lib/src/core/types.ts (100%) rename crates/{js => trusted-server-js}/lib/src/core/util.ts (100%) rename crates/{js => trusted-server-js}/lib/src/index.ts (100%) rename crates/{js => trusted-server-js}/lib/src/integrations/creative/click.ts (100%) rename crates/{js => trusted-server-js}/lib/src/integrations/creative/dynamic_src_guard.ts (100%) rename crates/{js => trusted-server-js}/lib/src/integrations/creative/iframe.ts (100%) rename crates/{js => trusted-server-js}/lib/src/integrations/creative/image.ts (100%) rename crates/{js => trusted-server-js}/lib/src/integrations/creative/index.ts (100%) rename crates/{js => trusted-server-js}/lib/src/integrations/creative/proxy_sign.ts (100%) rename crates/{js => trusted-server-js}/lib/src/integrations/datadome/index.ts (100%) rename crates/{js => trusted-server-js}/lib/src/integrations/datadome/script_guard.ts (100%) rename crates/{js => trusted-server-js}/lib/src/integrations/didomi/index.ts (100%) rename crates/{js => trusted-server-js}/lib/src/integrations/google_tag_manager/index.ts (100%) rename crates/{js => trusted-server-js}/lib/src/integrations/google_tag_manager/script_guard.ts (100%) rename crates/{js => trusted-server-js}/lib/src/integrations/gpt/index.ts (100%) rename crates/{js => trusted-server-js}/lib/src/integrations/gpt/script_guard.ts (100%) rename crates/{js => trusted-server-js}/lib/src/integrations/lockr/index.ts (100%) rename crates/{js => trusted-server-js}/lib/src/integrations/lockr/script_guard.ts (100%) rename crates/{js => trusted-server-js}/lib/src/integrations/permutive/index.ts (100%) rename crates/{js => trusted-server-js}/lib/src/integrations/permutive/script_guard.ts (100%) rename crates/{js => trusted-server-js}/lib/src/integrations/permutive/segments.ts (100%) rename crates/{js => trusted-server-js}/lib/src/integrations/prebid/_adapters.generated.ts (100%) rename crates/{js => trusted-server-js}/lib/src/integrations/prebid/_user_ids.generated.ts (100%) rename crates/{js => trusted-server-js}/lib/src/integrations/prebid/index.ts (100%) rename crates/{js => trusted-server-js}/lib/src/integrations/prebid/prebid_modules/aliases.d.ts (100%) rename crates/{js => trusted-server-js}/lib/src/integrations/prebid/prebid_modules/liveIntentIdSystem.ts (100%) rename crates/{js => trusted-server-js}/lib/src/integrations/prebid/user_id_modules.json (100%) rename crates/{js => trusted-server-js}/lib/src/integrations/prebid/user_id_modules.ts (100%) rename crates/{js => trusted-server-js}/lib/src/integrations/sourcepoint/index.ts (100%) rename crates/{js => trusted-server-js}/lib/src/integrations/sourcepoint/script_guard.ts (100%) rename crates/{js => trusted-server-js}/lib/src/integrations/testlight/index.ts (100%) rename crates/{js => trusted-server-js}/lib/src/shared/async.ts (100%) rename crates/{js => trusted-server-js}/lib/src/shared/beacon_guard.ts (100%) rename crates/{js => trusted-server-js}/lib/src/shared/dom_insertion_dispatcher.ts (100%) rename crates/{js => trusted-server-js}/lib/src/shared/globals.ts (100%) rename crates/{js => trusted-server-js}/lib/src/shared/scheduler.ts (100%) rename crates/{js => trusted-server-js}/lib/src/shared/script_guard.ts (100%) rename crates/{js => trusted-server-js}/lib/test/core/auction.test.ts (100%) rename crates/{js => trusted-server-js}/lib/test/core/config.test.ts (100%) rename crates/{js => trusted-server-js}/lib/test/core/context.test.ts (100%) rename crates/{js => trusted-server-js}/lib/test/core/index.test.ts (100%) rename crates/{js => trusted-server-js}/lib/test/core/registry.test.ts (100%) rename crates/{js => trusted-server-js}/lib/test/core/render.test.ts (100%) rename crates/{js => trusted-server-js}/lib/test/core/request.test.ts (100%) rename crates/{js => trusted-server-js}/lib/test/integrations/creative/click.test.ts (100%) rename crates/{js => trusted-server-js}/lib/test/integrations/creative/helpers.ts (100%) rename crates/{js => trusted-server-js}/lib/test/integrations/creative/iframe.test.ts (100%) rename crates/{js => trusted-server-js}/lib/test/integrations/creative/image.test.ts (100%) rename crates/{js => trusted-server-js}/lib/test/integrations/creative/proxy_sign.test.ts (100%) rename crates/{js => trusted-server-js}/lib/test/integrations/datadome/script_guard.test.ts (100%) rename crates/{js => trusted-server-js}/lib/test/integrations/didomi/index.test.ts (100%) rename crates/{js => trusted-server-js}/lib/test/integrations/google_tag_manager/script_guard.test.ts (100%) rename crates/{js => trusted-server-js}/lib/test/integrations/gpt/index.test.ts (100%) rename crates/{js => trusted-server-js}/lib/test/integrations/gpt/script_guard.test.ts (100%) rename crates/{js => trusted-server-js}/lib/test/integrations/lockr/script_guard.test.ts (100%) rename crates/{js => trusted-server-js}/lib/test/integrations/permutive/segments.test.ts (100%) rename crates/{js => trusted-server-js}/lib/test/integrations/prebid/index.test.ts (100%) rename crates/{js => trusted-server-js}/lib/test/integrations/prebid/user_id_modules.test.ts (100%) rename crates/{js => trusted-server-js}/lib/test/integrations/sourcepoint/index.test.ts (100%) rename crates/{js => trusted-server-js}/lib/test/integrations/sourcepoint/script_guard.test.ts (100%) rename crates/{js => trusted-server-js}/lib/test/shared/async.test.ts (100%) rename crates/{js => trusted-server-js}/lib/test/shared/beacon_guard.test.ts (100%) rename crates/{js => trusted-server-js}/lib/test/shared/dom_insertion_dispatcher.test.ts (100%) rename crates/{js => trusted-server-js}/lib/test/shared/scheduler.test.ts (100%) rename crates/{js => trusted-server-js}/lib/tsconfig.json (100%) rename crates/{js => trusted-server-js}/lib/vite.config.ts (100%) rename crates/{js => trusted-server-js}/lib/vitest.config.ts (100%) rename crates/{js => trusted-server-js}/src/bundle.rs (87%) rename crates/{js => trusted-server-js}/src/lib.rs (53%) rename crates/{openrtb-codegen => trusted-server-openrtb-codegen}/.gitignore (100%) rename crates/{openrtb-codegen => trusted-server-openrtb-codegen}/Cargo.lock (99%) rename crates/{openrtb-codegen => trusted-server-openrtb-codegen}/Cargo.toml (75%) rename crates/{openrtb-codegen => trusted-server-openrtb-codegen}/src/main.rs (77%) rename crates/{openrtb => trusted-server-openrtb}/Cargo.toml (100%) rename crates/{openrtb => trusted-server-openrtb}/README.md (89%) rename crates/{openrtb => trusted-server-openrtb}/generate.sh (90%) rename crates/{openrtb => trusted-server-openrtb}/proto/openrtb.proto (100%) rename crates/{openrtb => trusted-server-openrtb}/src/codegen.rs (100%) rename crates/{openrtb => trusted-server-openrtb}/src/generated.rs (99%) rename crates/{openrtb => trusted-server-openrtb}/src/lib.rs (81%) diff --git a/.github/actions/setup-integration-test-env/action.yml b/.github/actions/setup-integration-test-env/action.yml index b034ad2c..38ec1f56 100644 --- a/.github/actions/setup-integration-test-env/action.yml +++ b/.github/actions/setup-integration-test-env/action.yml @@ -50,8 +50,6 @@ runs: if: ${{ inputs.install-viceroy == 'true' }} shell: bash # `.tool-versions` is the single source of truth for the Viceroy pin. - # The pin matters because upstream Viceroy > v0.16.4 has bumped MSRV - # beyond the rustc pin in `rust-toolchain.toml`. run: echo "viceroy-version=$(grep '^viceroy ' .tool-versions | awk '{print $2}')" >> "$GITHUB_OUTPUT" - name: Set up Rust toolchain @@ -72,7 +70,7 @@ runs: - name: Install Viceroy if: ${{ inputs.install-viceroy == 'true' && steps.cache-viceroy.outputs.cache-hit != 'true' }} shell: bash - run: cargo install --git https://github.com/fastly/Viceroy --tag v${{ steps.viceroy-version.outputs.viceroy-version }} viceroy + run: cargo install viceroy --version "${{ steps.viceroy-version.outputs.viceroy-version }}" --locked --force - name: Build WASM binary if: ${{ inputs.build-wasm == 'true' }} @@ -91,7 +89,7 @@ runs: shell: bash run: | docker build -t test-wordpress:latest \ - crates/integration-tests/fixtures/frameworks/wordpress/ + crates/trusted-server-integration-tests/fixtures/frameworks/wordpress/ - name: Build Next.js test container if: ${{ inputs.build-test-images == 'true' }} @@ -100,4 +98,4 @@ runs: docker build \ --build-arg NODE_VERSION=${{ steps.node-version.outputs.node-version }} \ -t test-nextjs:latest \ - crates/integration-tests/fixtures/frameworks/nextjs/ + crates/trusted-server-integration-tests/fixtures/frameworks/nextjs/ diff --git a/.github/dependabot.yml b/.github/dependabot.yml index dac874da..9c5c2c93 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -11,7 +11,7 @@ updates: interval: "weekly" - package-ecosystem: "npm" # See documentation for possible values - directory: "crates/js/lib/" # Location of package manifests + directory: "crates/trusted-server-js/lib/" # Location of package manifests schedule: interval: "weekly" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index fbe95847..bac92b82 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -26,8 +26,8 @@ Closes # - [ ] `cargo test --workspace` - [ ] `cargo clippy --workspace --all-targets --all-features -- -D warnings` - [ ] `cargo fmt --all -- --check` -- [ ] JS tests: `cd crates/js/lib && npx vitest run` -- [ ] JS format: `cd crates/js/lib && npm run format` +- [ ] JS tests: `cd crates/trusted-server-js/lib && npx vitest run` +- [ ] JS format: `cd crates/trusted-server-js/lib && npm run format` - [ ] Docs format: `cd docs && npm run format` - [ ] WASM build: `cargo build --package trusted-server-adapter-fastly --release --target wasm32-wasip1` - [ ] Manual testing via `fastly compute serve` diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index b6aba137..6d990c95 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -39,7 +39,7 @@ jobs: runs-on: ubuntu-latest defaults: run: - working-directory: crates/js/lib + working-directory: crates/trusted-server-js/lib steps: - uses: actions/checkout@v4 @@ -55,7 +55,7 @@ jobs: with: node-version: ${{ steps.node-version.outputs.node-version }} cache: "npm" - cache-dependency-path: crates/js/lib/package.json + cache-dependency-path: crates/trusted-server-js/lib/package.json - name: Install dependencies run: npm ci diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index da467583..39be85e7 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -75,7 +75,7 @@ jobs: - name: Run integration tests run: >- cargo test - --manifest-path crates/integration-tests/Cargo.toml + --manifest-path crates/trusted-server-integration-tests/Cargo.toml --target x86_64-unknown-linux-gnu -- --include-ignored --skip test_wordpress_fastly --skip test_nextjs_fastly --test-threads=1 env: @@ -115,20 +115,20 @@ jobs: with: node-version: ${{ steps.shared-setup.outputs.node-version }} cache: npm - cache-dependency-path: crates/integration-tests/browser/package-lock.json + cache-dependency-path: crates/trusted-server-integration-tests/browser/package-lock.json - name: Install Playwright - working-directory: crates/integration-tests/browser + working-directory: crates/trusted-server-integration-tests/browser run: | npm ci npx playwright install --with-deps chromium - name: Run browser tests (Next.js) - working-directory: crates/integration-tests/browser + working-directory: crates/trusted-server-integration-tests/browser env: WASM_BINARY_PATH: ${{ env.WASM_ARTIFACT_PATH }} INTEGRATION_ORIGIN_PORT: ${{ env.ORIGIN_PORT }} - VICEROY_CONFIG_PATH: ${{ github.workspace }}/crates/integration-tests/fixtures/configs/viceroy-template.toml + VICEROY_CONFIG_PATH: ${{ github.workspace }}/crates/trusted-server-integration-tests/fixtures/configs/viceroy-template.toml TEST_FRAMEWORK: nextjs PLAYWRIGHT_HTML_REPORT: playwright-report-nextjs run: npx playwright test @@ -138,16 +138,16 @@ jobs: if: always() with: name: playwright-report-nextjs - path: crates/integration-tests/browser/playwright-report-nextjs/ + path: crates/trusted-server-integration-tests/browser/playwright-report-nextjs/ retention-days: 7 - name: Run browser tests (WordPress) if: always() - working-directory: crates/integration-tests/browser + working-directory: crates/trusted-server-integration-tests/browser env: WASM_BINARY_PATH: ${{ env.WASM_ARTIFACT_PATH }} INTEGRATION_ORIGIN_PORT: ${{ env.ORIGIN_PORT }} - VICEROY_CONFIG_PATH: ${{ github.workspace }}/crates/integration-tests/fixtures/configs/viceroy-template.toml + VICEROY_CONFIG_PATH: ${{ github.workspace }}/crates/trusted-server-integration-tests/fixtures/configs/viceroy-template.toml TEST_FRAMEWORK: wordpress PLAYWRIGHT_HTML_REPORT: playwright-report-wordpress run: npx playwright test @@ -157,7 +157,7 @@ jobs: if: always() with: name: playwright-report-wordpress - path: crates/integration-tests/browser/playwright-report-wordpress/ + path: crates/trusted-server-integration-tests/browser/playwright-report-wordpress/ retention-days: 7 - name: Upload Playwright traces and screenshots @@ -165,5 +165,5 @@ jobs: if: failure() with: name: playwright-traces - path: crates/integration-tests/browser/test-results/ + path: crates/trusted-server-integration-tests/browser/test-results/ retention-days: 7 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2da273aa..a4133b57 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,8 +25,6 @@ jobs: id: viceroy-version # `.tool-versions` is the single source of truth so this workflow and # `.github/actions/setup-integration-test-env/action.yml` can't drift. - # The pin matters because upstream Viceroy > v0.16.4 has bumped MSRV - # beyond the rustc pin in `rust-toolchain.toml`. run: echo "viceroy-version=$(grep '^viceroy ' .tool-versions | awk '{print $2}')" >> $GITHUB_OUTPUT shell: bash @@ -46,7 +44,7 @@ jobs: - name: Install Viceroy if: steps.cache-viceroy.outputs.cache-hit != 'true' - run: cargo install --git https://github.com/fastly/Viceroy --tag v${{ steps.viceroy-version.outputs.viceroy-version }} viceroy + run: cargo install viceroy --version "${{ steps.viceroy-version.outputs.viceroy-version }}" --locked --force - name: Run tests run: cargo test --workspace @@ -64,7 +62,7 @@ jobs: runs-on: ubuntu-latest defaults: run: - working-directory: crates/js/lib + working-directory: crates/trusted-server-js/lib steps: - uses: actions/checkout@v4 @@ -79,7 +77,7 @@ jobs: with: node-version: ${{ steps.node-version.outputs.node-version }} cache: "npm" - cache-dependency-path: crates/js/lib/package.json + cache-dependency-path: crates/trusted-server-js/lib/package.json - name: Install dependencies run: npm ci diff --git a/.gitignore b/.gitignore index af70c452..25e2fa11 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ /bin /pkg /target -/crates/integration-tests/target +/crates/trusted-server-integration-tests/target # env .env* @@ -32,7 +32,7 @@ src/*.html /benchmark-results/** # Playwright browser tests -/crates/integration-tests/browser/node_modules/ -/crates/integration-tests/browser/test-results/ -/crates/integration-tests/browser/playwright-report/ -/crates/integration-tests/browser/.browser-test-state.json +/crates/trusted-server-integration-tests/browser/node_modules/ +/crates/trusted-server-integration-tests/browser/test-results/ +/crates/trusted-server-integration-tests/browser/playwright-report/ +/crates/trusted-server-integration-tests/browser/.browser-test-state.json diff --git a/.tool-versions b/.tool-versions index 8d8751b8..75814680 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,4 +1,5 @@ -fastly 13.3.0 -rust 1.91.1 +fastly 15.1.0 +rust 1.95.0 nodejs 24.12.0 -viceroy 0.16.4 +viceroy 0.17.0 +wasmtime 44.0.1 diff --git a/AGENTS.md b/AGENTS.md index bcbcd179..4f89735b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,7 +19,7 @@ If you cannot read `CLAUDE.md`, follow these rules: 2. Keep changes minimal — do not refactor unrelated code. 3. Run `cargo test --workspace` after every code change. 4. Run `cargo fmt --all -- --check` and `cargo clippy --workspace --all-targets --all-features -- -D warnings`. -5. Run JS tests with `cd crates/js/lib && npx vitest run` when touching JS/TS code. +5. Run JS tests with `cd crates/trusted-server-js/lib && npx vitest run` when touching JS/TS code. 6. Use `error-stack` (`Report`) for error handling — not anyhow, eyre, or thiserror. 7. Use `log` macros (not `println!`) and `expect("should ...")` (not `unwrap()`). 8. Target is `wasm32-wasip1` — no Tokio or OS-specific dependencies in core crates. diff --git a/CLAUDE.md b/CLAUDE.md index f37a1ac3..9583fa2f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ real-time bidding integration, and publisher-side JavaScript injection. crates/ trusted-server-core/ # Core library — shared logic, integrations, HTML processing trusted-server-adapter-fastly/ # Fastly Compute entry point (wasm32-wasip1 binary) - js/ # TypeScript/JS build — per-integration IIFE bundles + trusted-server-js/ # TypeScript/JS build — per-integration IIFE bundles lib/ # TS source, Vitest tests, esbuild pipeline ``` @@ -26,10 +26,12 @@ Supporting files: `fastly.toml`, `trusted-server.toml`, `.env.dev`, | Tool | Version / Target | | ----------- | ---------------------------------------- | -| Rust | 1.91.1 (pinned in `rust-toolchain.toml`) | +| Rust | 1.95.0 (pinned in `rust-toolchain.toml`) | | WASM target | `wasm32-wasip1` | -| Node | LTS (for JS build) | -| Viceroy | Latest (Fastly local simulator) | +| Node | 24.12.0 (from `.tool-versions`) | +| Fastly CLI | 15.1.0 (from `.tool-versions`) | +| Viceroy | 0.17.0 (from `.tool-versions`) | +| Wasmtime | 44.0.1 (from `.tool-versions`) | --- @@ -67,22 +69,22 @@ cargo clippy --workspace --all-targets --all-features -- -D warnings cargo check # JS tests -cd crates/js/lib && npx vitest run +cd crates/trusted-server-js/lib && npx vitest run # JS format -cd crates/js/lib && npm run format +cd crates/trusted-server-js/lib && npm run format # Docs format cd docs && npm run format # JS build -cd crates/js/lib && node build-all.mjs +cd crates/trusted-server-js/lib && node build-all.mjs ``` ### Install prerequisites ```bash -cargo install viceroy # Fastly local test runtime +cargo install viceroy --version 0.17.0 --locked --force ``` --- @@ -266,7 +268,7 @@ IntegrationRegistration::builder(ID) | --------------------- | ---------------------------------------------------------- | | `fastly.toml` | Fastly service configuration and build settings | | `trusted-server.toml` | Application settings (ad servers, KV stores, ID templates) | -| `rust-toolchain.toml` | Pins Rust version to 1.91.1 | +| `rust-toolchain.toml` | Pins Rust version to 1.95.0 | | `.env.dev` | Local development environment variables | --- @@ -278,8 +280,8 @@ Every PR must pass: 1. `cargo fmt --all -- --check` 2. `cargo clippy --workspace --all-targets --all-features -- -D warnings` 3. `cargo test --workspace` -4. JS build and test (`cd crates/js/lib && npx vitest run`) -5. JS format (`cd crates/js/lib && npm run format`) +4. JS build and test (`cd crates/trusted-server-js/lib && npx vitest run`) +5. JS format (`cd crates/trusted-server-js/lib && npm run format`) 6. Docs format (`cd docs && npm run format`) --- @@ -378,8 +380,8 @@ both runtime behavior and build/tooling changes. | `crates/trusted-server-core/src/cookies.rs` | Cookie handling | | `crates/trusted-server-core/src/consent/mod.rs` | GDPR and broader consent management | | `crates/trusted-server-core/src/http_util.rs` | HTTP abstractions and request utilities | -| `crates/js/build.rs` | Discovers dist files, generates `tsjs_modules.rs` | -| `crates/js/src/bundle.rs` | Module map, concatenation, hashing | +| `crates/trusted-server-js/build.rs` | Discovers dist files, generates `tsjs_modules.rs` | +| `crates/trusted-server-js/src/bundle.rs` | Module map, concatenation, hashing | --- diff --git a/Cargo.lock b/Cargo.lock index 942d5f64..b0ec1b5f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -750,7 +750,7 @@ dependencies = [ [[package]] name = "edgezero-adapter-fastly" version = "0.1.0" -source = "git+https://github.com/stackpop/edgezero?rev=170b74b#170b74bd2c9933b7d561f7ccdb67c53b239e9527" +source = "git+https://github.com/stackpop/edgezero?rev=e78871407057c392d78fa4a0bcec5e4ca8ba80b5#e78871407057c392d78fa4a0bcec5e4ca8ba80b5" dependencies = [ "anyhow", "async-stream", @@ -766,12 +766,13 @@ dependencies = [ "futures-util", "log", "log-fastly", + "thiserror 2.0.18", ] [[package]] name = "edgezero-core" version = "0.1.0" -source = "git+https://github.com/stackpop/edgezero?rev=170b74b#170b74bd2c9933b7d561f7ccdb67c53b239e9527" +source = "git+https://github.com/stackpop/edgezero?rev=e78871407057c392d78fa4a0bcec5e4ca8ba80b5#e78871407057c392d78fa4a0bcec5e4ca8ba80b5" dependencies = [ "anyhow", "async-compression", @@ -799,7 +800,7 @@ dependencies = [ [[package]] name = "edgezero-macros" version = "0.1.0" -source = "git+https://github.com/stackpop/edgezero?rev=170b74b#170b74bd2c9933b7d561f7ccdb67c53b239e9527" +source = "git+https://github.com/stackpop/edgezero?rev=e78871407057c392d78fa4a0bcec5e4ca8ba80b5#e78871407057c392d78fa4a0bcec5e4ca8ba80b5" dependencies = [ "log", "proc-macro2", @@ -882,9 +883,9 @@ dependencies = [ [[package]] name = "fastly" -version = "0.11.13" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f767502306f09f6dcb76302d09cd2ea8542e228d5f155166f0c2da925e16c61" +checksum = "531e4c3df48350d9f4fc95b4deaf87fd29820336b7926bb84bf460457c2a126b" dependencies = [ "anyhow", "bytes", @@ -910,9 +911,9 @@ dependencies = [ [[package]] name = "fastly-macros" -version = "0.11.13" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51ae08eeeb5ed0c1a8b454fc89dca0e316e13b7889e81fc9a435503c1e84a2d7" +checksum = "cc2aef5f9690b04c8890f9a54ddb591b12b9779ec25ee0e572d207106e52e3d8" dependencies = [ "proc-macro2", "quote", @@ -921,9 +922,9 @@ dependencies = [ [[package]] name = "fastly-shared" -version = "0.11.13" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d64ed1bba12ca45d1a2a80c2c55d903297adb3eeb4edc9d327c1d51ee709d404" +checksum = "080ad138403159fd366d3e0b14bb49cb0c01dc18c25095bbbd1c85e3338f5413" dependencies = [ "bitflags 1.3.2", "http", @@ -931,14 +932,15 @@ dependencies = [ [[package]] name = "fastly-sys" -version = "0.11.13" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1b82ebd99583740a074d8962ca75d7d17065b185a94e4919c3a3f2193268b6" +checksum = "de75ef193f6c29c43d667458bede648970715aedd5db2d42c2eba3ffa3ad738b" dependencies = [ "bitflags 1.3.2", "fastly-shared", + "http", "wasip2", - "wit-bindgen 0.46.0", + "wit-bindgen 0.51.0", ] [[package]] @@ -1582,9 +1584,9 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "log-fastly" -version = "0.11.13" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58a5d864949b863161476a8129ef0322c56c77bb15f98d88991002072f497b1e" +checksum = "51dae5def13a2d557fdb63862d642f8d4641ec3773c036bb14092697b6764013" dependencies = [ "fastly", "log", @@ -3087,21 +3089,13 @@ dependencies = [ "memchr", ] -[[package]] -name = "wit-bindgen" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" -dependencies = [ - "bitflags 2.11.1", -] - [[package]] name = "wit-bindgen" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" dependencies = [ + "bitflags 2.11.1", "wit-bindgen-rust-macro", ] diff --git a/Cargo.toml b/Cargo.toml index 9f2f4c67..416fbc87 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,15 +3,15 @@ resolver = "2" members = [ "crates/trusted-server-core", "crates/trusted-server-adapter-fastly", - "crates/js", - "crates/openrtb", + "crates/trusted-server-js", + "crates/trusted-server-openrtb", ] -# integration-tests is intentionally excluded from workspace members because it +# trusted-server-integration-tests is intentionally excluded from workspace members because it # requires a native target (testcontainers, reqwest) while the workspace default # is wasm32-wasip1. Run it via: ./scripts/integration-tests.sh exclude = [ - "crates/integration-tests", - "crates/openrtb-codegen", + "crates/trusted-server-integration-tests", + "crates/trusted-server-openrtb-codegen", ] default-members = [ @@ -19,28 +19,6 @@ default-members = [ "crates/trusted-server-adapter-fastly", ] -[workspace.lints.clippy] -# Correctness - Force explicit error handling in production code -unwrap_used = "deny" -expect_used = "allow" # Allow expect with context message -panic = "deny" - -# Style -module_name_repetitions = "allow" -must_use_candidate = "warn" - -# Pedantic (selective) -doc_markdown = "warn" -missing_errors_doc = "warn" -missing_panics_doc = "warn" -needless_pass_by_value = "warn" # Encourage borrowing over ownership -redundant_closure_for_method_calls = "warn" - -# Restriction (selective) -print_stdout = "warn" -print_stderr = "warn" -dbg_macro = "warn" - [profile.release] debug = 1 @@ -56,12 +34,12 @@ config = "0.15.19" cookie = "0.18.1" derive_more = { version = "2.0", features = ["display", "error"] } ed25519-dalek = { version = "2.2", features = ["rand_core"] } -edgezero-adapter-axum = { git = "https://github.com/stackpop/edgezero", rev = "170b74b", default-features = false } -edgezero-adapter-cloudflare = { git = "https://github.com/stackpop/edgezero", rev = "170b74b", default-features = false } -edgezero-adapter-fastly = { git = "https://github.com/stackpop/edgezero", rev = "170b74b", default-features = false } -edgezero-core = { git = "https://github.com/stackpop/edgezero", rev = "170b74b", default-features = false } +edgezero-adapter-axum = { git = "https://github.com/stackpop/edgezero", rev = "e78871407057c392d78fa4a0bcec5e4ca8ba80b5", default-features = false } +edgezero-adapter-cloudflare = { git = "https://github.com/stackpop/edgezero", rev = "e78871407057c392d78fa4a0bcec5e4ca8ba80b5", default-features = false } +edgezero-adapter-fastly = { git = "https://github.com/stackpop/edgezero", rev = "e78871407057c392d78fa4a0bcec5e4ca8ba80b5", default-features = false } +edgezero-core = { git = "https://github.com/stackpop/edgezero", rev = "e78871407057c392d78fa4a0bcec5e4ca8ba80b5", default-features = false } error-stack = "0.6" -fastly = "0.11.12" +fastly = "0.12" fern = "0.7.1" flate2 = "1.1" futures = "0.3" @@ -71,7 +49,7 @@ http = "1.4.0" iab_gpp = "0.1" jose-jwk = "0.1.2" log = "0.4.29" -log-fastly = "0.11.12" +log-fastly = "0.12" lol_html = "2.7.2" matchit = "0.9" mime = "0.3" @@ -91,3 +69,129 @@ uuid = { version = "1.18", features = ["v4"] } validator = { version = "0.20", features = ["derive"] } which = "8" criterion = { version = "0.5", default-features = false, features = ["cargo_bench_support"] } + +[workspace.lints.clippy] +# Match EdgeZero's strict lint posture from stackpop/edgezero#257: keep +# pedantic visible, deny restriction by default, and require every broad allow +# to carry a workspace-level rationale. +pedantic = { level = "warn", priority = -1 } +restriction = { level = "deny", priority = -1 } + +# Meta - required when enabling `restriction` as a group. +blanket_clippy_restriction_lints = "allow" +# Local allows are permitted when they carry a reason; reasonless allows remain +# denied by `allow_attributes_without_reason`. +allow_attributes = "allow" + +# Documentation - private items don't need full docs. +missing_docs_in_private_items = "allow" +# Rust 1.95's restriction group requires every doc paragraph to end with +# punctuation. Existing field docs often use concise noun phrases; changing all +# of them is broad documentation churn unrelated to the EdgeZero tool sync. +doc_paragraphs_missing_punctuation = "allow" + +# Style / formatting - match idiomatic Rust conventions. +implicit_return = "allow" +question_mark_used = "allow" +single_call_fn = "allow" +separated_literal_suffix = "allow" +# `pub_with_shorthand` flags `pub(crate)` and suggests `pub(in crate)`, but +# rustfmt rewrites `pub(in crate)` back to `pub(crate)`. +pub_with_shorthand = "allow" +# Existing tests and small parsing helpers use short local names per Rust +# convention; renaming all of them creates churn without improving behavior. +min_ident_chars = "allow" +single_char_lifetime_names = "allow" +str_to_string = "allow" + +# Existing module/test organization is intentionally conventional for this +# codebase. Moving every `mod.rs`, alphabetizing every item, and renaming every +# `test_*` function would be broad churn unrelated to the EdgeZero sync. +arbitrary_source_item_ordering = "allow" +mod_module_files = "allow" +redundant_test_prefix = "allow" + +# Imports / paths - this workspace targets std-enabled Fastly Compute code. +std_instead_of_alloc = "allow" +std_instead_of_core = "allow" +absolute_paths = "allow" + +# API design - broad generated/public names are intentional in this workspace; +# reduce this by localizing allows where practical during cleanup. +exhaustive_enums = "allow" +exhaustive_structs = "allow" +field_scoped_visibility_modifiers = "allow" +impl_trait_in_params = "allow" +missing_inline_in_public_items = "allow" +missing_trait_methods = "allow" +module_name_repetitions = "allow" +multiple_inherent_impl = "allow" +partial_pub_fields = "allow" +pub_use = "allow" +return_self_not_must_use = "allow" +same_name_method = "allow" +struct_excessive_bools = "allow" +trivially_copy_pass_by_ref = "allow" +unused_self = "allow" + +# Pattern style - mutually exclusive with `ref_patterns`; use match ergonomics. +pattern_type_mismatch = "allow" +ref_patterns = "allow" + +# Defensive-coding restriction lints that are noisy in parsing, streaming, test, +# and benchmark code. Keep them visible as local cleanup candidates rather than +# blocking the EdgeZero dependency sync on whole-workspace rewrites. +arithmetic_side_effects = "allow" +as_conversions = "allow" +case_sensitive_file_extension_comparisons = "allow" +clone_on_ref_ptr = "allow" +default_numeric_fallback = "allow" +decimal_literal_representation = "allow" +else_if_without_else = "allow" +expect_used = "allow" +format_push_string = "allow" +implicit_hasher = "allow" +indexing_slicing = "allow" +integer_division = "allow" +integer_division_remainder_used = "allow" +iter_over_hash_type = "allow" +items_after_statements = "allow" +let_underscore_must_use = "allow" +let_underscore_untyped = "allow" +map_err_ignore = "allow" +map_with_unused_argument_over_ranges = "allow" +missing_assert_message = "allow" +missing_fields_in_debug = "allow" +semicolon_outside_block = "allow" +string_add = "allow" +string_slice = "allow" +too_many_lines = "allow" +unnecessary_safety_comment = "allow" +unnecessary_wraps = "allow" +unwrap_in_result = "allow" +use_debug = "allow" +wildcard_enum_match_arm = "allow" + +# Naming and literal style that is frequently intentional in tests and protocol +# fixtures. +non_ascii_literal = "allow" +shadow_reuse = "allow" +shadow_same = "allow" +shadow_unrelated = "allow" +similar_names = "allow" +used_underscore_binding = "allow" + +# Async function shapes are dictated by handler traits and future extension +# points even when some implementations do not currently await. +unused_async = "allow" + +# Some docs intentionally show JSON/string examples rather than intra-doc links. +doc_link_with_quotes = "allow" + +# Trait implementations sometimes use domain-specific parameter names that are +# clearer than the trait's placeholder names. +renamed_function_params = "allow" + +# Narrow casts remain at audited call sites where values are bounded by protocol, +# platform, or current-time invariants. +cast_possible_truncation = "allow" diff --git a/clippy.toml b/clippy.toml index 07564932..d767c2a6 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1,40 +1,13 @@ -# Clippy configuration for trusted-server -# See: https://doc.rust-lang.org/clippy/configuration.html - -# ============================================================================= -# Complexity Thresholds -# ============================================================================= - -# Cognitive complexity threshold for functions (default: 25) -# Set to 30 to accommodate existing complex HTML/RSC processing functions -# like `create_html_processor` and `rewrite_rsc_scripts_combined`. -# Consider refactoring functions that exceed this threshold. -cognitive-complexity-threshold = 30 - -# Maximum number of lines in a function (default: 100) -# Set to 200 to allow larger handler functions that process HTTP requests -# with multiple validation steps. Prefer extracting helpers when possible. -too-many-lines-threshold = 200 - -# ============================================================================= -# Test Allowances -# ============================================================================= - -# Allow expect() in tests for clearer failure messages than unwrap() +# Clippy configuration. See https://doc.rust-lang.org/clippy/lint_configuration.html +# +# Test code uses `.unwrap()`, `.expect()`, `panic!`, `assert!`, indexing, and +# other "if this fails, the test fails" idioms by convention. We keep the +# corresponding restriction lints active in production code but exempt tests. allow-expect-in-tests = true - -# Allow unwrap() in tests where panicking on failure is acceptable +allow-indexing-slicing-in-tests = true +allow-panic-in-tests = true allow-unwrap-in-tests = true -# ============================================================================= -# Future Considerations -# ============================================================================= -# -# As the codebase matures, consider tightening these thresholds: -# - cognitive-complexity-threshold = 25 (default) -# - too-many-lines-threshold = 100 (default) -# -# Additional lints to consider enabling in Cargo.toml: -# - needless_pass_by_value = "warn" -# - redundant_closure_for_method_calls = "warn" -# - missing_const_for_fn = "warn" +# Keep current Trusted Server thresholds while strict lint cleanup is underway. +cognitive-complexity-threshold = 30 +too-many-lines-threshold = 200 diff --git a/crates/trusted-server-adapter-fastly/src/error.rs b/crates/trusted-server-adapter-fastly/src/error.rs index 560a2e18..288b7297 100644 --- a/crates/trusted-server-adapter-fastly/src/error.rs +++ b/crates/trusted-server-adapter-fastly/src/error.rs @@ -4,7 +4,7 @@ use error_stack::Report; use fastly::Response; -use trusted_server_core::error::{IntoHttpResponse, TrustedServerError}; +use trusted_server_core::error::{IntoHttpResponse as _, TrustedServerError}; /// Converts a [`TrustedServerError`] into an HTTP error response. pub fn to_error_response(report: &Report) -> Response { @@ -12,7 +12,7 @@ pub fn to_error_response(report: &Report) -> Response { let root_error = report.current_context(); // Log the full error chain for debugging - log::error!("Error occurred: {:?}", report); + log::error!("Error occurred: {report:?}"); Response::from_status(root_error.status_code()) .with_body_text_plain(&format!("{}\n", root_error.user_message())) diff --git a/crates/trusted-server-adapter-fastly/src/main.rs b/crates/trusted-server-adapter-fastly/src/main.rs index 7f97cada..e5941b11 100644 --- a/crates/trusted-server-adapter-fastly/src/main.rs +++ b/crates/trusted-server-adapter-fastly/src/main.rs @@ -68,7 +68,7 @@ fn main() -> Result<(), Error> { let settings = match get_settings() { Ok(s) => s, Err(e) => { - log::error!("Failed to load settings: {:?}", e); + log::error!("Failed to load settings: {e:?}"); to_error_response(&e).send_to_client(); return Ok(()); } @@ -92,7 +92,7 @@ fn main() -> Result<(), Error> { let orchestrator = match build_orchestrator(&settings) { Ok(orchestrator) => orchestrator, Err(e) => { - log::error!("Failed to build auction orchestrator: {:?}", e); + log::error!("Failed to build auction orchestrator: {e:?}"); to_error_response(&e).send_to_client(); return Ok(()); } @@ -101,7 +101,7 @@ fn main() -> Result<(), Error> { let integration_registry = match IntegrationRegistry::new(&settings) { Ok(r) => r, Err(e) => { - log::error!("Failed to create integration registry: {:?}", e); + log::error!("Failed to create integration registry: {e:?}"); to_error_response(&e).send_to_client(); return Ok(()); } @@ -110,7 +110,7 @@ fn main() -> Result<(), Error> { let partner_registry = match PartnerRegistry::from_config(&settings.ec.partners) { Ok(registry) => registry, Err(e) => { - log::error!("Failed to build partner registry: {:?}", e); + log::error!("Failed to build partner registry: {e:?}"); to_error_response(&e).send_to_client(); return Ok(()); } @@ -166,8 +166,14 @@ fn build_ja4_debug_response(req: &Request) -> Response { .unwrap_or(FALLBACK_UNAVAILABLE); let cipher = req .get_tls_cipher_openssl_name() + .ok() + .flatten() + .unwrap_or(FALLBACK_UNAVAILABLE); + let tls_version = req + .get_tls_protocol() + .ok() + .flatten() .unwrap_or(FALLBACK_UNAVAILABLE); - let tls_version = req.get_tls_protocol().unwrap_or(FALLBACK_UNAVAILABLE); let ua = req.get_header_str("user-agent").unwrap_or(FALLBACK_NONE); let ch_mobile = req .get_header_str("sec-ch-ua-mobile") @@ -210,7 +216,10 @@ async fn route_request( compat::sanitize_fastly_forwarded_headers(&mut req); // Extract geo info before auth check or routing consumes the request. - #[allow(deprecated)] + #[allow( + deprecated, + reason = "Fastly adapter still reads legacy request geo metadata" + )] let geo_info = GeoInfo::from_request(&req); // Extract the Prebid EIDs cookie before routing consumes the request. @@ -245,7 +254,7 @@ async fn route_request( .and_then(|r| r) .unwrap_or_else(|e| to_error_response(&e)), Err(e) => { - log::error!("Failed to evaluate basic auth: {:?}", e); + log::error!("Failed to evaluate basic auth: {e:?}"); to_error_response(&e) } }; @@ -311,7 +320,7 @@ async fn route_request( } Ok(None) => {} Err(e) => { - log::error!("Failed to evaluate basic auth: {:?}", e); + log::error!("Failed to evaluate basic auth: {e:?}"); let mut response = to_error_response(&e); finalize_response(settings, geo_info.as_ref(), &mut response); return Ok(RouteOutcome { @@ -322,7 +331,7 @@ async fn route_request( } // Get path and method for routing - let path = req.get_path().to_string(); + let path = req.get_path().to_owned(); let method = req.get_method().clone(); // Match known routes and handle them @@ -346,15 +355,13 @@ async fn route_request( // Admin endpoints // Keep in sync with Settings::ADMIN_ENDPOINTS in crates/trusted-server-core/src/settings.rs - (Method::POST, "/admin/keys/rotate") | (Method::POST, "/_ts/admin/keys/rotate") => { + (Method::POST, "/admin/keys/rotate" | "/_ts/admin/keys/rotate") => { (handle_rotate_key(settings, runtime_services, req), false) } - (Method::POST, "/admin/keys/deactivate") | (Method::POST, "/_ts/admin/keys/deactivate") => { - ( - handle_deactivate_key(settings, runtime_services, req), - false, - ) - } + (Method::POST, "/admin/keys/deactivate" | "/_ts/admin/keys/deactivate") => ( + handle_deactivate_key(settings, runtime_services, req), + false, + ), (Method::GET, "/_ts/api/v1/identify") => ( require_identity_graph(settings) .and_then(|kv| handle_identify(settings, &kv, partner_registry, &req, &ec_context)), @@ -393,7 +400,7 @@ async fn route_request( (Method::GET, "/first-party/click") => { (handle_first_party_click(settings, req).await, false) } - (Method::GET, "/first-party/sign") | (Method::POST, "/first-party/sign") => { + (Method::GET | Method::POST, "/first-party/sign") => { (handle_first_party_proxy_sign(settings, req).await, false) } (Method::POST, "/first-party/proxy-rebuild") => { @@ -420,10 +427,7 @@ async fn route_request( // No known route matched, proxy to publisher origin as fallback _ => { - log::info!( - "No known route matched for path: {}, proxying to publisher origin", - path - ); + log::info!("No known route matched for path: {path}, proxying to publisher origin"); match handle_publisher_request( settings, @@ -488,7 +492,7 @@ async fn route_request( } Ok(PublisherResponse::Buffered(response)) => (Ok(response), true), Err(e) => { - log::error!("Failed to proxy to publisher origin: {:?}", e); + log::error!("Failed to proxy to publisher origin: {e:?}"); (Err(e), true) } } diff --git a/crates/trusted-server-adapter-fastly/src/management_api.rs b/crates/trusted-server-adapter-fastly/src/management_api.rs index 92ae8e6c..bbd90486 100644 --- a/crates/trusted-server-adapter-fastly/src/management_api.rs +++ b/crates/trusted-server-adapter-fastly/src/management_api.rs @@ -16,13 +16,13 @@ //! Credential values are never logged. Log messages include store IDs and //! operation names only. -use std::io::Read; +use std::io::Read as _; use base64::{engine::general_purpose, Engine as _}; -use error_stack::{Report, ResultExt}; +use error_stack::{Report, ResultExt as _}; use fastly::http::{Method, StatusCode}; use fastly::{Request, Response}; -use trusted_server_core::platform::{PlatformError, PlatformSecretStore, StoreName}; +use trusted_server_core::platform::{PlatformError, PlatformSecretStore as _, StoreName}; use crate::platform::FastlyPlatformSecretStore; @@ -179,7 +179,7 @@ impl FastlyManagementApiClient { } request.send(&self.backend_name).map_err(|e| { - Report::new(error_kind()).attach(format!("management API request failed: {}", e)) + Report::new(error_kind()).attach(format!("management API request failed: {e}")) }) } @@ -205,7 +205,7 @@ impl FastlyManagementApiClient { || PlatformError::ConfigStore, )?; - let entity_description = format!("key '{}'", key); + let entity_description = format!("key '{key}'"); check_response( &mut response, || PlatformError::ConfigStore, @@ -214,11 +214,7 @@ impl FastlyManagementApiClient { store_id, )?; - log::debug!( - "FastlyManagementApiClient: updated config key '{}' in store '{}'", - key, - store_id - ); + log::debug!("FastlyManagementApiClient: updated config key '{key}' in store '{store_id}'"); Ok(()) } @@ -246,7 +242,7 @@ impl FastlyManagementApiClient { return Ok(()); } - let entity_description = format!("key '{}'", key); + let entity_description = format!("key '{key}'"); check_response( &mut response, || PlatformError::ConfigStore, @@ -256,9 +252,7 @@ impl FastlyManagementApiClient { )?; log::debug!( - "FastlyManagementApiClient: deleted config key '{}' from store '{}'", - key, - store_id + "FastlyManagementApiClient: deleted config key '{key}' from store '{store_id}'" ); Ok(()) } @@ -285,7 +279,7 @@ impl FastlyManagementApiClient { || PlatformError::SecretStore, )?; - let entity_description = format!("name '{}'", secret_name); + let entity_description = format!("name '{secret_name}'"); check_response( &mut response, || PlatformError::SecretStore, @@ -295,9 +289,7 @@ impl FastlyManagementApiClient { )?; log::debug!( - "FastlyManagementApiClient: upserted secret '{}' in store '{}'", - secret_name, - store_id + "FastlyManagementApiClient: upserted secret '{secret_name}' in store '{store_id}'" ); Ok(()) } @@ -326,7 +318,7 @@ impl FastlyManagementApiClient { return Ok(()); } - let entity_description = format!("name '{}'", secret_name); + let entity_description = format!("name '{secret_name}'"); check_response( &mut response, || PlatformError::SecretStore, @@ -336,9 +328,7 @@ impl FastlyManagementApiClient { )?; log::debug!( - "FastlyManagementApiClient: deleted secret '{}' from store '{}'", - secret_name, - store_id + "FastlyManagementApiClient: deleted secret '{secret_name}' from store '{store_id}'" ); Ok(()) } @@ -400,9 +390,9 @@ mod tests { #[test] fn create_secret_uses_secret_store_error_for_transport_failures() { let client = FastlyManagementApiClient::with_components( - "test-api-key".to_string(), + "test-api-key".to_owned(), FASTLY_API_HOST, - "missing-management-backend".to_string(), + "missing-management-backend".to_owned(), ); let err = client diff --git a/crates/trusted-server-adapter-fastly/src/platform.rs b/crates/trusted-server-adapter-fastly/src/platform.rs index 64640dbd..88d52d60 100644 --- a/crates/trusted-server-adapter-fastly/src/platform.rs +++ b/crates/trusted-server-adapter-fastly/src/platform.rs @@ -10,7 +10,7 @@ use std::sync::Arc; use edgezero_adapter_fastly::key_value_store::FastlyKvStore; use edgezero_core::key_value_store::KvError; -use error_stack::{Report, ResultExt}; +use error_stack::{Report, ResultExt as _}; use fastly::geo::geo_lookup; use fastly::{ConfigStore, Request, SecretStore}; @@ -188,7 +188,7 @@ fn edge_request_to_fastly( ) -> Result> { let (parts, body) = request.into_parts(); let mut fastly_req = fastly::Request::new(parts.method, parts.uri.to_string()); - for (name, value) in parts.headers.iter() { + for (name, value) in &parts.headers { fastly_req.append_header(name.as_str(), value.as_bytes()); } match body { @@ -304,7 +304,7 @@ impl PlatformHttpClient for FastlyPlatformHttpClient { log::warn!("select: response has no backend name, correlation will fail"); "" }) - .to_string(); + .to_owned(); fastly_response_to_platform(fastly_resp, backend_name) } Err(e) => { @@ -362,8 +362,12 @@ pub fn build_runtime_services( .geo(Arc::new(FastlyPlatformGeo)) .client_info(ClientInfo { client_ip: req.get_client_ip_addr(), - tls_protocol: req.get_tls_protocol().map(str::to_string), - tls_cipher: req.get_tls_cipher_openssl_name().map(str::to_string), + tls_protocol: req.get_tls_protocol().ok().flatten().map(str::to_owned), + tls_cipher: req + .get_tls_cipher_openssl_name() + .ok() + .flatten() + .map(str::to_owned), }) .build() } @@ -374,7 +378,10 @@ pub fn build_runtime_services( /// /// Returns [`KvError::Unavailable`] when the store does not exist, or /// [`KvError::Internal`] when the Fastly SDK fails to open it. -#[allow(dead_code)] +#[allow( + dead_code, + reason = "helper is retained for direct KV-store construction in adapter tests" +)] pub fn open_kv_store(store_name: &str) -> Result, KvError> { FastlyKvStore::open(store_name).map(|store| Arc::new(store) as Arc) } @@ -405,8 +412,8 @@ mod tests { fn predict_name_produces_same_name_as_backend_config() { let backend = FastlyPlatformBackend; let spec = PlatformBackendSpec { - scheme: "https".to_string(), - host: "origin.example.com".to_string(), + scheme: "https".to_owned(), + host: "origin.example.com".to_owned(), port: None, certificate_check: true, first_byte_timeout: Duration::from_secs(15), @@ -426,8 +433,8 @@ mod tests { fn predict_name_includes_nocert_suffix_when_cert_check_disabled() { let backend = FastlyPlatformBackend; let spec = PlatformBackendSpec { - scheme: "https".to_string(), - host: "origin.example.com".to_string(), + scheme: "https".to_owned(), + host: "origin.example.com".to_owned(), port: None, certificate_check: false, first_byte_timeout: Duration::from_secs(15), @@ -447,7 +454,7 @@ mod tests { fn predict_name_returns_error_for_empty_host() { let backend = FastlyPlatformBackend; let spec = PlatformBackendSpec { - scheme: "https".to_string(), + scheme: "https".to_owned(), host: String::new(), port: None, certificate_check: true, @@ -463,11 +470,11 @@ mod tests { fn predict_name_encodes_custom_timeout() { let backend = FastlyPlatformBackend; let spec = PlatformBackendSpec { - scheme: "https".to_string(), - host: "origin.example.com".to_string(), + scheme: "https".to_owned(), + host: "origin.example.com".to_owned(), port: None, certificate_check: true, - first_byte_timeout: Duration::from_millis(2000), + first_byte_timeout: Duration::from_secs(2), }; let name = backend @@ -569,7 +576,7 @@ mod tests { fn fastly_platform_http_client_select_returns_error_for_wrong_inner_type() { let client = FastlyPlatformHttpClient; // Wrap a non-PendingRequest type to trigger the downcast failure. - let wrong = PlatformPendingRequest::new(42u32).with_backend_name("origin-a"); + let wrong = PlatformPendingRequest::new(42_u32).with_backend_name("origin-a"); let err = futures::executor::block_on(client.select(vec![wrong])) .expect_err("should return error for wrong inner type"); diff --git a/crates/trusted-server-adapter-fastly/src/route_tests.rs b/crates/trusted-server-adapter-fastly/src/route_tests.rs index 9aa4c54a..dc00ec80 100644 --- a/crates/trusted-server-adapter-fastly/src/route_tests.rs +++ b/crates/trusted-server-adapter-fastly/src/route_tests.rs @@ -24,10 +24,9 @@ struct StubJwksConfigStore; impl PlatformConfigStore for StubJwksConfigStore { fn get(&self, _store_name: &StoreName, key: &str) -> Result> { match key { - "active-kids" => Ok("test-kid-1".to_string()), + "active-kids" => Ok("test-kid-1".to_owned()), "test-kid-1" => Ok( - r#"{"kty":"OKP","crv":"Ed25519","x":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","kid":"test-kid-1","alg":"EdDSA"}"# - .to_string(), + r#"{"kty":"OKP","crv":"Ed25519","x":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","kid":"test-kid-1","alg":"EdDSA"}"#.to_owned(), ), _ => Err(Report::new(PlatformError::ConfigStore)), } @@ -170,8 +169,12 @@ fn test_runtime_services(req: &Request) -> RuntimeServices { .geo(Arc::new(NoopGeo)) .client_info(ClientInfo { client_ip: req.get_client_ip_addr(), - tls_protocol: req.get_tls_protocol().map(str::to_string), - tls_cipher: req.get_tls_cipher_openssl_name().map(str::to_string), + tls_protocol: req.get_tls_protocol().ok().flatten().map(str::to_owned), + tls_cipher: req + .get_tls_cipher_openssl_name() + .ok() + .flatten() + .map(str::to_owned), }) .build() } diff --git a/crates/trusted-server-core/Cargo.toml b/crates/trusted-server-core/Cargo.toml index 95ef3a03..2564e792 100644 --- a/crates/trusted-server-core/Cargo.toml +++ b/crates/trusted-server-core/Cargo.toml @@ -42,8 +42,8 @@ sha2 = { workspace = true } subtle = { workspace = true } tokio = { workspace = true } toml = { workspace = true } -trusted-server-js = { path = "../js" } -trusted-server-openrtb = { path = "../openrtb" } +trusted-server-js = { path = "../trusted-server-js" } +trusted-server-openrtb = { path = "../trusted-server-openrtb" } url = { workspace = true } urlencoding = { workspace = true } uuid = { workspace = true } diff --git a/crates/trusted-server-core/benches/consent_decode.rs b/crates/trusted-server-core/benches/consent_decode.rs index 80c2e4f1..d0978eef 100644 --- a/crates/trusted-server-core/benches/consent_decode.rs +++ b/crates/trusted-server-core/benches/consent_decode.rs @@ -40,7 +40,7 @@ fn build_tc_bytes(vendor_count: u16, use_range_encoding: bool) -> Vec { 213 + 17 + usize::from(vendor_count) }; let total_bytes = total_bits.div_ceil(8); - let mut buf = vec![0u8; total_bytes]; + let mut buf = vec![0_u8; total_bytes]; // Version (6 bits) = 2 write_bits(&mut buf, 0, 6, 2); diff --git a/crates/trusted-server-core/build.rs b/crates/trusted-server-core/build.rs index 6f56e85a..e5e5d94d 100644 --- a/crates/trusted-server-core/build.rs +++ b/crates/trusted-server-core/build.rs @@ -1,7 +1,14 @@ // Build script includes source modules (`error`, `auction_config_types`, etc.) // for compile-time config validation. Not all items from those modules are used // in the build context, so `dead_code` is expected. -#![allow(clippy::unwrap_used, clippy::panic, dead_code)] +#![allow( + dead_code, + clippy::expect_used, + clippy::pedantic, + clippy::panic, + clippy::restriction, + reason = "build script validates checked-in configuration and should fail Cargo on invalid input" +)] #[path = "src/error.rs"] mod error; @@ -33,8 +40,9 @@ fn main() { // Read init config let init_config_path = Path::new(TRUSTED_SERVER_INIT_CONFIG_PATH); - let toml_content = fs::read_to_string(init_config_path) - .unwrap_or_else(|_| panic!("Failed to read {init_config_path:?}")); + let toml_content = fs::read_to_string(init_config_path).unwrap_or_else(|err| { + panic!("Failed to read {}: {err}", init_config_path.display()); + }); // Merge base TOML with environment variable overrides and write output. // Panics if admin endpoints are not covered by a handler. @@ -48,7 +56,8 @@ fn main() { let dest_path = Path::new(TRUSTED_SERVER_OUTPUT_CONFIG_PATH); let current = fs::read_to_string(dest_path).unwrap_or_default(); if current != merged_toml { - fs::write(dest_path, merged_toml) - .unwrap_or_else(|_| panic!("Failed to write {dest_path:?}")); + fs::write(dest_path, merged_toml).unwrap_or_else(|err| { + panic!("Failed to write {}: {err}", dest_path.display()); + }); } } diff --git a/crates/trusted-server-core/src/auction/context.rs b/crates/trusted-server-core/src/auction/context.rs index 0e5c3ebb..f7fd7829 100644 --- a/crates/trusted-server-core/src/auction/context.rs +++ b/crates/trusted-server-core/src/auction/context.rs @@ -57,10 +57,10 @@ pub fn build_url_with_context_params( ) -> String { let Ok(mut url) = url::Url::parse(base_url) else { log::warn!("build_url_with_context_params: failed to parse base URL, returning as-is"); - return base_url.to_string(); + return base_url.to_owned(); }; - let mut appended = 0usize; + let mut appended = 0_usize; for (context_key, param_name) in mapping { if let Some(value) = context.get(context_key) { @@ -73,10 +73,7 @@ pub fn build_url_with_context_params( } if appended > 0 { - log::info!( - "build_url_with_context_params: appended {} context query params", - appended - ); + log::info!("build_url_with_context_params: appended {appended} context query params"); } url.to_string() @@ -100,10 +97,10 @@ mod tests { #[test] fn test_build_url_with_context_params_appends_array() { let context = HashMap::from([( - "permutive_segments".to_string(), + "permutive_segments".to_owned(), ContextValue::StringList(vec!["10000001".into(), "10000003".into(), "adv".into()]), )]); - let mapping = BTreeMap::from([("permutive_segments".to_string(), "permutive".to_string())]); + let mapping = BTreeMap::from([("permutive_segments".to_owned(), "permutive".to_owned())]); let url = build_url_with_context_params( "http://localhost:6767/adserver/mediate", @@ -119,10 +116,10 @@ mod tests { #[test] fn test_build_url_with_context_params_preserves_existing_query() { let context = HashMap::from([( - "permutive_segments".to_string(), + "permutive_segments".to_owned(), ContextValue::StringList(vec!["123".into(), "adv".into()]), )]); - let mapping = BTreeMap::from([("permutive_segments".to_string(), "permutive".to_string())]); + let mapping = BTreeMap::from([("permutive_segments".to_owned(), "permutive".to_owned())]); let url = build_url_with_context_params( "http://localhost:6767/adserver/mediate?debug=true", @@ -138,7 +135,7 @@ mod tests { #[test] fn test_build_url_with_context_params_no_matching_keys() { let context = HashMap::new(); - let mapping = BTreeMap::from([("permutive_segments".to_string(), "permutive".to_string())]); + let mapping = BTreeMap::from([("permutive_segments".to_owned(), "permutive".to_owned())]); let url = build_url_with_context_params( "http://localhost:6767/adserver/mediate", @@ -151,10 +148,10 @@ mod tests { #[test] fn test_build_url_with_context_params_empty_array_skipped() { let context = HashMap::from([( - "permutive_segments".to_string(), + "permutive_segments".to_owned(), ContextValue::StringList(vec![]), )]); - let mapping = BTreeMap::from([("permutive_segments".to_string(), "permutive".to_string())]); + let mapping = BTreeMap::from([("permutive_segments".to_owned(), "permutive".to_owned())]); let url = build_url_with_context_params( "http://localhost:6767/adserver/mediate", @@ -168,17 +165,17 @@ mod tests { fn test_build_url_with_context_params_multiple_mappings() { let context = HashMap::from([ ( - "permutive_segments".to_string(), + "permutive_segments".to_owned(), ContextValue::StringList(vec!["seg1".into()]), ), ( - "lockr_ids".to_string(), + "lockr_ids".to_owned(), ContextValue::Text("lockr-abc-123".into()), ), ]); let mapping = BTreeMap::from([ - ("lockr_ids".to_string(), "lockr".to_string()), - ("permutive_segments".to_string(), "permutive".to_string()), + ("lockr_ids".to_owned(), "lockr".to_owned()), + ("permutive_segments".to_owned(), "permutive".to_owned()), ]); let url = build_url_with_context_params( @@ -192,8 +189,8 @@ mod tests { #[test] fn test_build_url_with_context_params_scalar_number() { - let context = HashMap::from([("count".to_string(), ContextValue::Number(42.0))]); - let mapping = BTreeMap::from([("count".to_string(), "n".to_string())]); + let context = HashMap::from([("count".to_owned(), ContextValue::Number(42.0))]); + let mapping = BTreeMap::from([("count".to_owned(), "n".to_owned())]); let url = build_url_with_context_params( "http://localhost:6767/adserver/mediate", diff --git a/crates/trusted-server-core/src/auction/endpoints.rs b/crates/trusted-server-core/src/auction/endpoints.rs index d175be9f..e70c4467 100644 --- a/crates/trusted-server-core/src/auction/endpoints.rs +++ b/crates/trusted-server-core/src/auction/endpoints.rs @@ -1,6 +1,6 @@ //! HTTP endpoint handlers for auction requests. -use error_stack::{Report, ResultExt}; +use error_stack::{Report, ResultExt as _}; use fastly::http::StatusCode; use fastly::{Request, Response}; use serde_json::Value as JsonValue; @@ -72,7 +72,7 @@ pub async fn handle_auction( } let body: AdRequest = serde_json::from_slice(&body_bytes).change_context(TrustedServerError::Auction { - message: "Failed to parse auction request body".to_string(), + message: "Failed to parse auction request body".to_owned(), })?; log::info!( @@ -138,7 +138,7 @@ pub async fn handle_auction( .run_auction(&auction_request, &context) .await .change_context(TrustedServerError::Auction { - message: "Auction orchestration failed".to_string(), + message: "Auction orchestration failed".to_owned(), })?; log::info!( @@ -311,18 +311,17 @@ fn merge_auction_eids( continue; } - let source_index = match merged + let source_index = if let Some(index) = merged .iter() .position(|existing: &Eid| existing.source == eid.source) { - Some(index) => index, - None => { - merged.push(Eid { - source: eid.source.clone(), - uids: Vec::new(), - }); - merged.len() - 1 - } + index + } else { + merged.push(Eid { + source: eid.source.clone(), + uids: Vec::new(), + }); + merged.len() - 1 }; for uid in eid.uids { @@ -632,26 +631,26 @@ mod tests { #[test] fn merge_auction_eids_deduplicates_client_and_resolved_ids() { let client_eids = Some(vec![Eid { - source: "id5-sync.com".to_string(), + source: "id5-sync.com".to_owned(), uids: vec![Uid { - id: "ID5_abc".to_string(), + id: "ID5_abc".to_owned(), atype: Some(1), ext: None, }], }]); let resolved_eids = Some(vec![ Eid { - source: "id5-sync.com".to_string(), + source: "id5-sync.com".to_owned(), uids: vec![Uid { - id: "ID5_abc".to_string(), + id: "ID5_abc".to_owned(), atype: Some(1), ext: None, }], }, Eid { - source: "liveramp.com".to_string(), + source: "liveramp.com".to_owned(), uids: vec![Uid { - id: "LR_xyz".to_string(), + id: "LR_xyz".to_owned(), atype: Some(3), ext: None, }], @@ -670,17 +669,17 @@ mod tests { #[test] fn merge_auction_eids_preserves_multiple_uids_per_source() { let client_eids = Some(vec![Eid { - source: "sharedid.org".to_string(), + source: "sharedid.org".to_owned(), uids: vec![Uid { - id: "shared_client".to_string(), + id: "shared_client".to_owned(), atype: None, ext: None, }], }]); let resolved_eids = Some(vec![Eid { - source: "sharedid.org".to_string(), + source: "sharedid.org".to_owned(), uids: vec![Uid { - id: "shared_server".to_string(), + id: "shared_server".to_owned(), atype: Some(3), ext: None, }], @@ -697,17 +696,17 @@ mod tests { #[test] fn merge_auction_eids_prefers_server_resolved_metadata_on_conflict() { let client_eids = Some(vec![Eid { - source: "adserver.org".to_string(), + source: "adserver.org".to_owned(), uids: vec![Uid { - id: "shared_uid".to_string(), + id: "shared_uid".to_owned(), atype: Some(1), ext: Some(json!({ "provider": "client" })), }], }]); let resolved_eids = Some(vec![Eid { - source: "adserver.org".to_string(), + source: "adserver.org".to_owned(), uids: vec![Uid { - id: "shared_uid".to_string(), + id: "shared_uid".to_owned(), atype: Some(3), ext: Some(json!({ "provider": "server" })), }], diff --git a/crates/trusted-server-core/src/auction/formats.rs b/crates/trusted-server-core/src/auction/formats.rs index 08d6a6cd..f0ab5ab4 100644 --- a/crates/trusted-server-core/src/auction/formats.rs +++ b/crates/trusted-server-core/src/auction/formats.rs @@ -4,7 +4,7 @@ //! - Parsing incoming tsjs/Prebid.js format requests //! - Converting internal auction results to `OpenRTB` 2.x responses -use error_stack::{ensure, Report, ResultExt}; +use error_stack::{ensure, Report, ResultExt as _}; use fastly::http::{header, StatusCode}; use fastly::{Request, Response}; use serde::Deserialize; @@ -19,7 +19,9 @@ use crate::creative; use crate::ec::eids::encode_eids_header; use crate::error::TrustedServerError; use crate::geo::GeoInfo; -use crate::openrtb::{to_openrtb_i32, OpenRtbBid, OpenRtbResponse, ResponseExt, SeatBid, ToExt}; +use crate::openrtb::{ + to_openrtb_i32, OpenRtbBid, OpenRtbResponse, ResponseExt, SeatBid, ToExt as _, +}; use crate::settings::Settings; use super::orchestrator::OrchestrationResult; @@ -100,7 +102,7 @@ pub fn convert_tsjs_to_auction_request( ensure!( size.len() == 2, TrustedServerError::BadRequest { - message: "Invalid banner size; expected [width, height]".to_string(), + message: "Invalid banner size; expected [width, height]".to_owned(), } ); @@ -136,7 +138,10 @@ pub fn convert_tsjs_to_auction_request( .get_header_str("user-agent") .map(std::string::ToString::to_string), ip: req.get_client_ip_addr().map(|ip| ip.to_string()), - #[allow(deprecated)] + #[allow( + deprecated, + reason = "legacy Fastly geo extraction remains supported for auction requests" + )] geo: GeoInfo::from_request(req), }); @@ -155,13 +160,12 @@ pub fn convert_tsjs_to_auction_request( } Err(_) => { log::debug!( - "Auction context: dropping key '{}' with unsupported type", - key + "Auction context: dropping key '{key}' with unsupported type" ); } } } else { - log::debug!("Auction context: dropping disallowed key '{}'", key); + log::debug!("Auction context: dropping disallowed key '{key}'"); } } if !context.is_empty() { @@ -236,7 +240,7 @@ pub fn convert_to_openrtb_response( let rewritten = creative::rewrite_creative_html(settings, &sanitized); log::debug!( - "Processed creative for auction {} slot {} ({} → {} → {} bytes)", + "Processed creative for auction {} slot {} ({} \u{2192} {} \u{2192} {} bytes)", auction_request.id, slot_id, raw_creative.len(), @@ -257,7 +261,7 @@ pub fn convert_to_openrtb_response( let openrtb_bid = OpenRtbBid { id: Some(format!("{}-{}", bid.bidder, slot_id)), - impid: Some(slot_id.to_string()), + impid: Some(slot_id.clone()), price: Some(price), adm: Some(creative_html), crid: Some(format!("{}-creative", bid.bidder)), @@ -289,11 +293,11 @@ pub fn convert_to_openrtb_response( .collect(); let response_body = OpenRtbResponse { - id: Some(auction_request.id.to_string()), + id: Some(auction_request.id.clone()), seatbid: seatbids, ext: ResponseExt { orchestrator: OrchestratorExt { - strategy: strategy_name.to_string(), + strategy: strategy_name.to_owned(), providers: result.provider_responses.len(), total_bids: result.total_bids(), time_ms: result.total_time_ms, @@ -306,7 +310,7 @@ pub fn convert_to_openrtb_response( let body_bytes = serde_json::to_vec(&response_body).change_context(TrustedServerError::Auction { - message: "Failed to serialize auction response".to_string(), + message: "Failed to serialize auction response".to_owned(), })?; let mut response = Response::from_status(StatusCode::OK) diff --git a/crates/trusted-server-core/src/auction/orchestrator.rs b/crates/trusted-server-core/src/auction/orchestrator.rs index fd59c778..c538f2ca 100644 --- a/crates/trusted-server-core/src/auction/orchestrator.rs +++ b/crates/trusted-server-core/src/auction/orchestrator.rs @@ -1,6 +1,6 @@ //! Auction orchestrator for managing multi-provider auctions. -use error_stack::{Report, ResultExt}; +use error_stack::{Report, ResultExt as _}; use fastly::http::request::{select, PendingRequest}; use std::collections::HashMap; use std::sync::Arc; @@ -78,8 +78,8 @@ impl AuctionOrchestrator { /// Register an auction provider. pub fn register_provider(&mut self, provider: Arc) { - let name = provider.provider_name().to_string(); - log::info!("Registering auction provider: {}", name); + let name = provider.provider_name().to_owned(); + log::info!("Registering auction provider: {name}"); self.providers.insert(name, provider); } @@ -120,8 +120,7 @@ impl AuctionOrchestrator { }; log::info!( - "Running auction with strategy: {} (auto-detected from mediator config)", - strategy_name + "Running auction with strategy: {strategy_name} (auto-detected from mediator config)" ); Ok(OrchestrationResult { @@ -163,7 +162,7 @@ impl AuctionOrchestrator { // lgtm[rust/cleartext-logging] // This warning reports timeout budget metadata only; no secret settings are logged. log::warn!( - "Auction timeout ({}ms) exhausted during bidding phase — skipping mediator", + "Auction timeout ({}ms) exhausted during bidding phase \u{2014} skipping mediator", context.timeout_ms ); let winning = self.select_winning_bids(&provider_responses, &floor_prices); @@ -273,7 +272,7 @@ impl AuctionOrchestrator { if provider_names.is_empty() { return Err(Report::new(TrustedServerError::Auction { - message: "No providers configured".to_string(), + message: "No providers configured".to_owned(), })); } @@ -293,12 +292,9 @@ impl AuctionOrchestrator { let mut responses = Vec::new(); for provider_name in provider_names { - let provider = match self.providers.get(provider_name) { - Some(p) => p, - None => { - log::warn!("Provider '{}' not registered, skipping", provider_name); - continue; - } + let Some(provider) = self.providers.get(provider_name) else { + log::warn!("Provider '{provider_name}' not registered, skipping"); + continue; }; if !provider.is_enabled() { @@ -320,7 +316,7 @@ impl AuctionOrchestrator { // lgtm[rust/cleartext-logging] // This warning reports timeout budget metadata only; no secret settings are logged. log::warn!( - "Auction timeout ({}ms) exhausted before launching '{}' — skipping", + "Auction timeout ({}ms) exhausted before launching '{}' \u{2014} skipping", context.timeout_ms, provider.provider_name() ); @@ -330,15 +326,12 @@ impl AuctionOrchestrator { // Get the backend name for this provider to map responses back. // Must be computed after effective_timeout since the timeout is // part of the backend name. - let backend_name = match provider.backend_name(effective_timeout) { - Some(name) => name, - None => { - log::warn!( - "Provider '{}' has no backend_name, skipping", - provider.provider_name() - ); - continue; - } + let Some(backend_name) = provider.backend_name(effective_timeout) else { + log::warn!( + "Provider '{}' has no backend_name, skipping", + provider.provider_name() + ); + continue; }; let provider_context = AuctionContext { @@ -410,7 +403,7 @@ impl AuctionOrchestrator { match result { Ok(response) => { // Identify the provider from the backend name - let backend_name = response.get_backend_name().unwrap_or_default().to_string(); + let backend_name = response.get_backend_name().unwrap_or_default().to_owned(); if let Some((provider_name, start_time, provider)) = backend_to_provider.remove(&backend_name) @@ -436,9 +429,7 @@ impl AuctionOrchestrator { // lgtm[rust/cleartext-logging] // This warning reports provider parse failures only; no secret values are logged. log::warn!( - "Provider '{}' failed to parse response: {:?}", - provider_name, - e + "Provider '{provider_name}' failed to parse response: {e:?}" ); responses.push(provider_error_response( provider_name, @@ -450,15 +441,14 @@ impl AuctionOrchestrator { } } else { log::warn!( - "Received response from unknown backend '{}', ignoring", - backend_name + "Received response from unknown backend '{backend_name}', ignoring" ); } } Err(e) => { // When select() returns an error, we can't easily identify which // provider failed since the PendingRequest is consumed - log::warn!("A provider request failed: {:?}", e); + log::warn!("A provider request failed: {e:?}"); } } @@ -498,16 +488,13 @@ impl AuctionOrchestrator { for bid in &response.bids { // Skip bids without decoded prices (e.g., APS bids) // These require mediation layer to decode - let bid_price = match bid.price { - Some(p) => p, - None => { - log::debug!( - "Skipping bid for slot '{}' from '{}' - price requires mediation to decode", - bid.slot_id, - bid.bidder - ); - continue; - } + let Some(bid_price) = bid.price else { + log::debug!( + "Skipping bid for slot '{}' from '{}' - price requires mediation to decode", + bid.slot_id, + bid.bidder + ); + continue; }; let should_replace = match winning_bids.get(&bid.slot_id) { @@ -544,15 +531,13 @@ impl AuctionOrchestrator { Some(price) if price >= *floor => true, Some(_) => { log::info!( - "Dropping winning bid below floor price for slot '{}'", - slot_id + "Dropping winning bid below floor price for slot '{slot_id}'" ); false } None => { log::debug!( - "Passing bid with encoded price for slot '{}' - floor check deferred to mediation", - slot_id + "Passing bid with encoded price for slot '{slot_id}' - floor check deferred to mediation" ); true } @@ -593,7 +578,7 @@ impl AuctionOrchestrator { self.providers.keys().collect::>() ); Report::new(TrustedServerError::Auction { - message: format!("Provider '{}' not registered", name), + message: format!("Provider '{name}' not registered"), }) }) } @@ -661,10 +646,10 @@ mod tests { fn create_test_auction_request() -> AuctionRequest { AuctionRequest { - id: "test-auction-123".to_string(), + id: "test-auction-123".to_owned(), slots: vec![ AdSlot { - id: "header-banner".to_string(), + id: "header-banner".to_owned(), formats: vec![AdFormat { media_type: MediaType::Banner, width: 728, @@ -675,7 +660,7 @@ mod tests { bidders: HashMap::new(), }, AdSlot { - id: "sidebar".to_string(), + id: "sidebar".to_owned(), formats: vec![AdFormat { media_type: MediaType::Banner, width: 300, @@ -687,11 +672,11 @@ mod tests { }, ], publisher: PublisherInfo { - domain: "test.com".to_string(), - page_url: Some("https://test.com/article".to_string()), + domain: "test.com".to_owned(), + page_url: Some("https://test.com/article".to_owned()), }, user: UserInfo { - id: Some("user-123".to_string()), + id: Some("user-123".to_owned()), consent: None, eids: None, }, @@ -709,7 +694,7 @@ mod tests { #[test] fn provider_error_response_includes_diagnostic_metadata() { let error = Report::new(TrustedServerError::Auction { - message: "parse failed".to_string(), + message: "parse failed".to_owned(), }) .attach("internal/source.rs:12:34"); @@ -785,20 +770,20 @@ mod tests { fn filters_winning_bids_below_floor() { let orchestrator = AuctionOrchestrator::new(AuctionConfig::default()); let mut floor_prices = HashMap::new(); - floor_prices.insert("slot-1".to_string(), 1.00); - floor_prices.insert("slot-2".to_string(), 2.00); + floor_prices.insert("slot-1".to_owned(), 1.00); + floor_prices.insert("slot-2".to_owned(), 2.00); // Arrange winning bids with one below floor. let mut winning_bids = HashMap::new(); winning_bids.insert( - "slot-1".to_string(), + "slot-1".to_owned(), Bid { - slot_id: "slot-1".to_string(), + slot_id: "slot-1".to_owned(), price: Some(0.50), - currency: "USD".to_string(), - creative: Some("
Ad
".to_string()), + currency: "USD".to_owned(), + creative: Some("
Ad
".to_owned()), adomain: None, - bidder: "test-bidder".to_string(), + bidder: "test-bidder".to_owned(), width: 300, height: 250, nurl: None, @@ -807,14 +792,14 @@ mod tests { }, ); winning_bids.insert( - "slot-2".to_string(), + "slot-2".to_owned(), Bid { - slot_id: "slot-2".to_string(), + slot_id: "slot-2".to_owned(), price: Some(2.00), - currency: "USD".to_string(), - creative: Some("
Ad
".to_string()), + currency: "USD".to_owned(), + creative: Some("
Ad
".to_owned()), adomain: None, - bidder: "test-bidder".to_string(), + bidder: "test-bidder".to_owned(), width: 300, height: 250, nurl: None, @@ -859,8 +844,8 @@ mod tests { providers: vec![], mediator: None, timeout_ms: 2000, - creative_store: "creative_store".to_string(), - allowed_context_keys: HashSet::from(["permutive_segments".to_string()]), + creative_store: "creative_store".to_owned(), + allowed_context_keys: HashSet::from(["permutive_segments".to_owned()]), }; let orchestrator = AuctionOrchestrator::new(config); @@ -874,7 +859,7 @@ mod tests { assert!(result.is_err()); let err = result.unwrap_err(); - assert!(format!("{}", err).contains("No providers configured")); + assert!(format!("{err}").contains("No providers configured")); } #[test] @@ -935,18 +920,18 @@ mod tests { // This is correct behavior for parallel-only strategy where mediation happens later let orchestrator = AuctionOrchestrator::new(AuctionConfig::default()); let mut floor_prices = HashMap::new(); - floor_prices.insert("slot-1".to_string(), 1.00); + floor_prices.insert("slot-1".to_owned(), 1.00); let mut winning_bids = HashMap::new(); winning_bids.insert( - "slot-1".to_string(), + "slot-1".to_owned(), Bid { - slot_id: "slot-1".to_string(), + slot_id: "slot-1".to_owned(), price: None, // APS bid with encoded price - currency: "USD".to_string(), - creative: Some("
Ad
".to_string()), + currency: "USD".to_owned(), + creative: Some("
Ad
".to_owned()), adomain: None, - bidder: "aps".to_string(), + bidder: "aps".to_owned(), width: 300, height: 250, nurl: None, @@ -954,7 +939,7 @@ mod tests { metadata: { let mut m = HashMap::new(); m.insert( - "amznbid".to_string(), + "amznbid".to_owned(), serde_json::json!("encoded_price_data"), ); m diff --git a/crates/trusted-server-core/src/auction/types.rs b/crates/trusted-server-core/src/auction/types.rs index 9b74d89e..b772df89 100644 --- a/crates/trusted-server-core/src/auction/types.rs +++ b/crates/trusted-server-core/src/auction/types.rs @@ -272,12 +272,12 @@ mod tests { fn make_bid(bidder: &str) -> Bid { Bid { - slot_id: "slot-1".to_string(), + slot_id: "slot-1".to_owned(), price: Some(1.0), - currency: "USD".to_string(), + currency: "USD".to_owned(), creative: None, adomain: None, - bidder: bidder.to_string(), + bidder: bidder.to_owned(), width: 300, height: 250, nurl: None, diff --git a/crates/trusted-server-core/src/auction_config_types.rs b/crates/trusted-server-core/src/auction_config_types.rs index bc486ded..11edd277 100644 --- a/crates/trusted-server-core/src/auction_config_types.rs +++ b/crates/trusted-server-core/src/auction_config_types.rs @@ -54,14 +54,17 @@ fn default_timeout() -> u32 { } fn default_creative_store() -> String { - "creative_store".to_string() + "creative_store".to_owned() } fn default_allowed_context_keys() -> HashSet { HashSet::new() } -#[allow(dead_code)] // Methods used in runtime but not in build script +#[allow( + dead_code, + reason = "methods are used by the runtime crate but not by build.rs path inclusion" +)] impl AuctionConfig { /// Get all provider names. #[must_use] diff --git a/crates/trusted-server-core/src/auth.rs b/crates/trusted-server-core/src/auth.rs index 088d27e8..2254e3c0 100644 --- a/crates/trusted-server-core/src/auth.rs +++ b/crates/trusted-server-core/src/auth.rs @@ -34,9 +34,8 @@ pub fn enforce_basic_auth( return Ok(None); }; - let (username, password) = match extract_credentials(req) { - Some(credentials) => credentials, - None => return Ok(Some(unauthorized_response())), + let Some((username, password)) = extract_credentials(req) else { + return Ok(Some(unauthorized_response())); }; // Hash before comparing to normalise lengths — `ct_eq` on raw byte slices @@ -80,8 +79,8 @@ fn extract_credentials(req: &Request) -> Option<(String, String)> { let credentials = String::from_utf8(decoded).ok()?; let mut credentials_parts = credentials.splitn(2, ':'); - let username = credentials_parts.next()?.to_string(); - let password = credentials_parts.next()?.to_string(); + let username = credentials_parts.next()?.to_owned(); + let password = credentials_parts.next()?.to_owned(); Some((username, password)) } diff --git a/crates/trusted-server-core/src/backend.rs b/crates/trusted-server-core/src/backend.rs index 468a3f83..01c68cf7 100644 --- a/crates/trusted-server-core/src/backend.rs +++ b/crates/trusted-server-core/src/backend.rs @@ -1,6 +1,6 @@ use std::time::Duration; -use error_stack::{Report, ResultExt}; +use error_stack::{Report, ResultExt as _}; use fastly::backend::Backend; use url::Url; @@ -26,10 +26,10 @@ fn default_port_for_scheme(scheme: &str) -> u16 { /// would generate URLs without the port when the Host header didn't include it. #[inline] fn compute_host_header(scheme: &str, host: &str, port: u16) -> String { - if port != default_port_for_scheme(scheme) { - format!("{}:{}", host, port) + if port == default_port_for_scheme(scheme) { + host.to_owned() } else { - host.to_string() + format!("{host}:{port}") } } @@ -101,17 +101,17 @@ impl<'a> BackendConfig<'a> { fn compute_name(&self) -> Result<(String, u16), Report> { if self.host.is_empty() { return Err(Report::new(TrustedServerError::Proxy { - message: "missing host".to_string(), + message: "missing host".to_owned(), })); } if self.host.chars().any(char::is_control) { return Err(Report::new(TrustedServerError::Proxy { - message: "host contains control characters".to_string(), + message: "host contains control characters".to_owned(), })); } if self.scheme.chars().any(char::is_control) { return Err(Report::new(TrustedServerError::Proxy { - message: "scheme contains control characters".to_string(), + message: "scheme contains control characters".to_owned(), })); } @@ -181,33 +181,25 @@ impl<'a> BackendConfig<'a> { .sni_hostname(self.host) .check_certificate(self.host); } else { - log::warn!( - "INSECURE: certificate check disabled for backend: {}", - backend_name - ); + log::warn!("INSECURE: certificate check disabled for backend: {backend_name}"); } - log::info!("enable ssl for backend: {}", backend_name); + log::info!("enable ssl for backend: {backend_name}"); } match builder.finish() { Ok(_) => { - log::info!( - "created dynamic backend: {} -> {}", - backend_name, - host_with_port - ); + log::info!("created dynamic backend: {backend_name} -> {host_with_port}"); Ok(backend_name) } Err(e) => { let msg = e.to_string(); if msg.contains("NameInUse") || msg.contains("already in use") { - log::info!("reusing existing dynamic backend: {}", backend_name); + log::info!("reusing existing dynamic backend: {backend_name}"); Ok(backend_name) } else { Err(Report::new(TrustedServerError::Proxy { message: format!( - "dynamic backend creation failed ({} -> {}): {}", - backend_name, host_with_port, msg + "dynamic backend creation failed ({backend_name} -> {host_with_port}): {msg}" ), })) } @@ -225,7 +217,7 @@ impl<'a> BackendConfig<'a> { origin_url: &str, ) -> Result<(String, String, Option), Report> { let parsed_url = Url::parse(origin_url).change_context(TrustedServerError::Proxy { - message: format!("Invalid origin_url: {}", origin_url), + message: format!("Invalid origin_url: {origin_url}"), })?; let scheme = parsed_url.scheme().to_owned(); @@ -233,7 +225,7 @@ impl<'a> BackendConfig<'a> { .host_str() .ok_or_else(|| { Report::new(TrustedServerError::Proxy { - message: "Missing host in origin_url".to_string(), + message: "Missing host in origin_url".to_owned(), }) })? .to_owned(); @@ -442,7 +434,7 @@ mod tests { use std::time::Duration; let (name_a, _) = BackendConfig::new("https", "origin.example.com") - .first_byte_timeout(Duration::from_millis(2000)) + .first_byte_timeout(Duration::from_secs(2)) .compute_name() .expect("should compute name with 2000ms timeout"); let (name_b, _) = BackendConfig::new("https", "origin.example.com") diff --git a/crates/trusted-server-core/src/consent/extraction.rs b/crates/trusted-server-core/src/consent/extraction.rs index 98633baa..110ac622 100644 --- a/crates/trusted-server-core/src/consent/extraction.rs +++ b/crates/trusted-server-core/src/consent/extraction.rs @@ -49,8 +49,7 @@ pub fn extract_consent_signals( .headers() .get(HEADER_SEC_GPC) .and_then(|v| v.to_str().ok()) - .map(|v| v.trim() == "1") - .unwrap_or(false); + .is_some_and(|v| v.trim() == "1"); RawConsentSignals { raw_tc_string, diff --git a/crates/trusted-server-core/src/consent/gpp.rs b/crates/trusted-server-core/src/consent/gpp.rs index ffb770c2..c7ed9808 100644 --- a/crates/trusted-server-core/src/consent/gpp.rs +++ b/crates/trusted-server-core/src/consent/gpp.rs @@ -195,12 +195,11 @@ pub fn parse_gpp_sid_cookie(raw: &str) -> Option> { .split(',') .filter_map(|s| { let s = s.trim(); - match s.parse::() { - Ok(id) => Some(id), - Err(_) => { - log::debug!("Ignoring invalid __gpp_sid entry: {s:?}"); - None - } + if let Ok(id) = s.parse::() { + Some(id) + } else { + log::debug!("Ignoring invalid __gpp_sid entry: {s:?}"); + None } }) .collect(); diff --git a/crates/trusted-server-core/src/consent/jurisdiction.rs b/crates/trusted-server-core/src/consent/jurisdiction.rs index bcc825c5..18c600ac 100644 --- a/crates/trusted-server-core/src/consent/jurisdiction.rs +++ b/crates/trusted-server-core/src/consent/jurisdiction.rs @@ -46,9 +46,8 @@ impl fmt::Display for Jurisdiction { /// Returns [`Jurisdiction::Unknown`] when no geo data is available. #[must_use] pub fn detect_jurisdiction(geo: Option<&GeoInfo>, config: &ConsentConfig) -> Jurisdiction { - let geo = match geo { - Some(g) => g, - None => return Jurisdiction::Unknown, + let Some(geo) = geo else { + return Jurisdiction::Unknown; }; // Check GDPR countries first (EU/EEA/UK). This ordering also resolves diff --git a/crates/trusted-server-core/src/consent/mod.rs b/crates/trusted-server-core/src/consent/mod.rs index cd73acc9..738ffc24 100644 --- a/crates/trusted-server-core/src/consent/mod.rs +++ b/crates/trusted-server-core/src/consent/mod.rs @@ -205,13 +205,14 @@ fn decode_or_warn( label: &str, decode: fn(&str) -> Result, ) -> Option { - raw.and_then(|s| match decode(s) { + let s = raw?; + match decode(s) { Ok(value) => Some(value), Err(e) => { log::warn!("Failed to decode {label}: {e}"); None } - }) + } } /// Builds a [`ConsentContext`] from previously extracted raw signals. @@ -286,9 +287,10 @@ fn has_eu_tcf_signal(raw_tc_present: bool, gpp_section_ids: Option<&[u16]>) -> b /// Returns the effective decoded TCF consent for enforcement decisions. #[must_use] fn effective_tcf(ctx: &ConsentContext) -> Option<&types::TcfConsent> { - ctx.tcf - .as_ref() - .or_else(|| ctx.gpp.as_ref().and_then(|g| g.eu_tcf.as_ref())) + ctx.tcf.as_ref().or_else(|| { + let g = ctx.gpp.as_ref()?; + g.eu_tcf.as_ref() + }) } /// Returns whether TCF consent allows EID transmission. @@ -582,7 +584,7 @@ fn log_consent_signals(signals: &RawConsentSignals) { if signals.is_empty() { log::debug!("No consent signals found on request"); } else { - log::info!("Consent signals: {}", signals); + log::info!("Consent signals: {signals}"); } } @@ -1050,7 +1052,7 @@ mod tests { }; assert!( !allows_ec_creation(&ctx), - "US state + no consent signals should block EC (spec §6.1.1: fail-closed)" + "US state + no consent signals should block EC (spec \u{a7}6.1.1: fail-closed)" ); } diff --git a/crates/trusted-server-core/src/consent/tcf.rs b/crates/trusted-server-core/src/consent/tcf.rs index f989e5e9..ff2ded06 100644 --- a/crates/trusted-server-core/src/consent/tcf.rs +++ b/crates/trusted-server-core/src/consent/tcf.rs @@ -190,22 +190,7 @@ fn decode_vendor_section( let max_vendor_id = raw_max_vendor_id.min(MAX_VENDOR_ID); let is_range = reader.read_bool(offset + 16); - if !is_range { - // Bitfield: one bit per vendor, 1..=max_vendor_id - let mut vendors = Vec::new(); - let bitfield_start = offset + 17; - for i in 0..usize::from(max_vendor_id) { - let bit_pos = bitfield_start + i; - if bit_pos >= reader.bit_len() { - break; - } - if reader.read_bool(bit_pos) { - // Vendor IDs are 1-indexed - vendors.push((i + 1) as u16); - } - } - Ok(vendors) - } else { + if is_range { // Range encoding let num_entries_offset = offset + 17; if num_entries_offset + 12 > reader.bit_len() { @@ -252,6 +237,21 @@ fn decode_vendor_section( } } Ok(vendors) + } else { + // Bitfield: one bit per vendor, 1..=max_vendor_id + let mut vendors = Vec::new(); + let bitfield_start = offset + 17; + for i in 0..usize::from(max_vendor_id) { + let bit_pos = bitfield_start + i; + if bit_pos >= reader.bit_len() { + break; + } + if reader.read_bool(bit_pos) { + // Vendor IDs are 1-indexed + vendors.push((i + 1) as u16); + } + } + Ok(vendors) } } @@ -270,9 +270,7 @@ fn vendor_section_end_offset( let max_vendor_id = reader.read_u16(offset, 16); let is_range = reader.read_bool(offset + 16); - if !is_range { - Ok(offset + 17 + usize::from(max_vendor_id)) - } else { + if is_range { let num_entries_offset = offset + 17; if num_entries_offset + 12 > reader.bit_len() { return Ok(num_entries_offset); @@ -294,6 +292,8 @@ fn vendor_section_end_offset( } } Ok(pos) + } else { + Ok(offset + 17 + usize::from(max_vendor_id)) } } @@ -445,7 +445,7 @@ mod tests { #[test] fn rejects_too_short() { - let encoded = URL_SAFE_NO_PAD.encode([0u8; 10]); // only 80 bits + let encoded = URL_SAFE_NO_PAD.encode([0_u8; 10]); // only 80 bits let result = decode_tc_string(&encoded); assert!(result.is_err(), "should reject short bitfield"); } @@ -484,7 +484,7 @@ mod tests { // + entry: isRange(1) + start(16) + end(16) = 262 bits let total_bits = core_bits + 16 + 1 + 12 + 1 + 16 + 16; let total_bytes = total_bits.div_ceil(8); - let mut buf = vec![0u8; total_bytes]; + let mut buf = vec![0_u8; total_bytes]; let mut writer = BitWriter::new(&mut buf); // Write minimal core fields (version=2, rest zeroed/defaults) @@ -544,7 +544,7 @@ mod tests { let core_bits: usize = 213; let total_bits = core_bits + 16 + 1 + 12 + 1 + 16 + 16; let total_bytes = total_bits.div_ceil(8); - let mut buf = vec![0u8; total_bytes]; + let mut buf = vec![0_u8; total_bytes]; let mut writer = BitWriter::new(&mut buf); writer.write(0, 6, 2); @@ -589,7 +589,7 @@ mod tests { // Core fields: 213 bits + 16 (maxVendorId) + 1 (isRange) + max_vendor_id (bitfield) let total_bits = 213 + 17 + usize::from(max_vendor_id); let total_bytes = total_bits.div_ceil(8); - let mut buf = vec![0u8; total_bytes]; + let mut buf = vec![0_u8; total_bytes]; let mut writer = BitWriter::new(&mut buf); diff --git a/crates/trusted-server-core/src/consent/types.rs b/crates/trusted-server-core/src/consent/types.rs index 44f1a3df..5781c8b4 100644 --- a/crates/trusted-server-core/src/consent/types.rs +++ b/crates/trusted-server-core/src/consent/types.rs @@ -80,13 +80,13 @@ impl fmt::Display for RawConsentSignals { write!(f, ", __gpp_sid=")?; match &self.raw_gpp_sid { - Some(s) => write!(f, "\"{}\"", s)?, + Some(s) => write!(f, "\"{s}\"")?, None => write!(f, "absent")?, } write!(f, ", us_privacy=")?; match &self.raw_us_privacy { - Some(s) => write!(f, "\"{}\"", s)?, + Some(s) => write!(f, "\"{s}\"")?, None => write!(f, "absent")?, } diff --git a/crates/trusted-server-core/src/consent/us_privacy.rs b/crates/trusted-server-core/src/consent/us_privacy.rs index eea27e40..5537ab24 100644 --- a/crates/trusted-server-core/src/consent/us_privacy.rs +++ b/crates/trusted-server-core/src/consent/us_privacy.rs @@ -14,7 +14,7 @@ //! //! - [IAB US Privacy String specification](https://github.com/InteractiveAdvertisingBureau/USPrivacy/blob/master/CCPA/US%20Privacy%20String.md) -use error_stack::{Report, ResultExt}; +use error_stack::{Report, ResultExt as _}; use super::types::{ConsentDecodeError, PrivacyFlag, UsPrivacy}; @@ -34,10 +34,10 @@ pub fn decode_us_privacy(s: &str) -> Result 1u8, + '1' => 1_u8, other => { return Err(Report::new(ConsentDecodeError::InvalidUsPrivacy { - reason: format!("unsupported version '{}', expected '1'", other), + reason: format!("unsupported version '{other}', expected '1'"), })); } }; diff --git a/crates/trusted-server-core/src/consent_config.rs b/crates/trusted-server-core/src/consent_config.rs index e5fed1a9..dc106b69 100644 --- a/crates/trusted-server-core/src/consent_config.rs +++ b/crates/trusted-server-core/src/consent_config.rs @@ -96,7 +96,7 @@ impl ConsentConfig { let clamped = self.max_consent_age_days.clamp(1, 3650); if clamped != self.max_consent_age_days { log::warn!( - "max_consent_age_days={} is outside the valid range 1–3650; clamped to {clamped}", + "max_consent_age_days={} is outside the valid range 1\u{2013}3650; clamped to {clamped}", self.max_consent_age_days ); self.max_consent_age_days = clamped; diff --git a/crates/trusted-server-core/src/cookies.rs b/crates/trusted-server-core/src/cookies.rs index e56c6834..f1cb3030 100644 --- a/crates/trusted-server-core/src/cookies.rs +++ b/crates/trusted-server-core/src/cookies.rs @@ -5,7 +5,7 @@ use cookie::{Cookie, CookieJar}; use edgezero_core::body::Body as EdgeBody; -use error_stack::{Report, ResultExt}; +use error_stack::{Report, ResultExt as _}; use http::header; use http::Request; @@ -51,21 +51,18 @@ pub fn parse_cookies_to_jar(s: &str) -> CookieJar { pub fn handle_request_cookies( req: &Request, ) -> Result, Report> { - match req.headers().get(header::COOKIE) { - Some(header_value) => { - let header_value_str = - header_value - .to_str() - .change_context(TrustedServerError::InvalidHeaderValue { - message: "Cookie header contains invalid UTF-8".to_string(), - })?; - let jar = parse_cookies_to_jar(header_value_str); - Ok(Some(jar)) - } - None => { - log::debug!("No cookie header found in request"); - Ok(None) - } + if let Some(header_value) = req.headers().get(header::COOKIE) { + let header_value_str = + header_value + .to_str() + .change_context(TrustedServerError::InvalidHeaderValue { + message: "Cookie header contains invalid UTF-8".to_owned(), + })?; + let jar = parse_cookies_to_jar(header_value_str); + Ok(Some(jar)) + } else { + log::debug!("No cookie header found in request"); + Ok(None) } } diff --git a/crates/trusted-server-core/src/creative.rs b/crates/trusted-server-core/src/creative.rs index 17727e61..99cafb47 100644 --- a/crates/trusted-server-core/src/creative.rs +++ b/crates/trusted-server-core/src/creative.rs @@ -74,9 +74,9 @@ pub(super) fn to_abs(settings: &Settings, u: &str) -> Option { } if t.starts_with("//") { - Some(format!("https:{}", t)) + Some(format!("https:{t}")) } else if lower.starts_with("http://") || lower.starts_with("https://") { - Some(t.to_string()) + Some(t.to_owned()) } else { None } @@ -87,20 +87,19 @@ pub(super) fn rewrite_style_urls(settings: &Settings, style: &str) -> String { // naive url(...) rewrite for absolute/protocol-relative URLs let lower = style.to_ascii_lowercase(); let mut out = String::with_capacity(style.len() + 16); - let mut write_pos = 0usize; - let mut scan = 0usize; + let mut write_pos = 0_usize; + let mut scan = 0_usize; while let Some(off) = lower[scan..].find("url(") { let start = scan + off; let open = start + 4; // after 'url(' // write prefix including 'url(' out.push_str(&style[write_pos..open]); // find closing ')' - let close = match lower[open..].find(')') { - Some(c) => open + c, - None => { - out.push_str(&style[open..]); - return out; - } + let close = if let Some(c) = lower[open..].find(')') { + open + c + } else { + out.push_str(&style[open..]); + return out; }; // trim spaces and quotes let bytes = style.as_bytes(); @@ -123,7 +122,7 @@ pub(super) fn rewrite_style_urls(settings: &Settings, style: &str) -> String { let new_val = if let Some(abs) = to_abs(settings, url_val) { build_proxy_url(settings, &abs) } else { - url_val.to_string() + url_val.to_owned() }; if quoted { let q = style.as_bytes()[s] as char; @@ -149,7 +148,7 @@ fn build_signed_url_for( extra: &[(String, String)], ) -> String { let Ok(mut u) = url::Url::parse(clear_url) else { - return clear_url.to_string(); + return clear_url.to_owned(); }; let mut pairs: Vec<(String, String)> = u @@ -223,8 +222,8 @@ pub(super) fn proxy_if_abs(settings: &Settings, val: &str) -> Option { pub(super) fn split_srcset_candidates(s: &str) -> Vec<&str> { let bytes = s.as_bytes(); let mut items = Vec::new(); - let mut start = 0usize; - let mut i = 0usize; + let mut start = 0_usize; + let mut i = 0_usize; while i < bytes.len() { if bytes[i] == b',' { // Determine if this comma is the mediatype/data separator in a data: URL. @@ -277,12 +276,12 @@ pub(super) fn rewrite_srcset(settings: &Settings, srcset: &str) -> String { let rewritten = if let Some(abs) = to_abs(settings, url) { build_proxy_url(settings, &abs) } else { - url.to_string() + url.to_owned() }; if descriptor.is_empty() { out_items.push(rewritten); } else { - out_items.push(format!("{} {}", rewritten, descriptor)); + out_items.push(format!("{rewritten} {descriptor}")); } } out_items.join(", ") @@ -388,7 +387,7 @@ pub fn sanitize_creative_html(markup: &str) -> String { .attributes() .iter() .filter(|a| a.name().to_ascii_lowercase().starts_with("on")) - .map(|a| a.name().to_string()) + .map(|a| a.name().clone()) .collect(); for attr in &on_attrs { el.remove_attribute(attr); @@ -674,7 +673,7 @@ pub fn rewrite_creative_html(settings: &Settings, markup: &str) -> String { let _ = rewriter.write(markup.as_bytes()); let _ = rewriter.end(); - String::from_utf8(out).unwrap_or_else(|_| markup.to_string()) + String::from_utf8(out).unwrap_or_else(|_| markup.to_owned()) } /// Stream processor for creative HTML that rewrites URLs to first-party proxy. @@ -701,15 +700,14 @@ impl StreamProcessor for CreativeHtmlProcessor<'_> { fn process_chunk(&mut self, chunk: &[u8], is_last: bool) -> Result, io::Error> { if self.buffer.len() + chunk.len() > MAX_REWRITABLE_BODY_SIZE { return Err(io::Error::other(format!( - "HTML response body exceeds maximum rewritable size of {} bytes", - MAX_REWRITABLE_BODY_SIZE + "HTML response body exceeds maximum rewritable size of {MAX_REWRITABLE_BODY_SIZE} bytes" ))); } self.buffer.extend_from_slice(chunk); if is_last { let markup = String::from_utf8(std::mem::take(&mut self.buffer)) - .map_err(|e| io::Error::other(format!("Invalid UTF-8 in HTML: {}", e)))?; + .map_err(|e| io::Error::other(format!("Invalid UTF-8 in HTML: {e}")))?; let rewritten = rewrite_creative_html(self.settings, &markup); Ok(rewritten.into_bytes()) @@ -747,15 +745,14 @@ impl StreamProcessor for CreativeCssProcessor<'_> { fn process_chunk(&mut self, chunk: &[u8], is_last: bool) -> Result, io::Error> { if self.buffer.len() + chunk.len() > MAX_REWRITABLE_BODY_SIZE { return Err(io::Error::other(format!( - "CSS response body exceeds maximum rewritable size of {} bytes", - MAX_REWRITABLE_BODY_SIZE + "CSS response body exceeds maximum rewritable size of {MAX_REWRITABLE_BODY_SIZE} bytes" ))); } self.buffer.extend_from_slice(chunk); if is_last { let css = String::from_utf8(std::mem::take(&mut self.buffer)) - .map_err(|e| io::Error::other(format!("Invalid UTF-8 in CSS: {}", e)))?; + .map_err(|e| io::Error::other(format!("Invalid UTF-8 in CSS: {e}")))?; let rewritten = rewrite_css_body(self.settings, &css); Ok(rewritten.into_bytes()) @@ -777,7 +774,7 @@ mod tests { fn rewrite_srcset_attr(attr_name: &str, attr_value: &str) -> String { let settings = crate::test_support::tests::create_test_settings(); - let html = format!(r#""#, attr_name, attr_value); + let html = format!(r#""#); rewrite_creative_html(&settings, &html) } @@ -839,12 +836,11 @@ mod tests { #[test] fn injects_tsjs_creative_when_body_present() { let settings = crate::test_support::tests::create_test_settings(); - let html = r#"

hello

"#; + let html = "

hello

"; let out = rewrite_creative_html(&settings, html); assert!( out.contains("/static/tsjs=tsjs-unified.min.js"), - "expected unified tsjs injection: {}", - out + "expected unified tsjs injection: {out}" ); // Inject only once assert_eq!(out.matches("/static/tsjs=tsjs-unified.min.js").count(), 1); @@ -853,7 +849,7 @@ mod tests { #[test] fn injects_tsjs_unified_once_with_multiple_bodies() { let settings = crate::test_support::tests::create_test_settings(); - let html = r#"onetwo"#; + let html = "onetwo"; let out = rewrite_creative_html(&settings, html); assert_eq!(out.matches("/static/tsjs=tsjs-unified.min.js").count(), 1); } @@ -863,20 +859,20 @@ mod tests { let settings = crate::test_support::tests::create_test_settings(); assert_eq!( to_abs(&settings, "//cdn.example/x"), - Some("https://cdn.example/x".to_string()) + Some("https://cdn.example/x".to_owned()) ); assert_eq!( to_abs(&settings, "HTTPS://cdn.example/x"), - Some("HTTPS://cdn.example/x".to_string()) + Some("HTTPS://cdn.example/x".to_owned()) ); assert_eq!( to_abs(&settings, "http://cdn.example/x"), - Some("http://cdn.example/x".to_string()) + Some("http://cdn.example/x".to_owned()) ); assert_eq!(to_abs(&settings, "/local/x"), None); assert_eq!( to_abs(&settings, " //cdn.example/y "), - Some("https://cdn.example/y".to_string()) + Some("https://cdn.example/y".to_owned()) ); assert_eq!(to_abs(&settings, "data:image/png;base64,abcd"), None); assert_eq!(to_abs(&settings, "javascript:alert(1)"), None); @@ -888,12 +884,12 @@ mod tests { let settings = crate::test_support::tests::create_test_settings(); assert_eq!( to_abs(&settings, "//cdn.example.com:8080/asset.js"), - Some("https://cdn.example.com:8080/asset.js".to_string()), + Some("https://cdn.example.com:8080/asset.js".to_owned()), "should preserve port 8080 in protocol-relative URL" ); assert_eq!( to_abs(&settings, "//cdn.example.com:9443/img.png"), - Some("https://cdn.example.com:9443/img.png".to_string()), + Some("https://cdn.example.com:9443/img.png".to_owned()), "should preserve port 9443 in protocol-relative URL" ); } @@ -916,8 +912,7 @@ mod tests { // Port 9443 should be preserved (URL-encoded as %3A9443) assert!( out.contains("cdn.example.com%3A9443"), - "Port 9443 should be preserved in rewritten URLs: {}", - out + "Port 9443 should be preserved in rewritten URLs: {out}" ); } @@ -1037,8 +1032,7 @@ mod tests { // Two rewritten absolute candidates expected assert!( out.matches("/first-party/proxy?tsurl=").count() >= 2, - "srcset not fully rewritten: {}", - out + "srcset not fully rewritten: {out}" ); // Relative preserved assert!(out.contains("/local/img.webp 1x")); @@ -1068,7 +1062,7 @@ mod tests { "#; let out = rewrite_creative_html(&settings, html); let cnt = out.matches("/first-party/proxy?tsurl=").count(); - assert!(cnt >= 3, "expected 3 rewritten links: {}", out); + assert!(cnt >= 3, "expected 3 rewritten links: {out}"); } #[test] @@ -1141,8 +1135,7 @@ mod tests { let out = rewrite_creative_html(&settings, html); assert!( out.matches("/first-party/proxy?tsurl=").count() >= 4, - "svg hrefs not rewritten: {}", - out + "svg hrefs not rewritten: {out}" ); } @@ -1157,8 +1150,7 @@ mod tests { let out = rewrite_creative_html(&settings, html); assert!( out.matches("/first-party/proxy?tsurl=").count() >= 3, - "style url() not rewritten: {}", - out + "style url() not rewritten: {out}" ); assert!(!out.contains("https://cdn.example/bg.png")); } @@ -1166,17 +1158,16 @@ mod tests { #[test] fn rewrites_style_block_url_variants() { let settings = crate::test_support::tests::create_test_settings(); - let html = r#" + let html = " - "#; + "; let out = rewrite_creative_html(&settings, html); assert!( out.matches("/first-party/proxy?tsurl=").count() >= 2, - "style block url() not rewritten: {}", - out + "style block url() not rewritten: {out}" ); } @@ -1208,7 +1199,7 @@ mod tests { fn split_srcset_handles_no_space_after_commas() { let s = "https://cdn.example/a.png 1x,//cdn.example/b.png 2x,/local/c.png 1x"; let items = super::split_srcset_candidates(s); - assert_eq!(items.len(), 3, "{:?}", items); + assert_eq!(items.len(), 3, "{items:?}"); assert!(items[0].contains("a.png 1x")); assert!(items[1].contains("b.png 2x")); assert!(items[2].contains("/local/c.png 1x")); @@ -1218,7 +1209,7 @@ mod tests { fn split_srcset_preserves_data_url_comma() { let s = "data:image/png;base64,AAAA 1x,//cdn.example/b.png 2x"; let items = super::split_srcset_candidates(s); - assert_eq!(items.len(), 2, "{:?}", items); + assert_eq!(items.len(), 2, "{items:?}"); assert_eq!(items[0].trim(), "data:image/png;base64,AAAA 1x"); assert!(items[1].trim().starts_with("//cdn.example/b.png 2x")); } @@ -1226,9 +1217,9 @@ mod tests { #[test] fn link_rel_case_and_multi_values_rewritten() { let settings = crate::test_support::tests::create_test_settings(); - let html = r#" + let html = " - "#; + "; let out = rewrite_creative_html(&settings, html); // href + one imagesrcset candidate should be rewritten assert!( @@ -1300,7 +1291,7 @@ mod tests { let settings = crate::test_support::tests::create_test_settings(); assert_eq!( to_abs(&settings, " https://cdn.example/a "), - Some("https://cdn.example/a".to_string()) + Some("https://cdn.example/a".to_owned()) ); assert_eq!(to_abs(&settings, "blob:xyz"), None); assert_eq!(to_abs(&settings, "tel:+123"), None); @@ -1324,7 +1315,7 @@ mod tests { #[test] fn to_abs_respects_exclude_domains() { let mut settings = crate::test_support::tests::create_test_settings(); - settings.rewrite.exclude_domains = vec!["trusted-cdn.example.com".to_string()]; + settings.rewrite.exclude_domains = vec!["trusted-cdn.example.com".to_owned()]; // Excluded domain should return None (not proxied) assert_eq!( @@ -1335,14 +1326,14 @@ mod tests { // Non-excluded domain should return Some assert_eq!( to_abs(&settings, "https://other-cdn.example.com/lib.js"), - Some("https://other-cdn.example.com/lib.js".to_string()) + Some("https://other-cdn.example.com/lib.js".to_owned()) ); } #[test] fn to_abs_respects_wildcard_domains() { let mut settings = crate::test_support::tests::create_test_settings(); - settings.rewrite.exclude_domains = vec!["*.cloudflare.com".to_string()]; + settings.rewrite.exclude_domains = vec!["*.cloudflare.com".to_owned()]; // Should exclude base domain assert_eq!(to_abs(&settings, "https://cloudflare.com/cdn.js"), None); @@ -1356,14 +1347,14 @@ mod tests { // Should not exclude different domain assert_eq!( to_abs(&settings, "https://notcloudflare.com/lib.js"), - Some("https://notcloudflare.com/lib.js".to_string()) + Some("https://notcloudflare.com/lib.js".to_owned()) ); } #[test] fn rewrite_html_excludes_blacklisted_domains() { let mut settings = crate::test_support::tests::create_test_settings(); - settings.rewrite.exclude_domains = vec!["trusted-cdn.example.com".to_string()]; + settings.rewrite.exclude_domains = vec!["trusted-cdn.example.com".to_owned()]; let html = r#" @@ -1383,7 +1374,7 @@ mod tests { #[test] fn rewrite_srcset_excludes_blacklisted_domains() { let mut settings = crate::test_support::tests::create_test_settings(); - settings.rewrite.exclude_domains = vec!["trusted.example.com".to_string()]; + settings.rewrite.exclude_domains = vec!["trusted.example.com".to_owned()]; let html = r#" @@ -1402,9 +1393,9 @@ mod tests { #[test] fn rewrite_style_urls_excludes_blacklisted_domains() { let mut settings = crate::test_support::tests::create_test_settings(); - settings.rewrite.exclude_domains = vec!["fonts.googleapis.com".to_string()]; + settings.rewrite.exclude_domains = vec!["fonts.googleapis.com".to_owned()]; - let html = r#" + let html = " - "#; + "; let out = rewrite_creative_html(&settings, html); @@ -1429,7 +1420,7 @@ mod tests { #[test] fn rewrite_click_urls_excludes_blacklisted_domains() { let mut settings = crate::test_support::tests::create_test_settings(); - settings.rewrite.exclude_domains = vec!["trusted-landing.example.com".to_string()]; + settings.rewrite.exclude_domains = vec!["trusted-landing.example.com".to_owned()]; let html = r#" Trusted Link @@ -1729,7 +1720,7 @@ mod tests { fn sanitize_removes_style_element() { // "#; + let html = "
ad
"; let out = sanitize_creative_html(html); assert!(!out.contains(" bool { /// attributes (e.g. adding `Partitioned`) only need updating in one place. fn format_set_cookie(domain: &str, value: &str, max_age: i32) -> String { format!( - "{}={}; Domain={}; Path=/; Secure; SameSite=Lax; Max-Age={}; HttpOnly", - COOKIE_TS_EC, value, domain, max_age, + "{COOKIE_TS_EC}={value}; Domain={domain}; Path=/; Secure; SameSite=Lax; Max-Age={max_age}; HttpOnly", ) } @@ -201,7 +200,7 @@ mod tests { let settings = create_test_settings(); let result = create_ec_cookie(&settings, "evil;injected\r\nfoo=bar\0baz"); let value = result - .strip_prefix(&format!("{}=", COOKIE_TS_EC)) + .strip_prefix(&format!("{COOKIE_TS_EC}=")) .and_then(|s| s.split_once(';').map(|(v, _)| v)) .expect("should have cookie value portion"); @@ -217,7 +216,7 @@ mod tests { let id = "abc123def0123456789abcdef0123456789abcdef0123456789abcdef01234567.xk92ab"; let result = create_ec_cookie(&settings, id); let value = result - .strip_prefix(&format!("{}=", COOKIE_TS_EC)) + .strip_prefix(&format!("{COOKIE_TS_EC}=")) .and_then(|s| s.split_once(';').map(|(v, _)| v)) .expect("should have cookie value portion"); @@ -276,7 +275,7 @@ mod tests { #[test] fn is_safe_cookie_value_rejects_non_ascii() { assert!( - !is_safe_cookie_value("valüe"), + !is_safe_cookie_value("val\u{fc}e"), "should reject non-ASCII UTF-8 characters" ); } @@ -317,7 +316,7 @@ mod tests { "should set Max-Age=0 to expire cookie" ); assert!( - cookie_str.starts_with(&format!("{}=;", COOKIE_TS_EC)), + cookie_str.starts_with(&format!("{COOKIE_TS_EC}=;")), "should clear cookie value" ); assert!( diff --git a/crates/trusted-server-core/src/ec/eids.rs b/crates/trusted-server-core/src/ec/eids.rs index 727c62b4..1dd8b179 100644 --- a/crates/trusted-server-core/src/ec/eids.rs +++ b/crates/trusted-server-core/src/ec/eids.rs @@ -5,7 +5,7 @@ //! build base64-encoded response headers. use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _}; -use error_stack::{Report, ResultExt}; +use error_stack::{Report, ResultExt as _}; use crate::error::TrustedServerError; use crate::openrtb::{Eid, Uid}; diff --git a/crates/trusted-server-core/src/ec/generation.rs b/crates/trusted-server-core/src/ec/generation.rs index 48068292..ab78450e 100644 --- a/crates/trusted-server-core/src/ec/generation.rs +++ b/crates/trusted-server-core/src/ec/generation.rs @@ -5,9 +5,9 @@ use std::net::IpAddr; -use error_stack::{Report, ResultExt}; -use hmac::{Hmac, Mac}; -use rand::Rng; +use error_stack::{Report, ResultExt as _}; +use hmac::{Hmac, Mac as _}; +use rand::Rng as _; use sha2::Sha256; use crate::error::TrustedServerError; @@ -86,7 +86,7 @@ pub fn generate_ec_id( ) -> Result> { let mut mac = HmacSha256::new_from_slice(settings.ec.passphrase.expose().as_bytes()) .change_context(TrustedServerError::EdgeCookie { - message: "Failed to create HMAC instance".to_string(), + message: "Failed to create HMAC instance".to_owned(), })?; mac.update(client_ip.as_bytes()); let hmac_hash = hex::encode(mac.finalize().into_bytes()); @@ -112,7 +112,7 @@ pub fn generate_ec_id( pub fn extract_client_ip(req: &fastly::Request) -> Result> { req.get_client_ip_addr().map(normalize_ip).ok_or_else(|| { Report::new(TrustedServerError::EdgeCookie { - message: "Client IP required for EC generation but unavailable".to_string(), + message: "Client IP required for EC generation but unavailable".to_owned(), }) }) } diff --git a/crates/trusted-server-core/src/ec/identify.rs b/crates/trusted-server-core/src/ec/identify.rs index e9da1bd6..7008b560 100644 --- a/crates/trusted-server-core/src/ec/identify.rs +++ b/crates/trusted-server-core/src/ec/identify.rs @@ -3,7 +3,7 @@ //! Partners authenticate with a Bearer token and receive only their own //! synced UID for the active EC ID. -use error_stack::{Report, ResultExt}; +use error_stack::{Report, ResultExt as _}; use fastly::http::{header, StatusCode}; use fastly::{Request, Response}; use url::Url; @@ -75,12 +75,7 @@ pub fn handle_identify( match kv.get(ec_id) { Ok(Some((entry, generation))) => { - if !entry.consent.ok { - // Tombstone entries preserve the withdrawal signal for 24 hours. - // Do not extract IDs or evaluate cluster size because that would - // write back with the live-entry TTL. - log::trace!("Identify found tombstone for '{}'", log_id(ec_id)); - } else { + if entry.consent.ok { // Extract only this partner's UID. if let Some(partner_uid) = entry.ids.get(&partner.source_domain) { if !partner_uid.uid.is_empty() { @@ -98,6 +93,11 @@ pub fn handle_identify( log::warn!("Cluster evaluation failed for '{}': {err:?}", log_id(ec_id)); } } + } else { + // Tombstone entries preserve the withdrawal signal for 24 hours. + // Do not extract IDs or evaluate cluster size because that would + // write back with the live-entry TTL. + log::trace!("Identify found tombstone for '{}'", log_id(ec_id)); } } Ok(None) => {} diff --git a/crates/trusted-server-core/src/ec/kv.rs b/crates/trusted-server-core/src/ec/kv.rs index 6f269026..e674b888 100644 --- a/crates/trusted-server-core/src/ec/kv.rs +++ b/crates/trusted-server-core/src/ec/kv.rs @@ -10,7 +10,7 @@ use std::collections::BTreeMap; use std::time::Duration; -use error_stack::{Report, ResultExt}; +use error_stack::{Report, ResultExt as _}; use fastly::kv_store::{InsertMode, KVStore}; use crate::error::TrustedServerError; @@ -29,10 +29,10 @@ const MAX_CAS_RETRIES: u32 = 5; const CLUSTER_LIST_LIMIT: u32 = 100; /// TTL for live entries (1 year), matching the EC cookie `Max-Age`. -const ENTRY_TTL: Duration = Duration::from_secs(365 * 24 * 60 * 60); +const ENTRY_TTL: Duration = Duration::from_hours(8_760); /// TTL for withdrawal tombstones (24 hours). -const TOMBSTONE_TTL: Duration = Duration::from_secs(24 * 60 * 60); +const TOMBSTONE_TTL: Duration = Duration::from_hours(24); /// Outcome of an [`KvIdentityGraph::upsert_partner_id_if_exists`] call. /// @@ -248,9 +248,8 @@ impl KvIdentityGraph { } }; - let meta_bytes = match response.metadata() { - Some(bytes) => bytes, - None => return Ok(None), + let Some(meta_bytes) = response.metadata() else { + return Ok(None); }; let meta: KvMetadata = @@ -345,10 +344,9 @@ impl KvIdentityGraph { } // Key exists — read it to determine if it's live or a tombstone. - let (existing, generation) = match self.get(ec_id)? { - Some(pair) => pair, + let Some((existing, generation)) = self.get(ec_id)? else { // Raced with a delete — try create again. - None => return self.create(ec_id, entry), + return self.create(ec_id, entry); }; // Live entry — nothing to do. @@ -440,23 +438,21 @@ impl KvIdentityGraph { let store = self.open_store()?; for attempt in 0..MAX_CAS_RETRIES { - let (mut entry, generation) = match Self::lookup_entry(&store, &self.store_name, ec_id)? - { - Some(pair) => pair, - None => { - log::info!( - "upsert_partner_ids: no entry for '{}', rejecting {} partner updates", - log_id(ec_id), + let Some((mut entry, generation)) = + Self::lookup_entry(&store, &self.store_name, ec_id)? + else { + log::info!( + "upsert_partner_ids: no entry for '{}', rejecting {} partner updates", + log_id(ec_id), + updates.len(), + ); + return Err(Report::new(TrustedServerError::KvStore { + store_name: self.store_name.clone(), + message: format!( + "Cannot upsert {} partner IDs for missing key '{ec_id}'", updates.len(), - ); - return Err(Report::new(TrustedServerError::KvStore { - store_name: self.store_name.clone(), - message: format!( - "Cannot upsert {} partner IDs for missing key '{ec_id}'", - updates.len(), - ), - })); - } + ), + })); }; // Reject upserts on withdrawn entries — a late sync must not @@ -546,20 +542,17 @@ impl KvIdentityGraph { let store = self.open_store()?; for attempt in 0..MAX_CAS_RETRIES { - let (mut entry, generation) = match self.get(ec_id)? { - Some(pair) => pair, - None => { - log::info!( - "upsert_partner_id: no entry for '{}', rejecting partner upsert", - log_id(ec_id) - ); - return Err(Report::new(TrustedServerError::KvStore { - store_name: self.store_name.clone(), - message: format!( - "Cannot upsert partner '{partner_id}' for missing key '{ec_id}'" - ), - })); - } + let Some((mut entry, generation)) = self.get(ec_id)? else { + log::info!( + "upsert_partner_id: no entry for '{}', rejecting partner upsert", + log_id(ec_id) + ); + return Err(Report::new(TrustedServerError::KvStore { + store_name: self.store_name.clone(), + message: format!( + "Cannot upsert partner '{partner_id}' for missing key '{ec_id}'" + ), + })); }; // Reject upserts on withdrawn entries — a late sync must not @@ -655,9 +648,8 @@ impl KvIdentityGraph { let store = self.open_store()?; for attempt in 0..MAX_CAS_RETRIES { - let (mut entry, generation) = match self.get(ec_id)? { - Some(pair) => pair, - None => return Ok(UpsertResult::NotFound), + let Some((mut entry, generation)) = self.get(ec_id)? else { + return Ok(UpsertResult::NotFound); }; if !entry.consent.ok { @@ -783,7 +775,10 @@ impl KvIdentityGraph { ), })?; - #[allow(clippy::cast_possible_truncation)] + #[allow( + clippy::cast_possible_truncation, + reason = "Fastly list page sizes are bounded well below u32::MAX" + )] let count = page.keys().len() as u32; Ok(count) } @@ -895,8 +890,8 @@ mod tests { #[test] fn constants_have_expected_values() { assert_eq!(MAX_CAS_RETRIES, 5); - assert_eq!(ENTRY_TTL, Duration::from_secs(31_536_000)); - assert_eq!(TOMBSTONE_TTL, Duration::from_secs(86_400)); + assert_eq!(ENTRY_TTL, Duration::from_hours(8_760)); + assert_eq!(TOMBSTONE_TTL, Duration::from_hours(24)); assert_eq!(CLUSTER_LIST_LIMIT, 100); } diff --git a/crates/trusted-server-core/src/ec/kv_types.rs b/crates/trusted-server-core/src/ec/kv_types.rs index 5f2c4300..7a6996ea 100644 --- a/crates/trusted-server-core/src/ec/kv_types.rs +++ b/crates/trusted-server-core/src/ec/kv_types.rs @@ -442,11 +442,7 @@ impl KvGeo { pub fn from_geo_info(geo: Option<&GeoInfo>) -> Self { match geo { Some(info) => { - let dma = if info.metro_code > 0 { - Some(info.metro_code) - } else { - None - }; + let dma = (info.metro_code > 0).then_some(info.metro_code); Self { country: info.country.clone(), region: info.region.clone(), @@ -493,7 +489,7 @@ mod tests { fn entry_serialization_roundtrip() { let geo = sample_geo_info(); let consent = sample_consent_context(); - let mut entry = KvEntry::new(&consent, Some(&geo), 1741824000, "example.com"); + let mut entry = KvEntry::new(&consent, Some(&geo), 1_741_824_000, "example.com"); entry.ids.insert( "liveramp".to_owned(), KvPartnerId { @@ -506,7 +502,7 @@ mod tests { serde_json::from_str(&json).expect("should deserialize KvEntry"); assert_eq!(deserialized.v, SCHEMA_VERSION); - assert_eq!(deserialized.created, 1741824000); + assert_eq!(deserialized.created, 1_741_824_000); assert_eq!( deserialized.consent.tcf.as_deref(), Some("CP_test_tc_string") @@ -846,7 +842,7 @@ mod tests { #[test] fn minimal_entry_has_partner_id_and_placeholder_geo() { - let entry = KvEntry::minimal("ssp_x", "abc123", 1741824000); + let entry = KvEntry::minimal("ssp_x", "abc123", 1_741_824_000); assert_eq!(entry.v, SCHEMA_VERSION); assert!(entry.consent.ok, "should be a live entry"); @@ -862,13 +858,13 @@ mod tests { #[test] fn tombstone_entry_has_correct_shape() { - let entry = KvEntry::tombstone(1741910400); + let entry = KvEntry::tombstone(1_741_910_400); assert_eq!(entry.v, SCHEMA_VERSION); assert!(!entry.consent.ok, "should be a tombstone"); assert!(entry.ids.is_empty(), "tombstone should have no partner IDs"); assert_eq!(entry.geo.country, "ZZ"); - assert_eq!(entry.consent.updated, 1741910400); + assert_eq!(entry.consent.updated, 1_741_910_400); assert!( entry.pub_properties.is_none(), "tombstone should have no pub_properties" diff --git a/crates/trusted-server-core/src/ec/mod.rs b/crates/trusted-server-core/src/ec/mod.rs index 8fb9f5b7..7b507abc 100644 --- a/crates/trusted-server-core/src/ec/mod.rs +++ b/crates/trusted-server-core/src/ec/mod.rs @@ -54,7 +54,7 @@ pub mod registry; #[must_use] pub fn log_id(ec_id: &str) -> String { let prefix = ec_id.get(..8).unwrap_or(ec_id); - format!("{prefix}…") + format!("{prefix}\u{2026}") } use cookie::CookieJar; @@ -266,7 +266,7 @@ impl EcContext { let client_ip = self.client_ip.as_deref().ok_or_else(|| { Report::new(TrustedServerError::EdgeCookie { - message: "Client IP required for EC generation but unavailable".to_string(), + message: "Client IP required for EC generation but unavailable".to_owned(), }) })?; @@ -469,11 +469,13 @@ impl EcContext { pub(crate) fn current_timestamp() -> u64 { std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_secs()) - .unwrap_or_else(|err| { - log::error!("SystemTime::now() failed, falling back to epoch 0: {err}"); - 0 - }) + .map_or_else( + |err| { + log::error!("SystemTime::now() failed, falling back to epoch 0: {err}"); + 0 + }, + |duration| duration.as_secs(), + ) } #[cfg(test)] diff --git a/crates/trusted-server-core/src/ec/partner.rs b/crates/trusted-server-core/src/ec/partner.rs index 27753c93..d824b3f4 100644 --- a/crates/trusted-server-core/src/ec/partner.rs +++ b/crates/trusted-server-core/src/ec/partner.rs @@ -3,7 +3,7 @@ //! Provides source-domain normalization for EC partner configuration and //! API key hashing. The partner registry is in [`super::registry`]. -use sha2::{Digest, Sha256}; +use sha2::{Digest as _, Sha256}; /// Maximum allowed length for partner source domains. const MAX_SOURCE_DOMAIN_LENGTH: usize = 255; diff --git a/crates/trusted-server-core/src/ec/prebid_eids.rs b/crates/trusted-server-core/src/ec/prebid_eids.rs index 476b7721..22620e59 100644 --- a/crates/trusted-server-core/src/ec/prebid_eids.rs +++ b/crates/trusted-server-core/src/ec/prebid_eids.rs @@ -29,7 +29,10 @@ const MAX_EIDS_COOKIE_BYTES: usize = 8 * 1024; struct LegacyCookieEid { source: String, id: String, - #[allow(dead_code)] + #[allow( + dead_code, + reason = "legacy cookie field is deserialized for compatibility but not emitted" + )] atype: u8, } @@ -180,12 +183,9 @@ fn collect_prebid_eid_updates( cookie_value: &str, registry: &PartnerRegistry, ) -> Vec { - let eids = match parse_prebid_eids_cookie(cookie_value) { - Ok(eids) => eids, - Err(_) => { - log::trace!("Prebid EIDs: failed to decode ts-eids cookie; dropping"); - return Vec::new(); - } + let Ok(eids) = parse_prebid_eids_cookie(cookie_value) else { + log::trace!("Prebid EIDs: failed to decode ts-eids cookie; dropping"); + return Vec::new(); }; let mut updates = Vec::new(); diff --git a/crates/trusted-server-core/src/ec/pull_sync.rs b/crates/trusted-server-core/src/ec/pull_sync.rs index 6c935bad..6bef93c7 100644 --- a/crates/trusted-server-core/src/ec/pull_sync.rs +++ b/crates/trusted-server-core/src/ec/pull_sync.rs @@ -268,10 +268,7 @@ fn drain_pull_batch(kv: &KvIdentityGraph, ec_id: &str, in_flight: &mut Vec response, Err(err) => { - log::warn!( - "Pull sync: request failed for partner '{}': {err:?}", - source_domain - ); + log::warn!("Pull sync: request failed for partner '{source_domain}': {err:?}"); continue; } }; @@ -303,25 +300,21 @@ fn response_content_length_exceeds_limit(response: &fastly::Response, source_dom let Some(value) = value.to_str().ok() else { log::warn!( - "Pull sync: partner '{}' returned invalid Content-Length header, rejecting", - source_domain + "Pull sync: partner '{source_domain}' returned invalid Content-Length header, rejecting" ); return true; }; let Ok(length) = value.parse::() else { log::warn!( - "Pull sync: partner '{}' returned malformed Content-Length header, rejecting", - source_domain + "Pull sync: partner '{source_domain}' returned malformed Content-Length header, rejecting" ); return true; }; if length > MAX_PULL_RESPONSE_BYTES { log::warn!( - "Pull sync: partner '{}' returned oversized Content-Length ({} bytes), rejecting", - source_domain, - length + "Pull sync: partner '{source_domain}' returned oversized Content-Length ({length} bytes), rejecting" ); return true; } @@ -333,19 +326,12 @@ fn extract_pull_uid(mut response: fastly::Response, source_domain: &str) -> Opti let status = response.get_status(); if status == StatusCode::NOT_FOUND { - log::debug!( - "Pull sync: partner '{}' returned 404, treating as no-op", - source_domain - ); + log::debug!("Pull sync: partner '{source_domain}' returned 404, treating as no-op"); return None; } if !status.is_success() { - log::warn!( - "Pull sync: partner '{}' returned non-success status {}", - source_domain, - status - ); + log::warn!("Pull sync: partner '{source_domain}' returned non-success status {status}"); return None; } @@ -365,10 +351,7 @@ fn extract_pull_uid(mut response: fastly::Response, source_domain: &str) -> Opti let payload = match serde_json::from_slice::(&body) { Ok(payload) => payload, Err(err) => { - log::warn!( - "Pull sync: partner '{}' returned invalid JSON body: {err}", - source_domain - ); + log::warn!("Pull sync: partner '{source_domain}' returned invalid JSON body: {err}"); return None; } }; @@ -379,8 +362,7 @@ fn extract_pull_uid(mut response: fastly::Response, source_domain: &str) -> Opti match uid { None => { log::debug!( - "Pull sync: partner '{}' returned null/empty uid, treating as no-op", - source_domain + "Pull sync: partner '{source_domain}' returned null/empty uid, treating as no-op" ); None } @@ -634,15 +616,15 @@ mod tests { assert_eq!(offset_h0, 0, "hour 0 should start at index 0"); // Hour 1: offset = (3600 / 3600) % 3 = 1 → [beta, gamma, alpha] - let offset_h1 = (3600u64 / 3600) as usize % ids.len(); + let offset_h1 = (3600_u64 / 3600) as usize % ids.len(); assert_eq!(offset_h1, 1, "hour 1 should start at index 1"); // Hour 2: offset = (7200 / 3600) % 3 = 2 → [gamma, alpha, beta] - let offset_h2 = (7200u64 / 3600) as usize % ids.len(); + let offset_h2 = (7200_u64 / 3600) as usize % ids.len(); assert_eq!(offset_h2, 2, "hour 2 should start at index 2"); // Hour 3: offset = (10800 / 3600) % 3 = 0 → wraps back to [alpha, beta, gamma] - let offset_h3 = (10800u64 / 3600) as usize % ids.len(); + let offset_h3 = (10800_u64 / 3600) as usize % ids.len(); assert_eq!(offset_h3, 0, "hour 3 should wrap back to index 0"); // Verify rotate_left produces expected ordering diff --git a/crates/trusted-server-core/src/ec/registry.rs b/crates/trusted-server-core/src/ec/registry.rs index 7ed144bf..6b688d30 100644 --- a/crates/trusted-server-core/src/ec/registry.rs +++ b/crates/trusted-server-core/src/ec/registry.rs @@ -6,7 +6,7 @@ use std::collections::HashMap; -use error_stack::{Report, ResultExt}; +use error_stack::{Report, ResultExt as _}; use crate::error::TrustedServerError; use crate::redacted::Redacted; @@ -82,10 +82,7 @@ impl PartnerRegistry { if by_source_domain.contains_key(&normalized_source) { return Err(Report::new(TrustedServerError::Configuration { - message: format!( - "ec.partners: duplicate source_domain '{}'", - normalized_source - ), + message: format!("ec.partners: duplicate source_domain '{normalized_source}'"), })); } @@ -96,9 +93,8 @@ impl PartnerRegistry { if by_api_key_hash.contains_key(&api_key_hash) { return Err(Report::new(TrustedServerError::Configuration { message: format!( - "ec.partners: source_domain '{}' has an API token that collides \ - with another partner's token hash", - normalized_source + "ec.partners: source_domain '{normalized_source}' has an API token that collides \ + with another partner's token hash" ), })); } @@ -150,9 +146,8 @@ impl PartnerRegistry { /// Looks up a partner by the SHA-256 hex hash of their API token. #[must_use] pub fn find_by_api_key_hash(&self, hash: &str) -> Option<&PartnerConfig> { - self.by_api_key_hash - .get(hash) - .and_then(|source_domain| self.by_source_domain.get(source_domain)) + let source_domain = self.by_api_key_hash.get(hash)?; + self.by_source_domain.get(source_domain) } /// Looks up a partner by their `source_domain`. @@ -262,8 +257,7 @@ fn validate_pull_sync(config: &PartnerConfig) -> Result<(), Report StatusCode; @@ -111,22 +117,22 @@ pub trait IntoHttpResponse { impl IntoHttpResponse for TrustedServerError { fn status_code(&self) -> StatusCode { match self { - Self::Auction { .. } => StatusCode::BAD_GATEWAY, - Self::BadRequest { .. } => StatusCode::BAD_REQUEST, - Self::Configuration { .. } | Self::Settings { .. } => StatusCode::INTERNAL_SERVER_ERROR, - Self::Gam { .. } => StatusCode::BAD_GATEWAY, - Self::GdprConsent { .. } => StatusCode::BAD_REQUEST, - Self::InvalidHeaderValue { .. } => StatusCode::BAD_REQUEST, - Self::InvalidUtf8 { .. } => StatusCode::INTERNAL_SERVER_ERROR, + Self::Auction { .. } + | Self::Gam { .. } + | Self::Prebid { .. } + | Self::Integration { .. } + | Self::Proxy { .. } => StatusCode::BAD_GATEWAY, + Self::BadRequest { .. } + | Self::GdprConsent { .. } + | Self::InvalidHeaderValue { .. } => StatusCode::BAD_REQUEST, + Self::Configuration { .. } + | Self::Settings { .. } + | Self::InvalidUtf8 { .. } + | Self::EdgeCookie { .. } + | Self::InsecureDefault { .. } => StatusCode::INTERNAL_SERVER_ERROR, Self::KvStore { .. } => StatusCode::SERVICE_UNAVAILABLE, - Self::Prebid { .. } => StatusCode::BAD_GATEWAY, - Self::Integration { .. } => StatusCode::BAD_GATEWAY, - Self::Proxy { .. } => StatusCode::BAD_GATEWAY, - Self::Forbidden { .. } => StatusCode::FORBIDDEN, - Self::AllowlistViolation { .. } => StatusCode::FORBIDDEN, - Self::EdgeCookie { .. } => StatusCode::INTERNAL_SERVER_ERROR, + Self::Forbidden { .. } | Self::AllowlistViolation { .. } => StatusCode::FORBIDDEN, Self::PartnerNotFound { .. } => StatusCode::NOT_FOUND, - Self::InsecureDefault { .. } => StatusCode::INTERNAL_SERVER_ERROR, } } @@ -135,11 +141,11 @@ impl IntoHttpResponse for TrustedServerError { // Client errors (4xx) — safe to surface a brief description Self::BadRequest { message } => format!("Bad request: {message}"), // Consent strings may contain user data; return category only. - Self::GdprConsent { .. } => "GDPR consent error".to_string(), - Self::InvalidHeaderValue { .. } => "Invalid header value".to_string(), + Self::GdprConsent { .. } => "GDPR consent error".to_owned(), + Self::InvalidHeaderValue { .. } => "Invalid header value".to_owned(), // Server/integration errors (5xx/502/503) — generic message only. // Full details are already logged via log::error! in to_error_response. - _ => "An internal error occurred".to_string(), + _ => "An internal error occurred".to_owned(), } } } diff --git a/crates/trusted-server-core/src/geo.rs b/crates/trusted-server-core/src/geo.rs index d71f1f58..271932a6 100644 --- a/crates/trusted-server-core/src/geo.rs +++ b/crates/trusted-server-core/src/geo.rs @@ -27,8 +27,8 @@ pub fn geo_from_fastly(geo: &Geo) -> GeoInfo { n => Some(n), }; GeoInfo { - city: geo.city().to_string(), - country: geo.country_code().to_string(), + city: geo.city().to_owned(), + country: geo.country_code().to_owned(), continent: format!("{:?}", geo.continent()), latitude: geo.latitude(), longitude: geo.longitude(), @@ -127,13 +127,13 @@ mod tests { fn sample_geo_info() -> GeoInfo { GeoInfo { - city: "San Francisco".to_string(), - country: "US".to_string(), - continent: "NorthAmerica".to_string(), + city: "San Francisco".to_owned(), + country: "US".to_owned(), + continent: "NorthAmerica".to_owned(), latitude: 37.7749, longitude: -122.4194, metro_code: 807, - region: Some("CA".to_string()), + region: Some("CA".to_owned()), asn: Some(7922), } } diff --git a/crates/trusted-server-core/src/host_rewrite.rs b/crates/trusted-server-core/src/host_rewrite.rs index cb15f407..ce0cd8d7 100644 --- a/crates/trusted-server-core/src/host_rewrite.rs +++ b/crates/trusted-server-core/src/host_rewrite.rs @@ -35,7 +35,7 @@ pub(crate) fn rewrite_bare_host_at_boundaries( replaced_any = true; search = end; } else { - out.push_str(&text[search..pos + 1]); + out.push_str(&text[search..=pos]); search = pos + 1; } } diff --git a/crates/trusted-server-core/src/html_processor.rs b/crates/trusted-server-core/src/html_processor.rs index 18765f0c..a11202d1 100644 --- a/crates/trusted-server-core/src/html_processor.rs +++ b/crates/trusted-server-core/src/html_processor.rs @@ -133,9 +133,9 @@ impl HtmlProcessorConfig { request_scheme: &str, ) -> Self { Self { - origin_host: origin_host.to_string(), - request_host: request_host.to_string(), - request_scheme: request_scheme.to_string(), + origin_host: origin_host.to_owned(), + request_host: request_host.to_owned(), + request_scheme: request_scheme.to_owned(), integrations: integrations.clone(), } } @@ -194,10 +194,7 @@ pub fn create_html_processor(config: HtmlProcessorConfig) -> impl StreamProcesso if rewritten.starts_with(&self.origin_host) { let suffix = &rewritten[self.origin_host.len()..]; let boundary_ok = suffix.is_empty() - || matches!( - suffix.as_bytes().first(), - Some(b'/') | Some(b'?') | Some(b'#') - ); + || matches!(suffix.as_bytes().first(), Some(b'/' | b'?' | b'#')); if boundary_ok { rewritten = format!("{}{}", self.request_host, suffix); } @@ -522,9 +519,9 @@ mod tests { fn create_test_config() -> HtmlProcessorConfig { HtmlProcessorConfig { - origin_host: "origin.example.com".to_string(), - request_host: "test.example.com".to_string(), - request_scheme: "https".to_string(), + origin_host: "origin.example.com".to_owned(), + request_host: "test.example.com".to_owned(), + request_scheme: "https".to_owned(), integrations: IntegrationRegistry::default(), } } @@ -593,11 +590,11 @@ mod tests { } fn head_inserts(&self, _ctx: &IntegrationHtmlContext<'_>) -> Vec { - vec![r#""#.to_string()] + vec!["".to_owned()] } } - let html = r#"Test"#; + let html = "Test"; let mut config = create_test_config(); config.integrations = IntegrationRegistry::from_rewriters_with_head_injectors( @@ -721,14 +718,14 @@ mod tests { let protocol_relative_urls = html.matches("//www.test-publisher.com").count(); println!("Test HTML stats:"); - println!(" Total URLs: {}", original_urls); - println!(" HTTPS URLs: {}", https_urls); - println!(" Protocol-relative URLs: {}", protocol_relative_urls); + println!(" Total URLs: {original_urls}"); + println!(" HTTPS URLs: {https_urls}"); + println!(" Protocol-relative URLs: {protocol_relative_urls}"); // Process - replace test-publisher.com with our edge domain let mut config = create_test_config(); - config.origin_host = "www.test-publisher.com".to_string(); // Match what's in the HTML - config.request_host = "test-publisher-ts.edgecompute.app".to_string(); + config.origin_host = "www.test-publisher.com".to_owned(); // Match what's in the HTML + config.request_host = "test-publisher-ts.edgecompute.app".to_owned(); let processor = create_html_processor(config); let pipeline_config = PipelineConfig { @@ -751,8 +748,8 @@ mod tests { let replaced_urls = result.matches("test-publisher-ts.edgecompute.app").count(); println!("After processing:"); - println!(" Remaining original URLs: {}", remaining_urls); - println!(" Edge domain URLs: {}", replaced_urls); + println!(" Remaining original URLs: {remaining_urls}"); + println!(" Edge domain URLs: {replaced_urls}"); // Expect at least some replacements and fewer originals than before assert!(replaced_urls > 0, "Should replace some URLs in attributes"); @@ -794,7 +791,7 @@ mod tests { "#; let mut settings = Settings::default(); - let shim_src = "https://edge.example.com/static/testlight.js".to_string(); + let shim_src = "https://edge.example.com/static/testlight.js".to_owned(); settings .integrations .insert_config( @@ -822,7 +819,7 @@ mod tests { let mut output = Vec::new(); let result = pipeline.process(Cursor::new(html.as_bytes()), &mut output); - assert!(result.is_ok()); + result.unwrap(); let processed = String::from_utf8_lossy(&output); assert!( @@ -840,7 +837,7 @@ mod tests { use flate2::read::GzDecoder; use flate2::write::GzEncoder; use flate2::Compression as GzCompression; - use std::io::{Read, Write}; + use std::io::{Read as _, Write as _}; let html = include_str!("html_processor.test.html"); @@ -858,8 +855,8 @@ mod tests { // Process with compression let mut config = create_test_config(); - config.origin_host = "www.test-publisher.com".to_string(); // Match what's in the HTML - config.request_host = "test-publisher-ts.edgecompute.app".to_string(); + config.origin_host = "www.test-publisher.com".to_owned(); // Match what's in the HTML + config.request_host = "test-publisher-ts.edgecompute.app".to_owned(); let processor = create_html_processor(config); let pipeline_config = PipelineConfig { @@ -881,7 +878,7 @@ mod tests { ); // Decompress and verify - let mut decoder = GzDecoder::new(&compressed_output[..]); + let mut decoder = GzDecoder::new(&*compressed_output); let mut decompressed = String::new(); decoder .read_to_string(&mut decompressed) @@ -930,10 +927,10 @@ mod tests { // This simulates receiving already-truncated HTML from origin let truncated_html = - r#"Test

This is a test that gets cut o"#; + "Test

This is a test that gets cut o"; println!("Testing already-truncated HTML"); - println!("Input: '{}'", truncated_html); + println!("Input: '{truncated_html}'"); let config = create_test_config(); let processor = create_html_processor(config); @@ -953,7 +950,7 @@ mod tests { ); let processed = String::from_utf8_lossy(&output); - println!("Output: '{}'", processed); + println!("Output: '{processed}'"); // The processor should pass through the truncated HTML // It might add some closing tags, but shouldn't truncate further @@ -988,8 +985,8 @@ mod tests { // Process it through our pipeline let mut config = create_test_config(); - config.origin_host = "www.test-publisher.com".to_string(); // Match what's in the HTML - config.request_host = "test-publisher-ts.edgecompute.app".to_string(); + config.origin_host = "www.test-publisher.com".to_owned(); // Match what's in the HTML + config.request_host = "test-publisher-ts.edgecompute.app".to_owned(); let processor = create_html_processor(config); let pipeline_config = PipelineConfig { @@ -1039,7 +1036,7 @@ mod tests { #[test] fn post_processors_accumulate_while_streaming_path_passes_through() { - use crate::streaming_processor::{HtmlRewriterAdapter, StreamProcessor}; + use crate::streaming_processor::{HtmlRewriterAdapter, StreamProcessor as _}; use lol_html::Settings; // --- Streaming path: no post-processors → output emitted per chunk --- @@ -1124,7 +1121,7 @@ mod tests { #[test] fn active_post_processor_receives_full_document_and_mutates_output() { - use crate::streaming_processor::{HtmlRewriterAdapter, StreamProcessor}; + use crate::streaming_processor::{HtmlRewriterAdapter, StreamProcessor as _}; use lol_html::Settings; struct AppendCommentProcessor; diff --git a/crates/trusted-server-core/src/http_util.rs b/crates/trusted-server-core/src/http_util.rs index 50314f4c..ae902751 100644 --- a/crates/trusted-server-core/src/http_util.rs +++ b/crates/trusted-server-core/src/http_util.rs @@ -1,8 +1,8 @@ use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}; -use chacha20poly1305::{aead::Aead, aead::KeyInit, XChaCha20Poly1305, XNonce}; +use chacha20poly1305::{aead::Aead as _, aead::KeyInit as _, XChaCha20Poly1305, XNonce}; use edgezero_core::body::Body as EdgeBody; use http::{header, Request, Response, StatusCode}; -use sha2::{Digest, Sha256}; +use sha2::{Digest as _, Sha256}; use subtle::ConstantTimeEq as _; use crate::constants::INTERNAL_HEADERS; @@ -50,7 +50,7 @@ pub(crate) const SPOOFABLE_FORWARDED_HEADERS: &[&str] = &[ pub fn sanitize_forwarded_headers(req: &mut Request) { for header in SPOOFABLE_FORWARDED_HEADERS { if req.headers().contains_key(*header) { - log::debug!("Stripped spoofable header: {}", header); + log::debug!("Stripped spoofable header: {header}"); req.headers_mut().remove(*header); } } @@ -158,7 +158,7 @@ fn extract_request_host(req: &Request) -> String { .and_then(|h| h.to_str().ok()) }) .unwrap_or_default() - .to_string() + .to_owned() } fn parse_forwarded_param<'a>(forwarded: &'a str, param: &str) -> Option<&'a str> { @@ -201,11 +201,7 @@ fn strip_quotes(value: &str) -> &str { fn normalize_scheme(value: &str) -> Option { let scheme = value.trim().to_ascii_lowercase(); - if scheme == "https" || scheme == "http" { - Some(scheme) - } else { - None - } + (scheme == "https" || scheme == "http").then_some(scheme) } /// Detects the request scheme (HTTP or HTTPS) using Fastly SDK methods and headers. @@ -223,14 +219,14 @@ fn detect_request_scheme( ) -> String { // 1. First try ClientInfo TLS fields populated at the adapter entry point. if let Some(tls_protocol) = tls_protocol { - log::debug!("TLS protocol detected: {}", tls_protocol); - return "https".to_string(); + log::debug!("TLS protocol detected: {tls_protocol}"); + return "https".to_owned(); } // Also check TLS cipher - if present, connection is HTTPS. if tls_cipher.is_some() { log::debug!("TLS cipher detected, using HTTPS"); - return "https".to_string(); + return "https".to_owned(); } // 2. Try the Forwarded header (RFC 7239) @@ -259,13 +255,13 @@ fn detect_request_scheme( if let Some(ssl) = req.headers().get("fastly-ssl") { if let Ok(ssl_str) = ssl.to_str() { if ssl_str == "1" || ssl_str.to_lowercase() == "true" { - return "https".to_string(); + return "https".to_owned(); } } } // Default to HTTP - "http".to_string() + "http".to_owned() } /// Build a static text response with strong `ETag` and standard caching headers. @@ -337,7 +333,7 @@ pub fn encode_url(settings: &Settings, plaintext_url: &str) -> String { hasher.update(settings.publisher.proxy_secret.expose().as_bytes()); hasher.update(plaintext_url.as_bytes()); let nonce_full = hasher.finalize(); - let mut nonce = [0u8; 24]; + let mut nonce = [0_u8; 24]; nonce[..24].copy_from_slice(&nonce_full[..24]); let nonce = XNonce::from_slice(&nonce); @@ -368,10 +364,8 @@ pub fn decode_url(settings: &Settings, token: &str) -> Option { let key_bytes = Sha256::digest(settings.publisher.proxy_secret.expose().as_bytes()); let cipher = XChaCha20Poly1305::new(&key_bytes); - cipher - .decrypt(nonce, ciphertext) - .ok() - .and_then(|pt| String::from_utf8(pt).ok()) + let pt = cipher.decrypt(nonce, ciphertext).ok()?; + String::from_utf8(pt).ok() } /// Compute a deterministic signature token (tstoken) for a clear-text URL using the @@ -479,11 +473,8 @@ mod tests { let src = "https://t.example/p.gif"; let enc = encode_url(&settings, src); assert!(!enc.ends_with('=')); - let dec = match decode_url(&settings, &enc) { - Some(s) => s, - None => { - panic!("decode failed for token: {}", enc); - } + let Some(dec) = decode_url(&settings, &enc) else { + panic!("decode failed for token: {enc}"); }; assert_eq!(dec, src); } @@ -820,7 +811,7 @@ mod tests { let req = build_request(Method::GET, "https://test.example.com/page"); let client_info = ClientInfo { client_ip: None, - tls_protocol: Some("TLSv1.3".to_string()), + tls_protocol: Some("TLSv1.3".to_owned()), tls_cipher: None, }; @@ -838,7 +829,7 @@ mod tests { let client_info = ClientInfo { client_ip: None, tls_protocol: None, - tls_cipher: Some("TLS_AES_128_GCM_SHA256".to_string()), + tls_cipher: Some("TLS_AES_128_GCM_SHA256".to_owned()), }; let info = RequestInfo::from_request(&req, &client_info); diff --git a/crates/trusted-server-core/src/integrations/adserver_mock.rs b/crates/trusted-server-core/src/integrations/adserver_mock.rs index 50b1f9fa..af68cc6e 100644 --- a/crates/trusted-server-core/src/integrations/adserver_mock.rs +++ b/crates/trusted-server-core/src/integrations/adserver_mock.rs @@ -4,7 +4,7 @@ //! This integration acts as a mediator in the auction flow, selecting winning bids //! based on price (highest price wins). -use error_stack::{Report, ResultExt}; +use error_stack::{Report, ResultExt as _}; use fastly::http::Method; use fastly::Request; use serde::{Deserialize, Serialize}; @@ -70,7 +70,7 @@ impl Default for AdServerMockConfig { fn default() -> Self { Self { enabled: default_enabled(), - endpoint: "http://localhost:6767/adserver/mediate".to_string(), + endpoint: "http://localhost:6767/adserver/mediate".to_owned(), timeout_ms: default_timeout_ms(), price_floor: None, context_query_params: BTreeMap::new(), @@ -205,7 +205,7 @@ impl AdServerMockProvider { // Build consent summary from ConsentContext let consent_json = request.user.consent.as_ref().map(|ctx| { json!({ - "gdpr": if ctx.gdpr_applies { 1 } else { 0 }, + "gdpr": i32::from(ctx.gdpr_applies), "consent": ctx.raw_tc_string, "us_privacy": ctx.raw_us_privacy, "gpp": ctx.raw_gpp_string, @@ -242,13 +242,13 @@ impl AdServerMockProvider { for bid in bids { // Mediation layer returns decoded prices for all bids all_bids.push(Bid { - slot_id: bid["impid"].as_str().unwrap_or("").to_string(), + slot_id: bid["impid"].as_str().unwrap_or("").to_owned(), price: bid["price"].as_f64(), // Now properly decoded by mediation - currency: "USD".to_string(), + currency: "USD".to_owned(), creative: bid["adm"].as_str().map(String::from), width: bid["w"].as_u64().unwrap_or(0) as u32, height: bid["h"].as_u64().unwrap_or(0) as u32, - bidder: seat_name.to_string(), + bidder: seat_name.to_owned(), adomain: bid["adomain"].as_array().map(|arr| { arr.iter() .filter_map(|v| v.as_str().map(String::from)) @@ -292,10 +292,10 @@ impl AuctionProvider for AdServerMockProvider { let mediation_req = self .build_mediation_request(request, bidder_responses) .change_context(TrustedServerError::Auction { - message: "Failed to build mediation request".to_string(), + message: "Failed to build mediation request".to_owned(), })?; - log::trace!("AdServer Mock: mediation request: {:?}", mediation_req); + log::trace!("AdServer Mock: mediation request: {mediation_req:?}"); // Build endpoint URL with context-driven query parameters let endpoint_url = self.build_endpoint_url(request); @@ -307,9 +307,9 @@ impl AuctionProvider for AdServerMockProvider { if let Ok(url) = url::Url::parse(&self.config.endpoint) { if let Some(host) = url.host_str() { let host_with_port = if let Some(port) = url.port() { - format!("{}:{}", host, port) + format!("{host}:{port}") } else { - host.to_string() + host.to_owned() }; req.set_header("Host", &host_with_port); } @@ -317,7 +317,7 @@ impl AuctionProvider for AdServerMockProvider { req.set_body_json(&mediation_req) .change_context(TrustedServerError::Auction { - message: "Failed to set mediation request body".to_string(), + message: "Failed to set mediation request body".to_owned(), })?; // Send async with auction-scoped timeout @@ -336,7 +336,7 @@ impl AuctionProvider for AdServerMockProvider { let pending = req .send_async(backend_name) .change_context(TrustedServerError::Auction { - message: "Failed to send mediation request".to_string(), + message: "Failed to send mediation request".to_owned(), })?; Ok(pending) @@ -358,10 +358,10 @@ impl AuctionProvider for AdServerMockProvider { let body_bytes = response.take_body_bytes(); let response_json: Json = serde_json::from_slice(&body_bytes).change_context(TrustedServerError::Auction { - message: "Failed to parse mediation response".to_string(), + message: "Failed to parse mediation response".to_owned(), })?; - log::trace!("AdServer Mock response: {:?}", response_json); + log::trace!("AdServer Mock response: {response_json:?}"); let auction_response = self.parse_mediation_response(&response_json, response_time_ms); @@ -448,9 +448,9 @@ mod tests { fn create_test_auction_request() -> AuctionRequest { AuctionRequest { - id: "test-auction-123".to_string(), + id: "test-auction-123".to_owned(), slots: vec![AdSlot { - id: "header-banner".to_string(), + id: "header-banner".to_owned(), formats: vec![AdFormat { media_type: MediaType::Banner, width: 728, @@ -461,17 +461,17 @@ mod tests { bidders: HashMap::new(), }], publisher: PublisherInfo { - domain: "test.com".to_string(), - page_url: Some("https://test.com/article".to_string()), + domain: "test.com".to_owned(), + page_url: Some("https://test.com/article".to_owned()), }, user: UserInfo { - id: Some("user-123".to_string()), + id: Some("user-123".to_owned()), consent: None, eids: None, }, device: Some(DeviceInfo { - user_agent: Some("Mozilla/5.0".to_string()), - ip: Some("192.168.1.1".to_string()), + user_agent: Some("Mozilla/5.0".to_owned()), + ip: Some("192.168.1.1".to_owned()), geo: None, }), site: None, @@ -483,7 +483,7 @@ mod tests { fn test_build_mediation_request() { let config = AdServerMockConfig { enabled: true, - endpoint: "http://localhost:6767/adserver/mediate".to_string(), + endpoint: "http://localhost:6767/adserver/mediate".to_owned(), timeout_ms: 500, price_floor: Some(1.00), context_query_params: BTreeMap::new(), @@ -494,17 +494,17 @@ mod tests { let bidder_responses = vec![ AuctionResponse { - provider: "amazon-aps".to_string(), + provider: "amazon-aps".to_owned(), status: BidStatus::Success, bids: vec![Bid { - slot_id: "header-banner".to_string(), + slot_id: "header-banner".to_owned(), price: Some(3.00), - currency: "USD".to_string(), - creative: Some("

APS Ad
".to_string()), + currency: "USD".to_owned(), + creative: Some("
APS Ad
".to_owned()), width: 728, height: 90, - bidder: "amazon-aps".to_string(), - adomain: Some(vec!["amazon.com".to_string()]), + bidder: "amazon-aps".to_owned(), + adomain: Some(vec!["amazon.com".to_owned()]), nurl: None, burl: None, metadata: HashMap::new(), @@ -513,16 +513,16 @@ mod tests { metadata: HashMap::new(), }, AuctionResponse { - provider: "test-bidder".to_string(), + provider: "test-bidder".to_owned(), status: BidStatus::Success, bids: vec![Bid { - slot_id: "header-banner".to_string(), + slot_id: "header-banner".to_owned(), price: Some(3.50), - currency: "USD".to_string(), - creative: Some("
Test Ad
".to_string()), + currency: "USD".to_owned(), + creative: Some("
Test Ad
".to_owned()), width: 728, height: 90, - bidder: "test-bidder".to_string(), + bidder: "test-bidder".to_owned(), adomain: None, nurl: None, burl: None, @@ -622,9 +622,9 @@ mod tests { let provider = AdServerMockProvider::new(config); let auction_request = AuctionRequest { - id: "test-auction".to_string(), + id: "test-auction".to_owned(), slots: vec![AdSlot { - id: "slot-1".to_string(), + id: "slot-1".to_owned(), formats: vec![AdFormat { media_type: MediaType::Banner, width: 300, @@ -635,11 +635,11 @@ mod tests { bidders: HashMap::new(), }], publisher: PublisherInfo { - domain: "test.com".to_string(), + domain: "test.com".to_owned(), page_url: None, }, user: UserInfo { - id: Some("user-1".to_string()), + id: Some("user-1".to_owned()), consent: None, eids: None, }, @@ -650,20 +650,20 @@ mod tests { // APS bid with encoded price (price=None, amznbid in metadata) let mut aps_metadata = HashMap::new(); - aps_metadata.insert("amznbid".to_string(), json!("encoded-price-value")); + aps_metadata.insert("amznbid".to_owned(), json!("encoded-price-value")); let bidder_responses = vec![AuctionResponse { - provider: "aps".to_string(), + provider: "aps".to_owned(), status: BidStatus::Success, bids: vec![Bid { - slot_id: "slot-1".to_string(), + slot_id: "slot-1".to_owned(), price: None, // APS bids have no decoded price - currency: "USD".to_string(), + currency: "USD".to_owned(), creative: None, // APS doesn't provide creative width: 300, height: 250, - bidder: "amazon-aps".to_string(), - adomain: Some(vec!["amazon.com".to_string()]), + bidder: "amazon-aps".to_owned(), + adomain: Some(vec!["amazon.com".to_owned()]), nurl: None, burl: None, metadata: aps_metadata, @@ -726,7 +726,7 @@ mod tests { let config = AdServerMockConfig { enabled: true, - endpoint: "http://localhost:6767/adserver/mediate".to_string(), + endpoint: "http://localhost:6767/adserver/mediate".to_owned(), timeout_ms: 500, price_floor: None, context_query_params: BTreeMap::new(), @@ -736,10 +736,10 @@ mod tests { let mut request = create_test_auction_request(); request.user.consent = Some(ConsentContext { - raw_tc_string: Some("BOEFEAyO".to_string()), + raw_tc_string: Some("BOEFEAyO".to_owned()), gdpr_applies: true, - raw_us_privacy: Some("1YNN".to_string()), - raw_gpp_string: Some("DBACNYA~CPXxRfAPXxRfA".to_string()), + raw_us_privacy: Some("1YNN".to_owned()), + raw_gpp_string: Some("DBACNYA~CPXxRfAPXxRfA".to_owned()), gpp_section_ids: Some(vec![2, 6]), ..Default::default() }); @@ -830,19 +830,19 @@ mod tests { fn test_build_endpoint_url_with_context_query_params() { let config = AdServerMockConfig { enabled: true, - endpoint: "http://localhost:6767/adserver/mediate".to_string(), + endpoint: "http://localhost:6767/adserver/mediate".to_owned(), timeout_ms: 500, price_floor: None, context_query_params: BTreeMap::from([( - "permutive_segments".to_string(), - "permutive".to_string(), + "permutive_segments".to_owned(), + "permutive".to_owned(), )]), }; let provider = AdServerMockProvider::new(config); let mut request = create_test_auction_request(); request.context.insert( - "permutive_segments".to_string(), + "permutive_segments".to_owned(), ContextValue::StringList(vec![ "10000001".into(), "10000003".into(), @@ -864,7 +864,7 @@ mod tests { // even if context contains data. let config = AdServerMockConfig { enabled: true, - endpoint: "http://localhost:6767/adserver/mediate".to_string(), + endpoint: "http://localhost:6767/adserver/mediate".to_owned(), timeout_ms: 500, price_floor: None, context_query_params: BTreeMap::new(), @@ -873,7 +873,7 @@ mod tests { let mut request = create_test_auction_request(); request.context.insert( - "permutive_segments".to_string(), + "permutive_segments".to_owned(), ContextValue::StringList(vec!["10000001".into()]), ); @@ -885,8 +885,8 @@ mod tests { fn test_build_endpoint_url_empty_array_skipped() { let config = AdServerMockConfig { context_query_params: BTreeMap::from([( - "permutive_segments".to_string(), - "permutive".to_string(), + "permutive_segments".to_owned(), + "permutive".to_owned(), )]), ..Default::default() }; @@ -894,7 +894,7 @@ mod tests { let mut request = create_test_auction_request(); request.context.insert( - "permutive_segments".to_string(), + "permutive_segments".to_owned(), ContextValue::StringList(vec![]), ); @@ -909,19 +909,19 @@ mod tests { fn test_build_endpoint_url_preserves_existing_query_params() { let config = AdServerMockConfig { enabled: true, - endpoint: "http://localhost:6767/adserver/mediate?debug=true".to_string(), + endpoint: "http://localhost:6767/adserver/mediate?debug=true".to_owned(), timeout_ms: 500, price_floor: None, context_query_params: BTreeMap::from([( - "permutive_segments".to_string(), - "permutive".to_string(), + "permutive_segments".to_owned(), + "permutive".to_owned(), )]), }; let provider = AdServerMockProvider::new(config); let mut request = create_test_auction_request(); request.context.insert( - "permutive_segments".to_string(), + "permutive_segments".to_owned(), ContextValue::StringList(vec!["123".into(), "adv".into()]), ); diff --git a/crates/trusted-server-core/src/integrations/aps.rs b/crates/trusted-server-core/src/integrations/aps.rs index 1c966e71..4cde6968 100644 --- a/crates/trusted-server-core/src/integrations/aps.rs +++ b/crates/trusted-server-core/src/integrations/aps.rs @@ -2,7 +2,7 @@ //! //! This module provides the APS auction provider for server-side bidding. -use error_stack::{Report, ResultExt}; +use error_stack::{Report, ResultExt as _}; use fastly::http::Method; use fastly::Request; use serde::{Deserialize, Serialize}; @@ -215,7 +215,7 @@ where struct PubIdVisitor; - impl<'de> Visitor<'de> for PubIdVisitor { + impl Visitor<'_> for PubIdVisitor { type Value = String; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { @@ -226,7 +226,7 @@ where where E: de::Error, { - Ok(value.to_string()) + Ok(value.to_owned()) } fn visit_string(self, value: String) -> Result @@ -259,7 +259,7 @@ fn default_enabled() -> bool { } fn default_endpoint() -> String { - "https://aax.amazon-adsystem.com/e/dtb/bid".to_string() + "https://aax.amazon-adsystem.com/e/dtb/bid".to_owned() } fn default_timeout_ms() -> u32 { @@ -388,19 +388,19 @@ impl ApsAuctionProvider { // Build metadata from targeting keys - includes encoded price for mediation let mut metadata = HashMap::new(); if let Some(ref amzniid) = slot.amzniid { - metadata.insert("amzniid".to_string(), json!(amzniid)); + metadata.insert("amzniid".to_owned(), json!(amzniid)); } if let Some(ref amznbid) = slot.amznbid { - metadata.insert("amznbid".to_string(), json!(amznbid)); + metadata.insert("amznbid".to_owned(), json!(amznbid)); } if let Some(ref amznp) = slot.amznp { - metadata.insert("amznp".to_string(), json!(amznp)); + metadata.insert("amznp".to_owned(), json!(amznp)); } if let Some(ref amznsz) = slot.amznsz { - metadata.insert("amznsz".to_string(), json!(amznsz)); + metadata.insert("amznsz".to_owned(), json!(amznsz)); } if let Some(ref amznactt) = slot.amznactt { - metadata.insert("amznactt".to_string(), json!(amznactt)); + metadata.insert("amznactt".to_owned(), json!(amznactt)); } // APS doesn't return creative HTML - only targeting keys @@ -409,10 +409,10 @@ impl ApsAuctionProvider { Ok(Bid { slot_id: slot.slot_id.clone(), price: None, // Encoded price in metadata, decoded by mediation - currency: "USD".to_string(), + currency: "USD".to_owned(), creative: None, adomain: None, // APS doesn't provide adomain in response - bidder: "amazon-aps".to_string(), + bidder: "amazon-aps".to_owned(), width, height, nurl: None, // Real APS uses client-side event tracking @@ -451,7 +451,7 @@ impl ApsAuctionProvider { ); bids.push(bid); } - Err(_) => { + Err(()) => { log::debug!("APS: skipped slot (no fill or invalid)"); } } @@ -490,10 +490,10 @@ impl AuctionProvider for ApsAuctionProvider { // Serialize to JSON let aps_json = serde_json::to_value(&aps_request).change_context(TrustedServerError::Auction { - message: "Failed to serialize APS bid request".to_string(), + message: "Failed to serialize APS bid request".to_owned(), })?; - log::trace!("APS: sending bid request: {:?}", aps_json); + log::trace!("APS: sending bid request: {aps_json:?}"); // Create HTTP POST request let mut aps_req = Request::new(Method::POST, &self.config.endpoint); @@ -501,7 +501,7 @@ impl AuctionProvider for ApsAuctionProvider { aps_req .set_body_json(&aps_json) .change_context(TrustedServerError::Auction { - message: "Failed to set APS request body".to_string(), + message: "Failed to set APS request body".to_owned(), })?; // Send request asynchronously with auction-scoped timeout @@ -521,7 +521,7 @@ impl AuctionProvider for ApsAuctionProvider { aps_req .send_async(backend_name) .change_context(TrustedServerError::Auction { - message: "Failed to send async request to APS".to_string(), + message: "Failed to send async request to APS".to_owned(), })?; Ok(pending) @@ -542,10 +542,10 @@ impl AuctionProvider for ApsAuctionProvider { let body_bytes = response.take_body_bytes(); let response_json: Json = serde_json::from_slice(&body_bytes).change_context(TrustedServerError::Auction { - message: "Failed to parse APS response JSON".to_string(), + message: "Failed to parse APS response JSON".to_owned(), })?; - log::trace!("APS: received response: {:?}", response_json); + log::trace!("APS: received response: {response_json:?}"); // Transform to unified format let auction_response = self.parse_aps_response(&response_json, response_time_ms); @@ -640,10 +640,10 @@ mod tests { fn create_test_auction_request() -> AuctionRequest { AuctionRequest { - id: "test-auction-123".to_string(), + id: "test-auction-123".to_owned(), slots: vec![ AdSlot { - id: "header-banner".to_string(), + id: "header-banner".to_owned(), formats: vec![ AdFormat { media_type: MediaType::Banner, @@ -661,7 +661,7 @@ mod tests { bidders: HashMap::new(), }, AdSlot { - id: "sidebar".to_string(), + id: "sidebar".to_owned(), formats: vec![AdFormat { media_type: MediaType::Banner, width: 300, @@ -673,17 +673,17 @@ mod tests { }, ], publisher: PublisherInfo { - domain: "test.com".to_string(), - page_url: Some("https://test.com/article".to_string()), + domain: "test.com".to_owned(), + page_url: Some("https://test.com/article".to_owned()), }, user: UserInfo { - id: Some("user-123".to_string()), + id: Some("user-123".to_owned()), consent: None, eids: None, }, device: Some(DeviceInfo { - user_agent: Some("Mozilla/5.0".to_string()), - ip: Some("192.168.1.1".to_string()), + user_agent: Some("Mozilla/5.0".to_owned()), + ip: Some("192.168.1.1".to_owned()), geo: None, }), site: None, @@ -695,8 +695,8 @@ mod tests { fn test_aps_request_transformation() { let config = ApsConfig { enabled: true, - pub_id: "5128".to_string(), - endpoint: "https://aax.amazon-adsystem.com/e/dtb/bid".to_string(), + pub_id: "5128".to_owned(), + endpoint: "https://aax.amazon-adsystem.com/e/dtb/bid".to_owned(), timeout_ms: 800, }; @@ -709,9 +709,9 @@ mod tests { assert_eq!(aps_request.slots.len(), 2); assert_eq!( aps_request.page_url, - Some("https://test.com/article".to_string()) + Some("https://test.com/article".to_owned()) ); - assert_eq!(aps_request.user_agent, Some("Mozilla/5.0".to_string())); + assert_eq!(aps_request.user_agent, Some("Mozilla/5.0".to_owned())); assert_eq!(aps_request.timeout, Some(800)); // Verify first slot @@ -732,8 +732,8 @@ mod tests { fn test_aps_response_parsing_success() { let config = ApsConfig { enabled: true, - pub_id: "5128".to_string(), - endpoint: "https://aax.amazon-adsystem.com/e/dtb/bid".to_string(), + pub_id: "5128".to_owned(), + endpoint: "https://aax.amazon-adsystem.com/e/dtb/bid".to_owned(), timeout_ms: 800, }; @@ -813,8 +813,8 @@ mod tests { fn test_aps_response_parsing_no_bid() { let config = ApsConfig { enabled: true, - pub_id: "5128".to_string(), - endpoint: "https://aax.amazon-adsystem.com/e/dtb/bid".to_string(), + pub_id: "5128".to_owned(), + endpoint: "https://aax.amazon-adsystem.com/e/dtb/bid".to_owned(), timeout_ms: 800, }; @@ -839,8 +839,8 @@ mod tests { fn test_aps_response_parsing_invalid_bids() { let config = ApsConfig { enabled: true, - pub_id: "5128".to_string(), - endpoint: "https://aax.amazon-adsystem.com/e/dtb/bid".to_string(), + pub_id: "5128".to_owned(), + endpoint: "https://aax.amazon-adsystem.com/e/dtb/bid".to_owned(), timeout_ms: 800, }; @@ -881,26 +881,26 @@ mod tests { fn test_aps_slot_parsing() { let config = ApsConfig { enabled: true, - pub_id: "5128".to_string(), - endpoint: "https://aax.amazon-adsystem.com/e/dtb/bid".to_string(), + pub_id: "5128".to_owned(), + endpoint: "https://aax.amazon-adsystem.com/e/dtb/bid".to_owned(), timeout_ms: 800, }; let provider = ApsAuctionProvider::new(config); let aps_slot = ApsSlotResponse { - slot_id: "test-slot".to_string(), - size: "728x90".to_string(), - crid: Some("crid-123".to_string()), - media_type: Some("d".to_string()), - fif: Some("1".to_string()), - targeting: vec!["amzniid".to_string(), "amznbid".to_string()], - meta: vec!["slotID".to_string()], - amzniid: Some("impression-id-123".to_string()), - amznbid: Some("1c7d4ow".to_string()), // Encoded price (to be decoded by mediation) - amznp: Some("1c7d4ow".to_string()), - amznsz: Some("728x90".to_string()), - amznactt: Some("OPEN".to_string()), + slot_id: "test-slot".to_owned(), + size: "728x90".to_owned(), + crid: Some("crid-123".to_owned()), + media_type: Some("d".to_owned()), + fif: Some("1".to_owned()), + targeting: vec!["amzniid".to_owned(), "amznbid".to_owned()], + meta: vec!["slotID".to_owned()], + amzniid: Some("impression-id-123".to_owned()), + amznbid: Some("1c7d4ow".to_owned()), // Encoded price (to be decoded by mediation) + amznp: Some("1c7d4ow".to_owned()), + amznsz: Some("728x90".to_owned()), + amznactt: Some("OPEN".to_owned()), }; let bid = provider @@ -940,7 +940,7 @@ mod tests { let config = ApsConfig { enabled: true, - pub_id: "5128".to_string(), + pub_id: "5128".to_owned(), endpoint: default_endpoint(), timeout_ms: 800, }; @@ -948,10 +948,10 @@ mod tests { let mut request = create_test_auction_request(); request.user.consent = Some(ConsentContext { - raw_tc_string: Some("BOEFEAyOEFEAyAHABDENAI4AAAB9vABAASA".to_string()), + raw_tc_string: Some("BOEFEAyOEFEAyAHABDENAI4AAAB9vABAASA".to_owned()), gdpr_applies: true, - raw_us_privacy: Some("1YNN".to_string()), - raw_gpp_string: Some("DBACNYA~CPXxRfAPXxRfA".to_string()), + raw_us_privacy: Some("1YNN".to_owned()), + raw_gpp_string: Some("DBACNYA~CPXxRfAPXxRfA".to_owned()), gpp_section_ids: Some(vec![2, 6]), ..Default::default() }); @@ -978,7 +978,7 @@ mod tests { fn test_aps_request_no_consent() { let config = ApsConfig { enabled: true, - pub_id: "5128".to_string(), + pub_id: "5128".to_owned(), endpoint: default_endpoint(), timeout_ms: 800, }; @@ -999,7 +999,7 @@ mod tests { let config = ApsConfig { enabled: true, - pub_id: "5128".to_string(), + pub_id: "5128".to_owned(), endpoint: default_endpoint(), timeout_ms: 800, }; @@ -1007,7 +1007,7 @@ mod tests { let mut request = create_test_auction_request(); request.user.consent = Some(ConsentContext { - raw_tc_string: Some("BOE".to_string()), + raw_tc_string: Some("BOE".to_owned()), gdpr_applies: true, ..Default::default() }); @@ -1032,26 +1032,26 @@ mod tests { // The creative will be generated by the mediation layer (e.g., GAM or ad server) let config = ApsConfig { enabled: true, - pub_id: "5128".to_string(), - endpoint: "https://aax.amazon-adsystem.com/e/dtb/bid".to_string(), + pub_id: "5128".to_owned(), + endpoint: "https://aax.amazon-adsystem.com/e/dtb/bid".to_owned(), timeout_ms: 800, }; let provider = ApsAuctionProvider::new(config); let aps_slot = ApsSlotResponse { - slot_id: "test-slot".to_string(), - size: "300x250".to_string(), - crid: Some("test-creative".to_string()), - media_type: Some("d".to_string()), - fif: Some("1".to_string()), - targeting: vec!["amzniid".to_string(), "amznbid".to_string()], - meta: vec!["slotID".to_string()], - amzniid: Some("imp-123".to_string()), - amznbid: Some("encoded-price".to_string()), - amznp: Some("encoded-price-alt".to_string()), - amznsz: Some("300x250".to_string()), - amznactt: Some("OPEN".to_string()), + slot_id: "test-slot".to_owned(), + size: "300x250".to_owned(), + crid: Some("test-creative".to_owned()), + media_type: Some("d".to_owned()), + fif: Some("1".to_owned()), + targeting: vec!["amzniid".to_owned(), "amznbid".to_owned()], + meta: vec!["slotID".to_owned()], + amzniid: Some("imp-123".to_owned()), + amznbid: Some("encoded-price".to_owned()), + amznp: Some("encoded-price-alt".to_owned()), + amznsz: Some("300x250".to_owned()), + amznactt: Some("OPEN".to_owned()), }; let bid = provider.parse_aps_slot(&aps_slot).expect("should parse"); diff --git a/crates/trusted-server-core/src/integrations/datadome.rs b/crates/trusted-server-core/src/integrations/datadome.rs index da3a64a2..0fe3bd99 100644 --- a/crates/trusted-server-core/src/integrations/datadome.rs +++ b/crates/trusted-server-core/src/integrations/datadome.rs @@ -58,7 +58,7 @@ use std::sync::{Arc, LazyLock}; use async_trait::async_trait; -use error_stack::{Report, ResultExt}; +use error_stack::{Report, ResultExt as _}; use fastly::http::{header, Method, StatusCode}; use fastly::{Request, Response}; use regex::Regex; @@ -131,11 +131,11 @@ fn default_enabled() -> bool { } fn default_sdk_origin() -> String { - "https://js.datadome.co".to_string() + "https://js.datadome.co".to_owned() } fn default_api_origin() -> String { - "https://api-js.datadome.co".to_string() + "https://api-js.datadome.co".to_owned() } fn default_cache_ttl() -> u32 { @@ -176,7 +176,7 @@ impl DataDomeIntegration { fn error(message: impl Into) -> TrustedServerError { TrustedServerError::Integration { - integration: DATADOME_INTEGRATION_ID.to_string(), + integration: DATADOME_INTEGRATION_ID.to_owned(), message: message.into(), } } @@ -208,13 +208,10 @@ impl DataDomeIntegration { // The path already includes the leading slash if present if path.is_empty() { // Bare domain reference: "js.datadome.co" or "api-js.datadome.co" - format!("{}/integrations/datadome{}", open_quote, close_quote) + format!("{open_quote}/integrations/datadome{close_quote}") } else { // Domain with path: "js.datadome.co/js/check" or "api-js.datadome.co/js/check" - format!( - "{}/integrations/datadome{}{}", - open_quote, path, close_quote - ) + format!("{open_quote}/integrations/datadome{path}{close_quote}") } }) .into_owned() @@ -224,8 +221,8 @@ impl DataDomeIntegration { fn build_sdk_url(&self, path: &str, query: Option<&str>) -> String { let base = self.config.sdk_origin.trim_end_matches('/'); match query { - Some(q) => format!("{}{}?{}", base, path, q), - None => format!("{}{}", base, path), + Some(q) => format!("{base}{path}?{q}"), + None => format!("{base}{path}"), } } @@ -233,8 +230,8 @@ impl DataDomeIntegration { fn build_api_url(&self, path: &str, query: Option<&str>) -> String { let base = self.config.api_origin.trim_end_matches('/'); match query { - Some(q) => format!("{}{}?{}", base, path, q), - None => format!("{}{}", base, path), + Some(q) => format!("{base}{path}?{q}"), + None => format!("{base}{path}"), } } @@ -251,7 +248,7 @@ impl DataDomeIntegration { async fn handle_tags_js(&self, req: Request) -> Result> { let target_url = self.build_sdk_url("/tags.js", req.get_query_str()); - log::info!("[datadome] Fetching tags.js from {}", target_url); + log::info!("[datadome] Fetching tags.js from {target_url}"); let backend = BackendConfig::from_url(&target_url, true) .change_context(Self::error("Invalid SDK URL"))?; @@ -371,13 +368,7 @@ impl DataDomeIntegration { /// Returns the path (including leading slash) or `/tags.js` as default. fn extract_datadome_path(url: &str) -> &str { url.split_once("js.datadome.co") - .and_then(|(_, after)| { - if after.starts_with('/') { - Some(after) - } else { - None - } - }) + .and_then(|(_, after)| after.starts_with('/').then_some(after)) .unwrap_or("/tags.js") } } @@ -414,8 +405,7 @@ impl IntegrationProxy for DataDomeIntegration { self.handle_js_api(req).await } else { Err(Report::new(Self::error(format!( - "Unknown DataDome route: {}", - path + "Unknown DataDome route: {path}" )))) } } @@ -450,11 +440,7 @@ impl IntegrationAttributeRewriter for DataDomeIntegration { ctx.request_scheme, ctx.request_host, path ); - log::info!( - "[datadome] Rewriting script src from {} to {}", - attr_value, - new_url - ); + log::info!("[datadome] Rewriting script src from {attr_value} to {new_url}"); AttributeRewriteAction::Replace(new_url) } @@ -506,8 +492,8 @@ mod tests { fn test_config() -> DataDomeConfig { DataDomeConfig { enabled: true, - sdk_origin: "https://js.datadome.co".to_string(), - api_origin: "https://api-js.datadome.co".to_string(), + sdk_origin: "https://js.datadome.co".to_owned(), + api_origin: "https://api-js.datadome.co".to_owned(), cache_ttl_seconds: 3600, rewrite_sdk: true, } @@ -528,24 +514,20 @@ mod tests { // All URLs should be rewritten to root-relative /integrations/datadome/... assert!( rewritten.contains("\"/integrations/datadome/js/\""), - "Bare domain with path should be rewritten to root-relative. Got: {}", - rewritten + "Bare domain with path should be rewritten to root-relative. Got: {rewritten}" ); assert!( rewritten.contains("\"/integrations/datadome/js/endpoint\""), - "Absolute URL should be rewritten to root-relative. Got: {}", - rewritten + "Absolute URL should be rewritten to root-relative. Got: {rewritten}" ); assert!( rewritten.contains("\"/integrations/datadome\""), - "Bare domain should be rewritten to root-relative. Got: {}", - rewritten + "Bare domain should be rewritten to root-relative. Got: {rewritten}" ); // Original domain should not appear assert!( !rewritten.contains("js.datadome.co"), - "Original domain should be replaced. Got: {}", - rewritten + "Original domain should be replaced. Got: {rewritten}" ); } @@ -570,14 +552,14 @@ mod tests { // Check each format is rewritten correctly to root-relative paths assert!(rewritten.contains(r#"var a = "/integrations/datadome/js/check""#)); - assert!(rewritten.contains(r#"var b = '/integrations/datadome/js/check'"#)); + assert!(rewritten.contains("var b = '/integrations/datadome/js/check'")); assert!(rewritten.contains(r#"var c = "/integrations/datadome/js/check""#)); - assert!(rewritten.contains(r#"var d = '/integrations/datadome/js/check'"#)); + assert!(rewritten.contains("var d = '/integrations/datadome/js/check'")); assert!(rewritten.contains(r#"var e = "/integrations/datadome/js/check""#)); - assert!(rewritten.contains(r#"var f = '/integrations/datadome/js/check'"#)); + assert!(rewritten.contains("var f = '/integrations/datadome/js/check'")); assert!(rewritten.contains(r#"var g = "/integrations/datadome/js/check""#)); assert!(rewritten.contains(r#"var h = "/integrations/datadome""#)); - assert!(rewritten.contains(r#"var i = '/integrations/datadome'"#)); + assert!(rewritten.contains("var i = '/integrations/datadome'")); // No original domain should remain assert!(!rewritten.contains("js.datadome.co")); @@ -621,36 +603,30 @@ mod tests { // api-js.datadome.co URLs should be rewritten to root-relative paths assert!( rewritten.contains(r#""/integrations/datadome/js/""#), - "Absolute api-js URL should be rewritten. Got: {}", - rewritten + "Absolute api-js URL should be rewritten. Got: {rewritten}" ); assert!( rewritten.contains(r#""/integrations/datadome/js/check""#), - "Bare api-js URL should be rewritten. Got: {}", - rewritten + "Bare api-js URL should be rewritten. Got: {rewritten}" ); assert!( rewritten.contains(r#""/integrations/datadome/js/signal""#), - "Protocol-relative api-js URL should be rewritten. Got: {}", - rewritten + "Protocol-relative api-js URL should be rewritten. Got: {rewritten}" ); // js.datadome.co should also be rewritten assert!( rewritten.contains(r#""/integrations/datadome/tags.js""#), - "SDK URL should be rewritten. Got: {}", - rewritten + "SDK URL should be rewritten. Got: {rewritten}" ); // No original DataDome domains should remain assert!( !rewritten.contains("api-js.datadome.co"), - "api-js.datadome.co should be replaced. Got: {}", - rewritten + "api-js.datadome.co should be replaced. Got: {rewritten}" ); assert!( !rewritten.contains("js.datadome.co"), - "js.datadome.co should be replaced. Got: {}", - rewritten + "js.datadome.co should be replaced. Got: {rewritten}" ); } diff --git a/crates/trusted-server-core/src/integrations/didomi.rs b/crates/trusted-server-core/src/integrations/didomi.rs index 7042af70..539e1ceb 100644 --- a/crates/trusted-server-core/src/integrations/didomi.rs +++ b/crates/trusted-server-core/src/integrations/didomi.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use async_trait::async_trait; -use error_stack::{Report, ResultExt}; +use error_stack::{Report, ResultExt as _}; use fastly::http::{header, Method}; use fastly::{Request, Response}; use serde::{Deserialize, Serialize}; @@ -43,11 +43,11 @@ fn default_enabled() -> bool { } fn default_sdk_origin() -> String { - "https://sdk.privacy-center.org".to_string() + "https://sdk.privacy-center.org".to_owned() } fn default_api_origin() -> String { - "https://api.privacy-center.org".to_string() + "https://api.privacy-center.org".to_owned() } enum DidomiBackend { @@ -66,7 +66,7 @@ impl DidomiIntegration { fn error(message: impl Into) -> TrustedServerError { TrustedServerError::Integration { - integration: DIDOMI_INTEGRATION_ID.to_string(), + integration: DIDOMI_INTEGRATION_ID.to_owned(), message: message.into(), } } diff --git a/crates/trusted-server-core/src/integrations/google_tag_manager.rs b/crates/trusted-server-core/src/integrations/google_tag_manager.rs index 64f27415..0dccbab8 100644 --- a/crates/trusted-server-core/src/integrations/google_tag_manager.rs +++ b/crates/trusted-server-core/src/integrations/google_tag_manager.rs @@ -15,7 +15,7 @@ use std::sync::{Arc, LazyLock, Mutex}; use async_trait::async_trait; -use error_stack::{Report, ResultExt}; +use error_stack::{Report, ResultExt as _}; use fastly::http::{Method, StatusCode}; use fastly::{Request, Response}; use regex::Regex; @@ -43,7 +43,7 @@ enum PayloadSizeError { /// Regex pattern for validating GTM container IDs. /// Format: GTM-XXXXXX where X is alphanumeric. static GTM_CONTAINER_ID_PATTERN: LazyLock = LazyLock::new(|| { - Regex::new(r"^GTM-[A-Z0-9]{4,20}$").expect("GTM container ID regex should compile") + Regex::new("^GTM-[A-Z0-9]{4,20}$").expect("GTM container ID regex should compile") }); /// Regex pattern for matching and rewriting GTM and Google Analytics URLs. @@ -94,7 +94,7 @@ pub struct GoogleTagManagerConfig { /// Maximum allowed size for POST beacon bodies in bytes (default: 65536 / 64KB). /// Prevents memory pressure from oversized payloads on public /collect endpoints. #[serde(default = "default_max_beacon_body_size")] - #[validate(range(min = 1024, max = 1048576))] + #[validate(range(min = 1024, max = 1_048_576))] pub max_beacon_body_size: usize, } @@ -109,7 +109,7 @@ fn default_enabled() -> bool { } fn default_upstream() -> String { - DEFAULT_UPSTREAM.to_string() + DEFAULT_UPSTREAM.to_owned() } fn default_cache_max_age() -> u32 { @@ -196,7 +196,7 @@ impl GoogleTagManagerIntegration { fn error(message: impl Into) -> TrustedServerError { TrustedServerError::Integration { - integration: GTM_INTEGRATION_ID.to_string(), + integration: GTM_INTEGRATION_ID.to_owned(), message: message.into(), } } @@ -210,7 +210,7 @@ impl GoogleTagManagerIntegration { /// Uses [`GTM_URL_PATTERN`] to handle all URL variants (https, protocol-relative) /// for `googletagmanager.com` and `google-analytics.com`. fn rewrite_gtm_urls(content: &str) -> String { - let replacement = format!("/integrations/{}$2", GTM_INTEGRATION_ID); + let replacement = format!("/integrations/{GTM_INTEGRATION_ID}$2"); GTM_URL_PATTERN .replace_all(content, replacement.as_str()) .into_owned() @@ -231,9 +231,9 @@ impl GoogleTagManagerIntegration { // Parse URL to extract host and path // Support both absolute URLs (https://...) and protocol-relative URLs (//...) let url_to_parse = if url.starts_with("//") { - format!("https:{}", url) + format!("https:{url}") } else if url.starts_with("http://") || url.starts_with("https://") { - url.to_string() + url.to_owned() } else { // Relative URLs or other formats - not rewritable via this integration return false; @@ -246,7 +246,7 @@ impl GoogleTagManagerIntegration { .trim_start_matches("http://"); let (host, path_with_query) = match without_protocol.split_once('/') { - Some((h, p)) => (h, format!("/{}", p)), + Some((h, p)) => (h, format!("/{p}")), None => return false, // No path component }; @@ -281,19 +281,19 @@ impl GoogleTagManagerIntegration { let upstream_base = self.upstream_url(); let mut target_url = if path.ends_with("/gtm.js") { - format!("{}/gtm.js", upstream_base) + format!("{upstream_base}/gtm.js") } else if path.ends_with("/gtag/js") || path.ends_with("/gtag.js") { - format!("{}/gtag/js", upstream_base) // Always normalize to /gtag/js upstream as it's canonical + format!("{upstream_base}/gtag/js") // Always normalize to /gtag/js upstream as it's canonical } else if path.ends_with("/g/collect") { - "https://www.google-analytics.com/g/collect".to_string() + "https://www.google-analytics.com/g/collect".to_owned() } else if path.ends_with("/collect") { - "https://www.google-analytics.com/collect".to_string() + "https://www.google-analytics.com/collect".to_owned() } else { return None; }; if let Some(query) = req.get_url().query() { - target_url = format!("{}?{}", target_url, query); + target_url = format!("{target_url}?{query}"); } else if path.ends_with("/gtm.js") { target_url = format!("{}?id={}", target_url, self.config.container_id); } @@ -322,7 +322,7 @@ impl GoogleTagManagerIntegration { for chunk_result in body.read_chunks(CHUNK_SIZE) { let chunk = chunk_result.map_err(|e| { - log::error!("Error reading request body: {}", e); + log::error!("Error reading request body: {e}"); // Convert I/O error to size error for uniform handling PayloadSizeError::TooLarge { actual: 0, @@ -335,9 +335,7 @@ impl GoogleTagManagerIntegration { if body_bytes.len() + chunk.len() > max_size { let total_size = body_bytes.len() + chunk.len(); log::warn!( - "POST body size {} exceeds max {} (rejected during chunked read)", - total_size, - max_size + "POST body size {total_size} exceeds max {max_size} (rejected during chunked read)" ); return Err(PayloadSizeError::TooLarge { actual: total_size, @@ -429,9 +427,9 @@ impl IntegrationProxy for GoogleTagManagerIntegration { settings: &Settings, mut req: Request, ) -> Result> { - let path = req.get_path().to_string(); + let path = req.get_path().to_owned(); let method = req.get_method(); - log::debug!("Handling GTM request: {} {}", method, path); + log::debug!("Handling GTM request: {method} {path}"); // Validate body size for POST requests to prevent memory pressure // Check Content-Length header if present for early rejection @@ -439,23 +437,20 @@ impl IntegrationProxy for GoogleTagManagerIntegration { if let Some(content_length_str) = req.get_header_str(fastly::http::header::CONTENT_LENGTH) { - match content_length_str.parse::() { - Ok(content_length) => { - // Early rejection based on Content-Length - if content_length > self.config.max_beacon_body_size { - log::warn!( - "Rejecting POST beacon with Content-Length {} exceeding max {}", - content_length, - self.config.max_beacon_body_size - ); - return Ok(Response::from_status(StatusCode::PAYLOAD_TOO_LARGE)); - } - } - Err(_) => { - // Invalid Content-Length header - log::warn!("POST request with malformed Content-Length header"); - return Ok(Response::from_status(StatusCode::BAD_REQUEST)); + if let Ok(content_length) = content_length_str.parse::() { + // Early rejection based on Content-Length + if content_length > self.config.max_beacon_body_size { + log::warn!( + "Rejecting POST beacon with Content-Length {} exceeding max {}", + content_length, + self.config.max_beacon_body_size + ); + return Ok(Response::from_status(StatusCode::PAYLOAD_TOO_LARGE)); } + } else { + // Invalid Content-Length header + log::warn!("POST request with malformed Content-Length header"); + return Ok(Response::from_status(StatusCode::BAD_REQUEST)); } } // If Content-Length is missing, we'll check actual size after read @@ -466,7 +461,7 @@ impl IntegrationProxy for GoogleTagManagerIntegration { return Ok(Response::from_status(StatusCode::NOT_FOUND)); }; - log::debug!("Proxying to upstream: {}", target_url); + log::debug!("Proxying to upstream: {target_url}"); // Handle payload size errors explicitly to return 413 instead of 502 let proxy_config = match self.build_proxy_config(&path, &mut req, &target_url) { @@ -474,9 +469,7 @@ impl IntegrationProxy for GoogleTagManagerIntegration { Err(PayloadSizeError::TooLarge { actual, max }) => { // This catches cases where Content-Length was incorrect log::warn!( - "Returning 413: actual body size {} exceeds max {} (Content-Length mismatch)", - actual, - max + "Returning 413: actual body size {actual} exceeds max {max} (Content-Length mismatch)" ); return Ok(Response::from_status(StatusCode::PAYLOAD_TOO_LARGE)); } @@ -595,7 +588,7 @@ impl IntegrationScriptRewriter for GoogleTagManagerIntegration { // No GTM content — if we accumulated fragments, emit them unchanged. // Intermediate fragments were already suppressed via RemoveNode. if full_content.is_some() { - return ScriptRewriteAction::replace(text.to_string()); + return ScriptRewriteAction::replace(text.to_owned()); } ScriptRewriteAction::keep() @@ -698,8 +691,8 @@ mod tests { fn test_attribute_rewriter() { let config = GoogleTagManagerConfig { enabled: true, - container_id: "GTM-TEST1234".to_string(), - upstream_url: "https://www.googletagmanager.com".to_string(), + container_id: "GTM-TEST1234".to_owned(), + upstream_url: "https://www.googletagmanager.com".to_owned(), cache_max_age: default_cache_max_age(), max_beacon_body_size: default_max_beacon_body_size(), }; @@ -725,7 +718,7 @@ mod tests { "/integrations/google_tag_manager/gtm.js?id=GTM-TEST1234" ); } else { - panic!("Expected Replace action for HTTPS URL, got {:?}", action); + panic!("Expected Replace action for HTTPS URL, got {action:?}"); } // Case 2: Protocol-relative URL @@ -741,10 +734,7 @@ mod tests { "/integrations/google_tag_manager/gtm.js?id=GTM-TEST1234" ); } else { - panic!( - "Expected Replace action for protocol-relative URL, got {:?}", - action - ); + panic!("Expected Replace action for protocol-relative URL, got {action:?}"); } // Case 3: gtag/js URL in href (preload link) @@ -760,10 +750,7 @@ mod tests { "/integrations/google_tag_manager/gtag/js?id=G-DQMZGMPHXN" ); } else { - panic!( - "Expected Replace action for gtag/js preload href, got {:?}", - action - ); + panic!("Expected Replace action for gtag/js preload href, got {action:?}"); } // Case 4: google-analytics.com URL in href @@ -776,10 +763,7 @@ mod tests { if let AttributeRewriteAction::Replace(val) = action { assert_eq!(val, "/integrations/google_tag_manager/g/collect"); } else { - panic!( - "Expected Replace action for google-analytics href, got {:?}", - action - ); + panic!("Expected Replace action for google-analytics href, got {action:?}"); } // Case 5: analytics.google.com URL in href @@ -792,10 +776,7 @@ mod tests { if let AttributeRewriteAction::Replace(val) = action { assert_eq!(val, "/integrations/google_tag_manager/g/collect?v=2"); } else { - panic!( - "Expected Replace action for analytics.google.com href, got {:?}", - action - ); + panic!("Expected Replace action for analytics.google.com href, got {action:?}"); } // Case 6: Other URL (should be kept) @@ -814,8 +795,8 @@ mod tests { // This verifies the fix for P2: proper URL parsing instead of substring matching let config = GoogleTagManagerConfig { enabled: true, - container_id: "GTM-TEST1234".to_string(), - upstream_url: "https://www.googletagmanager.com".to_string(), + container_id: "GTM-TEST1234".to_owned(), + upstream_url: "https://www.googletagmanager.com".to_owned(), cache_max_age: default_cache_max_age(), max_beacon_body_size: default_max_beacon_body_size(), }; @@ -893,8 +874,8 @@ mod tests { fn test_script_rewriter() { let config = GoogleTagManagerConfig { enabled: true, - container_id: "GTM-TEST1234".to_string(), - upstream_url: "https://www.googletagmanager.com".to_string(), + container_id: "GTM-TEST1234".to_owned(), + upstream_url: "https://www.googletagmanager.com".to_owned(), cache_max_age: default_cache_max_age(), max_beacon_body_size: default_max_beacon_body_size(), }; @@ -911,31 +892,28 @@ mod tests { }; // Case 1: Inline GTM snippet - let snippet = r#"(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': + let snippet = "(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0], j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f); -})(window,document,'script','dataLayer','GTM-XXXX');"#; +})(window,document,'script','dataLayer','GTM-XXXX');"; let action = IntegrationScriptRewriter::rewrite(&*integration, snippet, &ctx); if let ScriptRewriteAction::Replace(val) = action { assert!(val.contains("/integrations/google_tag_manager/gtm.js")); assert!(!val.contains("https://www.googletagmanager.com/gtm.js")); } else { - panic!("Expected Replace action for GTM snippet, got {:?}", action); + panic!("Expected Replace action for GTM snippet, got {action:?}"); } // Case 2: Protocol relative - let snippet_proto = r#"j.src='//www.googletagmanager.com/gtm.js?id='+i+dl;"#; + let snippet_proto = "j.src='//www.googletagmanager.com/gtm.js?id='+i+dl;"; let action = IntegrationScriptRewriter::rewrite(&*integration, snippet_proto, &ctx); if let ScriptRewriteAction::Replace(val) = action { assert!(val.contains("/integrations/google_tag_manager/gtm.js")); assert!(!val.contains("//www.googletagmanager.com/gtm.js")); } else { - panic!( - "Expected Replace action for proto-relative snippet, got {:?}", - action - ); + panic!("Expected Replace action for proto-relative snippet, got {action:?}"); } // Case 3: Irrelevant script @@ -948,7 +926,7 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= fn test_default_configuration() { let config = GoogleTagManagerConfig { enabled: default_enabled(), - container_id: "GTM-DEFAULT".to_string(), + container_id: "GTM-DEFAULT".to_owned(), upstream_url: default_upstream(), cache_max_age: default_cache_max_age(), max_beacon_body_size: default_max_beacon_body_size(), @@ -963,7 +941,7 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= // Default upstream (via serde default) let config_default = GoogleTagManagerConfig { enabled: true, - container_id: "GTM-TEST1234123".to_string(), + container_id: "GTM-TEST1234123".to_owned(), upstream_url: default_upstream(), cache_max_age: default_cache_max_age(), max_beacon_body_size: default_max_beacon_body_size(), @@ -977,8 +955,8 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= // Custom upstream let config_custom = GoogleTagManagerConfig { enabled: true, - container_id: "GTM-TEST1234123".to_string(), - upstream_url: "https://gtm.example.com".to_string(), + container_id: "GTM-TEST1234123".to_owned(), + upstream_url: "https://gtm.example.com".to_owned(), cache_max_age: default_cache_max_age(), max_beacon_body_size: default_max_beacon_body_size(), }; @@ -990,7 +968,7 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= fn test_routes_registered() { let config = GoogleTagManagerConfig { enabled: true, - container_id: "GTM-TEST1234".to_string(), + container_id: "GTM-TEST1234".to_owned(), upstream_url: default_upstream(), cache_max_age: default_cache_max_age(), max_beacon_body_size: default_max_beacon_body_size(), @@ -1022,7 +1000,7 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= fn test_post_collect_proxy_config_includes_payload() { let config = GoogleTagManagerConfig { enabled: true, - container_id: "GTM-TEST1234".to_string(), + container_id: "GTM-TEST1234".to_owned(), upstream_url: default_upstream(), cache_max_age: default_cache_max_age(), max_beacon_body_size: default_max_beacon_body_size(), @@ -1036,7 +1014,7 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= ); req.set_body(payload.clone()); - let path = req.get_path().to_string(); + let path = req.get_path().to_owned(); let target_url = integration .build_target_url(&req, &path) .expect("should resolve collect target URL"); @@ -1056,7 +1034,7 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= let max_size = default_max_beacon_body_size(); let config = GoogleTagManagerConfig { enabled: true, - container_id: "GTM-TEST1234".to_string(), + container_id: "GTM-TEST1234".to_owned(), upstream_url: default_upstream(), cache_max_age: default_cache_max_age(), max_beacon_body_size: max_size, @@ -1071,7 +1049,7 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= ); req.set_body(oversized_payload.clone()); - let path = req.get_path().to_string(); + let path = req.get_path().to_owned(); let target_url = integration .build_target_url(&req, &path) .expect("should resolve collect target URL"); @@ -1095,7 +1073,7 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= let custom_max_size = 1024; // 1KB let config = GoogleTagManagerConfig { enabled: true, - container_id: "GTM-TEST1234".to_string(), + container_id: "GTM-TEST1234".to_owned(), upstream_url: default_upstream(), cache_max_age: default_cache_max_age(), max_beacon_body_size: custom_max_size, @@ -1110,7 +1088,7 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= ); req1.set_body(acceptable_payload.clone()); - let path = req1.get_path().to_string(); + let path = req1.get_path().to_owned(); let target_url = integration .build_target_url(&req1, &path) .expect("should resolve collect target URL"); @@ -1144,7 +1122,7 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= let max_size = default_max_beacon_body_size(); let config = GoogleTagManagerConfig { enabled: true, - container_id: "GTM-TEST1234".to_string(), + container_id: "GTM-TEST1234".to_owned(), upstream_url: default_upstream(), cache_max_age: default_cache_max_age(), max_beacon_body_size: max_size, @@ -1164,7 +1142,7 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= (max_size / 2).to_string(), ); - let path = req.get_path().to_string(); + let path = req.get_path().to_owned(); let target_url = integration .build_target_url(&req, &path) .expect("should resolve collect target URL"); @@ -1192,7 +1170,7 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= let max_size = 1024; // Use small size for testing let config = GoogleTagManagerConfig { enabled: true, - container_id: "GTM-TEST1234".to_string(), + container_id: "GTM-TEST1234".to_owned(), upstream_url: default_upstream(), cache_max_age: default_cache_max_age(), max_beacon_body_size: max_size, @@ -1230,7 +1208,7 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= // Verify that handle() returns 400 Bad Request for malformed Content-Length let config = GoogleTagManagerConfig { enabled: true, - container_id: "GTM-TEST1234".to_string(), + container_id: "GTM-TEST1234".to_owned(), upstream_url: default_upstream(), cache_max_age: default_cache_max_age(), max_beacon_body_size: default_max_beacon_body_size(), @@ -1267,7 +1245,7 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= let max_size = default_max_beacon_body_size(); let config = GoogleTagManagerConfig { enabled: true, - container_id: "GTM-TEST1234".to_string(), + container_id: "GTM-TEST1234".to_owned(), upstream_url: default_upstream(), cache_max_age: default_cache_max_age(), max_beacon_body_size: max_size, @@ -1283,7 +1261,7 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= req.set_body(small_payload); // Intentionally NOT setting Content-Length header (HTTP/2 scenario) - let path = req.get_path().to_string(); + let path = req.get_path().to_owned(); let target_url = integration .build_target_url(&req, &path) .expect("should resolve collect target URL"); @@ -1301,7 +1279,7 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= fn test_collect_proxy_config_strips_client_ip_forwarding() { let config = GoogleTagManagerConfig { enabled: true, - container_id: "GTM-TEST1234".to_string(), + container_id: "GTM-TEST1234".to_owned(), upstream_url: default_upstream(), cache_max_age: default_cache_max_age(), max_beacon_body_size: default_max_beacon_body_size(), @@ -1314,7 +1292,7 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= ); req.set_header(crate::constants::HEADER_X_FORWARDED_FOR, "198.51.100.42"); - let path = req.get_path().to_string(); + let path = req.get_path().to_owned(); let target_url = integration .build_target_url(&req, &path) .expect("should resolve collect target URL"); @@ -1340,7 +1318,7 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= fn test_gtag_proxy_config_requests_identity_encoding() { let config = GoogleTagManagerConfig { enabled: true, - container_id: "GT-123".to_string(), + container_id: "GT-123".to_owned(), upstream_url: default_upstream(), cache_max_age: default_cache_max_age(), max_beacon_body_size: default_max_beacon_body_size(), @@ -1352,7 +1330,7 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= "https://edge.example.com/integrations/google_tag_manager/gtag/js?id=G-123", ); - let path = req.get_path().to_string(); + let path = req.get_path().to_owned(); let target_url = integration .build_target_url(&req, &path) .expect("should resolve gtag target URL"); @@ -1500,7 +1478,7 @@ container_id = "GTM-DEFAULT" let mut output = Vec::new(); let result = pipeline.process(Cursor::new(html.as_bytes()), &mut output); - assert!(result.is_ok()); + result.unwrap(); let processed = String::from_utf8_lossy(&output); @@ -1605,7 +1583,7 @@ container_id = "GTM-DEFAULT" let mut pipeline = StreamingPipeline::new(pipeline_config, processor); // Synthetic HTML with inline script - let html_input = r#" + let html_input = " "#; + let html_input = ""; let mut output = Vec::new(); pipeline diff --git a/crates/trusted-server-core/src/integrations/gpt.rs b/crates/trusted-server-core/src/integrations/gpt.rs index 0affbe95..654a8445 100644 --- a/crates/trusted-server-core/src/integrations/gpt.rs +++ b/crates/trusted-server-core/src/integrations/gpt.rs @@ -35,7 +35,7 @@ use std::sync::Arc; use async_trait::async_trait; -use error_stack::{Report, ResultExt}; +use error_stack::{Report, ResultExt as _}; use fastly::http::header; use fastly::{Request, Response}; use serde::{Deserialize, Serialize}; @@ -105,7 +105,7 @@ impl GptIntegration { fn error(message: impl Into) -> TrustedServerError { TrustedServerError::Integration { - integration: GPT_INTEGRATION_ID.to_string(), + integration: GPT_INTEGRATION_ID.to_owned(), message: message.into(), } } @@ -118,7 +118,7 @@ impl GptIntegration { /// Returns `None` if `request_path` does not start with [`ROUTE_PREFIX`]. fn build_upstream_url(request_path: &str, query: Option<&str>) -> Option { let upstream_path = request_path.strip_prefix(ROUTE_PREFIX)?; - let query_part = query.map(|q| format!("?{}", q)).unwrap_or_default(); + let query_part = query.map(|q| format!("?{q}")).unwrap_or_default(); Some(format!( "https://{SECUREPUBADS_HOST}{upstream_path}{query_part}" )) @@ -163,11 +163,7 @@ impl GptIntegration { } let status = response.get_status(); - log::error!( - "GPT proxy upstream returned status {} for {}", - status, - context - ); + log::error!("GPT proxy upstream returned status {status} for {context}"); Err(Report::new(Self::error(format!( "{context}: upstream returned {status}" )))) @@ -220,18 +216,18 @@ impl GptIntegration { fn vary_with_accept_encoding(upstream_vary: Option<&str>) -> String { match upstream_vary.map(str::trim) { - Some("*") => "*".to_string(), + Some("*") => "*".to_owned(), Some(vary) if !vary.is_empty() => { if vary .split(',') .any(|header_name| header_name.trim().eq_ignore_ascii_case("accept-encoding")) { - vary.to_string() + vary.to_owned() } else { format!("{vary}, Accept-Encoding") } } - _ => "Accept-Encoding".to_string(), + _ => "Accept-Encoding".to_owned(), } } @@ -290,7 +286,7 @@ impl GptIntegration { req: Request, ) -> Result> { let script_url = &self.config.script_url; - log::info!("Fetching GPT script from: {}", script_url); + log::info!("Fetching GPT script from: {script_url}"); self.proxy_gpt_asset( settings, req, @@ -315,9 +311,9 @@ impl GptIntegration { let query = req.get_url().query(); let target_url = Self::build_upstream_url(original_path, query) - .ok_or_else(|| Self::error(format!("Invalid GPT pagead path: {}", original_path)))?; + .ok_or_else(|| Self::error(format!("Invalid GPT pagead path: {original_path}")))?; - log::info!("GPT proxy: forwarding to {}", target_url); + log::info!("GPT proxy: forwarding to {target_url}"); self.proxy_gpt_asset( settings, req, @@ -388,8 +384,7 @@ impl IntegrationProxy for GptIntegration { self.handle_pagead_proxy(settings, req).await } else { Err(Report::new(Self::error(format!( - "Unknown GPT route: {}", - path + "Unknown GPT route: {path}" )))) } } @@ -436,8 +431,7 @@ impl IntegrationHeadInjector for GptIntegration { // when it sees the pre-set flag, so this works regardless of whether // the inline bootstrap runs before or after the TSJS bundle. vec![ - "" - .to_string(), + "".to_owned(), ] } } @@ -449,7 +443,7 @@ fn default_enabled() -> bool { } fn default_script_url() -> String { - "https://securepubads.g.doubleclick.net/tag/js/gpt.js".to_string() + "https://securepubads.g.doubleclick.net/tag/js/gpt.js".to_owned() } fn default_cache_ttl() -> u32 { @@ -556,7 +550,7 @@ mod tests { "should rewrite to first-party script endpoint" ); } - other => panic!("Expected Replace action, got {:?}", other), + other => panic!("Expected Replace action, got {other:?}"), } } diff --git a/crates/trusted-server-core/src/integrations/lockr.rs b/crates/trusted-server-core/src/integrations/lockr.rs index 9a480aec..05d5e656 100644 --- a/crates/trusted-server-core/src/integrations/lockr.rs +++ b/crates/trusted-server-core/src/integrations/lockr.rs @@ -10,7 +10,7 @@ use std::sync::Arc; use async_trait::async_trait; -use error_stack::{Report, ResultExt}; +use error_stack::{Report, ResultExt as _}; use fastly::http::{header, Method, StatusCode}; use fastly::{Request, Response}; use serde::Deserialize; @@ -88,7 +88,7 @@ impl LockrIntegration { fn error(message: impl Into) -> TrustedServerError { TrustedServerError::Integration { - integration: LOCKR_INTEGRATION_ID.to_string(), + integration: LOCKR_INTEGRATION_ID.to_owned(), message: message.into(), } } @@ -108,7 +108,7 @@ impl LockrIntegration { _req: Request, ) -> Result> { let sdk_url = &self.config.sdk_url; - log::info!("Fetching Lockr SDK from {}", sdk_url); + log::info!("Fetching Lockr SDK from {sdk_url}"); // TODO: Check KV store cache first (future enhancement) @@ -123,8 +123,7 @@ impl LockrIntegration { lockr_req .send(backend_name) .change_context(Self::error(format!( - "Failed to fetch Lockr SDK from {}", - sdk_url + "Failed to fetch Lockr SDK from {sdk_url}" )))?; if !lockr_response.get_status().is_success() { @@ -167,22 +166,22 @@ impl LockrIntegration { let original_path = req.get_path(); let method = req.get_method(); - log::info!("Proxying Lockr API request: {} {}", method, original_path); + log::info!("Proxying Lockr API request: {method} {original_path}"); // Extract path after /integrations/lockr/api and pass through directly. // This allows the Lockr SDK to use any API endpoint without hardcoded mappings. let target_path = original_path .strip_prefix("/integrations/lockr/api") - .ok_or_else(|| Self::error(format!("Invalid Lockr API path: {}", original_path)))?; + .ok_or_else(|| Self::error(format!("Invalid Lockr API path: {original_path}")))?; let query = req .get_url() .query() - .map(|q| format!("?{}", q)) + .map(|q| format!("?{q}")) .unwrap_or_default(); let target_url = format!("{}{}{}", self.config.api_endpoint, target_path, query); - log::info!("Forwarding to Lockr API: {}", target_url); + log::info!("Forwarding to Lockr API: {target_url}"); let mut target_req = Request::new(method.clone(), &target_url); self.copy_request_headers(&req, &mut target_req); @@ -313,8 +312,7 @@ impl IntegrationProxy for LockrIntegration { self.handle_api_proxy(settings, req).await } else { Err(Report::new(Self::error(format!( - "Unknown Lockr route: {}", - path + "Unknown Lockr route: {path}" )))) } } @@ -344,7 +342,7 @@ impl IntegrationAttributeRewriter for LockrIntegration { "{}://{}/integrations/lockr/sdk", ctx.request_scheme, ctx.request_host ); - log::debug!("Rewriting Lockr SDK URL to {}", replacement); + log::debug!("Rewriting Lockr SDK URL to {replacement}"); AttributeRewriteAction::Replace(replacement) } else { AttributeRewriteAction::Keep @@ -357,11 +355,11 @@ fn default_enabled() -> bool { } fn default_api_endpoint() -> String { - "https://identity.loc.kr".to_string() + "https://identity.loc.kr".to_owned() } fn default_sdk_url() -> String { - "https://aim.loc.kr/identity-lockr-trust-server.js".to_string() + "https://aim.loc.kr/identity-lockr-trust-server.js".to_owned() } fn default_cache_ttl() -> u32 { @@ -382,7 +380,7 @@ mod tests { fn test_config() -> LockrConfig { LockrConfig { enabled: true, - app_id: "test-app-id".to_string(), + app_id: "test-app-id".to_owned(), api_endpoint: default_api_endpoint(), sdk_url: default_sdk_url(), cache_ttl_seconds: 3600, @@ -450,7 +448,7 @@ mod tests { assert_eq!( result, AttributeRewriteAction::Replace( - "https://edge.example.com/integrations/lockr/sdk".to_string() + "https://edge.example.com/integrations/lockr/sdk".to_owned() ), "should rewrite Lockr SDK URL to first-party proxy" ); @@ -509,11 +507,7 @@ mod tests { let result = input .strip_prefix("/integrations/lockr/api") .expect("should strip prefix"); - assert_eq!( - result, expected, - "should preserve casing for path: {}", - input - ); + assert_eq!(result, expected, "should preserve casing for path: {input}"); } } diff --git a/crates/trusted-server-core/src/integrations/nextjs/html_post_process.rs b/crates/trusted-server-core/src/integrations/nextjs/html_post_process.rs index d7d3c000..1000980d 100644 --- a/crates/trusted-server-core/src/integrations/nextjs/html_post_process.rs +++ b/crates/trusted-server-core/src/integrations/nextjs/html_post_process.rs @@ -139,16 +139,13 @@ impl NextJsHtmlPostProcessor { let expected = rewritten_payloads.len(); if replaced != expected { log::warn!( - "NextJs post-process placeholder substitution count mismatch: expected={}, replaced={}", - expected, - replaced + "NextJs post-process placeholder substitution count mismatch: expected={expected}, replaced={replaced}" ); } if contains_rsc_payload_placeholders(&updated) { log::error!( - "NextJs post-process left RSC placeholders in output; attempting fallback substitution (scripts={})", - expected + "NextJs post-process left RSC placeholders in output; attempting fallback substitution (scripts={expected})" ); let fallback = @@ -156,8 +153,7 @@ impl NextJsHtmlPostProcessor { if contains_rsc_payload_placeholders(&fallback) { log::error!( - "NextJs post-process fallback substitution still left RSC placeholders in output; hydration may break (scripts={})", - expected + "NextJs post-process fallback substitution still left RSC placeholders in output; hydration may break (scripts={expected})" ); } @@ -171,7 +167,7 @@ impl NextJsHtmlPostProcessor { } fn contains_rsc_payload_placeholders(html: &str) -> bool { - let mut cursor = 0usize; + let mut cursor = 0_usize; while let Some(next) = html[cursor..].find(RSC_PAYLOAD_PLACEHOLDER_PREFIX) { let start = cursor + next; let after_prefix = start + RSC_PAYLOAD_PLACEHOLDER_PREFIX.len(); @@ -189,8 +185,8 @@ fn contains_rsc_payload_placeholders(html: &str) -> bool { fn substitute_rsc_payload_placeholders(html: &str, replacements: &[String]) -> (String, usize) { let mut output = String::with_capacity(html.len()); - let mut cursor = 0usize; - let mut replaced = 0usize; + let mut cursor = 0_usize; + let mut replaced = 0_usize; while let Some(next) = html[cursor..].find(RSC_PAYLOAD_PLACEHOLDER_PREFIX) { let start = cursor + next; @@ -237,7 +233,7 @@ fn substitute_rsc_payload_placeholders(html: &str, replacements: &[String]) -> ( } fn substitute_rsc_payload_placeholders_exact(html: &str, replacements: &[String]) -> String { - let mut out = html.to_string(); + let mut out = html.to_owned(); for (index, replacement) in replacements.iter().enumerate() { let placeholder = format!("{RSC_PAYLOAD_PLACEHOLDER_PREFIX}{index}{RSC_PAYLOAD_PLACEHOLDER_SUFFIX}"); @@ -260,7 +256,7 @@ fn find_rsc_push_scripts(html: &str) -> Vec { let ranges: Rc>> = Rc::new(RefCell::new(Vec::new())); let buffer: Rc> = Rc::new(RefCell::new(String::new())); let buffering = Rc::new(Cell::new(false)); - let buffer_start = Rc::new(Cell::new(0usize)); + let buffer_start = Rc::new(Cell::new(0_usize)); let settings = RewriterSettings { element_content_handlers: vec![text!("script", { @@ -353,8 +349,8 @@ pub fn post_process_rsc_html( request_host: &str, request_scheme: &str, ) -> String { - let mut result = html.to_string(); - #[allow(deprecated)] + let mut result = html.to_owned(); + #[allow(deprecated, reason = "wrapper preserves the deprecated legacy API")] post_process_rsc_html_in_place(&mut result, origin_host, request_host, request_scheme); result } @@ -400,7 +396,7 @@ fn post_process_rsc_html_in_place_with_limit( } scripts.sort_by_key(|s| s.payload_start); - let mut previous_end = 0usize; + let mut previous_end = 0_usize; for script in &scripts { if script.payload_start > script.payload_end { log::warn!( @@ -507,7 +503,10 @@ fn post_process_rsc_html_in_place_with_limit( } #[cfg(test)] -#[allow(deprecated)] // Tests use deprecated post_process_rsc_html for legacy API coverage +#[allow( + deprecated, + reason = "tests cover deprecated post_process_rsc_html legacy API" +)] mod tests { use super::*; @@ -522,7 +521,7 @@ mod tests { let ranges: Rc>> = Rc::new(RefCell::new(Vec::new())); let buffer: Rc> = Rc::new(RefCell::new(String::new())); let buffering = Rc::new(Cell::new(false)); - let buffer_start = Rc::new(Cell::new(0usize)); + let buffer_start = Rc::new(Cell::new(0_usize)); let saw_partial = Rc::new(Cell::new(false)); let settings = RewriterSettings { @@ -617,13 +616,11 @@ mod tests { assert!( result.contains("test.example.com/page"), - "URL should be rewritten. Got: {}", - result + "URL should be rewritten. Got: {result}" ); assert!( result.contains(":T3c,"), - "T-chunk length should be updated. Got: {}", - result + "T-chunk length should be updated. Got: {result}" ); assert!(result.contains("") && result.contains("")); assert!(result.contains("self.__next_f.push")); @@ -676,7 +673,7 @@ mod tests { #[test] fn finds_window_next_f_push_with_case_insensitive_script_tags() { - let html = r#""#; + let html = ""; let scripts = find_rsc_push_scripts(html); assert_eq!( scripts.len(), @@ -708,18 +705,15 @@ mod tests { assert!( result.contains("test.example.com/news"), - "First URL should be rewritten. Got: {}", - result + "First URL should be rewritten. Got: {result}" ); assert!( result.contains("test.example.com/reviews"), - "Second URL should be rewritten. Got: {}", - result + "Second URL should be rewritten. Got: {result}" ); assert!( !result.contains("origin.example.com"), - "No origin URLs should remain. Got: {}", - result + "No origin URLs should remain. Got: {result}" ); assert!(result.contains("") && result.contains("")); assert!(result.contains("self.__next_f.push")); @@ -729,8 +723,8 @@ mod tests { fn post_process_rewrites_html_href_inside_tchunk() { fn calculate_unescaped_byte_length_for_test(s: &str) -> usize { let bytes = s.as_bytes(); - let mut pos = 0usize; - let mut count = 0usize; + let mut pos = 0_usize; + let mut count = 0_usize; while pos < bytes.len() { if bytes[pos] == b'\\' && pos + 1 < bytes.len() { @@ -773,7 +767,7 @@ mod tests { } } - let c = char::from_u32(code_unit as u32).unwrap_or('\u{FFFD}'); + let c = char::from_u32(u32::from(code_unit)).unwrap_or('\u{FFFD}'); pos += 6; count += c.len_utf8(); continue; @@ -801,14 +795,14 @@ mod tests { calculate_unescaped_byte_length_for_test(tchunk_content) ); let html = format!( - r#" + " -"# +" ); let result = @@ -816,18 +810,15 @@ mod tests { assert!( result.contains("test.example.com/about-us"), - "HTML href URL in T-chunk should be rewritten. Got: {}", - result + "HTML href URL in T-chunk should be rewritten. Got: {result}" ); assert!( !result.contains("origin.example.com"), - "No origin URLs should remain. Got: {}", - result + "No origin URLs should remain. Got: {result}" ); assert!( !result.contains(&format!(":T{declared_len_hex},")), - "T-chunk length should have been recalculated. Got: {}", - result + "T-chunk length should have been recalculated. Got: {result}" ); } diff --git a/crates/trusted-server-core/src/integrations/nextjs/mod.rs b/crates/trusted-server-core/src/integrations/nextjs/mod.rs index 6414284d..dd446f56 100644 --- a/crates/trusted-server-core/src/integrations/nextjs/mod.rs +++ b/crates/trusted-server-core/src/integrations/nextjs/mod.rs @@ -18,7 +18,10 @@ mod shared; // Re-export deprecated legacy functions for backward compatibility. // Production code should use the placeholder-based approach via NextJsHtmlPostProcessor. -#[allow(deprecated)] +#[allow( + deprecated, + reason = "legacy HTML post-processing functions remain re-exported for compatibility" +)] pub use html_post_process::{post_process_rsc_html, post_process_rsc_html_in_place}; pub use rsc::rewrite_rsc_scripts_combined; @@ -51,7 +54,7 @@ fn default_enabled() -> bool { } fn default_rewrite_attributes() -> Vec { - vec!["href".to_string(), "link".to_string(), "url".to_string()] + vec!["href".to_owned(), "link".to_owned(), "url".to_owned()] } fn default_max_combined_payload_bytes() -> usize { @@ -76,20 +79,17 @@ pub(super) fn configuration_error(message: impl Into) -> Report Result, Report> { - let config = match build(settings)? { - Some(config) => { - log::info!( - "NextJS integration registered: enabled={}, rewrite_attributes={:?}, max_combined_payload_bytes={}", - config.enabled, - config.rewrite_attributes, - config.max_combined_payload_bytes - ); - config - } - None => { - log::info!("NextJS integration not registered (disabled or missing config)"); - return Ok(None); - } + let config = if let Some(config) = build(settings)? { + log::info!( + "NextJS integration registered: enabled={}, rewrite_attributes={:?}, max_combined_payload_bytes={}", + config.enabled, + config.rewrite_attributes, + config.max_combined_payload_bytes + ); + config + } else { + log::info!("NextJS integration not registered (disabled or missing config)"); + return Ok(None); }; // Register a structured (Pages Router __NEXT_DATA__) rewriter. let structured = Arc::new(NextJsNextDataRewriter::new(config.clone())?); @@ -336,13 +336,11 @@ mod tests { // RSC payloads should be rewritten via end-of-document post-processing assert!( final_html.contains("test.example.com"), - "RSC stream payloads should be rewritten to proxy host via post-processing. Output: {}", - final_html + "RSC stream payloads should be rewritten to proxy host via post-processing. Output: {final_html}" ); assert!( !final_html.contains(RSC_PAYLOAD_PLACEHOLDER_PREFIX), - "RSC placeholder markers should not appear in final HTML. Output: {}", - final_html + "RSC placeholder markers should not appear in final HTML. Output: {final_html}" ); } @@ -384,13 +382,11 @@ mod tests { // RSC payloads should be rewritten via end-of-document post-processing assert!( final_html.contains("test.example.com"), - "RSC stream payloads should be rewritten to proxy host with chunked input. Output: {}", - final_html + "RSC stream payloads should be rewritten to proxy host with chunked input. Output: {final_html}" ); assert!( !final_html.contains(RSC_PAYLOAD_PLACEHOLDER_PREFIX), - "RSC placeholder markers should not appear in final HTML. Output: {}", - final_html + "RSC placeholder markers should not appear in final HTML. Output: {final_html}" ); } @@ -434,18 +430,15 @@ mod tests { assert!( final_html.contains("https://origin.example.com/page"), - "Origin URL should remain when rewrite is skipped due to size limit. Output: {}", - final_html + "Origin URL should remain when rewrite is skipped due to size limit. Output: {final_html}" ); assert!( !final_html.contains("test.example.com"), - "Proxy host should not be introduced when rewrite is skipped. Output: {}", - final_html + "Proxy host should not be introduced when rewrite is skipped. Output: {final_html}" ); assert!( !final_html.contains(RSC_PAYLOAD_PLACEHOLDER_PREFIX), - "RSC placeholder markers should not appear in final HTML. Output: {}", - final_html + "RSC placeholder markers should not appear in final HTML. Output: {final_html}" ); } @@ -500,8 +493,7 @@ mod tests { // RSC payloads should be rewritten via post-processing assert!( final_html.contains("test.example.com"), - "RSC payload URLs should be rewritten to proxy host. Output: {}", - final_html + "RSC payload URLs should be rewritten to proxy host. Output: {final_html}" ); // Verify the RSC payload structure is preserved @@ -520,14 +512,12 @@ mod tests { // Verify \n separators are preserved (crucial for RSC parsing) assert!( - final_html.contains(r#"\n442:"#), - "RSC record separator \\n should be preserved. Output: {}", - final_html + final_html.contains(r"\n442:"), + "RSC record separator \\n should be preserved. Output: {final_html}" ); assert!( !final_html.contains(RSC_PAYLOAD_PLACEHOLDER_PREFIX), - "RSC placeholder markers should not appear in final HTML. Output: {}", - final_html + "RSC placeholder markers should not appear in final HTML. Output: {final_html}" ); } @@ -573,30 +563,25 @@ mod tests { // Non-RSC scripts should be preserved assert!( final_html.contains(r#"console.log("hello world");"#), - "First non-RSC script should be preserved intact. Output: {}", - final_html + "First non-RSC script should be preserved intact. Output: {final_html}" ); assert!( final_html.contains("window.analytics"), - "Third non-RSC script should be preserved. Output: {}", - final_html + "Third non-RSC script should be preserved. Output: {final_html}" ); assert!( final_html.contains("track: function(e)"), - "Third non-RSC script content should be intact. Output: {}", - final_html + "Third non-RSC script content should be intact. Output: {final_html}" ); // RSC scripts should be rewritten assert!( final_html.contains("test.example.com"), - "RSC URL should be rewritten. Output: {}", - final_html + "RSC URL should be rewritten. Output: {final_html}" ); assert!( !final_html.contains(RSC_PAYLOAD_PLACEHOLDER_PREFIX), - "No placeholders should remain. Output: {}", - final_html + "No placeholders should remain. Output: {final_html}" ); } @@ -720,7 +705,7 @@ mod tests { ); assert!( processed.contains(r#""])"#), - "push call must close properly — `\"])` followed by . Got: {processed}" + "push call must close properly \u{2014} `\"])` followed by . Got: {processed}" ); } } diff --git a/crates/trusted-server-core/src/integrations/nextjs/rsc.rs b/crates/trusted-server-core/src/integrations/nextjs/rsc.rs index 66a5e26f..c7983552 100644 --- a/crates/trusted-server-core/src/integrations/nextjs/rsc.rs +++ b/crates/trusted-server-core/src/integrations/nextjs/rsc.rs @@ -9,7 +9,7 @@ use super::shared::RscUrlRewriter; /// This is a static code-defined literal rather than a config-derived pattern, /// so it intentionally stays outside startup preparation. static TCHUNK_PATTERN: LazyLock = - LazyLock::new(|| Regex::new(r"([0-9a-fA-F]+):T([0-9a-fA-F]+),").expect("valid T-chunk regex")); + LazyLock::new(|| Regex::new("([0-9a-fA-F]+):T([0-9a-fA-F]+),").expect("valid T-chunk regex")); /// Marker used to track script boundaries when combining RSC content. pub(crate) const RSC_MARKER: &str = "\x00SPLIT\x00"; @@ -141,7 +141,7 @@ impl Iterator for EscapeSequenceIter<'_> { } } - let c = char::from_u32(code_unit as u32).unwrap_or('\u{FFFD}'); + let c = char::from_u32(u32::from(code_unit)).unwrap_or('\u{FFFD}'); self.pos += 6; return Some(EscapeElement { byte_count: c.len_utf8(), @@ -206,11 +206,7 @@ struct TChunkInfo { fn find_tchunks_impl(content: &str, skip_markers: bool) -> Option> { let mut chunks = Vec::new(); let mut search_pos = 0; - let marker = if skip_markers { - Some(RSC_MARKER.as_bytes()) - } else { - None - }; + let marker = skip_markers.then(|| RSC_MARKER.as_bytes()); while search_pos < content.len() { if let Some(cap) = TCHUNK_PATTERN.captures(&content[search_pos..]) { @@ -289,7 +285,7 @@ pub(crate) fn rewrite_rsc_tchunks_with_rewriter( log::warn!( "RSC payload contains invalid or incomplete T-chunks; skipping rewriting to avoid breaking hydration" ); - return content.to_string(); + return content.to_owned(); }; if chunks.is_empty() { @@ -406,7 +402,7 @@ pub(crate) fn rewrite_rsc_scripts_combined_with_limit( // Early exit if no payload contains the origin host - avoids regex compilation if !payloads.iter().any(|p| p.contains(origin_host)) { - return payloads.iter().map(|p| (*p).to_string()).collect(); + return payloads.iter().map(|p| (*p).to_owned()).collect(); } if payloads.len() == 1 { @@ -434,9 +430,7 @@ pub(crate) fn rewrite_rsc_scripts_combined_with_limit( // per-script rewriting is unsafe because it may rewrite T-chunk content without updating // the original header, breaking React hydration. log::warn!( - "RSC combined payload size {} exceeds limit {}, skipping cross-script combining", - total_size, - max_combined_payload_bytes + "RSC combined payload size {total_size} exceeds limit {max_combined_payload_bytes}, skipping cross-script combining" ); if payloads @@ -446,7 +440,7 @@ pub(crate) fn rewrite_rsc_scripts_combined_with_limit( log::warn!( "RSC payloads contain cross-script T-chunks; skipping RSC URL rewriting to avoid breaking hydration (consider increasing integrations.nextjs.max_combined_payload_bytes)" ); - return payloads.iter().map(|p| (*p).to_string()).collect(); + return payloads.iter().map(|p| (*p).to_owned()).collect(); } return payloads @@ -474,7 +468,7 @@ pub(crate) fn rewrite_rsc_scripts_combined_with_limit( log::warn!( "RSC combined payload contains invalid or incomplete T-chunks; skipping rewriting to avoid breaking hydration" ); - return payloads.iter().map(|p| (*p).to_string()).collect(); + return payloads.iter().map(|p| (*p).to_owned()).collect(); }; if chunks.is_empty() { return payloads @@ -542,8 +536,7 @@ mod tests { ); assert!( result.starts_with("1a:T27,"), - "T-chunk length should be updated from 29 (41) to 27 (39). Got: {}", - result + "T-chunk length should be updated from 29 (41) to 27 (39). Got: {result}" ); } @@ -565,21 +558,20 @@ mod tests { ); assert!( result.starts_with("1a:T24,"), - "T-chunk length should be updated from 1c (28) to 24 (36). Got: {}", - result + "T-chunk length should be updated from 1c (28) to 24 (36). Got: {result}" ); } #[test] fn calculate_unescaped_byte_length_handles_common_escapes() { assert_eq!(calculate_unescaped_byte_length("hello"), 5); - assert_eq!(calculate_unescaped_byte_length(r#"\n"#), 1); - assert_eq!(calculate_unescaped_byte_length(r#"\r\n"#), 2); + assert_eq!(calculate_unescaped_byte_length(r"\n"), 1); + assert_eq!(calculate_unescaped_byte_length(r"\r\n"), 2); assert_eq!(calculate_unescaped_byte_length(r#"\""#), 1); - assert_eq!(calculate_unescaped_byte_length(r#"\\"#), 1); - assert_eq!(calculate_unescaped_byte_length(r#"\x41"#), 1); - assert_eq!(calculate_unescaped_byte_length(r#"\u0041"#), 1); - assert_eq!(calculate_unescaped_byte_length(r#"\u00e9"#), 2); + assert_eq!(calculate_unescaped_byte_length(r"\\"), 1); + assert_eq!(calculate_unescaped_byte_length(r"\x41"), 1); + assert_eq!(calculate_unescaped_byte_length(r"\u0041"), 1); + assert_eq!(calculate_unescaped_byte_length(r"\u00e9"), 2); } #[test] @@ -604,15 +596,12 @@ mod tests { #[test] fn cross_script_tchunk_rewriting() { - let script0 = r#"other:data\n1a:T3e,partial content"#; - let script1 = r#" with https://origin.example.com/page goes here"#; + let script0 = r"other:data\n1a:T3e,partial content"; + let script1 = " with https://origin.example.com/page goes here"; let combined_content = "partial content with https://origin.example.com/page goes here"; let combined_len = calculate_unescaped_byte_length(combined_content); - println!( - "Combined T-chunk content length: {} bytes = 0x{:x}", - combined_len, combined_len - ); + println!("Combined T-chunk content length: {combined_len} bytes = 0x{combined_len:x}"); let payloads: Vec<&str> = vec![script0, script1]; let results = rewrite_rsc_scripts_combined( @@ -631,7 +620,7 @@ mod tests { let rewritten_content = "partial content with https://test.example.com/page goes here"; let rewritten_len = calculate_unescaped_byte_length(rewritten_content); - let expected_header = format!(":T{:x},", rewritten_len); + let expected_header = format!(":T{rewritten_len:x},"); assert!( results[0].contains(&expected_header), "T-chunk length in script 0 should be updated to {}. Got: {}", @@ -643,7 +632,7 @@ mod tests { #[test] fn cross_script_preserves_non_tchunk_content() { let script0 = r#"{"url":"https://origin.example.com/first"}\n1a:T38,partial"#; - let script1 = r#" content with https://origin.example.com/page end"#; + let script1 = " content with https://origin.example.com/page end"; let payloads: Vec<&str> = vec![script0, script1]; let results = rewrite_rsc_scripts_combined( @@ -799,8 +788,8 @@ mod tests { #[test] fn size_limit_skips_rewrite_when_cross_script_tchunk_detected() { - let script0 = r#"other:data\n1a:T40,partial content"#; - let script1 = r#" with https://origin.example.com/page goes here"#; + let script0 = r"other:data\n1a:T40,partial content"; + let script1 = " with https://origin.example.com/page goes here"; let payloads: Vec<&str> = vec![script0, script1]; let results = rewrite_rsc_scripts_combined_with_limit( diff --git a/crates/trusted-server-core/src/integrations/nextjs/rsc_placeholders.rs b/crates/trusted-server-core/src/integrations/nextjs/rsc_placeholders.rs index 10101a70..88bd85d4 100644 --- a/crates/trusted-server-core/src/integrations/nextjs/rsc_placeholders.rs +++ b/crates/trusted-server-core/src/integrations/nextjs/rsc_placeholders.rs @@ -98,7 +98,7 @@ impl IntegrationScriptRewriter for NextJsRscPlaceholderRewriter { .payloads .push(content[payload_start..payload_end].to_string()); - let mut rewritten = content.to_string(); + let mut rewritten = content.to_owned(); rewritten.replace_range(payload_start..payload_end, &placeholder); ScriptRewriteAction::replace(rewritten) } @@ -109,10 +109,10 @@ mod tests { use super::*; use crate::integrations::IntegrationDocumentState; - fn ctx<'a>( + fn ctx( is_last_in_text_node: bool, - document_state: &'a IntegrationDocumentState, - ) -> IntegrationScriptContext<'a> { + document_state: &IntegrationDocumentState, + ) -> IntegrationScriptContext<'_> { IntegrationScriptContext { selector: "script", request_host: "proxy.example.com", diff --git a/crates/trusted-server-core/src/integrations/nextjs/script_rewriter.rs b/crates/trusted-server-core/src/integrations/nextjs/script_rewriter.rs index eaf00a16..b78ba1c2 100644 --- a/crates/trusted-server-core/src/integrations/nextjs/script_rewriter.rs +++ b/crates/trusted-server-core/src/integrations/nextjs/script_rewriter.rs @@ -143,13 +143,11 @@ impl UrlRewriter { .collect::>() .join("|"); let pattern = format!( - r#"(?P(?:\\*")?(?:{attrs})(?:\\*")?\s*:\s*\\*")(?P[^"\\]*)(?P\\*")"#, - attrs = attr_alternation, + r#"(?P(?:\\*")?(?:{attr_alternation})(?:\\*")?\s*:\s*\\*")(?P[^"\\]*)(?P\\*")"#, ); Some(Regex::new(&pattern).map_err(|err| { super::configuration_error(format!( - "failed to compile __NEXT_DATA__ URL rewrite regex for attributes {:?}: {err}", - attributes + "failed to compile __NEXT_DATA__ URL rewrite regex for attributes {attributes:?}: {err}" )) })?) }; @@ -177,7 +175,7 @@ impl UrlRewriter { return Some(format!("//{request_host}{path}")); } } else if url == origin_host { - return Some(request_host.to_string()); + return Some(request_host.to_owned()); } else if let Some(path) = strip_origin_host_with_optional_port(url, origin_host) { return Some(format!("{request_host}{path}")); } @@ -210,7 +208,7 @@ impl UrlRewriter { caps.get(0) .expect("should capture matched attribute value") .as_str() - .to_string() + .to_owned() } }); diff --git a/crates/trusted-server-core/src/integrations/nextjs/shared.rs b/crates/trusted-server-core/src/integrations/nextjs/shared.rs index 673d8956..ce7ad057 100644 --- a/crates/trusted-server-core/src/integrations/nextjs/shared.rs +++ b/crates/trusted-server-core/src/integrations/nextjs/shared.rs @@ -71,10 +71,7 @@ pub(crate) fn strip_origin_host_with_optional_port<'a>( return Some(suffix); } - if matches!( - suffix.as_bytes().first(), - Some(b'/') | Some(b'?') | Some(b'#') - ) { + if matches!(suffix.as_bytes().first(), Some(b'/' | b'?' | b'#')) { return Some(suffix); } @@ -85,16 +82,8 @@ pub(crate) fn strip_origin_host_with_optional_port<'a>( } let rest = &port_and_rest[port_len..]; - if rest.is_empty() - || matches!( - rest.as_bytes().first(), - Some(b'/') | Some(b'?') | Some(b'#') - ) - { - Some(suffix) - } else { - None - } + (rest.is_empty() || matches!(rest.as_bytes().first(), Some(b'/' | b'?' | b'#'))) + .then_some(suffix) } // ============================================================================= @@ -111,7 +100,7 @@ pub(crate) fn strip_origin_host_with_optional_port<'a>( fn build_origin_url_pattern(origin_host: &str) -> Result { let escaped = regex::escape(origin_host); Regex::new(&format!( - r#"(https?)?(:)?(\\\\\\\\\\\\\\\\//|\\\\\\\\//|\\/\\/|//)(?P{escaped}(?::\d+)?)"# + r"(https?)?(:)?(\\\\\\\\\\\\\\\\//|\\\\\\\\//|\\/\\/|//)(?P{escaped}(?::\d+)?)" )) } @@ -174,7 +163,7 @@ impl RscUrlRewriter { Ok(pattern) => { let result = Self::apply_rewrite(input, &pattern, origin_host, request_host, request_scheme); - *self.cached_pattern.borrow_mut() = Some((origin_host.to_string(), pattern)); + *self.cached_pattern.borrow_mut() = Some((origin_host.to_owned(), pattern)); result } Err(e) => { @@ -199,7 +188,7 @@ impl RscUrlRewriter { .get(0) .expect("should capture the matched RSC URL") .as_str() - .to_string(); + .to_owned(); }; let slashes = caps.get(3).map_or("//", |m| m.as_str()); @@ -252,7 +241,7 @@ mod tests { #[test] fn finds_single_quoted_payload() { - let script = r#"self.__next_f.push([1,'hello world'])"#; + let script = "self.__next_f.push([1,'hello world'])"; let (start, end) = find_rsc_push_payload_range(script).expect("should find payload"); assert_eq!(&script[start..end], "hello world"); } diff --git a/crates/trusted-server-core/src/integrations/permutive.rs b/crates/trusted-server-core/src/integrations/permutive.rs index b3e59456..6e71787f 100644 --- a/crates/trusted-server-core/src/integrations/permutive.rs +++ b/crates/trusted-server-core/src/integrations/permutive.rs @@ -6,7 +6,7 @@ use std::sync::Arc; use async_trait::async_trait; -use error_stack::{Report, ResultExt}; +use error_stack::{Report, ResultExt as _}; use fastly::http::{header, Method, StatusCode}; use fastly::{Request, Response}; use serde::Deserialize; @@ -80,7 +80,7 @@ impl PermutiveIntegration { fn error(message: impl Into) -> TrustedServerError { TrustedServerError::Integration { - integration: PERMUTIVE_INTEGRATION_ID.to_string(), + integration: PERMUTIVE_INTEGRATION_ID.to_owned(), message: message.into(), } } @@ -110,7 +110,7 @@ impl PermutiveIntegration { log::info!("Handling Permutive SDK request"); let sdk_url = self.sdk_url(); - log::info!("Fetching Permutive SDK from: {}", sdk_url); + log::info!("Fetching Permutive SDK from: {sdk_url}"); // TODO: Check KV store cache first (future enhancement) @@ -126,8 +126,7 @@ impl PermutiveIntegration { permutive_req .send(backend_name) .change_context(Self::error(format!( - "Failed to fetch Permutive SDK from {}", - sdk_url + "Failed to fetch Permutive SDK from {sdk_url}" )))?; if !permutive_response.get_status().is_success() { @@ -172,26 +171,22 @@ impl PermutiveIntegration { let original_path = req.get_path(); let method = req.get_method(); - log::info!( - "Proxying Permutive API request: {} {}", - method, - original_path - ); + log::info!("Proxying Permutive API request: {method} {original_path}"); // Extract path after /integrations/permutive/api let api_path = original_path .strip_prefix("/integrations/permutive/api") - .ok_or_else(|| Self::error(format!("Invalid Permutive API path: {}", original_path)))?; + .ok_or_else(|| Self::error(format!("Invalid Permutive API path: {original_path}")))?; // Build full target URL with query parameters let query = req .get_url() .query() - .map(|q| format!("?{}", q)) + .map(|q| format!("?{q}")) .unwrap_or_default(); let target_url = format!("{}{}{}", self.config.api_endpoint, api_path, query); - log::info!("Forwarding to Permutive API: {}", target_url); + log::info!("Forwarding to Permutive API: {target_url}"); // Create new request let mut target_req = Request::new(method.clone(), &target_url); @@ -212,8 +207,7 @@ impl PermutiveIntegration { let response = target_req .send(backend_name) .change_context(Self::error(format!( - "Failed to forward request to {}", - target_url + "Failed to forward request to {target_url}" )))?; log::info!( @@ -233,19 +227,14 @@ impl PermutiveIntegration { let original_path = req.get_path(); let method = req.get_method(); - log::info!( - "Proxying Permutive Secure Signals request: {} {}", - method, - original_path - ); + log::info!("Proxying Permutive Secure Signals request: {method} {original_path}"); // Extract path after /integrations/permutive/secure-signal let signal_path = original_path .strip_prefix("/integrations/permutive/secure-signal") .ok_or_else(|| { Self::error(format!( - "Invalid Permutive Secure Signals path: {}", - original_path + "Invalid Permutive Secure Signals path: {original_path}" )) })?; @@ -253,14 +242,14 @@ impl PermutiveIntegration { let query = req .get_url() .query() - .map(|q| format!("?{}", q)) + .map(|q| format!("?{q}")) .unwrap_or_default(); let target_url = format!( "{}{}{}", self.config.secure_signals_endpoint, signal_path, query ); - log::info!("Forwarding to Permutive Secure Signals: {}", target_url); + log::info!("Forwarding to Permutive Secure Signals: {target_url}"); // Create new request let mut target_req = Request::new(method.clone(), &target_url); @@ -283,8 +272,7 @@ impl PermutiveIntegration { let response = target_req .send(backend_name) .change_context(Self::error(format!( - "Failed to forward request to {}", - target_url + "Failed to forward request to {target_url}" )))?; log::info!( @@ -304,28 +292,24 @@ impl PermutiveIntegration { let original_path = req.get_path(); let method = req.get_method(); - log::info!( - "Proxying Permutive Events request: {} {}", - method, - original_path - ); + log::info!("Proxying Permutive Events request: {method} {original_path}"); // Extract path after /integrations/permutive/events let events_path = original_path .strip_prefix("/integrations/permutive/events") .ok_or_else(|| { - Self::error(format!("Invalid Permutive Events path: {}", original_path)) + Self::error(format!("Invalid Permutive Events path: {original_path}")) })?; // Build full target URL with query parameters let query = req .get_url() .query() - .map(|q| format!("?{}", q)) + .map(|q| format!("?{q}")) .unwrap_or_default(); - let target_url = format!("https://events.permutive.app{}{}", events_path, query); + let target_url = format!("https://events.permutive.app{events_path}{query}"); - log::info!("Forwarding to Permutive Events: {}", target_url); + log::info!("Forwarding to Permutive Events: {target_url}"); // Create new request let mut target_req = Request::new(method.clone(), &target_url); @@ -346,8 +330,7 @@ impl PermutiveIntegration { let response = target_req .send(backend_name) .change_context(Self::error(format!( - "Failed to forward request to {}", - target_url + "Failed to forward request to {target_url}" )))?; log::info!( @@ -367,28 +350,22 @@ impl PermutiveIntegration { let original_path = req.get_path(); let method = req.get_method(); - log::info!( - "Proxying Permutive Sync request: {} {}", - method, - original_path - ); + log::info!("Proxying Permutive Sync request: {method} {original_path}"); // Extract path after /integrations/permutive/sync let sync_path = original_path .strip_prefix("/integrations/permutive/sync") - .ok_or_else(|| { - Self::error(format!("Invalid Permutive Sync path: {}", original_path)) - })?; + .ok_or_else(|| Self::error(format!("Invalid Permutive Sync path: {original_path}")))?; // Build full target URL with query parameters let query = req .get_url() .query() - .map(|q| format!("?{}", q)) + .map(|q| format!("?{q}")) .unwrap_or_default(); - let target_url = format!("https://sync.permutive.com{}{}", sync_path, query); + let target_url = format!("https://sync.permutive.com{sync_path}{query}"); - log::info!("Forwarding to Permutive Sync: {}", target_url); + log::info!("Forwarding to Permutive Sync: {target_url}"); // Create new request let mut target_req = Request::new(method.clone(), &target_url); @@ -409,8 +386,7 @@ impl PermutiveIntegration { let response = target_req .send(backend_name) .change_context(Self::error(format!( - "Failed to forward request to {}", - target_url + "Failed to forward request to {target_url}" )))?; log::info!( @@ -430,26 +406,22 @@ impl PermutiveIntegration { let original_path = req.get_path(); let method = req.get_method(); - log::info!( - "Proxying Permutive CDN request: {} {}", - method, - original_path - ); + log::info!("Proxying Permutive CDN request: {method} {original_path}"); // Extract path after /integrations/permutive/cdn let cdn_path = original_path .strip_prefix("/integrations/permutive/cdn") - .ok_or_else(|| Self::error(format!("Invalid Permutive CDN path: {}", original_path)))?; + .ok_or_else(|| Self::error(format!("Invalid Permutive CDN path: {original_path}")))?; // Build full target URL with query parameters let query = req .get_url() .query() - .map(|q| format!("?{}", q)) + .map(|q| format!("?{q}")) .unwrap_or_default(); - let target_url = format!("https://cdn.permutive.com{}{}", cdn_path, query); + let target_url = format!("https://cdn.permutive.com{cdn_path}{query}"); - log::info!("Forwarding to Permutive CDN: {}", target_url); + log::info!("Forwarding to Permutive CDN: {target_url}"); // Create new request let mut target_req = Request::new(method.clone(), &target_url); @@ -464,8 +436,7 @@ impl PermutiveIntegration { let response = target_req .send(backend_name) .change_context(Self::error(format!( - "Failed to forward request to {}", - target_url + "Failed to forward request to {target_url}" )))?; log::info!( @@ -578,8 +549,7 @@ impl IntegrationProxy for PermutiveIntegration { self.handle_sdk_serving(settings, req).await } else { Err(Report::new(Self::error(format!( - "Unknown Permutive route: {}", - path + "Unknown Permutive route: {path}" )))) } } @@ -622,11 +592,11 @@ fn default_enabled() -> bool { } fn default_api_endpoint() -> String { - "https://api.permutive.com".to_string() + "https://api.permutive.com".to_owned() } fn default_secure_signals_endpoint() -> String { - "https://secure-signals.permutive.app".to_string() + "https://secure-signals.permutive.app".to_owned() } fn default_cache_ttl() -> u32 { @@ -646,9 +616,9 @@ mod tests { fn test_permutive_sdk_url_generation() { let config = PermutiveConfig { enabled: true, - organization_id: "myorg".to_string(), - workspace_id: "workspace-123".to_string(), - project_id: "project-456".to_string(), + organization_id: "myorg".to_owned(), + workspace_id: "workspace-123".to_owned(), + project_id: "project-456".to_owned(), api_endpoint: default_api_endpoint(), secure_signals_endpoint: default_secure_signals_endpoint(), cache_ttl_seconds: 3600, @@ -666,8 +636,8 @@ mod tests { fn test_permutive_sdk_url_detection() { let config = PermutiveConfig { enabled: true, - organization_id: "myorg".to_string(), - workspace_id: "workspace-123".to_string(), + organization_id: "myorg".to_owned(), + workspace_id: "workspace-123".to_owned(), project_id: String::new(), api_endpoint: default_api_endpoint(), secure_signals_endpoint: default_secure_signals_endpoint(), @@ -692,8 +662,8 @@ mod tests { fn test_attribute_rewriter_rewrites_sdk_urls() { let config = PermutiveConfig { enabled: true, - organization_id: "myorg".to_string(), - workspace_id: "workspace-123".to_string(), + organization_id: "myorg".to_owned(), + workspace_id: "workspace-123".to_owned(), project_id: String::new(), api_endpoint: default_api_endpoint(), secure_signals_endpoint: default_secure_signals_endpoint(), @@ -725,8 +695,8 @@ mod tests { fn test_attribute_rewriter_noop_when_disabled() { let config = PermutiveConfig { enabled: true, - organization_id: "myorg".to_string(), - workspace_id: "workspace-123".to_string(), + organization_id: "myorg".to_owned(), + workspace_id: "workspace-123".to_owned(), project_id: String::new(), api_endpoint: default_api_endpoint(), secure_signals_endpoint: default_secure_signals_endpoint(), @@ -767,8 +737,8 @@ mod tests { fn test_routes_registration() { let config = PermutiveConfig { enabled: true, - organization_id: "myorg".to_string(), - workspace_id: "workspace-123".to_string(), + organization_id: "myorg".to_owned(), + workspace_id: "workspace-123".to_owned(), project_id: String::new(), api_endpoint: default_api_endpoint(), secure_signals_endpoint: default_secure_signals_endpoint(), diff --git a/crates/trusted-server-core/src/integrations/prebid.rs b/crates/trusted-server-core/src/integrations/prebid.rs index 63b60e3d..1f35210a 100644 --- a/crates/trusted-server-core/src/integrations/prebid.rs +++ b/crates/trusted-server-core/src/integrations/prebid.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use std::time::Duration; use async_trait::async_trait; -use error_stack::{Report, ResultExt}; +use error_stack::{Report, ResultExt as _}; use fastly::http::{header, Method, StatusCode, Url}; use fastly::{Request, Response}; use serde::{Deserialize, Serialize}; @@ -27,8 +27,8 @@ use crate::integrations::{ }; use crate::openrtb::{ to_openrtb_i32, Banner, ConsentedProvidersSettings, Device, Format, Geo, Imp, ImpExt, - OpenRtbRequest, PrebidExt, PrebidImpExt, Publisher, Regs, RegsExt, RequestExt, Site, ToExt, - TrustedServerExt, User, UserExt, + OpenRtbRequest, PrebidExt, PrebidImpExt, Publisher, Regs, RegsExt, RequestExt, Site, + ToExt as _, TrustedServerExt, User, UserExt, }; use crate::request_signing::{RequestSigner, SigningParams, SIGNING_VERSION}; use crate::settings::{IntegrationConfig, Settings}; @@ -223,7 +223,7 @@ fn default_timeout_ms() -> u32 { } fn default_bidders() -> Vec { - vec!["mocktioneer".to_string()] + vec!["mocktioneer".to_owned()] } fn default_enabled() -> bool { @@ -362,9 +362,8 @@ fn build( for bidder in &config.client_side_bidders { if config.bidders.iter().any(|b| b == bidder) { log::warn!( - "prebid: bidder \"{}\" is in both bidders and client_side_bidders — \ - it will run server-side AND be left for client-side, which is likely unintended", - bidder + "prebid: bidder \"{bidder}\" is in both bidders and client_side_bidders \u{2014} \ + it will run server-side AND be left for client-side, which is likely unintended" ); } } @@ -422,7 +421,7 @@ impl IntegrationProxy for PrebidIntegration { _settings: &Settings, req: Request, ) -> Result> { - let path = req.get_path().to_string(); + let path = req.get_path().to_owned(); let method = req.get_method().clone(); match method { @@ -485,12 +484,12 @@ impl IntegrationHeadInjector for PrebidIntegration { let config_json = serde_json::to_string(&payload) .unwrap_or_else(|e| { log::warn!("Prebid: failed to serialize client config: {e}"); - "{}".to_string() + "{}".to_owned() }) .replace("window.pbjs=window.pbjs||{{}};window.pbjs.que=window.pbjs.que||[];window.pbjs.cmd=window.pbjs.cmd||[];window.__tsjs_prebid={config_json};"# + "" )] } } @@ -515,7 +514,7 @@ fn expand_trusted_server_bidders( .unwrap_or_else(|| { // No per-bidder map → use entire params as shared config if per_bidder.is_some() { - Json::Object(Default::default()) + Json::Object(serde_json::Map::default()) } else { params.clone() } @@ -557,12 +556,12 @@ fn warn_unconfigured_bidder(config: &PrebidIntegrationConfig, bidder: &str, fiel if config.client_side_bidders.iter().any(|b| b == bidder) { log::warn!( "prebid: {field} entry targets client-side-only bidder \ - '{bidder}' — server-side override will never apply" + '{bidder}' \u{2014} server-side override will never apply" ); } else { log::warn!( "prebid: {field} entry references unconfigured bidder \ - '{bidder}' — rule will never fire" + '{bidder}' \u{2014} rule will never fire" ); } } @@ -682,9 +681,8 @@ fn merged_rule_indices<'a>( std::iter::from_fn(move || match (wildcard.peek(), bidder.peek()) { (Some(wildcard_idx), Some(bidder_idx)) if wildcard_idx <= bidder_idx => wildcard.next(), - (Some(_), Some(_)) => bidder.next(), + (Some(_) | None, Some(_)) => bidder.next(), (Some(_), None) => wildcard.next(), - (None, Some(_)) => bidder.next(), (None, None) => None, }) } @@ -801,7 +799,7 @@ fn validate_override_matcher_string( })); } - Ok(trimmed.to_string()) + Ok(trimmed.to_owned()) } fn non_empty_override_object( @@ -851,12 +849,12 @@ fn copy_request_headers( /// Returns the original URL unchanged if params are empty or already present. fn append_query_params(url: &str, params: &str) -> String { if params.is_empty() || url.contains(params) { - return url.to_string(); + return url.to_owned(); } if url.contains('?') { - format!("{}&{}", url, params) + format!("{url}&{params}") } else { - format!("{}?{}", url, params) + format!("{url}?{params}") } } @@ -922,7 +920,7 @@ impl PrebidAuctionProvider { if formats.is_empty() { log::warn!( - "prebid: dropping imp '{}' — no valid banner formats after filtering", + "prebid: dropping imp '{}' \u{2014} no valid banner formats after filtering", slot.id ); return None; @@ -972,7 +970,7 @@ impl PrebidAuctionProvider { // NOTE: Currency defaults to DEFAULT_CURRENCY. If // multi-currency support is needed, this should come from // config or the AdSlot itself. - bidfloorcur: slot.floor_price.map(|_| DEFAULT_CURRENCY.to_string()), + bidfloorcur: slot.floor_price.map(|_| DEFAULT_CURRENCY.to_owned()), secure: Some(true), // require HTTPS creatives tagid: Some(slot.id.clone()), ext: ImpExt { @@ -1024,13 +1022,10 @@ impl PrebidAuctionProvider { }); // Extract DNT header and Accept-Language from the original request - let dnt = context.request.get_header_str("DNT").and_then(|v| { - if v.trim() == "1" { - Some(true) - } else { - None - } - }); + let dnt = context + .request + .get_header_str("DNT") + .and_then(|v| (v.trim() == "1").then_some(true)); let language = context .request @@ -1047,7 +1042,7 @@ impl PrebidAuctionProvider { .next() .expect("should have at least one split segment") .trim() - .to_string() + .to_owned() }) .filter(|s| !s.is_empty()) }); @@ -1076,11 +1071,7 @@ impl PrebidAuctionProvider { // Fastly returns 0 for "no metro code"; negative values // are not realistic for DMA codes so the > 0 guard is // sufficient. - metro: if geo.metro_code > 0 { - Some(geo.metro_code.to_string()) - } else { - None - }, + metro: (geo.metro_code > 0).then(|| geo.metro_code.to_string()), r#type: Some(2), ..Default::default() }), @@ -1091,15 +1082,11 @@ impl PrebidAuctionProvider { ..Default::default() }) .or_else(|| { - if dnt.is_some() || language.is_some() { - Some(Device { - dnt, - language, - ..Default::default() - }) - } else { - None - } + (dnt.is_some() || language.is_some()).then(|| Device { + dnt, + language, + ..Default::default() + }) }); // Build regs object from ConsentContext, populating both OpenRTB 2.6 @@ -1109,16 +1096,15 @@ impl PrebidAuctionProvider { // Build ext object let http_req = compat::from_fastly_headers_ref(context.request); let request_info = RequestInfo::from_request(&http_req, &context.services.client_info); - let (version, signature, kid, ts) = signer - .map(|(s, sig, params)| { + let (version, signature, kid, ts) = + signer.map_or((None, None, None, None), |(s, sig, params)| { ( - Some(SIGNING_VERSION.to_string()), + Some(SIGNING_VERSION.to_owned()), Some(sig), Some(s.kid.clone()), Some(params.timestamp), ) - }) - .unwrap_or((None, None, None, None)); + }); let debug_enabled = self.config.debug; @@ -1164,7 +1150,7 @@ impl PrebidAuctionProvider { regs, test: self.config.test_mode.then_some(true), tmax, - cur: vec![DEFAULT_CURRENCY.to_string()], + cur: vec![DEFAULT_CURRENCY.to_owned()], ext, ..Default::default() } @@ -1253,17 +1239,16 @@ impl PrebidAuctionProvider { if let Some(bid_array) = seatbid.get("bid").and_then(|v| v.as_array()) { for bid_obj in bid_array { - match self.parse_bid(bid_obj, seat) { - Ok(bid) => bids.push(bid), - Err(()) => { - let impid = bid_obj - .get("impid") - .and_then(|v| v.as_str()) - .unwrap_or(""); - log::warn!( - "Prebid: failed to parse bid from seat '{seat}' for imp '{impid}'" - ); - } + if let Ok(bid) = self.parse_bid(bid_obj, seat) { + bids.push(bid); + } else { + let impid = bid_obj + .get("impid") + .and_then(|v| v.as_str()) + .unwrap_or(""); + log::warn!( + "Prebid: failed to parse bid from seat '{seat}' for imp '{impid}'" + ); } } } @@ -1293,17 +1278,17 @@ impl PrebidAuctionProvider { if let Some(rtm) = ext.and_then(|e| e.get("responsetimemillis")) { auction_response .metadata - .insert("responsetimemillis".to_string(), rtm.clone()); + .insert("responsetimemillis".to_owned(), rtm.clone()); } if let Some(errors) = ext.and_then(|e| e.get("errors")) { auction_response .metadata - .insert("errors".to_string(), errors.clone()); + .insert("errors".to_owned(), errors.clone()); } if let Some(warnings) = ext.and_then(|e| e.get("warnings")) { auction_response .metadata - .insert("warnings".to_string(), warnings.clone()); + .insert("warnings".to_owned(), warnings.clone()); } // When debug is enabled, surface httpcalls, resolvedrequest, and @@ -1312,7 +1297,7 @@ impl PrebidAuctionProvider { if let Some(debug) = ext.and_then(|e| e.get("debug")) { auction_response .metadata - .insert("debug".to_string(), debug.clone()); + .insert("debug".to_owned(), debug.clone()); } if let Some(bidstatus) = ext .and_then(|e| e.get("prebid")) @@ -1320,7 +1305,7 @@ impl PrebidAuctionProvider { { auction_response .metadata - .insert("bidstatus".to_string(), bidstatus.clone()); + .insert("bidstatus".to_owned(), bidstatus.clone()); } } } @@ -1331,7 +1316,7 @@ impl PrebidAuctionProvider { .get("impid") .and_then(|v| v.as_str()) .ok_or(())? - .to_string(); + .to_owned(); let price = bid_obj .get("price") @@ -1376,10 +1361,10 @@ impl PrebidAuctionProvider { Ok(AuctionBid { slot_id, price: Some(price), // Prebid provides decoded prices - currency: DEFAULT_CURRENCY.to_string(), + currency: DEFAULT_CURRENCY.to_owned(), creative, adomain, - bidder: seat.to_string(), + bidder: seat.to_owned(), width, height, nurl, @@ -1440,7 +1425,7 @@ impl PrebidAuctionProvider { body_bytes.len() ); return Err(Report::new(TrustedServerError::Prebid { - message: "Failed to parse Prebid response JSON".to_string(), + message: "Failed to parse Prebid response JSON".to_owned(), })); } }; @@ -1517,9 +1502,9 @@ impl AuctionProvider for PrebidAuctionProvider { // round-trip. This can happen when all slots are non-Banner or all // banner dimensions overflow `i32::MAX`. if openrtb.imp.is_empty() { - log::info!("Prebid: skipping request — no valid impressions after filtering"); + log::info!("Prebid: skipping request \u{2014} no valid impressions after filtering"); return Err(Report::new(TrustedServerError::Prebid { - message: "No valid impressions after filtering".to_string(), + message: "No valid impressions after filtering".to_owned(), })); } @@ -1532,7 +1517,7 @@ impl AuctionProvider for PrebidAuctionProvider { json ), Err(e) => { - log::warn!("Prebid: failed to serialize OpenRTB request for logging: {e}") + log::warn!("Prebid: failed to serialize OpenRTB request for logging: {e}"); } } } @@ -1551,7 +1536,7 @@ impl AuctionProvider for PrebidAuctionProvider { pbs_req .set_body_json(&openrtb) .change_context(TrustedServerError::Prebid { - message: "Failed to set request body".to_string(), + message: "Failed to set request body".to_owned(), })?; // Send request asynchronously with auction-scoped timeout @@ -1564,7 +1549,7 @@ impl AuctionProvider for PrebidAuctionProvider { pbs_req .send_async(backend_name) .change_context(TrustedServerError::Prebid { - message: "Failed to send async request to Prebid Server".to_string(), + message: "Failed to send async request to Prebid Server".to_owned(), })?; Ok(pending) @@ -1633,7 +1618,7 @@ pub fn register_auction_provider( ); if integration.config.debug { log::warn!( - "Prebid debug mode is ON — debug data (httpcalls, resolvedrequest, \ + "Prebid debug mode is ON \u{2014} debug data (httpcalls, resolvedrequest, \ bidstatus) will be included in /auction responses" ); } @@ -1670,10 +1655,10 @@ mod tests { fn base_config() -> PrebidIntegrationConfig { PrebidIntegrationConfig { enabled: true, - server_url: "https://prebid.example".to_string(), - account_id: Some("test-account".to_string()), + server_url: "https://prebid.example".to_owned(), + account_id: Some("test-account".to_owned()), timeout_ms: 1000, - bidders: vec!["exampleBidder".to_string()], + bidders: vec!["exampleBidder".to_owned()], debug: false, test_mode: false, debug_query_params: None, @@ -1688,9 +1673,9 @@ mod tests { fn create_test_auction_request() -> AuctionRequest { AuctionRequest { - id: "auction-123".to_string(), + id: "auction-123".to_owned(), slots: vec![AdSlot { - id: "slot-1".to_string(), + id: "slot-1".to_owned(), formats: vec![AdFormat { media_type: MediaType::Banner, width: 300, @@ -1701,11 +1686,11 @@ mod tests { bidders: HashMap::new(), }], publisher: PublisherInfo { - domain: "pub.example".to_string(), - page_url: Some("https://pub.example/article".to_string()), + domain: "pub.example".to_owned(), + page_url: Some("https://pub.example/article".to_owned()), }, user: UserInfo { - id: Some("user-123".to_string()), + id: Some("user-123".to_owned()), consent: None, eids: None, }, @@ -1760,7 +1745,7 @@ passphrase = "test-secret-key-32-bytes-minimum" /// Parse a TOML string containing only the `[integrations.prebid]` section /// (plus any sub-tables) into a [`PrebidIntegrationConfig`]. fn parse_prebid_toml(prebid_section: &str) -> PrebidIntegrationConfig { - let toml_str = format!("{}{}", TOML_BASE, prebid_section); + let toml_str = format!("{TOML_BASE}{prebid_section}"); let settings = Settings::from_toml(&toml_str).expect("should parse TOML"); settings .integration_config::("prebid") @@ -1771,13 +1756,13 @@ passphrase = "test-secret-key-32-bytes-minimum" fn parse_prebid_toml_result( prebid_section: &str, ) -> Result> { - let toml_str = format!("{}{}", TOML_BASE, prebid_section); + let toml_str = format!("{TOML_BASE}{prebid_section}"); let settings = Settings::from_toml(&toml_str)?; settings .integration_config::("prebid")? .ok_or_else(|| { Report::new(TrustedServerError::Configuration { - message: "prebid integration config should be present and enabled".to_string(), + message: "prebid integration config should be present and enabled".to_owned(), }) }) } @@ -1852,7 +1837,7 @@ passphrase = "test-secret-key-32-bytes-minimum" let mut output = Vec::new(); let result = pipeline.process(Cursor::new(html.as_bytes()), &mut output); - assert!(result.is_ok()); + result.unwrap(); let processed = String::from_utf8_lossy(&output); assert!( processed.contains("tsjs-unified"), @@ -1902,7 +1887,7 @@ passphrase = "test-secret-key-32-bytes-minimum" let mut output = Vec::new(); let result = pipeline.process(Cursor::new(html.as_bytes()), &mut output); - assert!(result.is_ok()); + result.unwrap(); let processed = String::from_utf8_lossy(&output); assert!( processed.contains("tsjs-unified"), @@ -1942,10 +1927,10 @@ script_patterns = ["/prebid.js", "/custom/prebid.min.js"] ); assert_eq!(config.script_patterns.len(), 2); - assert!(config.script_patterns.contains(&"/prebid.js".to_string())); + assert!(config.script_patterns.contains(&"/prebid.js".to_owned())); assert!(config .script_patterns - .contains(&"/custom/prebid.min.js".to_string())); + .contains(&"/custom/prebid.min.js".to_owned())); } #[test] @@ -1959,10 +1944,10 @@ server_url = "https://prebid.example" ); assert!(!config.script_patterns.is_empty()); - assert!(config.script_patterns.contains(&"/prebid.js".to_string())); + assert!(config.script_patterns.contains(&"/prebid.js".to_owned())); assert!(config .script_patterns - .contains(&"/prebid.min.js".to_string())); + .contains(&"/prebid.min.js".to_owned())); } #[test] @@ -2033,23 +2018,19 @@ server_url = "https://prebid.example" ); assert!( script.contains(r#""accountId":"test-account""#), - "should include accountId from config: {}", - script + "should include accountId from config: {script}" ); assert!( script.contains(r#""timeout":1000"#), - "should include timeout: {}", - script + "should include timeout: {script}" ); assert!( script.contains(r#""debug":false"#), - "should include debug flag: {}", - script + "should include debug flag: {script}" ); assert!( script.contains(r#""bidders":["exampleBidder"]"#), - "should include bidders array: {}", - script + "should include bidders array: {script}" ); } @@ -2070,15 +2051,14 @@ server_url = "https://prebid.example" let script = &inserts[0]; assert!( script.contains(r#""accountId":"""#), - "should emit empty accountId when not configured: {}", - script + "should emit empty accountId when not configured: {script}" ); } #[test] fn head_injector_escapes_closing_script_tags_in_values() { let mut config = base_config(); - config.account_id = Some("".to_string()); + config.account_id = Some("".to_owned()); let integration = PrebidIntegration::new(config); let document_state = IntegrationDocumentState::default(); let ctx = IntegrationHtmlContext { @@ -2092,8 +2072,7 @@ server_url = "https://prebid.example" let script = &inserts[0]; assert!( script.contains(r#""accountId":"<\/script>